ShardMap
The intros Sessions beat — a key lives on someones node; get forwards to the owner via Hyperlink.peers — is a pattern every multi-droplet app reinvents. ShardMap is that pattern as a Hyperlink factory: declare key / value, distribute across app/Droplet* nodes, and every routed get / put / delete finds the owner. Leaf *Local ops stay on this shard. Fleet folds report sizes. An unreachable owner degrades to a miss — never a silent write on the wrong droplet.
Declare the map
Schemas on the Tag. keyOf extracts the partition key from a value (routed put). Partition strategy is a runtime option on serve / layer (default: ShardMap.consistentHash).
class class DropletEastDropletEast extends import NodeNode.Tag<DropletEast, never>(): {
(key: string): NodeTagClass<DropletEast, never, BareAddress>;
(key: string, target: {
readonly path: string;
readonly kind?: "IpcSocket";
readonly onConflict?: Node.OnConflict;
}): NodeTagClass<DropletEast, never, IpcAddress>;
(key: string, target: number | `:${number}`): NodeTagClass<DropletEast, never, HttpAddress>;
(key: string, target: `ws://${string}` | `wss://${string}`): NodeTagClass<...>;
(key: string, target: `http://${string}` | `https://${string}`): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
<T>(key: string, target: T): NodeTagClass<...>;
(key: string, target: string | {
...;
}): NodeTagClass<...>;
(key: string, target?: LooseNodeTarget): NodeTagClass<...>;
}
export Tag
Declare a node — a named transport endpoint a HyperService connects to. Two-stage and keyed by
a string, mirroring Effect's Context.Service<Self, Shape>()(key) (a node is a Context.Key,
resolved by its key in the Context map) and every sibling factory (Hyperlink.Tag<Self>(), …).
The second call infers the target shape, so the { http, ws } shorthand types its
ProtocolKind
set precisely. Optional catalog type param ROut (C2) — prefer import type
for those handles (C4). Templates (no address until cloned) live on
Node
.Prototype:
class EdgeNode extends Node.Tag<EdgeNode>()("edge") {} // no address yet
class Worker extends Node.Tag<Worker>()("worker", 3001) {} // → http://localhost:3001/rpc, kind "Http"
class Mail extends Node.Tag<Mail>()("mail", "https://mail.internal/rpc") {} // full url, as-is, kind "Http"
class Live extends Node.Tag<Live>()("live", { url: "wss://live/rpc" }) {} // kind "WebSocket" (inferred from ws url)
class Push extends Node.Tag<Push>()("push", { url: "/rpc", kind: "WebSocket" }) {} // same-origin path, explicit kind
class Local extends Node.Tag<Local>()("local", { path: "/tmp/local.sock" }) {} // kind "IpcSocket" (Unix domain)
class Droplet extends Node.Tag<Droplet>()("droplet", { http: "http://d/rpc", ws: "ws://d/rpc" }) {} // multi-protocol
import type { Jobs, Emails } from "@app/contracts"
class AppWorker extends Node.Tag<AppWorker, Jobs | Emails>()("app/Worker", { path: "/tmp/w.sock" }) {}
class MailWorker extends Node.Prototype<MailWorker, Mail>("app/MailWorker") {}
The key is the service key. The optional address matches a dial target: a port
(3001 or ":3001" → http://localhost:3001/rpc), a full url (used as-is), { url, kind } for
an explicit endpoint, { path } for a Unix-domain socket (kind: "IpcSocket"), or the
{ http, ws, ipc } multi-protocol shorthand. The node carries
ProtocolKind
so the topology
is self-describing about where AND how:
connect
(node) derives the transport with no
protocol argument.
Dialable targets return an
AddressedNode
(kind: ProtocolKind) so
Hyperlink.client(Tag, Worker) can auto-wire
connect
. Bare Node.Tag()("x")
stays address-less (kind: undefined) — still needs explicit connect / lookup.
@categoryconstructors@publicTag<class DropletEastDropletEast>()("app/DropletEast") {}
class class DropletWestDropletWest extends import NodeNode.Tag<DropletWest, never>(): {
(key: string): NodeTagClass<DropletWest, never, BareAddress>;
(key: string, target: {
readonly path: string;
readonly kind?: "IpcSocket";
readonly onConflict?: Node.OnConflict;
}): NodeTagClass<DropletWest, never, IpcAddress>;
(key: string, target: number | `:${number}`): NodeTagClass<DropletWest, never, HttpAddress>;
(key: string, target: `ws://${string}` | `wss://${string}`): NodeTagClass<...>;
(key: string, target: `http://${string}` | `https://${string}`): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
<T>(key: string, target: T): NodeTagClass<...>;
(key: string, target: string | {
...;
}): NodeTagClass<...>;
(key: string, target?: LooseNodeTarget): NodeTagClass<...>;
}
export Tag
Declare a node — a named transport endpoint a HyperService connects to. Two-stage and keyed by
a string, mirroring Effect's Context.Service<Self, Shape>()(key) (a node is a Context.Key,
resolved by its key in the Context map) and every sibling factory (Hyperlink.Tag<Self>(), …).
The second call infers the target shape, so the { http, ws } shorthand types its
ProtocolKind
set precisely. Optional catalog type param ROut (C2) — prefer import type
for those handles (C4). Templates (no address until cloned) live on
Node
.Prototype:
class EdgeNode extends Node.Tag<EdgeNode>()("edge") {} // no address yet
class Worker extends Node.Tag<Worker>()("worker", 3001) {} // → http://localhost:3001/rpc, kind "Http"
class Mail extends Node.Tag<Mail>()("mail", "https://mail.internal/rpc") {} // full url, as-is, kind "Http"
class Live extends Node.Tag<Live>()("live", { url: "wss://live/rpc" }) {} // kind "WebSocket" (inferred from ws url)
class Push extends Node.Tag<Push>()("push", { url: "/rpc", kind: "WebSocket" }) {} // same-origin path, explicit kind
class Local extends Node.Tag<Local>()("local", { path: "/tmp/local.sock" }) {} // kind "IpcSocket" (Unix domain)
class Droplet extends Node.Tag<Droplet>()("droplet", { http: "http://d/rpc", ws: "ws://d/rpc" }) {} // multi-protocol
import type { Jobs, Emails } from "@app/contracts"
class AppWorker extends Node.Tag<AppWorker, Jobs | Emails>()("app/Worker", { path: "/tmp/w.sock" }) {}
class MailWorker extends Node.Prototype<MailWorker, Mail>("app/MailWorker") {}
The key is the service key. The optional address matches a dial target: a port
(3001 or ":3001" → http://localhost:3001/rpc), a full url (used as-is), { url, kind } for
an explicit endpoint, { path } for a Unix-domain socket (kind: "IpcSocket"), or the
{ http, ws, ipc } multi-protocol shorthand. The node carries
ProtocolKind
so the topology
is self-describing about where AND how:
connect
(node) derives the transport with no
protocol argument.
Dialable targets return an
AddressedNode
(kind: ProtocolKind) so
Hyperlink.client(Tag, Worker) can auto-wire
connect
. Bare Node.Tag()("x")
stays address-less (kind: undefined) — still needs explicit connect / lookup.
@categoryconstructors@publicTag<class DropletWestDropletWest>()("app/DropletWest") {}
class class DropletCentralDropletCentral extends import NodeNode.Tag<DropletCentral, never>(): {
(key: string): NodeTagClass<DropletCentral, never, BareAddress>;
(key: string, target: {
readonly path: string;
readonly kind?: "IpcSocket";
readonly onConflict?: Node.OnConflict;
}): NodeTagClass<DropletCentral, never, IpcAddress>;
(key: string, target: number | `:${number}`): NodeTagClass<DropletCentral, never, HttpAddress>;
(key: string, target: `ws://${string}` | `wss://${string}`): NodeTagClass<...>;
(key: string, target: `http://${string}` | `https://${string}`): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
(key: string, target: {
...;
}): NodeTagClass<...>;
<T>(key: string, target: T): NodeTagClass<...>;
(key: string, target: string | {
...;
}): NodeTagClass<...>;
(key: string, target?: LooseNodeTarget): NodeTagClass<...>;
}
export Tag
Declare a node — a named transport endpoint a HyperService connects to. Two-stage and keyed by
a string, mirroring Effect's Context.Service<Self, Shape>()(key) (a node is a Context.Key,
resolved by its key in the Context map) and every sibling factory (Hyperlink.Tag<Self>(), …).
The second call infers the target shape, so the { http, ws } shorthand types its
ProtocolKind
set precisely. Optional catalog type param ROut (C2) — prefer import type
for those handles (C4). Templates (no address until cloned) live on
Node
.Prototype:
class EdgeNode extends Node.Tag<EdgeNode>()("edge") {} // no address yet
class Worker extends Node.Tag<Worker>()("worker", 3001) {} // → http://localhost:3001/rpc, kind "Http"
class Mail extends Node.Tag<Mail>()("mail", "https://mail.internal/rpc") {} // full url, as-is, kind "Http"
class Live extends Node.Tag<Live>()("live", { url: "wss://live/rpc" }) {} // kind "WebSocket" (inferred from ws url)
class Push extends Node.Tag<Push>()("push", { url: "/rpc", kind: "WebSocket" }) {} // same-origin path, explicit kind
class Local extends Node.Tag<Local>()("local", { path: "/tmp/local.sock" }) {} // kind "IpcSocket" (Unix domain)
class Droplet extends Node.Tag<Droplet>()("droplet", { http: "http://d/rpc", ws: "ws://d/rpc" }) {} // multi-protocol
import type { Jobs, Emails } from "@app/contracts"
class AppWorker extends Node.Tag<AppWorker, Jobs | Emails>()("app/Worker", { path: "/tmp/w.sock" }) {}
class MailWorker extends Node.Prototype<MailWorker, Mail>("app/MailWorker") {}
The key is the service key. The optional address matches a dial target: a port
(3001 or ":3001" → http://localhost:3001/rpc), a full url (used as-is), { url, kind } for
an explicit endpoint, { path } for a Unix-domain socket (kind: "IpcSocket"), or the
{ http, ws, ipc } multi-protocol shorthand. The node carries
ProtocolKind
so the topology
is self-describing about where AND how:
connect
(node) derives the transport with no
protocol argument.
Dialable targets return an
AddressedNode
(kind: ProtocolKind) so
Hyperlink.client(Tag, Worker) can auto-wire
connect
. Bare Node.Tag()("x")
stays address-less (kind: undefined) — still needs explicit connect / lookup.
@categoryconstructors@publicTag<class DropletCentralDropletCentral>()("app/DropletCentral") {}
const const SessionId: Schema.StringSessionId = import SchemaSchema.const String: Schema.StringType-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
@categorymodels@since4.0.0@categoryschemas@since4.0.0String
const const Session: Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>
Session = import SchemaSchema.function Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>(fields: {
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}): Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>
Defines a struct schema from a map of field schemas.
Details
Each field value is a schema. Use
optionalKey
or
optional
to
mark fields as optional, and
mutableKey
to mark them as mutable.
The resulting schema's Type is a readonly object type with the fields'
decoded types. The Encoded form mirrors the field schemas' encoded types.
Example (Defining a basic struct)
import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number,
email: Schema.optionalKey(Schema.String)
})
// { readonly name: string; readonly age: number; readonly email?: string }
type Person = typeof Person.Type
const alice = Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 })
console.log(alice)
// { name: 'Alice', age: 30 }
@categoryconstructors@since3.10.0Struct({
id: Schema.Stringid: const SessionId: Schema.StringSessionId,
userId: Schema.StringuserId: import SchemaSchema.const String: Schema.StringType-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
@categorymodels@since4.0.0@categoryschemas@since4.0.0String,
seat: Schema.optionalKey<Schema.String>seat: import SchemaSchema.const optionalKey: optionalKeyLambda
<Schema.String>(self: Schema.String) => Schema.optionalKey<Schema.String>
Type-level representation returned by
optionalKey
.
Creates an exact optional key schema for struct fields. Unlike optional,
this creates exact optional properties (not | undefined) that can be
completely omitted from the object.
Example (Creating a struct with optional key)
import { Schema } from "effect"
const schema = Schema.Struct({
name: Schema.String,
age: Schema.optionalKey(Schema.Number)
})
// Type: { readonly name: string; readonly age?: number }
type Person = typeof schema["Type"]
@categorymodels@since4.0.0@categorycombinators@since4.0.0optionalKey(import SchemaSchema.const String: Schema.StringType-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
@categorymodels@since4.0.0@categoryschemas@since4.0.0String),
})
class class SessionsSessions extends import ShardMapShardMap.const Tag: <Sessions>() => <Key extends Schema.Top, Value extends Schema.Top, Error extends Schema.Top = typeof Schema.Never>(key: string, schemas: ShardMap.ShardMapSchemas<Key, Value, Error>) => ShardMap.ShardMapTag<Sessions, Key, Value, Error>Declare a ShardMap tag — schemas on the Tag; partition strategy is a runtime option on
layer
/
serve
.
@exampleclass Sessions extends ShardMap.Tag()("app/Sessions", {
key: SessionId,
value: Session,
keyOf: (s) => s.id,
}).pipe(Hyperlink.nodes([DropletEast, DropletWest])) {}@categoryconstructors@publicTag<class SessionsSessions>()("app/Sessions", {
ShardMapSchemas<String, Struct<{ readonly id: String; readonly userId: String; readonly seat: optionalKey<String>; }>, Never>.key: Schema.Stringkey: const SessionId: Schema.StringSessionId,
ShardMapSchemas<String, Struct<{ readonly id: String; readonly userId: String; readonly seat: optionalKey<String>; }>, Never>.value: Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>
value: const Session: Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>
Session,
ShardMapSchemas<String, Struct<{ readonly id: String; readonly userId: String; readonly seat: optionalKey<String>; }>, Never>.keyOf: (value: {
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}) => string
Extract the partition key from a value (routed put / putLocal).
keyOf: (s: {
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}
s) => s: {
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}
s.id: stringid,
}).Pipeable.pipe<ShardMap.ShardMapTag<Sessions, Schema.String, Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>, Schema.Never>, Hyperlink.HyperlinkTag<Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
...;
}, Hyperlink.Derive>;
... 7 more ...;
size: Hyperlink.Marked<...>;
}, {
...;
}> & {
...;
}>(this: ShardMap.ShardMapTag<...>, ab: (_: ShardMap.ShardMapTag<...>) => Hyperlink.HyperlinkTag<Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
...;
}, Hyperlink.Derive>;
... 7 more ...;
size: Hyperlink.Marked<...>;
}, {
...;
}> & {
...;
}): Hyperlink.HyperlinkTag<Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
...;
}, Hyperlink.Derive>;
... 7 more ...;
size: Hyperlink.Marked<...>;
}, {
...;
}> & {
...;
} (+21 overloads)
pipe(
import HyperlinkHyperlink.const nodes: <Hyperlink.HyperlinkTag<Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
description: string;
}, Hyperlink.Derive>;
put: Hyperlink.Method<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>, ... 4 more ..., Hyperlink.Derive>;
... 6 more ...;
size: Hyperlink.Marked<...>;
}, {
...;
}> & {
...;
}>(nodeSet: ReadonlyArray<Node.AnyNode>) => (tag: Hyperlink.HyperlinkTag<Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
description: string;
}, Hyperlink.Derive>;
put: Hyperlink.Method<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>, ... 4 more ..., Hyperlink.Derive>;
... 6 more ...;
size: Hyperlink.Marked<...>;
}, {
...;
}> & {
...;
}) => Hyperlink.HyperlinkTag<Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
description: string;
}, Hyperlink.Derive>;
put: Hyperlink.Method<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>, ... 4 more ..., Hyperlink.Derive>;
... 6 more ...;
size: Hyperlink.Marked<...>;
}, {
...;
}> & {
...;
} (+6 overloads)
nodes([class DropletEastDropletEast, class DropletWestDropletWest, class DropletCentralDropletCentral]),
) {}Bring a droplet online
One materialization — local shard + RPC handlers + peer clients. Swap DropletEast for West / Central on the other machines; the callers program does not change.
const const east: Layer.Layer<Sessions | Hyperlink.Local<Sessions> | Handler<"get"> | Handler<"put"> | Handler<"delete"> | Handler<"getLocal"> | Handler<"putLocal"> | Handler<"deleteLocal"> | Handler<"sizeLocal"> | Handler<"sizeByNode"> | Handler<"size">, ServeError, never>east = import ShardMapShardMap.const serve: <Sessions, Schema.String, Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>, Schema.Never>(tag: ShardMap.ShardMapTag<Sessions, Schema.String, Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>, Schema.Never>, options?: ShardMap.ShardMapOptions) => Layer.Layer<...>
Serve this ShardMap and grant its local instance from one materialization —
counterpart to
Hyperlink.serve
. Opens SQLite (:memory: by default; pass
{ filename } for a durable file). Requires the mesh capability:
@exampleShardMap.serve(Sessions).pipe(
Layer.provide(Hyperlink.peersLayer(Sessions, DropletEast)),
)
// Durable file:
ShardMap.serve(Sessions, { filename: ".hyperlink-ts/sessions.sqlite" })
@categorylayers & serving@publicserve(class SessionsSessions).Pipeable.pipe<Layer.Layer<Sessions | Hyperlink.Local<Sessions> | Handler<"get"> | Handler<"put"> | Handler<"delete"> | Handler<"getLocal"> | Handler<"putLocal"> | Handler<"deleteLocal"> | Handler<"sizeLocal"> | Handler<"sizeByNode"> | Handler<"size">, never, Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>>, Layer.Layer<Sessions | Hyperlink.Local<Sessions> | ... 8 more ... | Handler<...>, never, never>, Layer.Layer<...>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<...>) => Layer.Layer<...>, bc: (_: Layer.Layer<...>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)pipe(
import LayerLayer.const provide: <never, never, Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>>(that: Layer.Layer<Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, Exclude<RIn2, Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>>> (+3 overloads)Feeds the output services of the dependency layer into the requirements of
this layer, returning a layer that only provides the services from this layer.
When to use
Use when you need to hide an implementation dependency layer from callers.
Details
In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is
built first and is used to satisfy the requirements of serviceLayer.
Example (Providing layer dependencies)
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
@seeprovideMerge for retaining the dependency services@categoryproviding services@since2.0.0provide(import HyperlinkHyperlink.const peersLayer: <Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
description: string;
}, Hyperlink.Derive>;
put: Hyperlink.Method<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
readonly seat: Schema.optionalKey<Schema.String>;
}>, ... 4 more ..., Hyperlink.Derive>;
... 6 more ...;
size: Hyperlink.Marked<...>;
}, never, never>(tag: Hyperlink.HyperlinkTag<...>, self: Node.AnyNode, options?: {
...;
} | undefined) => Layer.Layer<...>
Provide the
peers
capability on this node: connect every OTHER node in the tag's
distributed
/
nodes
set and expose them as the peer clients. Also provides the
selfNode
capability (this node's key) for byNode-style folds. The opt-in mesh — add
it to a node's serve only where the HyperService's own logic reaches across nodes. self is the node
you are, so you're excluded from your own peer set.
Membership (D3):
- Fixed — non-empty
options.nodes or stamped nodes([…]) / distributed([…]).
- Directory — stamped empty set (bare
.pipe(Hyperlink.distributed) / nodes([])): read
Lookup Directory.nodesServing(tag.key) at layer build. Soft empty map when Directory is absent.
- Undeclared — no
nodesSym and no options.nodes → empty static peers (not directory).
Peer addresses: each
Node
's own url / path is the default. Pass options.url to
override the url per node — an env-specific port, a tunnel, or a value from Effect Config —
falling back to Node.url when the resolver returns undefined. A node with no dialable address
is skipped (never a throw), so a partial mesh degrades cleanly. IpcSocket peers dial via
protocolIpc
when only path is set. The resolver's error and requirements flow to the
layer (typed).
@categorynodes & fleet@publicpeersLayer(class SessionsSessions, class DropletEastDropletEast)),
const nodeServer: (port: number) => <A, E, R>(serviceKey: Layer.Layer<A, E, R>) => Layer.Layer<A, E | ServeError, Exclude<R, HttpServer | NodeServices | HttpPlatform | Generator>>nodeServer(3001),
)
// east: Layer — this droplet owns its shard and forwards the rest through peersPut and get from anywhere
From Easts HTTP edge or Wests poller — same handle. Ownership + the hop stay inside the Hyperlink.
const const sessions: {
readonly get: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
readonly put: (payload: {
id: string;
userId: string;
seat?: string | undefined;
}) => Effect.Effect<boolean, never, never>;
readonly delete: (payload: string) => Effect.Effect<boolean, never, never>;
readonly getLocal: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
... 4 more ...;
readonly size: Effect.Effect<...>;
}
sessions = yield* class Sessionsclass Sessions {
key: Identifier;
Service: {
get: (payload: string) => Effect.Effect<Option<{ readonly id: string; readonly userId: string; readonly seat?: string | undefined }>, never, never>;
put: (payload: { id: string; userId: string; seat?: string | undefined }) => Effect.Effect<boolean, never, never>;
delete: (payload: string) => Effect.Effect<boolean, never, never>;
getLocal: (payload: string) => Effect.Effect<Option<{ readonly id: string; readonly userId: string; readonly seat?: string | undefined }>, never, never>;
putLocal: (payload: { id: string; userId: string; seat?: string | undefined }) => Effect.Effect<void, never, never>;
deleteLocal: (payload: string) => Effect.Effect<boolean, never, never>;
sizeLocal: Effect.Effect<number, never, never>;
sizeByNode: Effect.Effect<{ readonly [x: string]: number }, never, never>;
size: Effect.Effect<number, never, never>;
};
description: string | undefined;
of: (this: void, self: { readonly get: (payload: string) => Effect.Effect<Option<{ readonly id: string; readonly userId: string; readonly seat?: string | undefined }>, never, never>; readonly put: (payload: { id: string; userId: string; seat?:…;
context: (self: { readonly get: (payload: string) => Effect.Effect<Option<{ readonly id: string; readonly userId: string; readonly seat?: string | undefined }>, never, never>; readonly put: (payload: { id: string; userId: string; seat?: string | un…;
use: (f: (service: { readonly get: (payload: string) => Effect.Effect<Option<{ readonly id: string; readonly userId: string; readonly seat?: string | undefined }>, never, never>; readonly put: (payload: { id: string; userId: string; seat?: stri…;
useSync: (f: (service: { readonly get: (payload: string) => Effect.Effect<Option<{ readonly id: string; readonly userId: string; readonly seat?: string | undefined }>, never, never>; readonly put: (payload: { id: string; userId: string; seat?: stri…;
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;
}
Sessions
const const wrote: booleanwrote = yield* const sessions: {
readonly get: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
readonly put: (payload: {
id: string;
userId: string;
seat?: string | undefined;
}) => Effect.Effect<boolean, never, never>;
readonly delete: (payload: string) => Effect.Effect<boolean, never, never>;
readonly getLocal: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
... 4 more ...;
readonly size: Effect.Effect<...>;
}
sessions.put: (payload: {
id: string
userId: string
seat?: string | undefined
}) => Effect.Effect<boolean, never, never>
put({
id: stringid: "fan-90210",
userId: stringuserId: "u_nik",
seat?: string | undefinedseat: "124-A",
})
// wrote: boolean — true when the owning node accepted the write
const const session: Option<{
readonly id: string
readonly userId: string
readonly seat?: string | undefined
}>
session = yield* const sessions: {
readonly get: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
readonly put: (payload: {
id: string;
userId: string;
seat?: string | undefined;
}) => Effect.Effect<boolean, never, never>;
readonly delete: (payload: string) => Effect.Effect<boolean, never, never>;
readonly getLocal: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
... 4 more ...;
readonly size: Effect.Effect<...>;
}
sessions.get: (payload: string) => Effect.Effect<
Option<{
readonly id: string
readonly userId: string
readonly seat?: string | undefined
}>,
never,
never
>
get("fan-90210")
// session: Option<Session> — from whoever owns the key; none on miss
const const dropped: booleandropped = yield* const sessions: {
readonly get: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
readonly put: (payload: {
id: string;
userId: string;
seat?: string | undefined;
}) => Effect.Effect<boolean, never, never>;
readonly delete: (payload: string) => Effect.Effect<boolean, never, never>;
readonly getLocal: (payload: string) => Effect.Effect<Option<{
readonly id: string;
readonly userId: string;
readonly seat?: string | undefined;
}>, never, never>;
... 4 more ...;
readonly size: Effect.Effect<...>;
}
sessions.delete: (
payload: string
) => Effect.Effect<boolean, never, never>
delete("fan-90210")
// dropped: boolean — true when an entry was removed on the owner
Leaf ops (getLocal / putLocal / deleteLocal / sizeLocal) stay on this shard — that is what peers fold and what routed ops forward to.
Fleet sizes
Ops across the pack without inventing a second dashboard tag:
const const sessions: {
readonly get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly userId: Schema.String;
}, "Type">>, never, never>;
readonly put: (payload: {
id: string;
userId: string;
}) => Effect.Effect<boolean, never, never>;
readonly delete: (payload: string) => Effect.Effect<boolean, never, never>;
readonly getLocal: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly userId: Schema.String;
}, "Type">>, never, never>;
... 4 more ...;
readonly size: Effect.Effect<...>;
}
sessions = yield* class Sessionsclass Sessions {
key: Identifier;
Service: {
get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{ readonly id: Schema.String; readonly userId: Schema.String }, 'Type'>>, never, never>;
put: (payload: { id: string; userId: string }) => Effect.Effect<boolean, never, never>;
delete: (payload: string) => Effect.Effect<boolean, never, never>;
getLocal: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{ readonly id: Schema.String; readonly userId: Schema.String }, 'Type'>>, never, never>;
putLocal: (payload: { id: string; userId: string }) => Effect.Effect<void, never, never>;
deleteLocal: (payload: string) => Effect.Effect<boolean, never, never>;
sizeLocal: Effect.Effect<number, never, never>;
sizeByNode: Effect.Effect<{ readonly [x: string]: number }, never, never>;
size: Effect.Effect<number, never, never>;
};
description: string | undefined;
of: (this: void, self: { readonly get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{ readonly id: Schema.String; readonly userId: Schema.String }, 'Type'>>, never, never>; readonly put: (payload: { id: string; userId: …;
context: (self: { readonly get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{ readonly id: Schema.String; readonly userId: Schema.String }, 'Type'>>, never, never>; readonly put: (payload: { id: string; userId: string }) =>…;
use: (f: (service: { readonly get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{ readonly id: Schema.String; readonly userId: Schema.String }, 'Type'>>, never, never>; readonly put: (payload: { id: string; userId: strin…;
useSync: (f: (service: { readonly get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{ readonly id: Schema.String; readonly userId: Schema.String }, 'Type'>>, never, never>; readonly put: (payload: { id: string; userId: strin…;
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;
}
Sessions
const const shards: {
readonly [x: string]: number
}
shards = yield* const sessions: {
readonly get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly userId: Schema.String;
}, "Type">>, never, never>;
readonly put: (payload: {
id: string;
userId: string;
}) => Effect.Effect<boolean, never, never>;
readonly delete: (payload: string) => Effect.Effect<boolean, never, never>;
readonly getLocal: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly userId: Schema.String;
}, "Type">>, never, never>;
... 4 more ...;
readonly size: Effect.Effect<...>;
}
sessions.sizeByNode: Effect.Effect<
{ readonly [x: string]: number },
never,
never
>
(property) sizeByNode: {
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;
}
sizeByNode
// shards: Record<string, number> — e.g. { "app/DropletEast": 14202, "app/DropletWest": 13880 }
const const fleet: numberfleet = yield* const sessions: {
readonly get: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly userId: Schema.String;
}, "Type">>, never, never>;
readonly put: (payload: {
id: string;
userId: string;
}) => Effect.Effect<boolean, never, never>;
readonly delete: (payload: string) => Effect.Effect<boolean, never, never>;
readonly getLocal: (payload: string) => Effect.Effect<Option<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly userId: Schema.String;
}, "Type">>, never, never>;
... 4 more ...;
readonly size: Effect.Effect<...>;
}
sessions.size: Effect.Effect<number, never, never>(property) size: {
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;
}
size
// fleet: number — sum across self + peers
Persist the shard
Local keys are SQLite SSOT — one row per live (scope, key), not an event log. ShardMap.layer / serve open :memory: by default (always on); pass { filename } for a durable file. Boot loads rows once; mutations UPSERT / DELETE.
const const live: Layer.Layer<Sessions | Hyperlink.Local<Sessions> | Handler<"get"> | Handler<"put"> | Handler<"delete"> | Handler<"getLocal"> | Handler<"putLocal"> | Handler<"deleteLocal"> | Handler<"sizeLocal"> | Handler<"sizeByNode"> | Handler<"size">, never, never>live = import ShardMapShardMap.const serve: <Sessions, Schema.String, Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
}>, Schema.Never>(tag: ShardMap.ShardMapTag<Sessions, Schema.String, Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
}>, Schema.Never>, options?: ShardMap.ShardMapOptions) => Layer.Layer<Sessions | Hyperlink.Local<Sessions> | Handler<"get"> | ... 7 more ... | Handler<...>, never, Hyperlink.PeersId<...> | Hyperlink.SelfNodeId<...>>
Serve this ShardMap and grant its local instance from one materialization —
counterpart to
Hyperlink.serve
. Opens SQLite (:memory: by default; pass
{ filename } for a durable file). Requires the mesh capability:
@exampleShardMap.serve(Sessions).pipe(
Layer.provide(Hyperlink.peersLayer(Sessions, DropletEast)),
)
// Durable file:
ShardMap.serve(Sessions, { filename: ".hyperlink-ts/sessions.sqlite" })
@categorylayers & serving@publicserve(class SessionsSessions, {
ShardMapOptions.filename?: string | undefinedSQLite filename for this shard's SSOT. Default :memory: (in-process, always on).
Pass a path for crash-surviving durability.
filename: ".hyperlink-ts/sessions.sqlite",
}).Pipeable.pipe<Layer.Layer<Sessions | Hyperlink.Local<Sessions> | Handler<"get"> | Handler<"put"> | Handler<"delete"> | Handler<"getLocal"> | Handler<"putLocal"> | Handler<"deleteLocal"> | Handler<"sizeLocal"> | Handler<"sizeByNode"> | Handler<"size">, never, Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>>, Layer.Layer<Sessions | Hyperlink.Local<Sessions> | ... 8 more ... | Handler<...>, never, never>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<...>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)pipe(import LayerLayer.const provide: <never, never, Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>>(that: Layer.Layer<Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, Exclude<RIn2, Hyperlink.PeersId<Sessions> | Hyperlink.SelfNodeId<Sessions>>> (+3 overloads)Feeds the output services of the dependency layer into the requirements of
this layer, returning a layer that only provides the services from this layer.
When to use
Use when you need to hide an implementation dependency layer from callers.
Details
In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is
built first and is used to satisfy the requirements of serviceLayer.
Example (Providing layer dependencies)
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
@seeprovideMerge for retaining the dependency services@categoryproviding services@since2.0.0provide(import HyperlinkHyperlink.const peersLayer: <Sessions, {
get: Hyperlink.Method<Schema.String, Schema.Option<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
}>>, Schema.Never, false, Hyperlink.MethodAnnotations & {
description: string;
}, Hyperlink.Derive>;
put: Hyperlink.Method<Schema.Struct<{
readonly id: Schema.String;
readonly userId: Schema.String;
}>, Schema.Boolean, Schema.Never, false, Hyperlink.MethodAnnotations & {
...;
}, Hyperlink.Derive>;
... 6 more ...;
size: Hyperlink.Marked<...>;
}, never, never>(tag: Hyperlink.HyperlinkTag<...>, self: Node.AnyNode, options?: {
...;
} | undefined) => Layer.Layer<...>
Provide the
peers
capability on this node: connect every OTHER node in the tag's
distributed
/
nodes
set and expose them as the peer clients. Also provides the
selfNode
capability (this node's key) for byNode-style folds. The opt-in mesh — add
it to a node's serve only where the HyperService's own logic reaches across nodes. self is the node
you are, so you're excluded from your own peer set.
Membership (D3):
- Fixed — non-empty
options.nodes or stamped nodes([…]) / distributed([…]).
- Directory — stamped empty set (bare
.pipe(Hyperlink.distributed) / nodes([])): read
Lookup Directory.nodesServing(tag.key) at layer build. Soft empty map when Directory is absent.
- Undeclared — no
nodesSym and no options.nodes → empty static peers (not directory).
Peer addresses: each
Node
's own url / path is the default. Pass options.url to
override the url per node — an env-specific port, a tunnel, or a value from Effect Config —
falling back to Node.url when the resolver returns undefined. A node with no dialable address
is skipped (never a throw), so a partial mesh degrades cleanly. IpcSocket peers dial via
protocolIpc
when only path is set. The resolver's error and requirements flow to the
layer (typed).
@categorynodes & fleet@publicpeersLayer(class SessionsSessions, class DropletEastDropletEast)))
// omit filename → in-memory SQLite (default)Partition ethic (v1)
ShardMap.consistentHash sorts node keys and picks with Hash.string modulo — stable for a fixed fleet. Membership change remaps keys; treat that as intentional. Unreachable owner → get is none, put returns false — miss beats silent wrong answer.
Runnable form: pnpm run example:shardmap-sessions. See also Fleets & Peers.