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
a typed RpcClient<CounterRpcs>. Each rpc method is a typed
Effect/Stream factory — no RpcClient.make setup needed.
import Counter from "./counter.ts";
Effect.gen(function* () { const counters = yield* Counter; yield* counters.getByName("global").setTitle({ title: "Hello" }); const title = yield* counters.getByName("global").getTitle({}); return title;});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; yield* self.getByName("peer-1").setTitle({ title: "Sibling call" });});