RpcDurableObjectNamespace
Source:
src/Cloudflare/Workers/RpcDurableObjectNamespace.ts
RpcDurableObjectNamespace is sugar over {@link DurableObjectNamespace}
for Durable Objects whose surface is a typed Effect RpcGroup. The
DO serves an RpcServer.toHttpEffect(group) on its own fetch, and
consumers see namespace.getByName(id) as a typed RpcClient
directly — no manual client wiring.
Use this over alchemy’s built-in DO method bridge whenever values
crossing the DO boundary contain Schema.Class instances. The
built-in bridge JSON.stringifys every method return value, which
strips class identity (e.g. an effect/ai Response.Usage instance
becomes a plain struct on the consumer side). With
RpcDurableObjectNamespace, both ends go through the same
RpcSerialization codec, so Schema.decode reconstructs class
instances correctly.
Defining the rpc group
Section titled “Defining the rpc group”The DO instance is the session, so the group payloads typically don’t include any per-session identifier — only the per-call inputs.
import * as Schema from "effect/Schema";import { Rpc, RpcGroup } from "effect/unstable/rpc";
const setTitle = Rpc.make("setTitle", { success: Schema.Void, payload: { title: Schema.String },});
const getTitle = Rpc.make("getTitle", { success: Schema.String, payload: {},});
export class CounterRpcs extends RpcGroup.make(setTitle, getTitle) {}Implementing the Durable Object
Section titled “Implementing the Durable Object”Mirrors Cloudflare.DurableObjectNamespace<Self>()(...) — same
outer/inner Effect pattern. The outer Effect resolves shared deps;
the per-instance inner Effect returns the
RpcServer.toHttpEffect(schema)-piped Effect directly.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import { RpcSerialization, RpcServer } from "effect/unstable/rpc";import { CounterRpcs } from "./rpcs.ts";
export default class Counter extends Cloudflare.RpcDurableObjectNamespace<Counter>()( "Counter", { schema: CounterRpcs }, Effect.gen(function* () { // outer init: shared deps for all instances return Effect.gen(function* () { // per-instance init: state + handlers const state = yield* Cloudflare.DurableObjectState; const handlers = CounterRpcs.toLayer({ setTitle: ({ title }) => state.storage.put("title", title), getTitle: () => Effect.map(state.storage.get<string>("title"), (t) => t ?? ""), }); return RpcServer.toHttpEffect(CounterRpcs).pipe( Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), ); }); }),) {}Calling the DO from a Worker
Section titled “Calling the DO from a Worker”yield* Counter resolves to a value whose getByName(id) returns
an Effect<RpcClient<CounterRpcs>>. Each rpc method is a typed
Effect/Stream factory — no RpcClient.make setup needed. Yield
the client inside a per-request Effect.scoped handler so it’s
freed with the request.
import Counter from "./counter.ts";
Effect.gen(function* () { const counters = yield* Counter; const client = yield* counters.getByName("global"); yield* client.setTitle({ title: "Hello" }); const title = yield* client.getTitle({}); return title;}).pipe(Effect.scoped);Modular form: separate the class from its runtime
Section titled “Modular form: separate the class from its runtime”The inline class form above bundles the runtime into the class
declaration. The two-arg form (name, { schema }) declares the
class as a pure tagged identifier; provide the runtime separately
via Class.make(impl). Consumer Workers can import the class for
binding (Counter.from(HostWorker)) without pulling the runtime
into their bundle.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import { RpcSerialization, RpcServer } from "effect/unstable/rpc";import { CounterRpcs } from "./rpcs.ts";
export class Counter extends Cloudflare.RpcDurableObjectNamespace<Counter>()( "Counter", { schema: CounterRpcs },) {}
// Only the host script imports this default export.export default Counter.make( Effect.gen(function* () { return Effect.gen(function* () { const state = yield* Cloudflare.DurableObjectState; const handlers = CounterRpcs.toLayer({ setTitle: ({ title }) => state.storage.put("title", title), getTitle: () => Effect.map(state.storage.get<string>("title"), (t) => t ?? ""), }); return RpcServer.toHttpEffect(CounterRpcs).pipe( Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)), ); }); }),);Cross-script binding via Counter.from(Worker)
Section titled “Cross-script binding via Counter.from(Worker)”Hosting on WorkerA, binding from WorkerB
The host Worker declares Counter in its Deps (third type
arg of Worker<Self, Bindings, Deps> or second of
RpcWorker<Self, Deps>) and provides CounterLive. Any other
Worker uses Counter.from(HostWorker) to bind to the same DO
instances — writes through HostWorker.getByName(name) are
visible from Counter.from(HostWorker).getByName(name).
// host worker (declares + provides Counter)import CounterLive, { Counter } from "./counter.ts";
export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()( "WorkerA", { main: import.meta.filename },) {}
export default WorkerA.make( Effect.gen(function* () { const counters = yield* Counter; // local host binding // ... fetch handler ... }).pipe(Effect.provide(CounterLive)),);
// consumer worker (binds via .from)export default class WorkerB extends Cloudflare.Worker<WorkerB>()( "WorkerB", { main: import.meta.filename }, Effect.gen(function* () { const counters = yield* Counter.from(WorkerA); return { fetch: Effect.gen(function* () { const client = yield* counters.getByName("shared"); yield* client.setTitle({ title: "via WorkerB" }); return HttpServerResponse.text("ok"); }).pipe(Effect.scoped), }; }),) {}Self-hosted isolated namespace
A Worker that declares Counter in its own Deps and provides
CounterLive hosts its own isolated namespace — instances under
it are separate from any other host’s. Use Counter.from(Self)
inside the host to be explicit about which script’s namespace
you’re binding to.
export class WorkerC extends Cloudflare.Worker<WorkerC, {}, Counter>()( "WorkerC", { main: import.meta.filename },) {}
export default WorkerC.make( Effect.gen(function* () { const counters = yield* Counter.from(WorkerC); // explicit self // ... fetch handler ... }).pipe(Effect.provide(CounterLive)),);Yielding the surrounding namespace from inside a DO
Section titled “Yielding the surrounding namespace from inside a DO”Lets a DO instance refer to its own namespace — e.g. to fan a call
out to sibling instances. Mirrors yield* DurableObjectNamespace
on the regular DurableObjectNamespace.
Effect.gen(function* () { const self = yield* Cloudflare.RpcDurableObjectNamespace; const peer = yield* self.getByName("peer-1"); yield* peer.setTitle({ title: "Sibling call" });}).pipe(Effect.scoped);