import type * as cf from "@cloudflare/workers-types"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type { Scope } from "effect/Scope"; import type { HttpServerError } from "effect/unstable/http/HttpServerError"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; import type { Dependencies } from "../../Dependencies.ts"; import type { HttpEffect } from "../../Http.ts"; import type { Input } from "../../Input.ts"; import * as Output from "../../Output.ts"; import type { MainRpc, PlatformServices } from "../../Platform.ts"; import type { RuntimeContext } from "../../RuntimeContext.ts"; import type { Container } from "../Containers/Container.ts"; import { DurableObjectState } from "./DurableObjectState.ts"; import { type WebSocket } from "./WebSocket.ts"; import { Worker, WorkerEnvironment, type WorkerServices } from "./Worker.ts"; export interface DurableObjectExport { readonly kind: "durableObject"; readonly constructor: Effect.Effect, never, DurableObjectState>; readonly services: Context.Context; } export declare const isDurableObjectExport: (value: unknown) => value is DurableObjectExport; export type DurableObjectId = cf.DurableObjectId; export type DurableObjectJurisdiction = cf.DurableObjectJurisdiction; export type DurableObjectGetDurableObjectOptions = cf.DurableObjectNamespaceGetDurableObjectOptions; /** * The regions a Durable Object can be *hinted* toward at creation — * `"wnam"`, `"enam"`, `"sam"`, `"weur"`, `"eeur"`, `"apac"`, `"oc"`, … See * the "Placing a Durable Object in a Region" section on {@link DurableObject}. */ export type DurableObjectLocationHint = cf.DurableObjectLocationHint; export type AlarmInvocationInfo = cf.AlarmInvocationInfo; type TypeId = "Cloudflare.DurableObject"; declare const TypeId = "Cloudflare.DurableObject"; export declare const isDurableObjectLike: (value: unknown) => value is DurableObjectLike; export interface DurableObjectLike { kind: TypeId; name: string; /** @internal phantom */ className?: string; /** @internal phantom */ scriptName?: Input; /** @internal phantom */ transferredFrom?: DurableObjectTransferSource | DurableObjectTransferSource[]; /** @internal phantom */ Shape?: Shape; } export interface DurableObject extends DurableObjectLike { Type: TypeId; name: string; namespaceId: Output.Output; getByName: (name: string, options?: DurableObjectGetDurableObjectOptions) => DurableObjectStub; newUniqueId: () => DurableObjectId; idFromName: (name: string) => DurableObjectId; idFromString: (id: string) => DurableObjectId; get: (id: DurableObjectId, options?: DurableObjectGetDurableObjectOptions) => DurableObjectStub; jurisdiction: (jurisdiction: DurableObjectJurisdiction) => DurableObject; } export interface DurableObjectShape { fetch?: HttpEffect; alarm?: (alarmInfo?: AlarmInvocationInfo) => Effect.Effect; webSocketMessage?: (socket: WebSocket, message: string | ArrayBuffer) => Effect.Effect; webSocketClose?: (socket: WebSocket, code: number, reason: string, wasClean: boolean) => Effect.Effect; } export type DurableObjectServices = DurableObject | DurableObjectState | WorkerServices | WorkerEnvironment | PlatformServices; /** * A reference to the Worker that previously hosted a Durable Object class, * accepted by {@link DurableObjectProps.transferredFrom}: * * - the Worker's **logical id** (same stack + stage) or **physical script * name** (cross-stack) as a string * - the Worker **resource** (`const b = yield* Cloudflare.Worker(...)`) * - the Worker **class** (`transferredFrom: WorkerA`) * - a **thunk** of any of the above (`() => WorkerA`) — use this for * forward references and import cycles; it is evaluated lazily at plan * time, after all modules have loaded * - a string `Output` (e.g. a cross-stack `stackRef`'s worker name) * * Every form is normalized at plan time to a plain identifier: a Worker * reference contributes its logical id, and a same-stack `workerName` * Output contributes its source resource's logical id rather than the * Output itself. Keeping the former host's *value* out of the binding data * matters — consuming its Output would add a dependency edge from the new * host onto the former host, inverting the deploy order the transfer * requires (the new host must upload first). */ export type DurableObjectTransferSource = Input | Worker | Effect.Effect | (() => DurableObjectTransferSource); /** * Normalize a `transferredFrom` declaration to plain host identifiers at * plan time. See {@link DurableObjectTransferSource} for the accepted forms * and why Worker references reduce to logical ids instead of Outputs. * * @internal */ export declare const normalizeTransferredFrom: (value: DurableObjectTransferSource | readonly DurableObjectTransferSource[] | undefined) => Input[] | undefined; export interface DurableObjectProps { /** * Name of the exported `DurableObject` class. * * @default name */ className?: string; /** * Worker script that hosts the Durable Object class. Omit this when the * namespace is hosted by the Worker that declares the binding. */ scriptName?: Input | undefined; /** * The Worker(s) that previously hosted this Durable Object class. When one * of them still holds the namespace, the deploy performs Cloudflare's * data-preserving `transferred_classes` migration, moving the namespace — * including all stored objects — from that script to this Worker. * * Each entry names a former host — see * {@link DurableObjectTransferSource} for the accepted forms: a string * (Worker logical id in this stack + stage, or physical script name for * cross-stack moves), the Worker class or resource itself, or a thunk * (`() => WorkerA`) for forward references. Moving a class is always * declared this way — there is no inference. A list is a host *history*: * as the class moves again, append the previous host so stages that lag * behind (or skipped an intermediate release) still transfer from * wherever their namespace currently lives. * * When no listed host holds the namespace — a fresh stage, or every * transfer already completed — the property is inert and the class is * created fresh (or left as-is), so it is safe to leave in place * indefinitely. */ transferredFrom?: DurableObjectTransferSource | DurableObjectTransferSource[] | undefined; } export interface DurableObjectClass extends Effect.Effect { (): { (name: Name, props?: Pick): Effect.Effect, never, Worker | Self> & { new (_: never): Shape & { /** @internal */ "~alchemy/name": Name; }; from(scriptName: Input): Effect.Effect, never, Worker>; from(worker: Dependencies | Effect.Effect, never, Req>): Effect.Effect, never, Worker | Req>; make(impl: Effect.Effect, never, DurableObjectServices | Req>): Layer.Layer; }; }; (): { , Req extends DurableObjectServices | Container.Application = never>(name: string, impl: Effect.Effect, never, Req>): Effect.Effect, never, Worker | Extract>> & { new (_: never): Shape; }; }; (name: string, props?: DurableObjectProps): DurableObjectLike; (name: string, impl: Effect.Effect): Effect.Effect, never, Worker | Exclude>; } declare const DurableObjectScope_base: Context.ServiceClass>; export declare class DurableObjectScope extends DurableObjectScope_base { } /** * A Cloudflare Durable Object namespace that manages globally unique, stateful * instances with WebSocket hibernation support. * * A Durable Object uses a two-phase pattern with two nested `Effect.gen` * blocks. The outer Effect resolves shared dependencies (other DOs, * containers, etc.) and the instance state *reference* * (`Cloudflare.DurableObjectState`). The inner Effect returns the * object's public methods and WebSocket handlers — and is the only * place the state's `RuntimeContext`-colored methods (`storage.get`, * `storage.put`, …) can actually run. * * ```typescript * Effect.gen(function* () { * // Phase 1: resolve shared dependencies + the instance state ref * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); * const state = yield* Cloudflare.DurableObjectState; * * return Effect.gen(function* () { * // Phase 2: per-instance setup and public API. `state`'s methods * // (storage.get/put, …) are RuntimeContext-colored, so the actual * // I/O happens here, in the runtime closure. * return { * save: (data: string) => db.exec("INSERT ..."), * fetch: Effect.gen(function* () { ... }), * webSocketMessage: Effect.fn(function* (ws, msg) { ... }), * }; * }); * }) * ``` * * There are two ways to define a Durable Object. See the * [Functions & Servers](/infrastructure-as-effects/functions-and-servers) page * for the full explanation. * * - **Inline** — Effect implementation passed directly, single file. * - **Modular** — class and implementation in separate files for tree-shaking. * * * ### Inline Durable Objects * Pass the Effect implementation as the second argument. This is the * simplest approach — everything lives in one file. Convenient when * the DO doesn't need to be referenced by other Workers or DOs that * would pull in its runtime dependencies. * * **Example:** Inline Durable Object * ```typescript * export default class Counter extends Cloudflare.DurableObject()( * "Counter", * Effect.gen(function* () { * // init: bind resources + resolve the instance state ref * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); * const state = yield* Cloudflare.DurableObjectState; * * return Effect.gen(function* () { * // runtime: state's storage methods are RuntimeContext-colored, * // so the reads/writes live here * const count = (yield* state.storage.get("count")) ?? 0; * * return { * increment: () => * Effect.gen(function* () { * const next = count + 1; * yield* state.storage.put("count", next); * return next; * }), * get: () => Effect.succeed(count), * }; * }); * }), * ) {} * ``` * * ### Modular Durable Objects * When a Worker and a DO reference each other, or multiple Workers * bind the same DO, define the class separately from its `.make()` * call. The class is a lightweight identifier; `.make()` provides * the runtime implementation as an `export default`. Rolldown treats * `.make()` as pure, so the bundler tree-shakes it and all its * runtime dependencies out of any consumer's bundle. * * The class and `.make()` can live in the same file. This is the * same pattern used by `Worker` and `Container`. * * **Example:** Modular Durable Object (class + .make() in one file) * ```typescript * // src/Counter.ts * export class Counter extends Cloudflare.DurableObject()( * "Counter", * ) {} * * export default Counter.make( * Effect.gen(function* () { * // init: bind resources + resolve the instance state ref * const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); * const state = yield* Cloudflare.DurableObjectState; * * return Effect.gen(function* () { * // runtime: state's storage methods are RuntimeContext-colored, * // so the reads/writes live here * const count = (yield* state.storage.get("count")) ?? 0; * * return { * increment: () => * Effect.gen(function* () { * const next = count + 1; * yield* state.storage.put("count", next); * yield* db.prepare("INSERT INTO logs (count) VALUES (?)").bind(next).run(); * return next; * }), * get: () => Effect.succeed(count), * }; * }); * }), * ); * ``` * * **Example:** Binding a modular DO from a Worker * ```typescript * // imports Counter; bundler tree-shakes .make() * import Counter from "./Counter.ts"; * * // init * const counters = yield* Counter; * * return { * fetch: Effect.gen(function* () { * const counter = counters.getByName("user-123"); * return HttpServerResponse.text(String(yield* counter.get())); * }), * }; * ``` * * ### Cross-Worker Binding * A Durable Object is _hosted_ by exactly one Worker, but any * number of other Workers can bind to the same DO. This is how * you share state across Workers: one Worker hosts the DO, every * other Worker addresses it by `scriptName` and gets a typed stub. * * To make this type-safe, the **host Worker** must declare the DO * as part of its public contract via the third type argument to * `Cloudflare.Worker()`. `Deps` is the set * of DO classes (or other Workers) the script exposes for other * scripts to bind to. * * **Example:** Host Worker declares the DO in its contract * ```typescript * // workerA.ts — hosts Counter * import { Counter, CounterLive } from "./object.ts"; * * // ^^^^^^^ declared as part of WorkerA's public contract * export class WorkerA extends Cloudflare.Worker()( * "WorkerA", * { main: import.meta.url }, * ) {} * * // WorkerA's Layer also provides the DO's Live implementation. * export default WorkerA.make( * Effect.gen(function* () { * const counter = yield* Counter; * return { fetch: Effect.gen(function* () { ... }) }; * }).pipe(Effect.provide(CounterLive)), * ); * ``` * * **Example:** Consumer Worker binds the DO via `Counter.from(WorkerA)` * ```typescript * // workerB.ts — binds to the same Counter, hosted by WorkerA * import { Counter } from "./object.ts"; * import { WorkerA } from "./workerA.ts"; * * export default class WorkerB extends Cloudflare.Worker()( * "WorkerB", * { main: import.meta.url }, * Effect.gen(function* () { * // ^^^^^^^^^^^^ scriptName-bound stub of WorkerA's Counter * const counter = yield* Counter.from(WorkerA); * return { * fetch: Effect.gen(function* () { * const value = yield* counter.getByName("shared").get(); * return HttpServerResponse.text(String(value)); * }), * }; * }), * ) {} * ``` * * :::tip * The `, Counter` in `Worker` is what makes * `Counter.from(WorkerA)` type-check. You can still bind across * scripts without it (the runtime works either way), but consumers * will get a TypeScript error because `WorkerA` doesn't declare * `Counter` as part of its public contract. * ::: * * Only the host Worker's Stack provides `CounterLive` — the * consumer Worker just imports the `Counter` class as a typed * identifier. Rolldown tree-shakes `CounterLive` (and its * dependencies) out of WorkerB's bundle. * * ### Using `.from(Self)` Inside the Host * Inside the host Worker, `yield* Counter` and * `yield* Counter.from(Self)` resolve to the same local namespace. * The `.from(Self)` form is preferred — especially in code that * may be extracted into a reusable Layer — because it makes the * scriptName explicit and lets the same Layer shape work whether * the consumer is the host or another script. * * **Example:** `Counter.from(WorkerA)` inside WorkerA itself * ```typescript * // workerA.ts — host uses `.from(Self)` instead of bare `yield* Counter` * export default WorkerA.make( * Effect.gen(function* () { * const counter = yield* Counter.from(WorkerA); // same as `yield* Counter` * return { fetch: Effect.gen(function* () { ... }) }; * }).pipe(Effect.provide(CounterLive)), * ); * ``` * * A Worker can also host its **own isolated** namespace this way. * If a second host Worker declares `Counter` in its contract and * provides `CounterLive`, the DO instances under that script are * separate from the original host's — same class, two namespaces. * * **Example:** Two hosts, two isolated namespaces * ```typescript * // workerC.ts — another host of Counter, isolated from WorkerA * export class WorkerC extends Cloudflare.Worker()( * "WorkerC", * { main: import.meta.url }, * ) {} * * export default WorkerC.make( * Effect.gen(function* () { * // .from(WorkerC) binds to WorkerC's own Counter namespace — * // writes here are NOT visible from WorkerA's Counter. * const counter = yield* Counter.from(WorkerC); * return { fetch: Effect.gen(function* () { ... }) }; * }).pipe(Effect.provide(CounterLive)), * ); * ``` * * ### RPC Methods * Any function you return from the inner Effect becomes an RPC method * that Workers can call through a stub. Methods must return an `Effect`. * The caller gets a fully typed stub — if your DO returns `increment` * and `get`, the stub exposes `counter.increment()` and `counter.get()`. * * **Example:** Defining RPC methods * ```typescript * return { * increment: () => Effect.succeed(++count), * get: () => Effect.succeed(count), * reset: () => Effect.sync(() => { count = 0; }), * }; * ``` * * ### Returning Streams from RPC * RPC methods can return an Effect `Stream` and the caller will see * the chunks as they're produced. Combine with `Stream.schedule` to * pace emission, or with `Stream.fromQueue` to bridge an inbound * subscription. * * **Example:** Streaming sequential numbers * ```typescript * import * as Schedule from "effect/Schedule"; * import * as Stream from "effect/Stream"; * * return { * tick: (n: number) => * Stream.iterate(0, (i) => i + 1).pipe( * Stream.take(n), * Stream.schedule(Schedule.spaced("100 millis")), * ), * }; * ``` * * **Example:** Forwarding the stream as a chunked HTTP response * ```typescript * // in a Worker fetch handler * const counter = counters.getByName("tick"); * const stream = counter.tick(5).pipe( * Stream.map((i) => `${i}\n`), * Stream.encodeText, * ); * return HttpServerResponse.stream(stream, { * headers: { "content-type": "text/plain" }, * }); * ``` * * ### Worker → DO HTTP forwarding * In addition to RPC methods, the typed stub exposes a `fetch` * method that forwards an `HttpServerRequest` straight to the DO. * The DO's own `fetch` Effect produces the response — useful for * WebSocket upgrades and other request-shaped interactions. * * **Example:** Forwarding an HTTP request to a DO * ```typescript * const room = rooms.getByName(roomId); * return yield* room.fetch(request); * ``` * * ### Placing a Durable Object in a Region * A Durable Object is created wherever its *first-ever* request * came from, and it stays there for life. That default is right for * an instance whose traffic all comes from the user who created it, * and wrong for one whose name is derived from something other than * geography (a shard index, a hash, a fixed roster) — a Sydney user * routed onto an instance that some Frankfurt request happened to * create first pays a cross-planet round trip on every call. * * Pass a `locationHint` to place the instance near the traffic you * expect instead. It only applies to *creation*: an instance that * already exists is unaffected, so a hint can't move a live DO. * * **Example:** Sharding instances by region * ```typescript * // Both the name and the hint derive from the caller's region, so * // each shard is created in the region whose users address it. * const region = hintFor(request.cf?.continent); // "apac", "weur", … * const shard = shards.getByName(`pool:${region}:${index}`, { * locationHint: region, * }); * ``` * * :::caution * A hint is a hint: Cloudflare places the instance in the nearest * location it *can*, which may not be the hinted one, and only the * first request to a given name has any say. Derive the name from * the same region you hint with — otherwise two regions race to * create one shared instance and the loser is stuck with it. * ::: * * ### Accessing Instance State * Each Durable Object instance has its own transactional key-value * storage via `Cloudflare.DurableObjectState`. Resolve the `state` * *reference* in the outer (init) Effect, but call its methods — * `storage.get`, `storage.put`, … — only from the inner (runtime) * Effect: those methods are `RuntimeContext`-colored, so the type * system only allows them inside the runtime closure. * * **Example:** Reading and writing durable storage * ```typescript * // inner (runtime) Effect — `state` was resolved in the outer Effect * yield* state.storage.put("counter", 42); * const value = yield* state.storage.get("counter"); * ``` * * ### Background Work & Scopes * Every RPC call and fetch into a Durable Object gets its own Effect * `Scope`. When the method finishes, the bridge closes that scope and * registers the close promise with workerd's `state.waitUntil` — so * finalizers added with `Effect.addFinalizer` inside a method run *after* * the result is returned to the caller, without blocking it, and the * object stays alive until they settle. * * For ad-hoc background work, `state.waitUntil(effect)` forks an Effect * with the caller's full context and keeps the object alive until it * settles. * * Attach cleanup to method scopes, not the constructor (init) closure — * the constructor runs once per in-memory instance under * `blockConcurrencyWhile`, and its scope is not tied to any call. * * **Example:** Responding before finishing the work * ```typescript * return { * record: Effect.fn(function* (entry: string) { * // runs after `record` returns; the DO stays alive until it settles * yield* Effect.addFinalizer(() => * state.storage.put(`audit:${entry}`, Date.now()).pipe(Effect.ignore), * ); * return "accepted" as const; * }), * * refresh: Effect.fn(function* () { * // same idea, explicit form * yield* state.waitUntil(recomputeExpensiveView()); * return "scheduled" as const; * }), * }; * ``` * * ### WebSocket Hibernation * Durable Objects support WebSocket hibernation — the runtime can * evict the object from memory while keeping connections open. Use * `Cloudflare.upgrade()` to accept a connection, and return * `webSocketMessage` / `webSocketClose` handlers to process events * when the object wakes back up. * * **Example:** Accepting a WebSocket connection * ```typescript * return { * fetch: Effect.gen(function* () { * const [response, socket] = yield* Cloudflare.upgrade(); * socket.serializeAttachment({ id: crypto.randomUUID() }); * return response; * }), * }; * ``` * * **Example:** Handling messages and close events * ```typescript * return { * webSocketMessage: Effect.fn(function* ( * socket: Cloudflare.WebSocket, * message: string | Uint8Array, * ) { * const text = typeof message === "string" * ? message * : new TextDecoder().decode(message); * // process the message * }), * webSocketClose: Effect.fn(function* ( * ws: Cloudflare.WebSocket, * code: number, * reason: string, * ) { * yield* ws.close(code, reason); * }), * }; * ``` * * **Example:** Recovering sessions after hibernation * Resolve the `state` reference in the outer Effect, but place the * rehydration loop (`state.getWebSockets()` is `RuntimeContext`-colored) * **inside the inner `Effect.gen`** so it runs every time the DO * instance is reconstructed (including after Cloudflare wakes the DO * from hibernation). * * ```typescript * Effect.gen(function* () { * const state = yield* Cloudflare.DurableObjectState; * * return Effect.gen(function* () { * const sessions = new Map(); * * // Rehydrate the in-memory session map after hibernation. * for (const socket of yield* state.getWebSockets()) { * const data = socket.deserializeAttachment<{ id: string }>(); * if (data) sessions.set(data.id, socket); * } * * return { * fetch: Effect.gen(function* () { * const [response, socket] = yield* Cloudflare.upgrade(); * const id = crypto.randomUUID(); * socket.serializeAttachment({ id }); * sessions.set(id, socket); * return response; * }), * webSocketMessage: Effect.fn(function* (socket, message) { * const text = * typeof message === "string" ? message : new TextDecoder().decode(message); * for (const peer of sessions.values()) { * yield* peer.send(text); * } * }), * }; * }); * }); * ``` * * ### Scheduled Alarms * Each Durable Object can have a single alarm timestamp. Alchemy * layers a small SQLite-backed scheduler on top via * `Cloudflare.Workers.scheduleEvent` and `Cloudflare.Workers.processScheduledEvents`, * so you can register many named events with arbitrary payloads and * fire them from a single `alarm` handler. * * **Example:** Scheduling and processing events * ```typescript * // schedule from a request or message handler * yield* Cloudflare.Workers.scheduleEvent( * "reminder-1", * new Date(Date.now() + 60_000), * { message: "your meeting starts in a minute" }, * ); * * return { * alarm: () => * Effect.gen(function* () { * const fired = yield* Cloudflare.Workers.processScheduledEvents; * for (const event of fired) { * const payload = event.payload as { message: string }; * // dispatch / broadcast / persist... * } * }), * }; * ``` * * ### Using from a Worker * Yield the DO class in your Worker's init phase to get a namespace * handle. Call `getByName` or `getById` to get a typed stub, then * call any RPC method or forward an HTTP request with `fetch`. * * **Example:** Calling RPC methods * ```typescript * // init * const counters = yield* Counter; * * return { * fetch: Effect.gen(function* () { * const counter = counters.getByName("user-123"); * yield* counter.increment(); * const value = yield* counter.get(); * return HttpServerResponse.text(String(value)); * }), * }; * ``` * * **Example:** Forwarding an HTTP request * ```typescript * // init * const rooms = yield* Room; * * return { * fetch: Effect.gen(function* () { * const request = yield* HttpServerRequest; * const room = rooms.getByName(roomId); * return yield* room.fetch(request); * }), * }; * ``` * * ### Binding in an Async Worker * When using an Async Worker (plain `async fetch` handler, no Effect * runtime), declare Durable Objects in the `bindings` prop of the * Worker resource. Pass a `DurableObject` reference with a * `className` matching the exported `DurableObject` subclass in your * worker source file. If `className` is omitted, it defaults to the * namespace name. Use `Cloudflare.InferEnv` to get a fully typed * `env` object that includes the namespace. * * **Example:** Declaring a DO binding in the stack * ```typescript * // alchemy.run.ts * import type { Counter } from "./src/worker.ts"; * * export type WorkerEnv = Cloudflare.InferEnv; * * export const Worker = Cloudflare.Worker("Worker", { * main: "./src/worker.ts", * bindings: { * Counter: Cloudflare.DurableObject("Counter"), * }, * }); * ``` * * **Example:** Using the DO from a plain async handler * ```typescript * // src/worker.ts * import { DurableObject } from "cloudflare:workers"; * import type { WorkerEnv } from "../alchemy.run.ts"; * * export default { * async fetch(request: Request, env: WorkerEnv) { * const counter = env.Counter.getByName("my-counter"); * const count = await counter.increment(); * return new Response(JSON.stringify({ count })); * }, * }; * * export class Counter extends DurableObject { * private counter = 0; * async increment() { * return ++this.counter; * } * } * ``` * * ### Cross-Script Binding in an Async Worker * Async Workers can also bind to a Durable Object hosted by another * Worker script. The host Worker declares and exports the DO class. The * consumer Worker declares a `DurableObject` with `scriptName` * set to the host Worker's script name. * * Cross-script async bindings are references only: the consumer uploads * the binding metadata, but Alchemy does not drive class migrations for * the foreign class. Deploy the host first so Cloudflare can verify that * the target script exports the requested class. * * **Example:** Host Worker owns the Durable Object class * ```typescript * const host = yield* Cloudflare.Worker("Host", { * main: "./src/host.ts", * bindings: { * Counter: Cloudflare.DurableObject("Counter"), * }, * }); * ``` * * **Example:** Consumer Worker binds to the host script * ```typescript * const consumer = yield* Cloudflare.Worker("Consumer", { * main: "./src/consumer.ts", * bindings: { * Counter: Cloudflare.DurableObject("Counter", { * scriptName: host.workerName, * }), * }, * }); * ``` * * **Example:** Binding to a different exported class name * ```typescript * const consumer = yield* Cloudflare.Worker("Consumer", { * main: "./src/consumer.ts", * bindings: { * Counter: Cloudflare.DurableObject("Counter", { * className: "CounterV2", * scriptName: host.workerName, * }), * }, * }); * ``` * * ### Container-Backed Durable Objects in an Async Worker * A container-backed class is declared by binding a `Cloudflare.Container` * directly in the async Worker's `env` — the Container *is* the Durable * Object binding plus its ContainerApplication. See the Async Workers * section on {@link Container} for the full walkthrough. * * **Example:** A Container binding declares the container-backed class * ```typescript * // `Sandbox` is the container-backed DO class exported by the worker * // script (extends `@cloudflare/containers`' `Container`). * import type { Sandbox } from "./src/worker.ts"; * * export const Worker = Cloudflare.Worker("Worker", { * main: "./src/worker.ts", * env: { * Sandbox: Cloudflare.Container("Sandbox", { * image: "docker.io/cloudflare/sandbox:0.1.3", * }), * }, * }); * ``` * * ### Moving a Class Between Workers * A Durable Object class can move from one Worker to another with its * data intact. The move is always **declared** — a class that disappears * from one worker and appears on another is otherwise ambiguous between * "transfer the data" and "delete it, start fresh", so Alchemy never * guesses (removing a DO deletes it; that is the default). Declare * `transferredFrom` on the Durable Object at its **new host**, naming the * former host, and the new host's deploy ships Cloudflare's * data-preserving `transferred_classes` migration. The former host's * deploy converges on its own — no delete migration is emitted for a * class that moved away. * * Each `transferredFrom` entry is either the former host's Worker * **logical id** (same stack + stage, resolved via alchemy's ownership * tags) or its **physical script name** (required for cross-stack moves). * When no listed host holds the namespace — a fresh stage, or the * transfer already completed — the declaration is inert, so it is safe to * leave in place indefinitely. * * **Example:** Move a class from WorkerB to WorkerA * ```typescript * // BEFORE: worker-b hosts the class * const b = yield* Cloudflare.Worker("WorkerB", { * main: "./src/worker-b.ts", // exports MyDOClass * bindings: { * MyDO: Cloudflare.DurableObject("MyDO", { className: "MyDOClass" }), * }, * }); * * // AFTER: worker-a hosts it and declares where it came from; * // worker-b keeps a cross-script reference. * const a = yield* Cloudflare.Worker("WorkerA", { * main: "./src/worker-a.ts", // now exports MyDOClass * bindings: { * MyDO: Cloudflare.DurableObject("MyDO", { * className: "MyDOClass", * transferredFrom: "WorkerB", // logical id (or its script name) * }), * }, * }); * const b = yield* Cloudflare.Worker("WorkerB", { * main: "./src/worker-b.ts", // no longer exports MyDOClass * bindings: { * MyDO: Cloudflare.DurableObject("MyDO", { * className: "MyDOClass", * scriptName: a.workerName, * }), * }, * }); * ``` * * Forgetting the declaration is safe: when the former host still * references the class cross-script, its deploy fails before any upload * with `DurableObjectTransferRequired`, telling you exactly what to * declare — data is never silently destroyed or forked. * * **Example:** Chained moves keep the host history * ```typescript * // The class moved WorkerB → WorkerA last release and WorkerA → * // WorkerC this release. Keep the full history so a stage that lagged * // behind (or skipped the intermediate release) still transfers from * // wherever its namespace currently lives. * Cloudflare.DurableObject("MyDO", { * className: "MyDOClass", * transferredFrom: ["WorkerB", "WorkerA"], * }) * ``` * * Two rules for multi-worker migrations: * * - **Pure moves (the former host drops the DO entirely, keeping no * cross-script reference) must be two deploys**: first add the class to * the new host with `transferredFrom` and deploy; then remove it from * the former host and deploy. In a single deploy nothing orders the * transfer before the former host's delete. * - **Cross-stack moves deploy the new host's stack first**, naming the * former host by physical script name; the former host's stack deploys * after and converges. * * ### Adopting an Existing Durable Object * When you adopt a Worker that already exists on Cloudflare — created * outside Alchemy via Wrangler, the dashboard, or the raw API — its * Durable Object classes are adopted along with it. You opt in to the * takeover the same way you adopt any foreign resource: with * `adopt(true)` (or the `--adopt` CLI flag), since `Worker.read` reports * a worker with no Alchemy ownership tags as `Unowned`. * * Alchemy normally tracks which class backs each binding through an * `alchemy:dos:` metadata tag it writes on the script (mapping each * binding's logical id to its class name). A * foreign worker has no such tag, so on the **adopting deploy** Alchemy * falls back to matching your binding to the live class **by binding * name**. The class is then reused in place — not recreated — so * Cloudflare's migration engine doesn't reject the upload for creating a * class that already exists. * * The consequence is a one-time constraint: **on the adopting deploy the * binding's `className` must match the class that already exists on the * worker.** You cannot rename the class in the same deploy that adopts * it. Once the deploy completes, Alchemy owns the worker and has written * the `alchemy:dos:` tag, so subsequent renames are driven by logical id * and work normally. * * **Example:** Adopting a worker whose `Counter` class already exists * ```typescript * // The worker + `Counter` class were created outside Alchemy. * // `className` must match the existing class on this first deploy. * const worker = yield* Cloudflare.Worker("Worker", { * name: "existing-worker", * main: "./src/worker.ts", * bindings: { * Counter: Cloudflare.DurableObject("Counter"), * }, * }).pipe(adopt(true)); * ``` * * **Example:** Renaming the class — only after adoption * ```typescript * // A SECOND deploy, after the one above. Alchemy now owns the worker * // and maps the binding by logical id, so the class can be renamed. * const worker = yield* Cloudflare.Worker("Worker", { * name: "existing-worker", * main: "./src/worker.ts", * bindings: { * Counter: Cloudflare.DurableObject("Counter", { * className: "CounterV2", * }), * }, * }); * ``` * * @resource * @product Workers * @category Workers & Compute */ export declare const DurableObject: DurableObjectClass; export type DurableObjectStub = { [key in keyof Shape]: Shape[key]; } & { fetch: (request: HttpServerRequest.HttpServerRequest) => Effect.Effect; }; export {}; //# sourceMappingURL=DurableObject.d.ts.map