1import type { ConfigError } from "effect/Config";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Fiber from "effect/Fiber";
6import * as Layer from "effect/Layer";
7import * as Scope from "effect/Scope";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import type { HttpServerResponse } from "effect/http/HttpServerResponse";
10import type { HttpServerError } from "effect/http/HttpServerError";
11import type { Rpc, RpcGroup } from "effect/rpc";
12import * as RpcMiddleware from "effect/rpc/RpcMiddleware";
13import * as RpcSerialization from "effect/rpc/RpcSerialization";
14import * as RpcServer from "effect/rpc/RpcServer";
15import * as RpcClient from "effect/rpc/RpcClient";
16import * as RpcClientError from "effect/rpc/RpcClientError";
17import type { Dependencies } from "../../Dependencies.ts";
18import type { HttpEffect } from "../../Http.ts";
19import type { Input } from "../../Input.ts";
20import { RuntimeContext } from "../../RuntimeContext.ts";
21import { effectClass, taggedFunction } from "../../Util/effect.ts";
22import {
23 DurableObject,
24 type DurableObjectGetDurableObjectOptions,
25 type DurableObjectLike,
26 type DurableObjectProps,
27 type DurableObject as DurableObjectType,
28 type DurableObjectServices,
29} from "./DurableObject.ts";
30import { DurableObjectState } from "./DurableObjectState.ts";
31import { bindEffectRpc } from "./Rpc.ts";
32import * as RpcWebSocket from "./RpcWebSocket.ts";
33import type { Worker as WorkerService } from "./Worker.ts";
34
35export interface RpcDurableObjectProps<Rpcs extends Rpc.Any> {
36 /** The RPC schema shared by the Durable Object and its clients. */
37 readonly schema: RpcGroup.RpcGroup<Rpcs>;
38}
39
40type HandlerImplementation<
41 Rpcs extends Rpc.Any,
42 Provided,
43 InnerR,
44 InitReq,
45> = Effect.Effect<
46 Effect.Effect<
47 Layer.Layer<Rpc.ToHandler<Rpcs> | Provided, never, InnerR | RuntimeContext>,
48 never,
49 DurableObjectServices | RuntimeContext
50 >,
51 ConfigError,
52 InitReq
53>;
54
55type HandlerRequirements<Rpcs extends Rpc.Any, Provided, InnerR, InitReq> =
56 | WorkerService
57 | Exclude<
58 | InitReq
59 | InnerR
60 | Exclude<Rpc.Middleware<Rpcs>, Provided>
61 | Rpc.ServicesServer<Rpcs>,
62 DurableObjectServices | RuntimeContext
63 >;
64
65/**
66 * The runtime value bound to a typed rpc Durable Object namespace.
67 * Same shape as the underlying {@link DurableObjectType} for
68 * binding metadata (name, namespaceId, kind), but `getByName(id)`
69 * returns a typed Effect `RpcClient` over the rpc server living on
70 * the DO's `fetch` handler.
71 */
72export interface RpcDurableObject<
73 Self,
74 Rpcs extends Rpc.Any = Rpc.Any,
75> extends Omit<
76 DurableObjectType<{ fetch: HttpEffect<DurableObjectState> }>,
77 "getByName" | "get" | "Shape"
78> {
79 /** @internal phantom — keeps `Self` reachable through the inferred type */
80 Self?: Self;
81 /**
82 * Select a named instance and forward an HTTP request or WebSocket upgrade.
83 * Every RPC on the upgraded socket targets that same instance.
84 */
85 readonly fetch: (
86 id: string,
87 request: HttpServerRequest,
88 options?: DurableObjectGetDurableObjectOptions,
89 ) => Effect.Effect<HttpServerResponse, HttpServerError>;
90 readonly getByName: (
91 id: string,
92 options?: DurableObjectGetDurableObjectOptions,
93 ) => Effect.Effect<
94 RpcClient.RpcClient<Rpcs, RpcClientError.RpcClientError>,
95 never,
96 Rpc.MiddlewareClient<Rpcs>
97 >;
98}
99
100// Context tag carrying the surrounding `RpcDurableObject`
101// inside an rpc DO impl. Yield it from within a DO handler to refer
102// back to the surrounding namespace (e.g. to fan a call out to
103// sibling instances). Documented as part of the main
104// `RpcDurableObject` JSDoc below.
105export class RpcDurableObjectScope extends Context.Service<
106 RpcDurableObjectScope,
107 RpcDurableObject<unknown>
108>()("Cloudflare.RpcDurableObject") {}
109
110export interface RpcDurableObjectClass extends Effect.Effect<
111 RpcDurableObject<unknown>,
112 never,
113 RpcDurableObjectScope
114> {
115 /**
116 * Class-based forms: `class Counter extends RpcDurableObject<Counter>()(...)`.
117 *
118 * Modular (no impl):
119 * ```ts
120 * class Counter extends RpcDurableObject<Counter>()(
121 * "Counter",
122 * { schema: CounterRpcs },
123 * ) {}
124 * export const CounterLive = Counter.make(/* impl *\/);
125 * ```
126 * Inline impl:
127 * ```ts
128 * class Counter extends RpcDurableObject<Counter>()(
129 * "Counter",
130 * { schema: CounterRpcs },
131 * Effect.gen(function* () { ... }),
132 * ) {}
133 * ```
134 */
135 <Self>(): {
136 /** Modular form: separate `static make(impl)` + `static from(scriptName | Worker)`. */
137 <Rpcs extends Rpc.Any>(
138 name: string,
139 props: RpcDurableObjectProps<Rpcs>,
140 ): Effect.Effect<
141 RpcDurableObject<Self, Rpcs>,
142 never,
143 WorkerService | Self
144 > & {
145 new (_: never): {};
146 from(
147 scriptName: Input<string>,
148 ): Effect.Effect<RpcDurableObject<Self, Rpcs>, never, WorkerService>;
149 from<Req = never>(
150 worker:
151 | Dependencies<Self>
152 | Effect.Effect<Dependencies<Self>, ConfigError, Req>,
153 ): Effect.Effect<
154 RpcDurableObject<Self, Rpcs>,
155 never,
156 WorkerService | Req
157 >;
158 make<Provided = never, InnerR = never, InitReq = never>(
159 impl: HandlerImplementation<Rpcs, Provided, InnerR, InitReq>,
160 ): Layer.Layer<
161 Self,
162 never,
163 HandlerRequirements<Rpcs, Provided, InnerR, InitReq>
164 >;
165 make<InnerR = never, InitReq = never>(
166 impl: Effect.Effect<
167 Effect.Effect<
168 Effect.Effect<HttpEffect<InnerR>, never, InnerR | RuntimeContext>,
169 never,
170 DurableObjectServices | RuntimeContext
171 >,
172 ConfigError,
173 InitReq
174 >,
175 ): Layer.Layer<
176 Self,
177 never,
178 WorkerService | Exclude<InitReq | InnerR, DurableObjectServices>
179 >;
180 };
181 /** Inline handler Layer; the runtime supplies the RPC transports. */
182 <Rpcs extends Rpc.Any, Provided = never, InnerR = never, InitReq = never>(
183 name: string,
184 props: RpcDurableObjectProps<Rpcs>,
185 impl: HandlerImplementation<Rpcs, Provided, InnerR, InitReq>,
186 ): Effect.Effect<
187 RpcDurableObject<Self, Rpcs>,
188 never,
189 HandlerRequirements<Rpcs, Provided, InnerR, InitReq>
190 > & { new (_: never): {} };
191 /** Inline-impl form. */
192 <Rpcs extends Rpc.Any, InnerR = never, InitReq = never>(
193 name: string,
194 props: RpcDurableObjectProps<Rpcs>,
195 impl: Effect.Effect<
196 Effect.Effect<
197 Effect.Effect<HttpEffect<InnerR>, never, InnerR | RuntimeContext>,
198 never,
199 DurableObjectServices | RuntimeContext
200 >,
201 ConfigError,
202 InitReq
203 >,
204 ): Effect.Effect<
205 RpcDurableObject<Self, Rpcs>,
206 never,
207 WorkerService | Exclude<InitReq | InnerR, DurableObjectServices>
208 > & {
209 new (_: never): {};
210 };
211 };
212 /** Descriptor-only form, for `worker.bind` declarations */
213 <Rpcs extends Rpc.Any>(
214 name: string,
215 props: {
216 readonly schema: RpcGroup.RpcGroup<Rpcs>;
217 } & Partial<DurableObjectProps>,
218 ): DurableObjectLike<{ fetch: HttpEffect<DurableObjectState> }>;
219 /** Bare handler-Layer form. */
220 <Rpcs extends Rpc.Any, Provided = never, InnerR = never, InitReq = never>(
221 name: string,
222 props: RpcDurableObjectProps<Rpcs>,
223 impl: HandlerImplementation<Rpcs, Provided, InnerR, InitReq>,
224 ): Effect.Effect<
225 RpcDurableObject<unknown, Rpcs>,
226 never,
227 HandlerRequirements<Rpcs, Provided, InnerR, InitReq>
228 >;
229 /** Bare form: `(name, { schema }, impl)` */
230 <Rpcs extends Rpc.Any, InnerR = never, InitReq = never>(
231 name: string,
232 props: RpcDurableObjectProps<Rpcs>,
233 impl: Effect.Effect<
234 Effect.Effect<
235 Effect.Effect<HttpEffect<InnerR>, never, InnerR>,
236 never,
237 DurableObjectServices
238 >,
239 ConfigError,
240 InitReq
241 >,
242 ): Effect.Effect<
243 RpcDurableObject<unknown, Rpcs>,
244 never,
245 WorkerService | Exclude<InitReq | InnerR, DurableObjectServices>
246 >;
247}
248
249/**
250 * `RpcDurableObject` is sugar over {@link DurableObject}
251 * for Durable Objects whose surface is a typed Effect `RpcGroup`. The
252 * inner Effect returns the group's handler Layer, automatically enabling
253 * HTTP RPC with NDJSON and hibernating WebSocket RPC with JSON. Incoming
254 * requests select the transport. Consumers see `namespace.getByName(id)`
255 * as a typed HTTP `RpcClient`.
256 * Existing implementations returning `RpcServer.toHttpEffect(group)` remain
257 * supported for HTTP.
258 *
259 * Use this over alchemy's built-in DO method bridge whenever values
260 * crossing the DO boundary contain `Schema.Class` instances. The
261 * built-in bridge `JSON.stringify`s every method return value, which
262 * strips class identity (e.g. an `effect/ai` `Response.Usage` instance
263 * becomes a plain struct on the consumer side). With
264 * `RpcDurableObject`, both ends go through the same
265 * `RpcSerialization` codec, so `Schema.decode` reconstructs class
266 * instances correctly.
267 *
268 *
269 * ### Defining the rpc group
270 * **Example:** DO-scoped rpc schemas
271 * The DO instance *is* the session, so the group payloads typically
272 * don't include any per-session identifier — only the per-call inputs.
273 * ```typescript
274 * import * as Schema from "effect/Schema";
275 * import { Rpc, RpcGroup } from "effect/rpc";
276 *
277 * const setTitle = Rpc.make("setTitle", {
278 * success: Schema.Void,
279 * payload: { title: Schema.String },
280 * });
281 *
282 * const getTitle = Rpc.make("getTitle", {
283 * success: Schema.String,
284 * payload: {},
285 * });
286 *
287 * export class CounterRpcs extends RpcGroup.make(setTitle, getTitle) {}
288 * ```
289 *
290 * ### Implementing the Durable Object
291 * **Example:** Class form (recommended)
292 * Mirrors `Cloudflare.DurableObject<Self>()(...)` — same
293 * outer/inner Effect pattern. The outer Effect resolves shared deps;
294 * the per-instance inner Effect returns the RPC handler Layer.
295 * ```typescript
296 * import * as Cloudflare from "alchemy/Cloudflare";
297 * import * as Effect from "effect/Effect";
298 * import { CounterRpcs } from "./rpcs.ts";
299 *
300 * export default class Counter extends Cloudflare.RpcDurableObject<Counter>()(
301 * "Counter",
302 * { schema: CounterRpcs },
303 * Effect.gen(function* () {
304 * // outer init: shared deps + the instance state reference
305 * const state = yield* Cloudflare.DurableObjectState;
306 * return Effect.gen(function* () {
307 * // inner (runtime): state.storage is RuntimeContext-colored, so
308 * // the handler closures that call it live here
309 * return CounterRpcs.toLayer({
310 * setTitle: ({ title }) => state.storage.put("title", title),
311 * getTitle: () =>
312 * Effect.map(state.storage.get<string>("title"), (t) => t ?? ""),
313 * });
314 * });
315 * }),
316 * ) {}
317 * ```
318 *
319 * ### Calling the DO from a Worker
320 * **Example:** Typed rpc client at the call site
321 * `yield* Counter` resolves to a value whose `getByName(id)` returns
322 * an `Effect<RpcClient<CounterRpcs>>`. Each rpc method is a typed
323 * Effect/Stream factory — no `RpcClient.make` setup needed. Yield
324 * the client inside a per-request `Effect.scoped` handler so it's
325 * freed with the request.
326 * ```typescript
327 * import Counter from "./counter.ts";
328 *
329 * Effect.gen(function* () {
330 * const counters = yield* Counter;
331 * const client = yield* counters.getByName("global");
332 * yield* client.setTitle({ title: "Hello" });
333 * const title = yield* client.getTitle({});
334 * return title;
335 * }).pipe(Effect.scoped);
336 * ```
337 *
338 * ### WebSocket RPC
339 * **Example:** Forward a browser connection from the Worker
340 * Handler-Layer implementations accept WebSocket upgrades automatically;
341 * ordinary HTTP RPC remains available on the same object.
342 * ```typescript
343 * import { HttpServerRequest } from "effect/http/HttpServerRequest";
344 * import * as HttpServerResponse from "effect/http/HttpServerResponse";
345 *
346 * const counters = yield* Counter;
347 * return {
348 * fetch: Effect.gen(function* () {
349 * const request = yield* HttpServerRequest;
350 * const path = new URL(request.url, "https://worker").pathname;
351 * const name = /^\/counters\/([a-zA-Z0-9_-]+)$/.exec(path)?.[1];
352 * if (!name) return HttpServerResponse.empty({ status: 404 });
353 * return yield* counters.fetch(name, request);
354 * }),
355 * };
356 * ```
357 * A client connecting to `wss://example.com/counters/alice` targets the
358 * `"alice"` instance. Every RPC on that socket stays on that object; RPC
359 * payloads do not need an object ID. Another name selects another object.
360 * The Worker defines this URL mapping, not Alchemy.
361 * Authenticate and authorize access to the selected name before forwarding.
362 * See the [Effect RPC guide](/cloudflare/apis/effect-rpc#connect-over-a-websocket)
363 * for a browser client whose Layer owns the connection lifetime. Clients use
364 * Effect's `RpcClient.layerProtocolSocket` with JSON serialization. Idle
365 * connections survive hibernation. Restored sockets with unfinished requests
366 * close with code `1012`; platform resets can also cause transport errors.
367 * Requests and streams are never replayed. No application acknowledgment
368 * methods or checkpoints are required.
369 *
370 * ### Modular form: separate the class from its runtime
371 * **Example:** Class declaration with no impl + `static make(impl)`
372 * The inline class form above bundles the runtime into the class
373 * declaration. The two-arg form `(name, { schema })` declares the
374 * class as a pure tagged identifier; provide the runtime separately
375 * via `Class.make(impl)`. Consumer Workers can import the class for
376 * binding (`Counter.from(HostWorker)`) without pulling the runtime
377 * into their bundle.
378 * ```typescript
379 * import * as Cloudflare from "alchemy/Cloudflare";
380 * import * as Effect from "effect/Effect";
381 * import { CounterRpcs } from "./rpcs.ts";
382 *
383 * export class Counter extends Cloudflare.RpcDurableObject<Counter>()(
384 * "Counter",
385 * { schema: CounterRpcs },
386 * ) {}
387 *
388 * // Only the host script imports this default export.
389 * export default Counter.make(
390 * Effect.gen(function* () {
391 * const state = yield* Cloudflare.DurableObjectState;
392 * return Effect.gen(function* () {
393 * return CounterRpcs.toLayer({
394 * setTitle: ({ title }) => state.storage.put("title", title),
395 * getTitle: () =>
396 * Effect.map(state.storage.get<string>("title"), (t) => t ?? ""),
397 * });
398 * });
399 * }),
400 * );
401 * ```
402 *
403 * ### Cross-script binding via `Counter.from(Worker)`
404 * **Example:** Hosting on WorkerA, binding from WorkerB
405 * The host Worker declares `Counter` in its `Deps` (third type
406 * arg of `Worker<Self, Bindings, Deps>` or second of
407 * `RpcWorker<Self, Deps>`) and provides `CounterLive`. Any other
408 * Worker uses `Counter.from(HostWorker)` to bind to the same DO
409 * instances — writes through `HostWorker.getByName(name)` are
410 * visible from `Counter.from(HostWorker).getByName(name)`.
411 * ```typescript
412 * // host worker (declares + provides Counter)
413 * import CounterLive, { Counter } from "./counter.ts";
414 *
415 * export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()(
416 * "WorkerA",
417 * { main: import.meta.url },
418 * ) {}
419 *
420 * export default WorkerA.make(
421 * Effect.gen(function* () {
422 * const counters = yield* Counter; // local host binding
423 * // ... fetch handler ...
424 * }).pipe(Effect.provide(CounterLive)),
425 * );
426 *
427 * // consumer worker (binds via .from)
428 * export default class WorkerB extends Cloudflare.Worker<WorkerB>()(
429 * "WorkerB",
430 * { main: import.meta.url },
431 * Effect.gen(function* () {
432 * const counters = yield* Counter.from(WorkerA);
433 * return {
434 * fetch: Effect.gen(function* () {
435 * const client = yield* counters.getByName("shared");
436 * yield* client.setTitle({ title: "via WorkerB" });
437 * return HttpServerResponse.text("ok");
438 * }).pipe(Effect.scoped),
439 * };
440 * }),
441 * ) {}
442 * ```
443 *
444 * **Example:** Self-hosted isolated namespace
445 * A Worker that declares `Counter` in its own `Deps` and provides
446 * `CounterLive` hosts its own isolated namespace — instances under
447 * it are separate from any other host's. Use `Counter.from(Self)`
448 * inside the host to be explicit about which script's namespace
449 * you're binding to.
450 * ```typescript
451 * export class WorkerC extends Cloudflare.Worker<WorkerC, {}, Counter>()(
452 * "WorkerC",
453 * { main: import.meta.url },
454 * ) {}
455 *
456 * export default WorkerC.make(
457 * Effect.gen(function* () {
458 * const counters = yield* Counter.from(WorkerC); // explicit self
459 * // ... fetch handler ...
460 * }).pipe(Effect.provide(CounterLive)),
461 * );
462 * ```
463 *
464 * ### Yielding the surrounding namespace from inside a DO
465 * **Example:** `yield* RpcDurableObject` inside the DO impl
466 * Lets a DO instance refer to its own namespace — e.g. to fan a call
467 * out to sibling instances. Mirrors `yield* DurableObject`
468 * on the regular `DurableObject`.
469 * ```typescript
470 * Effect.gen(function* () {
471 * const self = yield* Cloudflare.RpcDurableObject;
472 * const peer = yield* self.getByName("peer-1");
473 * yield* peer.setTitle({ title: "Sibling call" });
474 * }).pipe(Effect.scoped);
475 * ```
476 *
477 * @resource
478 * @product Workers
479 * @category Workers & Compute
480 */
481export const RpcDurableObject: RpcDurableObjectClass = taggedFunction(
482 RpcDurableObjectScope,
483 (...args: any[]) => {
484 // Class-form: zero args returns the inner builder. Inner-arg arity
485 // distinguishes modular (`(name, { schema })`, no impl — `static
486 // from`/`static make` provide the runtime) from inline-impl
487 // (`(name, { schema }, impl)`).
488 if (args.length === 0) {
489 return (...inner: any[]) => {
490 if (inner.length === 2) {
491 const [name, props] = inner as [string, RpcDurableObjectProps<any>];
492 return buildModular(name, props);
493 }
494 const [name, props, impl] = inner as [
495 string,
496 RpcDurableObjectProps<any>,
497 Effect.Effect<Effect.Effect<any>>,
498 ];
499 return build(name, props, impl);
500 };
501 }
502 // Descriptor-only form: `(name, { schema })` — no impl.
503 if (args.length === 2) {
504 const [name, props] = args as [
505 string,
506 {
507 readonly schema: RpcGroup.RpcGroup<any>;
508 } & Partial<DurableObjectProps>,
509 ];
510 return {
511 kind: "Cloudflare.DurableObject" as const,
512 name,
513 className: props?.className,
514 } satisfies DurableObjectLike<any>;
515 }
516 // Bare form: `(name, { schema }, impl)`.
517 const [name, props, impl] = args as [
518 string,
519 RpcDurableObjectProps<any>,
520 Effect.Effect<Effect.Effect<any>>,
521 ];
522 return build(name, props, impl);
523 },
524) as any;
525
526// Wrap a raw `DurableObject` so its `getByName` returns a typed
527// Effect `RpcClient` (via `bindEffectRpc`) instead of the built-in
528// method-bridge stub. Used in every branch that produces a yieldable
529// `RpcDurableObject` value.
530const rpcWrap = (
531 rawNs: DurableObjectType<any>,
532 schema: RpcGroup.RpcGroup<any>,
533): RpcDurableObject<any> => {
534 const rpcView = bindEffectRpc(rawNs as any, schema);
535 return Object.assign({}, rawNs, {
536 getByName: rpcView.getByName,
537 fetch: (
538 id: string,
539 request: HttpServerRequest,
540 options?: DurableObjectGetDurableObjectOptions,
541 ) => rawNs.getByName(id, options).fetch(request),
542 }) as unknown as RpcDurableObject<any>;
543};
544
545const wrapImpl = (
546 impl: Effect.Effect<Effect.Effect<any>>,
547 props: RpcDurableObjectProps<any>,
548) =>
549 impl.pipe(
550 Effect.map((inner) =>
551 inner.pipe(
552 Effect.flatMap((value) => {
553 if (Layer.isLayer(value)) {
554 return makeHandlers(props, value as Layer.Layer<any, never, any>);
555 }
556 return Effect.succeed({ fetch: value });
557 }),
558 ),
559 ),
560 ) as Effect.Effect<Effect.Effect<any>>;
561
562class RpcRequestLifetime extends RpcMiddleware.Service<RpcRequestLifetime>()(
563 "Cloudflare.RpcDurableObject.RequestLifetime",
564) {}
565
566const makeHandlers = Effect.fn(function* (
567 props: RpcDurableObjectProps<any>,
568 handlers: Layer.Layer<any, never, any>,
569) {
570 // The protocol outlives the constructor's temporary layer scope.
571 return yield* Effect.acquireUseRelease(
572 Scope.make(),
573 (instanceScope) =>
574 Effect.gen(function* () {
575 const memoMap = Layer.makeMemoMapUnsafe();
576 const context = yield* Layer.buildWithMemoMap(
577 handlers,
578 memoMap,
579 instanceScope,
580 );
581 const services = Layer.succeedContext(context);
582 const http = Effect.gen(function* () {
583 const handler = yield* RpcServer.toHttpEffect(props.schema).pipe(
584 Effect.provide(
585 Layer.mergeAll(services, RpcSerialization.layerNdjson),
586 ),
587 );
588 return yield* handler;
589 });
590 const state = yield* DurableObjectState;
591 const runtime = yield* RuntimeContext;
592 const lifetime = Layer.succeed(RpcRequestLifetime, (effect) =>
593 Effect.withFiber((fiber) =>
594 // RpcServer sends Exit before finalization and drops sends after disconnect.
595 state
596 .waitUntil(Fiber.await(fiber))
597 .pipe(
598 Effect.provideService(RuntimeContext, runtime),
599 Effect.andThen(effect),
600 ),
601 ),
602 );
603 const transport = yield* RpcWebSocket.make.pipe(
604 Effect.provide(RpcSerialization.layerJson),
605 );
606 yield* Layer.buildWithMemoMap(
607 RpcServer.layer(props.schema.middleware(RpcRequestLifetime)).pipe(
608 Layer.provide(
609 Layer.mergeAll(
610 services,
611 lifetime,
612 Layer.succeed(RpcServer.Protocol, transport.protocol),
613 ),
614 ),
615 ),
616 memoMap,
617 instanceScope,
618 );
619 return {
620 webSocketMessage: transport.webSocketMessage,
621 webSocketClose: transport.webSocketClose,
622 webSocketError: transport.webSocketError,
623 fetch: Effect.gen(function* () {
624 const request = yield* HttpServerRequest;
625 return yield* request.headers.upgrade?.toLowerCase() === "websocket"
626 ? transport.fetch
627 : http;
628 }),
629 };
630 }),
631 (instanceScope, exit) =>
632 Exit.isFailure(exit) ? Scope.close(instanceScope, exit) : Effect.void,
633 );
634});
635
636const build = (
637 name: string,
638 props: RpcDurableObjectProps<any>,
639 impl: Effect.Effect<Effect.Effect<any>>,
640) => {
641 // Inline-impl class form: delegate to `DurableObject`'s
642 // inline class form, then expose the rpc-wrapped view at yield
643 // time. No `static from`/`static make` because the impl is provided
644 // eagerly here (consumers wanting cross-script binding use the
645 // modular form below).
646 const underlying = (DurableObject as any)()(name, wrapImpl(impl, props));
647 // `underlying` is itself an Effect now, no `.asEffect()` hop required.
648 const underlyingEff = underlying as Effect.Effect<
649 DurableObjectType<any>,
650 never,
651 any
652 >;
653 const rpcBound = underlyingEff.pipe(
654 Effect.map((rawNs) => rpcWrap(rawNs, props.schema)),
655 ) as unknown as Effect.Effect<RpcDurableObject<any>>;
656 return effectClass(rpcBound);
657};
658
659const buildModular = (name: string, props: RpcDurableObjectProps<any>) => {
660 const { schema } = props;
661 // Delegate to `DurableObject<Self>()(name)` (no-impl class
662 // form) so we inherit its Self-tag plumbing for free:
663 // - yielding the class resolves to the live namespace via the tag
664 // (populated by `static make(impl)`'s Layer)
665 // - `static from(scriptName | Worker)` registers a foreign-script
666 // binding on the surrounding worker and yields a fresh handle
667 // We just rpc-wrap each output so consumers see a typed `getByName`.
668 const Underlying: any = (DurableObject as any)()(name);
669 // `Underlying` is itself an Effect now, no `.asEffect()` hop required.
670 const underlyingEff = Underlying as Effect.Effect<
671 DurableObjectType<any>,
672 never,
673 any
674 >;
675
676 return class extends effectClass(
677 underlyingEff.pipe(
678 Effect.map((rawNs) => rpcWrap(rawNs, schema)),
679 ) as unknown as Effect.Effect<RpcDurableObject<any>>,
680 ) {
681 static make = (impl: Effect.Effect<Effect.Effect<any>>) =>
682 Underlying.make(wrapImpl(impl, props));
683
684 static from = (
685 worker: string | object | Effect.Effect<any, any, any>,
686 ): Effect.Effect<RpcDurableObject<any>, any, any> =>
687 Underlying.from(worker).pipe(
688 Effect.map((rawNs: DurableObjectType<any>) => rpcWrap(rawNs, schema)),
689 );
690 };
691};