1import type * as cf from "@cloudflare/workers-types";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import type { Scope } from "effect/Scope";
6import type { HttpServerError } from "effect/http/HttpServerError";
7import * as HttpServerRequest from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import type { Dependencies } from "../../Dependencies.ts";
10import type { HttpEffect } from "../../Http.ts";
11import type { Input } from "../../Input.ts";
12import * as Output from "../../Output.ts";
13import { ALCHEMY_PHASE } from "../../Phase.ts";
14import type { MainRpc, PlatformServices } from "../../Platform.ts";
15import type { RuntimeContext } from "../../RuntimeContext.ts";
16import { effectClass, taggedFunction } from "../../Util/effect.ts";
17import { asEffect } from "../../Util/types.ts";
18import type { Container } from "../Containers/Container.ts";
19import {
20 DurableObjectState,
21 fromDurableObjectState,
22} from "./DurableObjectState.ts";
23import { makeRpcStub, type RpcErrorClass } from "./Rpc.ts";
24import { type WebSocket } from "./WebSocket.ts";
25import {
26 isWorker,
27 Worker,
28 WorkerEnvironment,
29 type WorkerServices,
30} from "./Worker.ts";
31
32export interface DurableObjectExport {
33 readonly kind: "durableObject";
34 readonly constructor: Effect.Effect<
35 Effect.Effect<DurableObjectShape, never, RuntimeContext>,
36 never,
37 DurableObjectState
38 >;
39 readonly services: Context.Context<never>;
40}
41
42export const isDurableObjectExport = (
43 value: unknown,
44): value is DurableObjectExport =>
45 typeof value === "object" && (value as any)?.kind === "durableObject";
46
47export type DurableObjectId = cf.DurableObjectId;
48export type DurableObjectJurisdiction = cf.DurableObjectJurisdiction;
49export type DurableObjectGetDurableObjectOptions =
50 cf.DurableObjectNamespaceGetDurableObjectOptions;
51/**
52 * The regions a Durable Object can be *hinted* toward at creation —
53 * `"wnam"`, `"enam"`, `"sam"`, `"weur"`, `"eeur"`, `"apac"`, `"oc"`, … See
54 * the "Placing a Durable Object in a Region" section on {@link DurableObject}.
55 */
56export type DurableObjectLocationHint = cf.DurableObjectLocationHint;
57
58export type AlarmInvocationInfo = cf.AlarmInvocationInfo;
59
60type TypeId = "Cloudflare.DurableObject";
61const TypeId = "Cloudflare.DurableObject";
62
63export const isDurableObjectLike = (
64 value: unknown,
65): value is DurableObjectLike =>
66 typeof value === "object" && (value as any)?.kind === TypeId;
67
68export interface DurableObjectLike<Shape = any> {
69 kind: TypeId;
70 name: string;
71 /** @internal phantom */
72 className?: string;
73 /** @internal phantom */
74 scriptName?: Input<string>;
75 /** @internal phantom */
76 transferredFrom?: DurableObjectTransferSource | DurableObjectTransferSource[];
77 /** @internal phantom */
78 Shape?: Shape;
79}
80
81export interface DurableObject<
82 Shape = unknown,
83> extends DurableObjectLike<Shape> {
84 Type: TypeId;
85 name: string;
86 namespaceId: Output.Output<string>;
87 getByName: (
88 name: string,
89 options?: DurableObjectGetDurableObjectOptions,
90 ) => DurableObjectStub<Shape>;
91 newUniqueId: () => DurableObjectId;
92 idFromName: (name: string) => DurableObjectId;
93 idFromString: (id: string) => DurableObjectId;
94 get: (
95 id: DurableObjectId,
96 options?: DurableObjectGetDurableObjectOptions,
97 ) => DurableObjectStub<Shape>;
98 jurisdiction: (
99 jurisdiction: DurableObjectJurisdiction,
100 ) => DurableObject<Shape>;
101}
102
103export interface DurableObjectShape {
104 fetch?: HttpEffect<DurableObjectState | RuntimeContext>;
105 alarm?: (
106 alarmInfo?: AlarmInvocationInfo,
107 ) => Effect.Effect<void, never, RuntimeContext>;
108 webSocketMessage?: (
109 socket: WebSocket,
110 message: string | ArrayBuffer,
111 ) => Effect.Effect<void>;
112 webSocketClose?: (
113 socket: WebSocket,
114 code: number,
115 reason: string,
116 wasClean: boolean,
117 ) => Effect.Effect<void>;
118 /**
119 * Called when a hibernatable WebSocket errors. The runtime closes the
120 * socket after this handler; use it to drop the peer's session state.
121 */
122 webSocketError?: (socket: WebSocket, error: unknown) => Effect.Effect<void>;
123}
124
125export type DurableObjectServices =
126 | DurableObject
127 | DurableObjectState
128 | DurableObjectScope
129 | WorkerServices
130 | WorkerEnvironment
131 | PlatformServices;
132
133/**
134 * A reference to the Worker that previously hosted a Durable Object class,
135 * accepted by {@link DurableObjectProps.transferredFrom}:
136 *
137 * - the Worker's **logical id** (same stack + stage) or **physical script
138 * name** (cross-stack) as a string
139 * - the Worker **resource** (`const b = yield* Cloudflare.Worker(...)`)
140 * - the Worker **class** (`transferredFrom: WorkerA`)
141 * - a **thunk** of any of the above (`() => WorkerA`) — use this for
142 * forward references and import cycles; it is evaluated lazily at plan
143 * time, after all modules have loaded
144 * - a string `Output` (e.g. a cross-stack `stackRef`'s worker name)
145 *
146 * Every form is normalized at plan time to a plain identifier: a Worker
147 * reference contributes its logical id, and a same-stack `workerName`
148 * Output contributes its source resource's logical id rather than the
149 * Output itself. Keeping the former host's *value* out of the binding data
150 * matters — consuming its Output would add a dependency edge from the new
151 * host onto the former host, inverting the deploy order the transfer
152 * requires (the new host must upload first).
153 */
154export type DurableObjectTransferSource =
155 | Input<string>
156 | Worker
157 | Effect.Effect<Worker, any, any>
158 | (() => DurableObjectTransferSource);
159
160const resolveTransferSourceRef = (
161 source: DurableObjectTransferSource,
162 depth = 0,
163): Input<string> => {
164 if (typeof source === "string") {
165 return source;
166 }
167 // A Worker resource — contribute its logical id.
168 if (isWorker(source)) {
169 return source.LogicalId;
170 }
171 // A Worker platform class — carries its logical id statically. (On an
172 // Output proxy this property access yields another Output, never a
173 // string, so the check cannot misfire there.)
174 const logicalId = (source as { LogicalId?: unknown }).LogicalId;
175 if (typeof logicalId === "string") {
176 return logicalId;
177 }
178 // A same-stack Output on a Worker (e.g. `b.workerName`): take the
179 // identity from its provenance, not the value.
180 if (Output.isPropExpr(source)) {
181 const expr = (source as Output.PropExpr<any, any>).expr;
182 if (Output.isResourceExpr(expr) && isWorker(expr.src)) {
183 return expr.src.LogicalId;
184 }
185 }
186 if (Output.isResourceExpr(source) && isWorker(source.src)) {
187 return source.src.LogicalId;
188 }
189 if (Output.isOutput(source)) {
190 // No local resource to take identity from (e.g. a cross-stack
191 // `stackRef`) — the engine resolves the string at deploy time.
192 return source as Input<string>;
193 }
194 if (typeof source === "function" && depth < 8) {
195 return resolveTransferSourceRef(
196 (source as () => DurableObjectTransferSource)(),
197 depth + 1,
198 );
199 }
200 throw new Error(
201 "Invalid transferredFrom entry: pass the former host's logical id or script name, its Worker class or resource, a thunk of one of those, or a string Output.",
202 );
203};
204
205/**
206 * Normalize a `transferredFrom` declaration to plain host identifiers at
207 * plan time. See {@link DurableObjectTransferSource} for the accepted forms
208 * and why Worker references reduce to logical ids instead of Outputs.
209 *
210 * @internal
211 */
212export const normalizeTransferredFrom = (
213 value:
214 | DurableObjectTransferSource
215 | readonly DurableObjectTransferSource[]
216 | undefined,
217): Input<string>[] | undefined =>
218 value === undefined
219 ? undefined
220 : (Array.isArray(value)
221 ? (value as readonly DurableObjectTransferSource[])
222 : [value as DurableObjectTransferSource]
223 ).map((source) => resolveTransferSourceRef(source));
224
225export interface DurableObjectProps {
226 /**
227 * Name of the exported `DurableObject` class.
228 *
229 * @default name
230 */
231 className?: string;
232 /**
233 * Worker script that hosts the Durable Object class. Omit this when the
234 * namespace is hosted by the Worker that declares the binding.
235 */
236 scriptName?: Input<string> | undefined;
237 /**
238 * The Worker(s) that previously hosted this Durable Object class. When one
239 * of them still holds the namespace, the deploy performs Cloudflare's
240 * data-preserving `transferred_classes` migration, moving the namespace —
241 * including all stored objects — from that script to this Worker.
242 *
243 * Each entry names a former host — see
244 * {@link DurableObjectTransferSource} for the accepted forms: a string
245 * (Worker logical id in this stack + stage, or physical script name for
246 * cross-stack moves), the Worker class or resource itself, or a thunk
247 * (`() => WorkerA`) for forward references. Moving a class is always
248 * declared this way — there is no inference. A list is a host *history*:
249 * as the class moves again, append the previous host so stages that lag
250 * behind (or skipped an intermediate release) still transfer from
251 * wherever their namespace currently lives.
252 *
253 * When no listed host holds the namespace — a fresh stage, or every
254 * transfer already completed — the property is inert and the class is
255 * created fresh (or left as-is), so it is safe to leave in place
256 * indefinitely.
257 */
258 transferredFrom?:
259 | DurableObjectTransferSource
260 | DurableObjectTransferSource[]
261 | undefined;
262 /**
263 * Tagged-error classes this Durable Object's RPC methods can fail with.
264 *
265 * Effect failures crossing the Worker↔DO RPC boundary are serialized to
266 * plain `{ _tag, ...fields }` objects; declaring the classes here lets
267 * the calling side reconstruct real instances (both sides import this
268 * same class declaration, so the schema is shared by construction) —
269 * `Effect.catchTag`, `instanceof`, and schema encoders (e.g. HttpApi
270 * error responses) then all see the class the DO actually failed with.
271 *
272 * ```typescript
273 * export class Repo extends Cloudflare.DurableObject<Repo, RepoShape>()(
274 * "Repo",
275 * { errors: [RepoNotFound, StoreError] },
276 * ) {}
277 * ```
278 */
279 errors?: ReadonlyArray<RpcErrorClass> | undefined;
280 // environment?: string | undefined;
281 // sqlite?: boolean | undefined;
282 // namespaceId?: string | undefined;
283}
284
285export interface DurableObjectClass extends Effect.Effect<
286 DurableObject,
287 never,
288 DurableObject
289> {
290 <Self, Shape>(): {
291 <Name extends string>(
292 name: Name,
293 props?: Pick<DurableObjectProps, "transferredFrom" | "errors">,
294 ): Effect.Effect<DurableObject<Self>, never, Worker | Self> & {
295 new (_: never): Shape & {
296 /** @internal */
297 "~alchemy/name": Name;
298 };
299 from(
300 scriptName: Input<string>,
301 ): Effect.Effect<DurableObject<Self>, never, Worker>;
302 from<Req = never>(
303 worker:
304 | Dependencies<Self>
305 | Effect.Effect<Dependencies<Self>, never, Req>,
306 ): Effect.Effect<DurableObject<Self>, never, Worker | Req>;
307 make<Req = never>(
308 impl: Effect.Effect<
309 Effect.Effect<
310 Shape,
311 never,
312 RuntimeContext | DurableObjectState | Scope
313 >,
314 never,
315 Req
316 >,
317 // `Exclude` (rather than `DurableObjectServices | Req` inference)
318 // so ambient DO services resolved in the outer init effect never
319 // leak into the host Worker's requirements — mirrors Worker.make's
320 // `Exclude<InitReq, Self | WorkerServices>`.
321 ): Layer.Layer<Self, never, Worker | Exclude<Req, DurableObjectServices>>;
322 };
323 };
324 <Self>(): {
325 <
326 Shape extends MainRpc<DurableObjectState>,
327 Req extends DurableObjectServices | Container.Application<any> = never,
328 >(
329 name: string,
330 impl: Effect.Effect<
331 Effect.Effect<
332 Shape,
333 never,
334 RuntimeContext | DurableObjectState | Scope
335 >,
336 never,
337 Req
338 >,
339 ): Effect.Effect<
340 DurableObject<Self>,
341 never,
342 Worker | Extract<Req, Container.Application<any>>
343 > & {
344 new (_: never): Shape;
345 };
346 };
347 <Shape>(name: string, props?: DurableObjectProps): DurableObjectLike<Shape>;
348 <Shape, InitReq = never>(
349 name: string,
350 impl: Effect.Effect<Shape, never, DurableObjectServices | InitReq>,
351 ): Effect.Effect<
352 DurableObject<Shape>,
353 never,
354 Worker | Exclude<InitReq, DurableObjectServices>
355 >;
356}
357
358export class DurableObjectScope extends Context.Service<
359 DurableObjectScope,
360 DurableObject
361>()("Cloudflare.DurableObject") {}
362
363/**
364 * A Cloudflare Durable Object namespace that manages globally unique, stateful
365 * instances with WebSocket hibernation support.
366 *
367 * A Durable Object uses a two-phase pattern with two nested `Effect.gen`
368 * blocks. The outer Effect resolves shared dependencies (other DOs,
369 * containers, etc.) and the instance state *reference*
370 * (`Cloudflare.DurableObjectState`). The inner Effect returns the
371 * object's public methods and WebSocket handlers — and is the only
372 * place the state's `RuntimeContext`-colored methods (`storage.get`,
373 * `storage.put`, …) can actually run.
374 *
375 * ```typescript
376 * Effect.gen(function* () {
377 * // Phase 1: resolve shared dependencies + the instance state ref
378 * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
379 * const state = yield* Cloudflare.DurableObjectState;
380 *
381 * return Effect.gen(function* () {
382 * // Phase 2: per-instance setup and public API. `state`'s methods
383 * // (storage.get/put, …) are RuntimeContext-colored, so the actual
384 * // I/O happens here, in the runtime closure.
385 * return {
386 * save: (data: string) => db.exec("INSERT ..."),
387 * fetch: Effect.gen(function* () { ... }),
388 * webSocketMessage: Effect.fn(function* (ws, msg) { ... }),
389 * };
390 * });
391 * })
392 * ```
393 *
394 * There are two ways to define a Durable Object. See the
395 * [Runtime](/infrastructure-as-effects/runtime) page
396 * for the full explanation.
397 *
398 * - **Inline** — Effect implementation passed directly, single file.
399 * - **Modular** — class and implementation in separate files for tree-shaking.
400 *
401 *
402 * ### Inline Durable Objects
403 * Pass the Effect implementation as the second argument. This is the
404 * simplest approach — everything lives in one file. Convenient when
405 * the DO doesn't need to be referenced by other Workers or DOs that
406 * would pull in its runtime dependencies.
407 *
408 * **Example:** Inline Durable Object
409 * ```typescript
410 * export default class Counter extends Cloudflare.DurableObject<Counter>()(
411 * "Counter",
412 * Effect.gen(function* () {
413 * // init: bind resources + resolve the instance state ref
414 * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
415 * const state = yield* Cloudflare.DurableObjectState;
416 *
417 * return Effect.gen(function* () {
418 * // runtime: state's storage methods are RuntimeContext-colored,
419 * // so the reads/writes live here
420 * const count = (yield* state.storage.get<number>("count")) ?? 0;
421 *
422 * return {
423 * increment: () =>
424 * Effect.gen(function* () {
425 * const next = count + 1;
426 * yield* state.storage.put("count", next);
427 * return next;
428 * }),
429 * get: () => Effect.succeed(count),
430 * };
431 * });
432 * }),
433 * ) {}
434 * ```
435 *
436 * ### Modular Durable Objects
437 * When a Worker and a DO reference each other, or multiple Workers
438 * bind the same DO, define the class separately from its `.make()`
439 * call. The class is a lightweight identifier; `.make()` provides
440 * the runtime implementation as an `export default`. Rolldown treats
441 * `.make()` as pure, so the bundler tree-shakes it and all its
442 * runtime dependencies out of any consumer's bundle.
443 *
444 * The class and `.make()` can live in the same file. This is the
445 * same pattern used by `Worker` and `Container`.
446 *
447 * **Example:** Modular Durable Object (class + .make() in one file)
448 * ```typescript
449 * // src/Counter.ts
450 * export class Counter extends Cloudflare.DurableObject<Counter>()(
451 * "Counter",
452 * ) {}
453 *
454 * export default Counter.make(
455 * Effect.gen(function* () {
456 * // init: bind resources + resolve the instance state ref
457 * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
458 * const state = yield* Cloudflare.DurableObjectState;
459 *
460 * return Effect.gen(function* () {
461 * // runtime: state's storage methods are RuntimeContext-colored,
462 * // so the reads/writes live here
463 * const count = (yield* state.storage.get<number>("count")) ?? 0;
464 *
465 * return {
466 * increment: () =>
467 * Effect.gen(function* () {
468 * const next = count + 1;
469 * yield* state.storage.put("count", next);
470 * yield* db.prepare("INSERT INTO logs (count) VALUES (?)").bind(next).run();
471 * return next;
472 * }),
473 * get: () => Effect.succeed(count),
474 * };
475 * });
476 * }),
477 * );
478 * ```
479 *
480 * **Example:** Binding a modular DO from a Worker
481 * ```typescript
482 * // imports Counter; bundler tree-shakes .make()
483 * import Counter from "./Counter.ts";
484 *
485 * // init
486 * const counters = yield* Counter;
487 *
488 * return {
489 * fetch: Effect.gen(function* () {
490 * const counter = counters.getByName("user-123");
491 * return HttpServerResponse.text(String(yield* counter.get()));
492 * }),
493 * };
494 * ```
495 *
496 * ### Cross-Worker Binding
497 * A Durable Object is _hosted_ by exactly one Worker, but any
498 * number of other Workers can bind to the same DO. This is how
499 * you share state across Workers: one Worker hosts the DO, every
500 * other Worker addresses it by `scriptName` and gets a typed stub.
501 *
502 * To make this type-safe, the **host Worker** must declare the DO
503 * as part of its public contract via the third type argument to
504 * `Cloudflare.Worker<Self, Bindings, Deps>()`. `Deps` is the set
505 * of DO classes (or other Workers) the script exposes for other
506 * scripts to bind to.
507 *
508 * **Example:** Host Worker declares the DO in its contract
509 * ```typescript
510 * // workerA.ts — hosts Counter
511 * import { Counter, CounterLive } from "./object.ts";
512 *
513 * // ^^^^^^^ declared as part of WorkerA's public contract
514 * export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()(
515 * "WorkerA",
516 * { main: import.meta.url },
517 * ) {}
518 *
519 * // WorkerA's Layer also provides the DO's Live implementation.
520 * export default WorkerA.make(
521 * Effect.gen(function* () {
522 * const counter = yield* Counter;
523 * return { fetch: Effect.gen(function* () { ... }) };
524 * }).pipe(Effect.provide(CounterLive)),
525 * );
526 * ```
527 *
528 * **Example:** Consumer Worker binds the DO via `Counter.from(WorkerA)`
529 * ```typescript
530 * // workerB.ts — binds to the same Counter, hosted by WorkerA
531 * import { Counter } from "./object.ts";
532 * import { WorkerA } from "./workerA.ts";
533 *
534 * export default class WorkerB extends Cloudflare.Worker<WorkerB>()(
535 * "WorkerB",
536 * { main: import.meta.url },
537 * Effect.gen(function* () {
538 * // ^^^^^^^^^^^^ scriptName-bound stub of WorkerA's Counter
539 * const counter = yield* Counter.from(WorkerA);
540 * return {
541 * fetch: Effect.gen(function* () {
542 * const value = yield* counter.getByName("shared").get();
543 * return HttpServerResponse.text(String(value));
544 * }),
545 * };
546 * }),
547 * ) {}
548 * ```
549 *
550 * :::tip
551 * The `, Counter` in `Worker<WorkerA, {}, Counter>` is what makes
552 * `Counter.from(WorkerA)` type-check. You can still bind across
553 * scripts without it (the runtime works either way), but consumers
554 * will get a TypeScript error because `WorkerA` doesn't declare
555 * `Counter` as part of its public contract.
556 * :::
557 *
558 * Only the host Worker's Stack provides `CounterLive` — the
559 * consumer Worker just imports the `Counter` class as a typed
560 * identifier. Rolldown tree-shakes `CounterLive` (and its
561 * dependencies) out of WorkerB's bundle.
562 *
563 * ### Using `.from(Self)` Inside the Host
564 * Inside the host Worker, `yield* Counter` and
565 * `yield* Counter.from(Self)` resolve to the same local namespace.
566 * The `.from(Self)` form is preferred — especially in code that
567 * may be extracted into a reusable Layer — because it makes the
568 * scriptName explicit and lets the same Layer shape work whether
569 * the consumer is the host or another script.
570 *
571 * **Example:** `Counter.from(WorkerA)` inside WorkerA itself
572 * ```typescript
573 * // workerA.ts — host uses `.from(Self)` instead of bare `yield* Counter`
574 * export default WorkerA.make(
575 * Effect.gen(function* () {
576 * const counter = yield* Counter.from(WorkerA); // same as `yield* Counter`
577 * return { fetch: Effect.gen(function* () { ... }) };
578 * }).pipe(Effect.provide(CounterLive)),
579 * );
580 * ```
581 *
582 * A Worker can also host its **own isolated** namespace this way.
583 * If a second host Worker declares `Counter` in its contract and
584 * provides `CounterLive`, the DO instances under that script are
585 * separate from the original host's — same class, two namespaces.
586 *
587 * **Example:** Two hosts, two isolated namespaces
588 * ```typescript
589 * // workerC.ts — another host of Counter, isolated from WorkerA
590 * export class WorkerC extends Cloudflare.Worker<WorkerC, {}, Counter>()(
591 * "WorkerC",
592 * { main: import.meta.url },
593 * ) {}
594 *
595 * export default WorkerC.make(
596 * Effect.gen(function* () {
597 * // .from(WorkerC) binds to WorkerC's own Counter namespace —
598 * // writes here are NOT visible from WorkerA's Counter.
599 * const counter = yield* Counter.from(WorkerC);
600 * return { fetch: Effect.gen(function* () { ... }) };
601 * }).pipe(Effect.provide(CounterLive)),
602 * );
603 * ```
604 *
605 * ### RPC Methods
606 * Any function you return from the inner Effect becomes an RPC method
607 * that Workers can call through a stub. Methods must return an `Effect`.
608 * The caller gets a fully typed stub — if your DO returns `increment`
609 * and `get`, the stub exposes `counter.increment()` and `counter.get()`.
610 *
611 * **Example:** Defining RPC methods
612 * ```typescript
613 * return {
614 * increment: () => Effect.succeed(++count),
615 * get: () => Effect.succeed(count),
616 * reset: () => Effect.sync(() => { count = 0; }),
617 * };
618 * ```
619 *
620 * ### Returning Streams from RPC
621 * RPC methods can return an Effect `Stream` and the caller will see
622 * the chunks as they're produced. Combine with `Stream.schedule` to
623 * pace emission, or with `Stream.fromQueue` to bridge an inbound
624 * subscription.
625 *
626 * **Example:** Streaming sequential numbers
627 * ```typescript
628 * import * as Schedule from "effect/Schedule";
629 * import * as Stream from "effect/Stream";
630 *
631 * return {
632 * tick: (n: number) =>
633 * Stream.iterate(0, (i) => i + 1).pipe(
634 * Stream.take(n),
635 * Stream.schedule(Schedule.spaced("100 millis")),
636 * ),
637 * };
638 * ```
639 *
640 * **Example:** Forwarding the stream as a chunked HTTP response
641 * ```typescript
642 * // in a Worker fetch handler
643 * const counter = counters.getByName("tick");
644 * const stream = counter.tick(5).pipe(
645 * Stream.map((i) => `${i}\n`),
646 * Stream.encodeText,
647 * );
648 * return HttpServerResponse.stream(stream, {
649 * headers: { "content-type": "text/plain" },
650 * });
651 * ```
652 *
653 * ### Worker → DO HTTP forwarding
654 * In addition to RPC methods, the typed stub exposes a `fetch`
655 * method that forwards an `HttpServerRequest` straight to the DO.
656 * The DO's own `fetch` Effect produces the response — useful for
657 * WebSocket upgrades and other request-shaped interactions.
658 *
659 * **Example:** Forwarding an HTTP request to a DO
660 * ```typescript
661 * const room = rooms.getByName(roomId);
662 * return yield* room.fetch(request);
663 * ```
664 *
665 * ### Placing a Durable Object in a Region
666 * A Durable Object is created wherever its *first-ever* request
667 * came from, and it stays there for life. That default is right for
668 * an instance whose traffic all comes from the user who created it,
669 * and wrong for one whose name is derived from something other than
670 * geography (a shard index, a hash, a fixed roster) — a Sydney user
671 * routed onto an instance that some Frankfurt request happened to
672 * create first pays a cross-planet round trip on every call.
673 *
674 * Pass a `locationHint` to place the instance near the traffic you
675 * expect instead. It only applies to *creation*: an instance that
676 * already exists is unaffected, so a hint can't move a live DO.
677 *
678 * **Example:** Sharding instances by region
679 * ```typescript
680 * // Both the name and the hint derive from the caller's region, so
681 * // each shard is created in the region whose users address it.
682 * const region = hintFor(request.cf?.continent); // "apac", "weur", …
683 * const shard = shards.getByName(`pool:${region}:${index}`, {
684 * locationHint: region,
685 * });
686 * ```
687 *
688 * :::caution
689 * A hint is a hint: Cloudflare places the instance in the nearest
690 * location it *can*, which may not be the hinted one, and only the
691 * first request to a given name has any say. Derive the name from
692 * the same region you hint with — otherwise two regions race to
693 * create one shared instance and the loser is stuck with it.
694 * :::
695 *
696 * ### Accessing Instance State
697 * Each Durable Object instance has its own transactional key-value
698 * storage via `Cloudflare.DurableObjectState`. Resolve the `state`
699 * *reference* in the outer (init) Effect, but call its methods —
700 * `storage.get`, `storage.put`, … — only from the inner (runtime)
701 * Effect: those methods are `RuntimeContext`-colored, so the type
702 * system only allows them inside the runtime closure.
703 *
704 * **Example:** Reading and writing durable storage
705 * ```typescript
706 * // inner (runtime) Effect — `state` was resolved in the outer Effect
707 * yield* state.storage.put("counter", 42);
708 * const value = yield* state.storage.get("counter");
709 * ```
710 *
711 * ### Background Work & Scopes
712 * Every RPC call and fetch into a Durable Object gets its own Effect
713 * `Scope`. When the method finishes, the bridge closes that scope and
714 * registers the close promise with workerd's `state.waitUntil` — so
715 * finalizers added with `Effect.addFinalizer` inside a method run *after*
716 * the result is returned to the caller, without blocking it, and the
717 * object stays alive until they settle.
718 *
719 * For ad-hoc background work, `state.waitUntil(effect)` forks an Effect
720 * with the caller's full context and keeps the object alive until it
721 * settles.
722 *
723 * Attach cleanup to method scopes, not the constructor (init) closure —
724 * the constructor runs once per in-memory instance under
725 * `blockConcurrencyWhile`, and its scope is not tied to any call.
726 *
727 * **Example:** Responding before finishing the work
728 * ```typescript
729 * return {
730 * record: Effect.fn(function* (entry: string) {
731 * // runs after `record` returns; the DO stays alive until it settles
732 * yield* Effect.addFinalizer(() =>
733 * state.storage.put(`audit:${entry}`, Date.now()).pipe(Effect.ignore),
734 * );
735 * return "accepted" as const;
736 * }),
737 *
738 * refresh: Effect.fn(function* () {
739 * // same idea, explicit form
740 * yield* state.waitUntil(recomputeExpensiveView());
741 * return "scheduled" as const;
742 * }),
743 * };
744 * ```
745 *
746 * ### WebSocket Hibernation
747 * Durable Objects support WebSocket hibernation — the runtime can
748 * evict the object from memory while keeping connections open. Use
749 * `Cloudflare.upgrade()` to accept a connection, and return
750 * `webSocketMessage` / `webSocketClose` / `webSocketError` handlers to
751 * process events when the object wakes back up.
752 *
753 * **Example:** Accepting a WebSocket connection
754 * ```typescript
755 * return {
756 * fetch: Effect.gen(function* () {
757 * const [response, socket] = yield* Cloudflare.upgrade();
758 * socket.serializeAttachment({ id: crypto.randomUUID() });
759 * return response;
760 * }),
761 * };
762 * ```
763 *
764 * **Example:** Handling messages and close events
765 * ```typescript
766 * return {
767 * webSocketMessage: Effect.fn(function* (
768 * socket: Cloudflare.WebSocket,
769 * message: string | Uint8Array,
770 * ) {
771 * const text = typeof message === "string"
772 * ? message
773 * : new TextDecoder().decode(message);
774 * // process the message
775 * }),
776 * webSocketClose: Effect.fn(function* (
777 * ws: Cloudflare.WebSocket,
778 * code: number,
779 * reason: string,
780 * ) {
781 * yield* ws.close(code, reason);
782 * }),
783 * webSocketError: Effect.fn(function* (
784 * ws: Cloudflare.WebSocket,
785 * error: unknown,
786 * ) {
787 * // the runtime closes the socket afterwards; clear its session here
788 * ws.serializeAttachment(null);
789 * }),
790 * };
791 * ```
792 *
793 * **Example:** Recovering sessions after hibernation
794 * Resolve the `state` reference in the outer Effect, but place the
795 * rehydration loop (`state.getWebSockets()` is `RuntimeContext`-colored)
796 * **inside the inner `Effect.gen`** so it runs every time the DO
797 * instance is reconstructed (including after Cloudflare wakes the DO
798 * from hibernation).
799 *
800 * ```typescript
801 * Effect.gen(function* () {
802 * const state = yield* Cloudflare.DurableObjectState;
803 *
804 * return Effect.gen(function* () {
805 * const sessions = new Map<string, Cloudflare.WebSocket>();
806 *
807 * // Rehydrate the in-memory session map after hibernation.
808 * for (const socket of yield* state.getWebSockets()) {
809 * const data = socket.deserializeAttachment<{ id: string }>();
810 * if (data) sessions.set(data.id, socket);
811 * }
812 *
813 * return {
814 * fetch: Effect.gen(function* () {
815 * const [response, socket] = yield* Cloudflare.upgrade();
816 * const id = crypto.randomUUID();
817 * socket.serializeAttachment({ id });
818 * sessions.set(id, socket);
819 * return response;
820 * }),
821 * webSocketMessage: Effect.fn(function* (socket, message) {
822 * const text =
823 * typeof message === "string" ? message : new TextDecoder().decode(message);
824 * for (const peer of sessions.values()) {
825 * yield* peer.send(text);
826 * }
827 * }),
828 * };
829 * });
830 * });
831 * ```
832 *
833 * ### Durable Callbacks
834 * Register `Alchemy.makeCallback` handlers in the inner, per-instance Effect.
835 * Durable Objects supply callback registration on their instance RuntimeContext
836 * using SQLite and native alarms. Scheduling participates in the current storage
837 * transaction, and each job is acknowledged only after its handler succeeds.
838 * No explicit `alarm` handler is needed.
839 *
840 * **Example:** Save state and schedule a typed callback atomically
841 * ```typescript
842 * const state = yield* Cloudflare.DurableObjectState;
843 * return Effect.gen(function* () {
844 * const onArchive = yield* Alchemy.makeCallback(
845 * "archive",
846 * Effect.fn(function* (payload: { key: string; body: string }) {
847 * yield* archive.put(payload.key, payload.body);
848 * }),
849 * );
850 * return {
851 * save: Effect.fn(function* (id: string, body: string) {
852 * yield* state.storage.transaction(
853 * Effect.gen(function* () {
854 * yield* state.storage.put(id, body);
855 * yield* onArchive.schedule(id, {
856 * after: "30 seconds",
857 * payload: { key: id, body },
858 * });
859 * }),
860 * );
861 * }),
862 * };
863 * });
864 * ```
865 *
866 * Callbacks receive JSON-serializable payloads and deliver at least once, so
867 * external writes must be idempotent. A recovery wake is persisted before each
868 * attempt; configure its delay with the third argument, `{ retry: { delay:
869 * "1 minute" } }`. Scheduling the same callback name and ID replaces the pending
870 * job; `onArchive.cancel(id)` cancels it. Retain handlers for old callback names
871 * while their jobs are pending. Each native alarm processes up to 100 due jobs;
872 * direct `setAlarm`/`deleteAlarm` calls bypass the scheduler's coordination.
873 * Leave native alarm retries enabled when aborting an instance. Passing
874 * `{ retryAlarm: false }` removes the automatic-recovery guarantee: Cloudflare
875 * can suppress a replacement wake even after its timestamp is persisted. Jobs
876 * remain stored, but may need an explicitly rearmed native alarm.
877 *
878 * The scheduler migrates its original unversioned SQLite schema to version 1
879 * atomically, preserving existing events. Old events still use the explicit
880 * `alarm` handler below; their rows have no callback name to infer. Both APIs
881 * coordinate the same native alarm. Unknown newer schema versions fail closed.
882 *
883 * ### Scheduled Alarms
884 * Each Durable Object can have a single alarm timestamp. Alchemy
885 * layers a small SQLite-backed scheduler on top via
886 * `Cloudflare.Workers.scheduleEvent` and `Cloudflare.Workers.processScheduledEvents`,
887 * so you can register many named events with arbitrary payloads and
888 * fire them from a single `alarm` handler.
889 *
890 * **Example:** Scheduling and processing events
891 * ```typescript
892 * // schedule from a request or message handler
893 * yield* Cloudflare.Workers.scheduleEvent(
894 * "reminder-1",
895 * new Date(Date.now() + 60_000),
896 * { message: "your meeting starts in a minute" },
897 * );
898 *
899 * return {
900 * alarm: () =>
901 * Effect.gen(function* () {
902 * const fired = yield* Cloudflare.Workers.processScheduledEvents;
903 * for (const event of fired) {
904 * const payload = event.payload as { message: string };
905 * // dispatch / broadcast / persist...
906 * }
907 * }),
908 * };
909 * ```
910 *
911 * ### Aborting a Durable Object
912 * `state.abort(reason?, options?)` forcibly resets the isolate. By
913 * default an in-progress alarm retries after the reset. Pass
914 * `{ retryAlarm: false }` when the alarm should stop instead — for
915 * example an alarm that deletes storage so the constructor does not
916 * recreate it.
917 *
918 * **Example:** Stop alarm retries after cleanup
919 * ```typescript
920 * export class CleanupTask extends Cloudflare.DurableObject<CleanupTask>()(
921 * "CleanupTask",
922 * Effect.gen(function* () {
923 * const state = yield* Cloudflare.DurableObjectState;
924 *
925 * return Effect.gen(function* () {
926 * // This won't be re-run after the alarm is aborted
927 * yield* state.storage.sql.exec(`
928 * CREATE TABLE IF NOT EXISTS foo (
929 * id INTEGER PRIMARY KEY
930 * )
931 * `);
932 *
933 * return {
934 * alarm: () =>
935 * Effect.gen(function* () {
936 * yield* state.storage.sql.exec("DROP TABLE foo");
937 * yield* state.abort("Cleanup complete", { retryAlarm: false });
938 * }),
939 * };
940 * });
941 * }),
942 * ) {}
943 * ```
944 *
945 * ### Using from a Worker
946 * Yield the DO class in your Worker's init phase to get a namespace
947 * handle. Call `getByName` or `getById` to get a typed stub, then
948 * call any RPC method or forward an HTTP request with `fetch`.
949 *
950 * **Example:** Calling RPC methods
951 * ```typescript
952 * // init
953 * const counters = yield* Counter;
954 *
955 * return {
956 * fetch: Effect.gen(function* () {
957 * const counter = counters.getByName("user-123");
958 * yield* counter.increment();
959 * const value = yield* counter.get();
960 * return HttpServerResponse.text(String(value));
961 * }),
962 * };
963 * ```
964 *
965 * **Example:** Forwarding an HTTP request
966 * ```typescript
967 * // init
968 * const rooms = yield* Room;
969 *
970 * return {
971 * fetch: Effect.gen(function* () {
972 * const request = yield* HttpServerRequest;
973 * const room = rooms.getByName(roomId);
974 * return yield* room.fetch(request);
975 * }),
976 * };
977 * ```
978 *
979 * ### Binding in an Async Worker
980 * When using an Async Worker (plain `async fetch` handler, no Effect
981 * runtime), declare Durable Objects in the `bindings` prop of the
982 * Worker resource. Pass a `DurableObject` reference with a
983 * `className` matching the exported `DurableObject` subclass in your
984 * worker source file. If `className` is omitted, it defaults to the
985 * namespace name. Use `Cloudflare.InferEnv` to get a fully typed
986 * `env` object that includes the namespace.
987 *
988 * **Example:** Declaring a DO binding in the stack
989 * ```typescript
990 * // alchemy.run.ts
991 * import type { Counter } from "./src/worker.ts";
992 *
993 * export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
994 *
995 * export const Worker = Cloudflare.Worker("Worker", {
996 * main: "./src/worker.ts",
997 * bindings: {
998 * Counter: Cloudflare.DurableObject<Counter>("Counter"),
999 * },
1000 * });
1001 * ```
1002 *
1003 * **Example:** Using the DO from a plain async handler
1004 * ```typescript
1005 * // src/worker.ts
1006 * import { DurableObject } from "cloudflare:workers";
1007 * import type { WorkerEnv } from "../alchemy.run.ts";
1008 *
1009 * export default {
1010 * async fetch(request: Request, env: WorkerEnv) {
1011 * const counter = env.Counter.getByName("my-counter");
1012 * const count = await counter.increment();
1013 * return new Response(JSON.stringify({ count }));
1014 * },
1015 * };
1016 *
1017 * export class Counter extends DurableObject {
1018 * private counter = 0;
1019 * async increment() {
1020 * return ++this.counter;
1021 * }
1022 * }
1023 * ```
1024 *
1025 * ### Cross-Script Binding in an Async Worker
1026 * Async Workers can also bind to a Durable Object hosted by another
1027 * Worker script. The host Worker declares and exports the DO class. The
1028 * consumer Worker declares a `DurableObject` with `scriptName`
1029 * set to the host Worker's script name.
1030 *
1031 * Cross-script async bindings are references only: the consumer uploads
1032 * the binding metadata, but Alchemy does not drive class migrations for
1033 * the foreign class. Deploy the host first so Cloudflare can verify that
1034 * the target script exports the requested class.
1035 *
1036 * **Example:** Host Worker owns the Durable Object class
1037 * ```typescript
1038 * const host = yield* Cloudflare.Worker("Host", {
1039 * main: "./src/host.ts",
1040 * bindings: {
1041 * Counter: Cloudflare.DurableObject<Counter>("Counter"),
1042 * },
1043 * });
1044 * ```
1045 *
1046 * **Example:** Consumer Worker binds to the host script
1047 * ```typescript
1048 * const consumer = yield* Cloudflare.Worker("Consumer", {
1049 * main: "./src/consumer.ts",
1050 * bindings: {
1051 * Counter: Cloudflare.DurableObject<Counter>("Counter", {
1052 * scriptName: host.workerName,
1053 * }),
1054 * },
1055 * });
1056 * ```
1057 *
1058 * **Example:** Binding to a different exported class name
1059 * ```typescript
1060 * const consumer = yield* Cloudflare.Worker("Consumer", {
1061 * main: "./src/consumer.ts",
1062 * bindings: {
1063 * Counter: Cloudflare.DurableObject<Counter>("Counter", {
1064 * className: "CounterV2",
1065 * scriptName: host.workerName,
1066 * }),
1067 * },
1068 * });
1069 * ```
1070 *
1071 * ### Container-Backed Durable Objects in an Async Worker
1072 * A container-backed class is declared by binding a `Cloudflare.Container`
1073 * directly in the async Worker's `env` — the Container *is* the Durable
1074 * Object binding plus its ContainerApplication. See the Async Workers
1075 * section on {@link Container} for the full walkthrough.
1076 *
1077 * **Example:** A Container binding declares the container-backed class
1078 * ```typescript
1079 * // `Sandbox` is the container-backed DO class exported by the worker
1080 * // script (extends `@cloudflare/containers`' `Container`).
1081 * import type { Sandbox } from "./src/worker.ts";
1082 *
1083 * export const Worker = Cloudflare.Worker("Worker", {
1084 * main: "./src/worker.ts",
1085 * env: {
1086 * Sandbox: Cloudflare.Container<Sandbox>("Sandbox", {
1087 * image: "docker.io/cloudflare/sandbox:0.1.3",
1088 * }),
1089 * },
1090 * });
1091 * ```
1092 *
1093 * ### Moving a Class Between Workers
1094 * A Durable Object class can move from one Worker to another with its
1095 * data intact. The move is always **declared** — a class that disappears
1096 * from one worker and appears on another is otherwise ambiguous between
1097 * "transfer the data" and "delete it, start fresh", so Alchemy never
1098 * guesses (removing a DO deletes it; that is the default). Declare
1099 * `transferredFrom` on the Durable Object at its **new host**, naming the
1100 * former host, and the new host's deploy ships Cloudflare's
1101 * data-preserving `transferred_classes` migration. The former host's
1102 * deploy converges on its own — no delete migration is emitted for a
1103 * class that moved away.
1104 *
1105 * Each `transferredFrom` entry is either the former host's Worker
1106 * **logical id** (same stack + stage, resolved via alchemy's ownership
1107 * tags) or its **physical script name** (required for cross-stack moves).
1108 * When no listed host holds the namespace — a fresh stage, or the
1109 * transfer already completed — the declaration is inert, so it is safe to
1110 * leave in place indefinitely.
1111 *
1112 * **Example:** Move a class from WorkerB to WorkerA
1113 * ```typescript
1114 * // BEFORE: worker-b hosts the class
1115 * const b = yield* Cloudflare.Worker("WorkerB", {
1116 * main: "./src/worker-b.ts", // exports MyDOClass
1117 * bindings: {
1118 * MyDO: Cloudflare.DurableObject("MyDO", { className: "MyDOClass" }),
1119 * },
1120 * });
1121 *
1122 * // AFTER: worker-a hosts it and declares where it came from;
1123 * // worker-b keeps a cross-script reference.
1124 * const a = yield* Cloudflare.Worker("WorkerA", {
1125 * main: "./src/worker-a.ts", // now exports MyDOClass
1126 * bindings: {
1127 * MyDO: Cloudflare.DurableObject("MyDO", {
1128 * className: "MyDOClass",
1129 * transferredFrom: "WorkerB", // logical id (or its script name)
1130 * }),
1131 * },
1132 * });
1133 * const b = yield* Cloudflare.Worker("WorkerB", {
1134 * main: "./src/worker-b.ts", // no longer exports MyDOClass
1135 * bindings: {
1136 * MyDO: Cloudflare.DurableObject("MyDO", {
1137 * className: "MyDOClass",
1138 * scriptName: a.workerName,
1139 * }),
1140 * },
1141 * });
1142 * ```
1143 *
1144 * Forgetting the declaration is safe: when the former host still
1145 * references the class cross-script, its deploy fails before any upload
1146 * with `DurableObjectTransferRequired`, telling you exactly what to
1147 * declare — data is never silently destroyed or forked.
1148 *
1149 * **Example:** Chained moves keep the host history
1150 * ```typescript
1151 * // The class moved WorkerB → WorkerA last release and WorkerA →
1152 * // WorkerC this release. Keep the full history so a stage that lagged
1153 * // behind (or skipped the intermediate release) still transfers from
1154 * // wherever its namespace currently lives.
1155 * Cloudflare.DurableObject("MyDO", {
1156 * className: "MyDOClass",
1157 * transferredFrom: ["WorkerB", "WorkerA"],
1158 * })
1159 * ```
1160 *
1161 * Two rules for multi-worker migrations:
1162 *
1163 * - **Pure moves (the former host drops the DO entirely, keeping no
1164 * cross-script reference) must be two deploys**: first add the class to
1165 * the new host with `transferredFrom` and deploy; then remove it from
1166 * the former host and deploy. In a single deploy nothing orders the
1167 * transfer before the former host's delete.
1168 * - **Cross-stack moves deploy the new host's stack first**, naming the
1169 * former host by physical script name; the former host's stack deploys
1170 * after and converges.
1171 *
1172 * ### Adopting an Existing Durable Object
1173 * When you adopt a Worker that already exists on Cloudflare — created
1174 * outside Alchemy via Wrangler, the dashboard, or the raw API — its
1175 * Durable Object classes are adopted along with it. You opt in to the
1176 * takeover the same way you adopt any foreign resource: with
1177 * `adopt(true)` (or the `--adopt` CLI flag), since `Worker.read` reports
1178 * a worker with no Alchemy ownership tags as `Unowned`.
1179 *
1180 * Alchemy normally tracks which class backs each binding through an
1181 * `alchemy:dos:` metadata tag it writes on the script (mapping each
1182 * binding's logical id to its class name). A
1183 * foreign worker has no such tag, so on the **adopting deploy** Alchemy
1184 * falls back to matching your binding to the live class **by binding
1185 * name**. The class is then reused in place — not recreated — so
1186 * Cloudflare's migration engine doesn't reject the upload for creating a
1187 * class that already exists.
1188 *
1189 * The consequence is a one-time constraint: **on the adopting deploy the
1190 * binding's `className` must match the class that already exists on the
1191 * worker.** You cannot rename the class in the same deploy that adopts
1192 * it. Once the deploy completes, Alchemy owns the worker and has written
1193 * the `alchemy:dos:` tag, so subsequent renames are driven by logical id
1194 * and work normally.
1195 *
1196 * **Example:** Adopting a worker whose `Counter` class already exists
1197 * ```typescript
1198 * // The worker + `Counter` class were created outside Alchemy.
1199 * // `className` must match the existing class on this first deploy.
1200 * const worker = yield* Cloudflare.Worker("Worker", {
1201 * name: "existing-worker",
1202 * main: "./src/worker.ts",
1203 * bindings: {
1204 * Counter: Cloudflare.DurableObject<Counter>("Counter"),
1205 * },
1206 * }).pipe(adopt(true));
1207 * ```
1208 *
1209 * **Example:** Renaming the class — only after adoption
1210 * ```typescript
1211 * // A SECOND deploy, after the one above. Alchemy now owns the worker
1212 * // and maps the binding by logical id, so the class can be renamed.
1213 * const worker = yield* Cloudflare.Worker("Worker", {
1214 * name: "existing-worker",
1215 * main: "./src/worker.ts",
1216 * bindings: {
1217 * Counter: Cloudflare.DurableObject<Counter>("Counter", {
1218 * className: "CounterV2",
1219 * }),
1220 * },
1221 * });
1222 * ```
1223 *
1224 * @resource
1225 * @product Workers
1226 * @category Workers & Compute
1227 */
1228export const DurableObject: DurableObjectClass = taggedFunction(
1229 DurableObjectScope,
1230 function (
1231 ...args:
1232 | []
1233 | [
1234 name: string,
1235 props?: DurableObjectProps,
1236 // phantom argument
1237 isClassForm?: true,
1238 ]
1239 | [
1240 name: string,
1241 impl: Effect.Effect<
1242 Effect.Effect<DurableObject<any>, never, DurableObjectState>
1243 >,
1244 // phantom argument
1245 isClassForm?: true,
1246 ]
1247 ) {
1248 if (args.length === 0) {
1249 return (name: string, propsOrImpl?: any) =>
1250 // @ts-expect-error
1251 DurableObject(name, propsOrImpl, true);
1252 }
1253 const namespace = args[0];
1254 const isClassForm = args[2] === true;
1255 const propsOrImpl = args[1];
1256 const tag = Context.Service(namespace);
1257
1258 const binding = (
1259 scriptName?: Input<string>,
1260 transferredFrom?:
1261 | DurableObjectTransferSource
1262 | DurableObjectTransferSource[],
1263 errors?: ReadonlyArray<RpcErrorClass>,
1264 ) =>
1265 Effect.gen(function* () {
1266 const worker = yield* Worker;
1267
1268 yield* worker.bind`${namespace}`({
1269 bindings: [
1270 {
1271 type: "durable_object_namespace",
1272 name: namespace,
1273 className: namespace,
1274 scriptName,
1275 transferredFrom: normalizeTransferredFrom(transferredFrom),
1276 },
1277 ],
1278 });
1279
1280 const binding = yield* Effect.all([
1281 WorkerEnvironment,
1282 ALCHEMY_PHASE,
1283 ]).pipe(
1284 Effect.flatMap(([env, phase]) => {
1285 if (env === undefined || phase === "plan") {
1286 // should be fine to return undefined here (it is only undefined at plantime)
1287 return Effect.succeed(undefined);
1288 }
1289 const ns = env[namespace];
1290 if (!ns) {
1291 return Effect.die(
1292 new Error(`DurableObject '${namespace}' not found`),
1293 );
1294 } else if (typeof ns.getByName === "function") {
1295 return Effect.succeed(ns);
1296 } else {
1297 return Effect.die(
1298 new Error(
1299 `DurableObject '${namespace}' is not a DurableObject`,
1300 ),
1301 );
1302 }
1303 }),
1304 );
1305
1306 return {
1307 Type: TypeId,
1308 LogicalId: namespace,
1309 name: namespace,
1310 namespaceId: worker.durableObjectNamespaces.pipe(
1311 Output.map(
1312 (durableObjectNamespaces) => durableObjectNamespaces?.[namespace],
1313 ),
1314 ),
1315 getByName: (
1316 name: string,
1317 options?: DurableObjectGetDurableObjectOptions,
1318 ) => makeRpcStub(binding.getByName(name, options), { errors }),
1319 // newUniqueId: () => use((ns) => ns.newUniqueId()),
1320 // idFromName: (name: string) => use((ns) => ns.idFromName(name)),
1321 // idFromString: (id: string) => use((ns) => ns.idFromString(id)),
1322 // get: (
1323 // id: cf.DurableObjectId,
1324 // options?: cf.DurableObjectNamespaceGetDurableObjectOptions,
1325 // ) => use((ns) => makeRpcStub(ns.get(id, options))),
1326 // jurisdiction: (jurisdiction: cf.DurableObjectJurisdiction) =>
1327 // use((ns) => ns.jurisdiction(jurisdiction) as any),
1328 };
1329 });
1330
1331 // Class-form declarations (`DurableObject<Self>()("Name", props?)`) can
1332 // carry `transferredFrom` so the host's local binding drives a
1333 // data-preserving transfer migration.
1334 const classProps =
1335 isClassForm && !Effect.isEffect(propsOrImpl)
1336 ? (propsOrImpl as
1337 | Pick<DurableObjectProps, "transferredFrom" | "errors">
1338 | undefined)
1339 : undefined;
1340
1341 const make = Effect.fn(function* (
1342 impl: Effect.Effect<
1343 Effect.Effect<DurableObjectShape>,
1344 never,
1345 DurableObjectState
1346 >,
1347 ) {
1348 // Register the local DO binding (no `scriptName`) and obtain the
1349 // namespace handle. We provide this same handle as
1350 // `DurableObjectScope` to the user's constructor effect
1351 // and also return it so a `Layer.effect(tag, make(impl))` Layer
1352 // resolves the tag to a concrete namespace value.
1353 const self = yield* binding(
1354 undefined,
1355 classProps?.transferredFrom,
1356 classProps?.errors,
1357 );
1358 const phase = yield* ALCHEMY_PHASE;
1359 const constructor = impl.pipe(
1360 Effect.provide(Layer.succeed(DurableObjectScope, self as any)),
1361 );
1362 if (phase === "plan") {
1363 // during plan time, we evaluate the constructor with a mock DurableObjectState
1364 // to trigger discovery of bindings
1365 yield* constructor.pipe(
1366 Effect.provide(
1367 Layer.succeed(
1368 DurableObjectState,
1369 // mock during plan time
1370 fromDurableObjectState({ storage: {} } as any),
1371 ),
1372 ),
1373 );
1374 }
1375 yield* (yield* Worker).export(namespace, {
1376 kind: "durableObject",
1377 // initialize the object's constructor (apply infra dependencies)
1378 constructor,
1379 // grab the object's infra dependencies so we can apply them when calling the instance's methods
1380 services: yield* Effect.context<Effect.Services<typeof impl>>(),
1381 } satisfies DurableObjectExport);
1382 return self;
1383 });
1384
1385 if (!isClassForm && !Effect.isEffect(args[1])) {
1386 // this is an in-line, async only DO (no implementation, props only)
1387 return {
1388 kind: TypeId,
1389 name: namespace,
1390 className: (args[1] as DurableObjectProps)?.className || namespace,
1391 scriptName: (args[1] as DurableObjectProps)?.scriptName,
1392 transferredFrom: (args[1] as DurableObjectProps)?.transferredFrom,
1393 };
1394 } else if (Effect.isEffect(propsOrImpl)) {
1395 // inline Effect DO
1396 return effectClass(
1397 Effect.tap(binding(), () =>
1398 make(propsOrImpl as any).pipe(
1399 Effect.provideService(
1400 DurableObjectState,
1401 fromDurableObjectState(
1402 // everything is lazy, so this should build a proper mock
1403 {
1404 storage: {},
1405 } as any,
1406 ),
1407 ),
1408 ),
1409 ),
1410 );
1411 } else {
1412 // Tagged Effect DO. Yielding the class resolves the `tag`, which
1413 // forces the `CounterLive` Layer (built by `Counter.make(impl)`) to
1414 // run so `worker.export(namespace, …)` is invoked — without this,
1415 // the class would never be registered with the Worker's exports
1416 // map and the deployed bundle would have no DO class for
1417 // Cloudflare to instantiate.
1418 return class extends effectClass(tag as Effect.Effect<any, never, any>) {
1419 static make = <Req = never>(
1420 impl: Effect.Effect<
1421 Effect.Effect<DurableObjectShape, never, DurableObjectState | Req>
1422 >,
1423 ) => Layer.effect(tag, make(impl as any));
1424
1425 static from = (
1426 worker: string | Worker | Effect.Effect<Worker, any, any>,
1427 ) => {
1428 // Resolve `worker` to an Effect that yields the actual Worker
1429 // instance (or a plain string scriptName).
1430 //
1431 // A class produced by `Cloudflare.Worker<T>()(...)` exposes
1432 // `asEffect()` returning the Self tag — yielding that tag
1433 // resolves to the live Worker instance whose `workerName`
1434 // is a `PropExpr`, which is what we need so the engine can:
1435 // 1) Track WorkerB → WorkerA as a binding-level upstream
1436 // dependency (so WorkerA reconciles first).
1437 // 2) Persist `scriptName` as a real value (not `undefined`)
1438 // on the cross-script binding so the migration code in
1439 // Worker.ts can detect it and skip emitting class
1440 // migrations for the foreign class.
1441 // Plain Effects and string literals are passed through as-is.
1442 const resolved: Effect.Effect<Worker | string, any, any> =
1443 typeof worker === "string"
1444 ? Effect.succeed(worker)
1445 : asEffect(worker);
1446
1447 return resolved.pipe(
1448 Effect.flatMap((w) =>
1449 binding(
1450 typeof w === "string" ? w : w.workerName,
1451 undefined,
1452 classProps?.errors,
1453 ),
1454 ),
1455 );
1456 };
1457 };
1458 }
1459 },
1460) as any;
1461
1462export type DurableObjectStub<Shape> = {
1463 // TODO(sam): do we need to transform? hopefully not
1464 [key in keyof Shape]: Shape[key];
1465} & {
1466 fetch: (
1467 request: HttpServerRequest.HttpServerRequest,
1468 ) => Effect.Effect<
1469 HttpServerResponse.HttpServerResponse,
1470 HttpServerError,
1471 never
1472 >;
1473};