Core Concepts
Every program depends on capabilities it does not build itself — a clock, a database, somewhere to send email. Effect models each of those as a Service. A Hyperlink Service (or HyperService) is still a Service — same Tag, same yield* — with one addition that lets the seam sit between processes, not only modules. This page starts with Services and adds that idea one step at a time.
Services and Tags
A Service is a capability your program depends on. Rather than thread it through function after function, you refer to it through a Tag: a typed name that stands for the Service wherever it is used. Your code declares what it needs; the type system keeps track of it.
Working with a Service is three steps — define it, use it, and provide it:
import { import ContextContext, import EffectEffect, import LayerLayer } from "effect"
// define: a service and its interface, named by a tag
class class Randomclass Random {
key: Identifier;
Service: {
next: Effect.Effect<number>;
};
}
Random extends import ContextContext.const Service: {
<Identifier, Shape = Identifier>(
key: string
): Service<Identifier, Shape>
<Self, Shape>(): <
Identifier extends string,
E,
R = Types.unassigned,
Args extends ReadonlyArray<any> = never
>(
id: Identifier,
options?:
| {
readonly make:
| ((
...args: Args
) => Effect<Shape, E, R>)
| Effect<Shape, E, R>
| undefined
}
| undefined
) => ServiceClass<Self, Identifier, Shape> &
([Types.unassigned] extends [R]
? unknown
: {
readonly make: [Args] extends [never]
? Effect<Shape, E, R>
: (
...args: Args
) => Effect<Shape, E, R>
})
<Self>(): <
Identifier extends string,
Make extends
| Effect<any, any, any>
| ((...args: any) => Effect<any, any, any>)
>(
id: Identifier,
options: { readonly make: Make }
) => ServiceClass<
Self,
Identifier,
Make extends
| Effect<infer _A, infer _E, infer _R>
| ((
...args: infer _Args
) => Effect<infer _A, infer _E, infer _R>)
? _A
: never
> & { readonly make: Make }
}
Creates a Context service key.
When to use
Use when you need to define a context service key for a dependency that must
be provided by the surrounding context.
Details
Call Context.Service("Key") for a function-style key, or use the two-stage
form Context.Service<Self, Shape>()("Key") for class-style service
declarations. The returned key can be yielded as an Effect and passed to
Context.make, Context.add, and the Context getter functions.
Gotchas
The string key is the runtime identity of the service. Reusing the same key
string for unrelated services makes them occupy the same slot in a
Context.
Example (Creating service keys)
import { Context } from "effect"
// Create a simple service
const Database = Context.Service<{
query: (sql: string) => string
}>("Database")
// Create a service class
class Config extends Context.Service<Config, {
port: number
}>()("Config") {}
// Use the services to create contexts
const db = Context.make(Database, {
query: (sql) => `Result: ${sql}`
})
const config = Context.make(Config, { port: 8080 })
Service<class Randomclass Random {
key: Identifier;
Service: {
next: Effect.Effect<number>;
};
}
Random, {
readonly next: Effect.Effect<number>(property) next: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
next: import EffectEffect.interface Effect<out A, out E = never, out R = never>The Effect interface defines a value that lazily describes a workflow or
job. The workflow requires some context R, and may fail with an error of
type E, or succeed with a value of type A.
When to use
Use when you need to represent a lazy, composable workflow that can require
services, fail with a typed error, or succeed with a typed value.
Details
Effect values model resourceful interaction with the outside world,
including synchronous, asynchronous, concurrent, and parallel interaction.
They use a fiber-based concurrency model, with built-in support for
scheduling, fine-grained interruption, structured concurrency, and high
scalability.
To run an Effect value, you need a Runtime, which is a type that is
capable of executing Effect values.
Effect<number>
}>()("app/Random") {}
// use: reach the service by yielding its Tag
const const program: Effect.Effect<
number,
never,
Random
>
const program: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
program = import EffectEffect.const gen: {
<Eff extends Effect<any, any, any>, AEff>(
f: () => Generator<Eff, AEff, never>
): Effect<
AEff,
[Eff] extends [never]
? never
: [Eff] extends [
Effect<infer _A, infer E, infer _R>
]
? E
: never,
[Eff] extends [never]
? never
: [Eff] extends [
Effect<infer _A, infer _E, infer R>
]
? R
: never
>
<Self, Eff extends Effect<any, any, any>, AEff>(
options: { readonly self: Self },
f: (this: Self) => Generator<Eff, AEff, never>
): Effect<
AEff,
[Eff] extends [never]
? never
: [Eff] extends [
Effect<infer _A, infer E, infer _R>
]
? E
: never,
[Eff] extends [never]
? never
: [Eff] extends [
Effect<infer _A, infer _E, infer R>
]
? R
: never
>
}
Provides a way to write effectful code using generator functions, simplifying
control flow and error handling.
When to use
Use when you want to write effectful code that looks and behaves like
synchronous code, while still handling asynchronous tasks, errors, and complex
control flow such as loops and conditions.
Generator functions work similarly to async/await but keep errors,
requirements, and interruption in the Effect type. You can yield* values
from effects and return the final result at the end.
Example (Sequencing effects with generators)
import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})
gen(function* () {
const const random: {
readonly next: Effect.Effect<number>
}
random = yield* class Randomclass Random {
key: Identifier;
Service: {
next: Effect.Effect<number>;
};
of: (this: void, self: { readonly next: Effect.Effect<number> }) => { readonly next: Effect.Effect<number> };
context: (self: { readonly next: Effect.Effect<number> }) => Context.Context<Random>;
use: (f: (service: { readonly next: Effect.Effect<number> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Random | R>;
useSync: (f: (service: { readonly next: Effect.Effect<number> }) => A) => Effect.Effect<A, never, Random>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Random
return yield* const random: {
readonly next: Effect.Effect<number>
}
random.next: Effect.Effect<number>(property) next: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
next
})
// provide: supply an implementation once, at the edge, with a Layer
const const random: Layer.Layer<
Random,
never,
never
>
const random: {
build: (memoMap: MemoMap, scope: Scope.Scope) => Effect<Context.Context<Random>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
random = import LayerLayer.const succeed: {
<I, S>(service: Context.Key<I, S>): (
resource: S
) => Layer<I>
<I, S>(
service: Context.Key<I, S>,
resource: Types.NoInfer<S>
): Layer<I>
}
Constructs a layer that provides a single service from an already available
value.
When to use
Use when you need a Layer that provides a service from an already
constructed implementation without effectful acquisition.
Example (Creating a layer from a service implementation)
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
const DatabaseLive = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Query result: ${sql}`))
})
succeed(class Randomclass Random {
key: Identifier;
Service: {
next: Effect.Effect<number>;
};
of: (this: void, self: { readonly next: Effect.Effect<number> }) => { readonly next: Effect.Effect<number> };
context: (self: { readonly next: Effect.Effect<number> }) => Context.Context<Random>;
use: (f: (service: { readonly next: Effect.Effect<number> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Random | R>;
useSync: (f: (service: { readonly next: Effect.Effect<number> }) => A) => Effect.Effect<A, never, Random>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Random, {
next: Effect.Effect<number, never, never>(property) next: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
next: import EffectEffect.const succeed: <A>(value: A) => Effect<A>Creates an Effect that always succeeds with a given value.
When to use
Use when an effect should complete successfully with a specific value without any errors
or external dependencies.
Example (Creating a successful effect)
import { Effect } from "effect"
// Creating an effect that represents a successful scenario
//
// ┌─── Effect<number, never, never>
// ▼
const success = Effect.succeed(42)
succeed(0.5),
})
const program: Effect.Effect<
number,
never,
Random
>
const program: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
program.Pipeable.pipe<Effect.Effect<number, never, Random>, Effect.Effect<number, never, never>>(this: Effect.Effect<number, never, Random>, ab: (_: Effect.Effect<number, never, Random>) => Effect.Effect<number, never, never>): Effect.Effect<number, never, never> (+21 overloads)pipe(import EffectEffect.const provide: {
<
Layers extends [
Layer.Any,
...Array<Layer.Any>
]
>(
layers: Layers,
options?:
| { readonly local?: boolean | undefined }
| undefined
): <A, E, R>(
self: Effect<A, E, R>
) => Effect<
A,
E | Layer.Error<Layers[number]>,
| Layer.Services<Layers[number]>
| Exclude<R, Layer.Success<Layers[number]>>
>
<ROut, E2, RIn>(
layer: Layer.Layer<ROut, E2, RIn>,
options?:
| { readonly local?: boolean | undefined }
| undefined
): <A, E, R>(
self: Effect<A, E, R>
) => Effect<A, E | E2, RIn | Exclude<R, ROut>>
<R2>(context: Context.Context<R2>): <A, E, R>(
self: Effect<A, E, R>
) => Effect<A, E, Exclude<R, R2>>
<
A,
E,
R,
Layers extends [
Layer.Any,
...Array<Layer.Any>
]
>(
self: Effect<A, E, R>,
layers: Layers,
options?:
| { readonly local?: boolean | undefined }
| undefined
): Effect<
A,
E | Layer.Error<Layers[number]>,
| Layer.Services<Layers[number]>
| Exclude<R, Layer.Success<Layers[number]>>
>
<A, E, R, ROut, E2, RIn>(
self: Effect<A, E, R>,
layer: Layer.Layer<ROut, E2, RIn>,
options?:
| { readonly local?: boolean | undefined }
| undefined
): Effect<A, E | E2, RIn | Exclude<R, ROut>>
<A, E, R, R2>(
self: Effect<A, E, R>,
context: Context.Context<R2>
): Effect<A, E, Exclude<R, R2>>
}
Provides dependencies to an effect using layers or a context. Use options.local
to build the layer every time; by default, layers are shared between provide
calls.
Example (Providing dependencies with a layer)
import { Context, Effect, Layer } from "effect"
interface Database {
readonly query: (sql: string) => Effect.Effect<string>
}
const Database = Context.Service<Database>("Database")
const DatabaseLive = Layer.succeed(Database)({
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))
})
const program = Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
const provided = Effect.provide(program, DatabaseLive)
Effect.runPromise(provided).then(console.log)
// Output: "Result for: SELECT * FROM users"
provide(const random: Layer.Layer<
Random,
never,
never
>
const random: {
build: (memoMap: MemoMap, scope: Scope.Scope) => Effect<Context.Context<Random>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
random))The Tag sits between the two sides: ask for a capability on one side, fulfil it on the other. Because that point is explicit, you can provide the real Service in production, a stub in a test, or swap one for another — without touching the code that depends on it.
From Services to Contracts
Hyperlink starts where Effects Services leave off. A Hyperlink Service is a Service whose Tag declares a Contract: the methods, together with a schema for every value that passes through them.
import * as import HyperlinkHyperlink from "hyperlink-ts/Hyperlink"
import { import SchemaSchema } from "effect"
class class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect<void, never, never>;
};
}
Counter extends import HyperlinkHyperlink.Tag<Counter>(): SchemaTagBuilder<Counter> (+2 overloads)
export Tag
Schema-driven solo: infer the service from spec; bare
local
is a compile error.
Tag<class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect<void, never, never>;
};
}
Counter>()("app/Counter", {
value: Hyperlink.RefField<
Hyperlink.Method<
undefined,
Schema.Number,
Schema.Never,
true,
Hyperlink.MethodAnnotations,
Hyperlink.Derive
>
>
value: import HyperlinkHyperlink.const ref: <Su extends Schema.Top>(
success: Su
) => RefField<
Method<undefined, Su, typeof Schema.Never, true>
>
Define a ref field — reactive state surfaced as a
Subscribable
(get + changes),
uniform local and remote. The impl owns a SubscriptionRef (writes it) and provides it via
subscribable
; consumers read (yield* svc.x.get) and observe (svc.x.changes) — a read
is an honest Effect, not a synchronous peek. For values fixed at acquire use
value
; for
on-demand calls use
effect
.
ref(import SchemaSchema.const Number: Numberconst Number: {
Rebuild: Rebuild;
Iso: Iso;
ast: Ast;
Type: T;
Encoded: E;
DecodingServices: RD;
EncodingServices: RE;
annotate: (annotations: Schema.Annotations.Bottom<number, readonly []>) => Schema.Number;
annotateKey: (annotations: Schema.Annotations.Key<number>) => Schema.Number;
check: (checks_0: Check<number>, ...checks: Array<Check<number>>) => Schema.Number;
rebuild: (ast: Number) => Schema.Number;
make: (input: number, options?: MakeOptions) => number;
makeOption: (input: number, options?: MakeOptions) => Option_.Option<number>;
makeEffect: (input: number, options?: MakeOptions) => Effect.Effect<number, SchemaError, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
Type-level representation of
Number
.
Schema for number values, including NaN, Infinity, and -Infinity.
Details
Default JSON serializer:
- Finite numbers are serialized as numbers.
- Non-finite values are serialized as strings (
"NaN", "Infinity", "-Infinity").
Number), // observable state
increment: Hyperlink.Method<
{ readonly by: Schema.Number },
Schema.Void,
Schema.Never,
false,
Hyperlink.MethodAnnotations,
Hyperlink.Derive
>
(property) increment: {
kind: MethodKind;
payload: P;
success: Su;
error: E;
stream: Str;
annotations: Ann;
annotate: <A extends MethodAnnotations>(annotations: A) => Method<P, Su, E, Str, Ann & A, Client>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
increment: import HyperlinkHyperlink.function effectFn<{
readonly by: Schema.Number;
}>(payload: {
readonly by: Schema.Number;
}): Hyperlink.Method<{
readonly by: Schema.Number;
}, Schema.Void, Schema.Never, false, Hyperlink.MethodAnnotations, Hyperlink.Derive> (+7 overloads)
Two-stage
effectFn
— override the client-facing type with a Client that must narrow
the schema-derived shape: effectFn<Client>()(payload). Reshape freely (e.g. add overloads), but a
Client that would accept payloads the wire rejects fails to compile (payload resolves to never).
For an override that can't be a narrowing (a generic library), use
unsafeEffectFn
.
effectFn({ by: Schema.Number(property) by: {
Rebuild: Rebuild;
Iso: Iso;
ast: Ast;
Type: T;
Encoded: E;
DecodingServices: RD;
EncodingServices: RE;
annotate: (annotations: Schema.Annotations.Bottom<number, readonly []>) => Schema.Number;
annotateKey: (annotations: Schema.Annotations.Key<number>) => Schema.Number;
check: (checks_0: Check<number>, ...checks: Array<Check<number>>) => Schema.Number;
rebuild: (ast: Number) => Schema.Number;
make: (input: number, options?: MakeOptions) => number;
makeOption: (input: number, options?: MakeOptions) => Option_.Option<number>;
makeEffect: (input: number, options?: MakeOptions) => Effect.Effect<number, SchemaError, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
by: import SchemaSchema.const Number: Numberconst Number: {
Rebuild: Rebuild;
Iso: Iso;
ast: Ast;
Type: T;
Encoded: E;
DecodingServices: RD;
EncodingServices: RE;
annotate: (annotations: Schema.Annotations.Bottom<number, readonly []>) => Schema.Number;
annotateKey: (annotations: Schema.Annotations.Key<number>) => Schema.Number;
check: (checks_0: Check<number>, ...checks: Array<Check<number>>) => Schema.Number;
rebuild: (ast: Number) => Schema.Number;
make: (input: number, options?: MakeOptions) => number;
makeOption: (input: number, options?: MakeOptions) => Option_.Option<number>;
makeEffect: (input: number, options?: MakeOptions) => Effect.Effect<number, SchemaError, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
Type-level representation of
Number
.
Schema for number values, including NaN, Infinity, and -Infinity.
Details
Default JSON serializer:
- Finite numbers are serialized as numbers.
- Non-finite values are serialized as strings (
"NaN", "Infinity", "-Infinity").
Number }), // a call, with a typed argument
}) {}That difference is what makes a HyperService cross-runtime. An ordinary Service is an interface for one runtime to satisfy. A Contract, because every value it names is a schema, is an interface that can be satisfied across runtimes — the schemas are enough to carry each call over the wire. The seam a Tag creates, once a line between modules, can now be a line between processes.
The same Tag, wherever it runs
You declare a HyperService once. Where it runs, you decide later — with the Layer you provide:
const const inProcess: Layer<
Counter | Hyperlink.Local<Counter>,
never,
never
>
const inProcess: {
build: (memoMap: MemoMap, scope: Scope.Scope) => Effect<Context.Context<Counter | Local<Counter>>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
inProcess = import HyperlinkHyperlink.layer<Counter, {
readonly value: Hyperlink.RefField<Hyperlink.Method<undefined, Schema.Number, Schema.Never, true, Hyperlink.MethodAnnotations, Hyperlink.Derive>>;
readonly increment: Hyperlink.Method<{
readonly by: Schema.Number;
}, Schema.Void, Schema.Never, false, Hyperlink.MethodAnnotations, Hyperlink.Derive>;
}, never>(tag: Hyperlink.HyperlinkTag<Counter, {
readonly value: Hyperlink.RefField<Hyperlink.Method<undefined, Schema.Number, Schema.Never, true, Hyperlink.MethodAnnotations, Hyperlink.Derive>>;
readonly increment: Hyperlink.Method<{
readonly by: Schema.Number;
}, Schema.Void, Schema.Never, false, Hyperlink.MethodAnnotations, Hyperlink.Derive>;
}, {
...;
}>, impl: Effect.Effect<...>): Layer<...> (+3 overloads)
export layer
layer(class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect.Effect<void, never, never>;
};
description: string | undefined;
of: (this: void, self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: {…;
context: (self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Context<Counter>;
use: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Counter | R>;
useSync: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => A) => Effect.Effect<A, never, Counter>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Counter, const counterImpl: Effect.Effect<
{
value: Hyperlink.Subscribable<number>
increment: ({
by,
}: {
readonly by: number
}) => Effect.Effect<void, never, never>
},
never,
never
>
const counterImpl: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
counterImpl) // run it in this runtime
const const served: Layer<
| Counter
| Hyperlink.Local<Counter>
| Handler<"value">
| Handler<"increment">,
never,
never
>
const served: {
build: (memoMap: MemoMap, scope: Scope.Scope) => Effect<Context.Context<Counter | Local<Counter> | Handler<'value'> | Handler<'increment'>>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
served = import HyperlinkHyperlink.const serve: <
Self,
S extends Spec,
R = never
>(
tag: HyperlinkTag<Self, S>,
impl:
| ImplWithDefaultOverrides<S>
| Driver<S, R>
| Effect.Effect<
| ImplWithDefaultOverrides<S>
| Driver<S, R>,
never,
R
>
) => Layer.Layer<
Self | Local<Self> | HandlerContextOf<S>,
ValueErrorsOf<S>,
R
>
Expose a
Tag
's implementation as an RPC server layer — the served counterpart of
layer
. Pair it with
Node.httpServer
/
Node.wsServer
(or
listen
) to
put it on a transport, and dial it with
client
/
connect
.
Shared-Spec instances (Tag(wireKey, spec) → Factory<Self>()(instanceKey)) share one
RpcGroup: Layer.mergeAll(serve(A, …), serve(B, …)) mounts handlers once and routes by
the per-call key header.
serve(class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect.Effect<void, never, never>;
};
description: string | undefined;
of: (this: void, self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: {…;
context: (self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Context<Counter>;
use: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Counter | R>;
useSync: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => A) => Effect.Effect<A, never, Counter>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Counter, const counterImpl: Effect.Effect<
{
value: Hyperlink.Subscribable<number>
increment: ({
by,
}: {
readonly by: number
}) => Effect.Effect<void, never, never>
},
never,
never
>
const counterImpl: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
counterImpl) // RPC handlers — mount with Node.http / Node.ws
const const client: Layer<Counter, never, never>const client: {
build: (memoMap: MemoMap, scope: Scope.Scope) => Effect<Context.Context<Counter>, never, never>;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
}
client = import HyperlinkHyperlink.const connect: <
Self,
S extends Spec,
E = never
>(
tag: HyperlinkTag<Self, S>,
protocol: Layer.Layer<RpcClient.Protocol, E>
) => Layer.Layer<Self, E | ValueErrorsOf<S>>
Dial a HyperService tag over a transport you provide — the no-batteries client. connect bakes in
no transport of its own (unlike
http
/
ws
/
unix
/
nPipe
, whose
wire is in the name and bundled): you hand it a
protocolHttp
/
protocolWebsocket
/
protocolIpc
layer, so a browser build pulls in only the one wire it passes.
program.pipe(Effect.provide(Hyperlink.connect(Emails, Hyperlink.protocolHttp(3009)))); // server
program.pipe(Effect.provide(Hyperlink.connect(Emails, Hyperlink.protocolWebsocket("/rpc")))); // browser (ws only)
The port shorthand (3009) resolves against
clientHost
(default "localhost"), so the same
3009 points at your production host once HYPERLINK_CLIENT_HOST is set.
Replaces the retired clientHttp(tag, target) — use connect(tag, protocolHttp(target)).
connect(class Counterclass Counter {
key: Identifier;
Service: {
value: Hyperlink.Subscribable<number>;
increment: (payload: { by: number }) => Effect.Effect<void, never, never>;
};
description: string | undefined;
of: (this: void, self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: {…;
context: (self: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Context<Counter>;
use: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, Counter | R>;
useSync: (f: (service: { readonly value: Hyperlink.Subscribable<number>; readonly increment: (payload: { by: number }) => Effect.Effect<void, never, never> }) => A) => Effect.Effect<A, never, Counter>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
Counter, import HyperlinkHyperlink.const protocolHttp: (
target?: number | string,
serialization?: Layer.Layer<RpcSerialization.RpcSerialization>
) => Layer.Layer<RpcClient.Protocol>
Build an http client Protocol (Fetch + ndjson serialization) for an endpoint — the value you
hand
layerProtocol
or
connect
. target is a port (3009 → http://${clientHost}:3009/rpc,
Config host default "localhost"), a full url, or a same-origin path (default "/rpc"). The
server/CLI transport; a browser should prefer
protocolWebsocket
(HTTP/1.1's ~6-connection cap
starves streams — protocolHttp dies loudly in a browser).
protocolHttp(4000)) // dial one running elsewhere
// A browser dashboard opens many live streams — serve with Node.ws(…, port) and connect with
// Hyperlink.ws (WebSocket), or an HTTP client starves at the browser's connection cap.
// See Managing Layers for the full set of provide / serve / client layers.
Whichever you choose, yield* Counter returns the same Handle. Reading a value, calling a method, watching it change — the call site reads identically whether the HyperService sits beside it or across a network. Only the Layer changes. That is what cross-runtime means.
The shape of a Contract
A Contracts methods take a small number of forms:
Hyperlink.effect(schema)— a value to read (or a command with no payload).Hyperlink.effectFn(input, output?)— a call that takes an argument.Hyperlink.ref(schema)— observable state: read it with.get, follow it through.changes.Hyperlink.stream(schema)— a continuous stream of values.Hyperlink.value(schema)— materialize once at acquire into a plain value on the handle.Hyperlink.local— local-only (needs the local Layer; uncallable through a client).Hyperlink.default(…)— Tag-baked literal or sync fn; identical local and remote, no wire. Several extras:Tag(…).pipe(Hyperlink.defaults({…}))(see Creating).
Building your own Contract end to end is Creating a Hyperlink Service. The package also ships a few included HyperServices (WorkPool, Daemon, and the rest) when you want a ready-made tool — secondary to building your own.
Nodes
When a program spans more than one runtime, each runtime is a Node. A Node carries the address at which its HyperServices can be reached, and served HyperServices find one another through the Nodes they share. You reach for Nodes only when a HyperService is served or distributed; a single-runtime program needs none. Fleets & Peers covers them in full; Managing Layers shows how to mount and dial them.
In brief
A Tag names a HyperService. Its Contract describes the methods and their schemas. An Implementation fulfils the Contract, and a Layer places it — in process, served, or reached as a client. The Handle you get from the Tag is the same in every case.
Next
Place one with the Layer vocabulary in Managing Layers, or build one end to end in Creating a Hyperlink Service.