effect-cf 0.32.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +31 -1072
  3. package/dist/{Artifacts-DnHQA-Ax.d.mts → Artifacts-B3r3RuTB.d.mts} +1 -48
  4. package/dist/{Artifacts-CSIk1Oml.mjs → Artifacts-CBxWGeZB.mjs} +2 -7
  5. package/dist/{Binding-pfilNJmP.mjs → Binding-C82jRhdr.mjs} +1 -2
  6. package/dist/{Binding-xJJJPmiD.d.mts → Binding-DKcLrQCM.d.mts} +0 -45
  7. package/dist/ComputerArtifacts.d.mts +1 -3
  8. package/dist/ComputerArtifacts.mjs +1 -2
  9. package/dist/ComputerWorkspace.d.mts +1 -10
  10. package/dist/ComputerWorkspace.mjs +1 -2
  11. package/dist/ComputerWorkspaceHost.d.mts +1 -9
  12. package/dist/{ContainerNamespace-CFjiLw8b.d.mts → ContainerNamespace-DbLaeFhN.d.mts} +1 -14
  13. package/dist/{DurableObjectState-B0A0Y1ef.d.mts → DurableObjectState-CrEunfQ-.d.mts} +86 -254
  14. package/dist/{DurableObjectState-D1Ibr6k0.mjs → DurableObjectState-De4ZojhY.mjs} +2 -40
  15. package/dist/{ErrorMessage-BM0K-Nik.mjs → ErrorMessage-Byx2OgNd.mjs} +0 -1
  16. package/dist/{Hyperdrive-z-9fFz15.d.mts → Hyperdrive-HrwFlZ6x.d.mts} +1 -4
  17. package/dist/HyperdrivePg.d.mts +2 -11
  18. package/dist/HyperdrivePg.mjs +0 -8
  19. package/dist/Sandbox.d.mts +2 -32
  20. package/dist/Sandbox.mjs +2 -11
  21. package/dist/Vitest.d.mts +3 -31
  22. package/dist/Vitest.mjs +2 -17
  23. package/dist/{WorkflowBinding-B9_S8tX2.d.mts → WorkflowBinding-BumVQTuW.d.mts} +2 -5
  24. package/dist/index.d.mts +34 -480
  25. package/dist/index.mjs +174 -312
  26. package/package.json +2 -2
  27. package/src/AiGateway.ts +0 -6
  28. package/src/AnalyticsEngine.ts +0 -9
  29. package/src/Artifacts.ts +0 -47
  30. package/src/Binding.ts +0 -8
  31. package/src/BrowserRendering.ts +0 -4
  32. package/src/Cache.ts +0 -19
  33. package/src/CloudflareOtlp.ts +3 -39
  34. package/src/ComputerArtifacts.ts +0 -2
  35. package/src/ComputerWorkspace.ts +0 -9
  36. package/src/ComputerWorkspaceHost.ts +0 -8
  37. package/src/ContainerNamespace.ts +0 -13
  38. package/src/D1.ts +0 -22
  39. package/src/DurableObject.ts +45 -25
  40. package/src/DurableObjectAlarm.ts +6 -210
  41. package/src/DurableObjectDefinition.ts +8 -20
  42. package/src/DurableObjectNamespace.ts +57 -112
  43. package/src/DurableObjectRpcWebSocket.ts +3 -19
  44. package/src/DurableObjectSqlite.ts +0 -12
  45. package/src/DurableObjectState.ts +6 -21
  46. package/src/DurableObjectStorage.ts +2 -35
  47. package/src/DurableObjectWebSocket.ts +0 -31
  48. package/src/Email.ts +0 -12
  49. package/src/Environment.ts +0 -37
  50. package/src/Hyperdrive.ts +0 -3
  51. package/src/HyperdrivePg.ts +0 -9
  52. package/src/Images.ts +0 -5
  53. package/src/Kv.ts +0 -15
  54. package/src/QueueBinding.ts +0 -2
  55. package/src/R2.ts +0 -5
  56. package/src/RpcDefinition.ts +0 -2
  57. package/src/RpcTracing.ts +133 -0
  58. package/src/Sandbox.ts +0 -30
  59. package/src/ServiceBinding.ts +48 -125
  60. package/src/Vectorize.ts +0 -5
  61. package/src/Vitest.ts +0 -28
  62. package/src/WebTransport.ts +0 -15
  63. package/src/Worker.ts +36 -57
  64. package/src/WorkerDefinition.ts +5 -14
  65. package/src/WorkersAi.ts +0 -4
  66. package/src/Workflow.ts +0 -5
  67. package/src/WorkflowBinding.ts +0 -3
  68. package/src/index.ts +1 -0
  69. package/src/internal/Entrypoint.ts +23 -4
  70. package/src/internal/ErrorMessage.ts +0 -1
  71. package/src/internal/RpcInvocation.ts +18 -1
  72. package/src/internal/Runtime.ts +12 -7
  73. package/src/internal/WorkerContext.ts +0 -11
package/README.md CHANGED
@@ -1,1109 +1,68 @@
1
1
  # effect-cf
2
2
 
3
- Effect-native Cloudflare primitives for Workers, Durable Objects, Containers, bindings, Cache, KV, D1, R2, Artifacts, Queues, Email, Analytics Engine, Workflows, and Durable Object storage.
3
+ Cloudflare entrypoints and bindings as Effect services.
4
4
 
5
- ## Install
6
-
7
- `effect-cf` targets Effect `^4.0.0-beta.105` and Cloudflare workerd `1.20260820.1`
8
- (`@cloudflare/workers-types@5.20260820.1`, recommended `compatibility_date` `2026-08-20`).
9
-
10
- ```bash
11
- bun add effect-cf "effect@^4.0.0-beta.105"
12
- ```
13
-
14
- ```bash
15
- pnpm add effect-cf "effect@^4.0.0-beta.105"
16
- ```
17
-
18
- ```bash
19
- npm install effect-cf "effect@^4.0.0-beta.105"
20
- ```
21
-
22
- The Computer workspace integration is an optional peer. Install it only in
23
- applications that import `effect-cf/computer-workspace`,
24
- `effect-cf/computer-artifacts`, or `effect-cf/computer-workspace-host`:
25
-
26
- ```bash
27
- bun add "@cloudflare/computer@^0.2.0"
28
- ```
29
-
30
- Workspace Git operations additionally require `@platformatic/vfs`,
31
- `@cloudflare/computer/git`'s own optional peer:
32
-
33
- ```bash
34
- bun add "@platformatic/vfs@^0.4.0"
35
- ```
36
-
37
- The Sandbox integration is an optional peer. Install the `@next` release line
38
- of `@cloudflare/sandbox` (the Sandbox SDK 1.0 API) only in applications that
39
- import `effect-cf/sandbox`:
40
-
41
- ```bash
42
- bun add "@cloudflare/sandbox@next"
43
- ```
44
-
45
- ## Goal
46
-
47
- Cloudflare APIs return promises and expose platform-specific bindings. `effect-cf` wraps those boundaries as `Context`, `Layer`, and `Effect` values so application code stays inside one managed Effect runtime.
48
-
49
- Runtime creation belongs at Cloudflare entrypoints, not inside binding helpers.
50
-
51
- ## Exports
52
-
53
- - `Worker` - Worker entrypoint factory, request services, and typed Worker bindings
54
- - `DurableObject` - Durable Object entrypoint factory and typed namespace helpers
55
- - `DurableObjectState` / `DurableObjectStorage` - Effect wrappers for state, alarms, SQL, and embedded KV
56
- - `DurableObjectWebSocket` - WebSocket upgrade helpers for Durable Objects
57
- - `ContainerNamespace` - named Cloudflare Container instances with Effect-wrapped request and lifecycle operations
58
- - `Cache` - Effect wrapper for Cloudflare's default and named Cache API instances
59
- - `Kv` - typed KV namespace helper
60
- - `D1` - typed D1 database binding helper with an `@effect/sql-d1` backed SQL layer
61
- - `R2` - typed R2 bucket binding helper with Effect-wrapped object and multipart operations
62
- - `Artifacts` - typed Cloudflare Artifacts namespace and repository helpers, including repository lifecycle, tokens, and Git object reads
63
- - `effect-cf/computer-workspace` - scoped Effect wrappers for the complete Cloudflare Computer filesystem, Git, runtime, Assets, Artifacts, and Think-compatible client surfaces
64
- - `effect-cf/computer-artifacts` - session-isolated Artifacts repositories backed by `@cloudflare/computer/artifacts`
65
- - `effect-cf/computer-workspace-host` - optional Durable Object host mixin, worker-shell backend wiring, and `WorkspaceServiceProxy`
66
- - `effect-cf/sandbox` - Effect-native client for Cloudflare Sandbox containers: processes with log streams, files and watches, preview URLs, tunnels, terminals, bucket mounts, and backups
67
- - `Hyperdrive` - typed Hyperdrive binding helper for connection strings and optional Postgres SQL integration
68
- - `Images` - typed Cloudflare Images binding helper with transformation APIs and optional hosted image operations
69
- - `Email` - typed Cloudflare Email Service binding helper for `send_email` bindings, with limit validation and typed error codes
70
- - `AnalyticsEngine` - typed Cloudflare Analytics Engine write bindings and SQL API query helpers
71
- - `Queue` - typed Queue producer/consumer tags plus client and error types
72
- - `Workflow` - typed Workflow entrypoints, steps, starter clients, and instance types
73
- - `Rpc` - Cloudflare RPC type helpers and scoped disposal utilities
74
- - `WebTransport` - truthful WebTransport/HTTP-3 boundary: typed runtime capabilities and decoded inbound protocol metadata
75
- - `WorkerConfig` - Effect `Config` helpers backed by Cloudflare `env`
76
- - `effect-cf/vitest` - Effect-native runners and scoped test helpers for Cloudflare's Workers Vitest plugin
77
-
78
- ## Vitest Workers Plugin
79
-
80
- Install and configure a `^1.0.0` release of
81
- [`@cloudflare/vitest-plugin`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vitest-plugin#readme),
82
- then import the test-only helpers from `effect-cf/vitest`. The `fetch` runner
83
- constructs the Worker with the current test environment and waits for all
84
- `waitUntil` work before completing the Effect.
85
-
86
- ```ts
87
- import { assert, it } from "@effect/vitest";
88
- import { Config, Effect } from "effect";
89
- import * as PoolWorkers from "effect-cf/vitest";
90
-
91
- import WorkerEntrypoint from "../src/index";
92
-
93
- it.effect("serves a request", () =>
94
- Effect.gen(function* () {
95
- const response = yield* PoolWorkers.fetch(
96
- WorkerEntrypoint,
97
- new Request("https://worker.test/health"),
98
- );
99
-
100
- assert.strictEqual(response.status, 200);
101
- }),
102
- );
103
-
104
- it.effect("reads Wrangler vars", () =>
105
- Config.string("APP_NAME").pipe(
106
- Effect.tap((appName) => assert.strictEqual(appName, "test-app")),
107
- Effect.provide(PoolWorkers.layer),
108
- ),
109
- );
110
- ```
111
-
112
- Queue consumers can be invoked with typed message bodies while retaining the
113
- plugin's acknowledgement and retry result:
114
-
115
- ```ts
116
- it.effect("acknowledges a job", () =>
117
- Effect.gen(function* () {
118
- const { result } = yield* PoolWorkers.queue(MyQueueConsumer, "jobs", [
119
- {
120
- id: "job-1",
121
- timestamp: new Date(),
122
- attempts: 1,
123
- body: { accountId: "account-1" },
124
- },
125
- ]);
126
-
127
- assert.deepStrictEqual(result.explicitAcks, ["job-1"]);
128
- }),
129
- );
130
- ```
131
-
132
- Scheduled handlers and Pages Functions use the same lifecycle behavior: their
133
- Effects complete only after the event's `waitUntil` work has settled. Pages
134
- tests must configure the `ASSETS` binding required by the Workers Vitest plugin.
135
-
136
- ```ts
137
- it.effect("runs the cron handler", () =>
138
- PoolWorkers.scheduled(worker.scheduled, {
139
- cron: "30 * * * *",
140
- scheduledTime: new Date("2030-01-01T00:00:00Z"),
141
- }),
142
- );
143
-
144
- it.effect("runs a Pages Function", () =>
145
- Effect.gen(function* () {
146
- const response = yield* PoolWorkers.pages(onRequest, {
147
- request: new Request("https://pages.test/users/dan"),
148
- params: { user: "dan" },
149
- data: { authenticated: true },
150
- });
151
-
152
- assert.strictEqual(response.status, 200);
153
- }),
154
- );
155
- ```
156
-
157
- `runInDurableObject` carries the test's Effect context into the Durable Object
158
- I/O context and provides the same wrapped `DurableObjectState` service used by
159
- production handlers. Typed failures remain in the Effect error channel.
160
-
161
- ```ts
162
- it.effect("seeds a Durable Object", () =>
163
- Effect.gen(function* () {
164
- const stub = env.COUNTERS.getByName("home");
165
-
166
- yield* PoolWorkers.runInDurableObject(stub, () =>
167
- Effect.gen(function* () {
168
- const state = yield* DurableObjectState;
169
- yield* state.storage.put("count", 41);
170
- }),
171
- );
172
-
173
- assert.strictEqual(yield* PoolWorkers.runDurableObjectAlarm(stub), true);
174
-
175
- // Recreate in-memory state while preserving Durable Object storage.
176
- yield* PoolWorkers.evictDurableObject(stub);
177
- }),
178
- );
179
- ```
180
-
181
- The subpath also exposes Effects for `listDurableObjectIds`, `reset`,
182
- `abortAllDurableObjects`, `evictAllDurableObjects`, and `applyD1Migrations`.
183
- `adminSecretsStore` returns an Effect-native admin client whose `create`,
184
- `update`, `duplicate`, `delete`, `list`, and `get` operations are Effects.
185
-
186
- `withExecutionContext` supports lower-level tests that need a native
187
- `ExecutionContext`. Workflow introspectors and all of their modifiers and query
188
- operations are Effect-native. They are disposed with the current Effect scope,
189
- and modifier callbacks carry the caller's Effect context and typed failures.
190
-
191
- ```ts
192
- it.effect("controls a Workflow instance", () =>
193
- Effect.scoped(
194
- Effect.gen(function* () {
195
- const instanceId = crypto.randomUUID();
196
- const instance = yield* PoolWorkers.introspectWorkflowInstance(
197
- env.REPORT_WORKFLOW,
198
- instanceId,
199
- );
200
-
201
- yield* instance.modify((modifier) =>
202
- Effect.gen(function* () {
203
- yield* modifier.disableSleeps();
204
- yield* modifier.mockStepResult({ name: "render-report" }, "reports/test.json");
205
- }),
206
- );
207
- yield* Effect.promise(() =>
208
- env.REPORT_WORKFLOW.create({
209
- id: instanceId,
210
- params: { reportId: "test", requestedBy: "dan@example.com" },
211
- }),
212
- );
213
-
214
- yield* instance.waitForStatus("complete");
215
- assert.deepStrictEqual(yield* instance.getOutput, {
216
- objectKey: "reports/test.json",
217
- notified: true,
218
- });
219
- }),
220
- ),
221
- );
5
+ ```sh
6
+ npm install effect-cf "effect@^4.0.0-rc.110"
222
7
  ```
223
8
 
224
- Use `introspectWorkflow` when the instance ID is not known before creation. Its
225
- `modifyAll` and `get` operations are Effects, and each instance returned by
226
- `get` uses the same Effect-native interface shown above.
9
+ The repository tests against workerd `1.20260825.1` and `@cloudflare/workers-types@5.20260825.1`. Use `compatibility_date: "2026-08-25"` in Wrangler.
227
10
 
228
- ## Worker Example
11
+ ## Worker
229
12
 
230
13
  ```ts
231
14
  import { Effect, Layer } from "effect";
232
- import { HttpServerResponse } from "effect/unstable/http";
233
- import { Worker } from "effect-cf";
234
-
235
- export default Worker.make(Layer.empty, Effect.succeed(HttpServerResponse.text("ok")));
236
- ```
237
-
238
- ## Durable Object Example
239
-
240
- ```ts
241
- import { Effect, Layer, Schema as S } from "effect";
242
- import { DurableObject, DurableObjectState } from "effect-cf";
243
-
244
- export class Counter extends DurableObject.Tag<Counter>()("Counter", {
245
- get: DurableObject.method({ success: S.Number }),
246
- }) {}
247
-
248
- export const CounterDurableObject = Counter.make(Layer.empty, {
249
- rpc: {
250
- get: () =>
251
- Effect.gen(function* () {
252
- const state = yield* DurableObjectState.DurableObjectState;
253
- const row = yield* (yield* state.storage.sql.exec<{ count: number }>(
254
- "SELECT 0 AS count",
255
- )).one();
256
- return row.count;
257
- }),
258
- },
259
- });
260
-
261
- export const CounterLayer = Counter.layer({ binding: "COUNTER" });
262
-
263
- export const readCounter = Effect.gen(function* () {
264
- const counters = yield* Counter;
265
- return yield* counters.byName("home").get();
266
- });
267
- ```
268
-
269
- Define Wrangler bindings and migrations in the consuming application. Durable Object namespace bindings are provided with `YourObject.layer({ binding })`, and consumers use `const namespace = yield* YourObject`.
270
-
271
- `DurableObjectState.waitUntil` accepts either a raw Promise or an Effect. The
272
- Effect form runs in the background with the caller's Effect context and the
273
- same failure modes as `WorkerContext.waitUntil`, so Durable Objects can
274
- schedule background Effects (for example a pump consuming an outbound
275
- WebSocket) without capturing a Context and calling `Effect.runPromiseWith`.
276
-
277
- ## Container Example
278
-
279
- Container entrypoints remain owned by `@cloudflare/containers`; `effect-cf`
280
- wraps the Container namespace used by a calling Worker.
281
-
282
- ```ts
283
- import { Container, switchPort } from "@cloudflare/containers";
284
- import { Effect, Layer } from "effect";
285
- import { ContainerNamespace, Worker } from "effect-cf";
286
-
287
- export class RendererContainer extends Container {
288
- defaultPort = 8080;
289
- sleepAfter = "10m";
290
- }
291
-
292
- class Renderers extends ContainerNamespace.Tag<Renderers>()("Renderers") {}
293
-
294
- const RenderersLive = Renderers.layer({ binding: "RENDERERS" });
295
-
296
- export default Worker.make(RenderersLive, {
297
- fetch: Effect.gen(function* () {
298
- const request = yield* Worker.NativeRequest;
299
- const renderer = Renderers.byName(new URL(request.url).pathname);
300
-
301
- yield* renderer.startAndWaitForPorts({ ports: 8080 });
302
- return yield* renderer.fetch(switchPort(request, 8080));
303
- }),
304
- });
305
- ```
306
-
307
- The instance client exposes `state`, `fetch`, `start`,
308
- `startAndWaitForPorts`, `waitForPort`, `stop` (named or numeric signals),
309
- `destroy`, the runtime host-policy operations (`setAllowedHosts`,
310
- `setDeniedHosts`, `allowHost`, `denyHost`, `removeAllowedHost`,
311
- `removeDeniedHost`), and the Effect-valued native `rawUnsafe` stub.
312
- Host-policy operations only forward the native remote calls; applications
313
- continue to own hostname policy and outbound handlers.
314
- Responses are returned unchanged, including non-2xx and WebSocket upgrade
315
- responses. If the Container uses outbound interception, also export
316
- `ContainerProxy` from `@cloudflare/containers`.
317
-
318
- `ContainerNamespace.Tag` accepts an optional second type parameter carrying
319
- the exact native namespace type. `rawUnsafe` on the namespace and on named
320
- instances then preserves that exact type, including extra subclass methods,
321
- instead of the minimal structural shape:
322
-
323
- ```ts
324
- class Sandboxes extends ContainerNamespace.Tag<Sandboxes, DurableObjectNamespace<CodexSandbox>>()(
325
- "Sandboxes",
326
- ) {}
327
-
328
- // Effect<DurableObjectStub<CodexSandbox>, ContainerOperationError, Sandboxes>
329
- const stub = Sandboxes.byName("codex").rawUnsafe;
330
- ```
331
-
332
- See [`examples/containers/README.md`](../../examples/containers/README.md) for
333
- the corresponding Wrangler configuration and entrypoint responsibilities.
334
-
335
- ## Sandbox Example
336
-
337
- `effect-cf/sandbox` wraps the [Cloudflare Sandbox SDK](https://github.com/cloudflare/sandbox-sdk)
338
- 1.0 API (`@cloudflare/sandbox@next`). The Sandbox Durable Object class remains
339
- owned by `@cloudflare/sandbox`; `effect-cf` wraps the namespace binding used by
340
- a calling Worker and loads the SDK lazily, so applications without the optional
341
- peer are unaffected.
342
-
343
- ```ts
344
- import { Effect, Option } from "effect";
345
15
  import { Worker } from "effect-cf";
346
- import * as Sandbox from "effect-cf/sandbox";
347
-
348
- export { Sandbox as SandboxDurableObject } from "@cloudflare/sandbox";
349
-
350
- class Sandboxes extends Sandbox.Tag<Sandboxes>()("Sandboxes") {}
351
-
352
- const SandboxesLive = Sandboxes.layer({ binding: "Sandbox" });
353
-
354
- export default Worker.make(SandboxesLive, {
355
- fetch: Effect.gen(function* () {
356
- const request = yield* Worker.NativeRequest;
357
-
358
- // Route preview-URL traffic before any other routing.
359
- const proxied = yield* Sandbox.proxyToSandbox(request);
360
- if (Option.isSome(proxied)) {
361
- return proxied.value;
362
- }
363
-
364
- const sandbox = yield* Sandboxes.get("my-sandbox");
365
- const handle = yield* sandbox.exec(["python3", "-c", "print(2 + 2)"]);
366
- const output = yield* handle.outputText();
367
-
368
- return new Response(output.stdout);
369
- }),
370
- });
371
- ```
372
-
373
- The instance client mirrors the complete `ISandbox` client contract plus the
374
- lifecycle, port, tunnel, and backup operations the SDK client proxies to the
375
- Sandbox Durable Object. `exec` returns a process handle whose `logs` is an
376
- Effect `Stream` of typed log events; `watch` decodes the SDK's Server-Sent
377
- Events into a `Stream` of file-watch events; `readFileStream` streams raw
378
- bytes. Interruptible waits (`waitForExit`, `waitForLog`, `output`,
379
- `waitForPort`) abort the underlying SDK call when the Effect is interrupted.
380
- Failures carry a typed `SandboxOperationError` whose `cause` preserves the
381
- SDK's rich error classes (`ProcessNotFoundError`, `PortNotExposedError`, and
382
- friends) for inspection.
383
-
384
- The corresponding `wrangler.jsonc` declares the container image and Durable
385
- Object binding; note the SDK requires the exported Durable Object class to be
386
- reachable under the configured `class_name`:
387
-
388
- ```jsonc
389
- {
390
- "containers": [
391
- {
392
- "class_name": "SandboxDurableObject",
393
- "image": "./Dockerfile",
394
- "instance_type": "lite",
395
- },
396
- ],
397
- "durable_objects": {
398
- "bindings": [{ "class_name": "SandboxDurableObject", "name": "Sandbox" }],
399
- },
400
- "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SandboxDurableObject"] }],
401
- }
402
- ```
403
-
404
- ## Queue Example
405
-
406
- Queues define the message contract once. The same class is the producer service tag and the consumer Worker definition: use `YourQueue.layer({ binding })` to provide a Cloudflare producer binding from `WorkerEnvironment`, and use `.make(...)` for the consumer Worker entrypoint.
407
-
408
- ```ts
409
- import { Effect, Layer, Schema as S } from "effect";
410
- import { Queue } from "effect-cf";
411
-
412
- class AvatarQueue extends Queue.Tag<AvatarQueue>()("AvatarQueue", {
413
- message: S.Struct({ userId: S.String, imageKey: S.String }),
414
- }) {}
415
-
416
- export const AvatarQueueConsumer = AvatarQueue.make(Layer.empty, {
417
- queue: (batch) =>
418
- Effect.gen(function* () {
419
- for (const message of batch.messages) {
420
- yield* Effect.logInfo("process avatar", message.body.userId);
421
- yield* message.ack;
422
- }
423
- }),
424
- });
425
-
426
- export const AvatarQueueLayer = AvatarQueue.layer({ binding: "AVATAR_QUEUE" });
427
-
428
- export const enqueueAvatar = (userId: string, imageKey: string) =>
429
- Effect.gen(function* () {
430
- const queue = yield* AvatarQueue;
431
- yield* queue.send({ userId, imageKey });
432
- });
433
- ```
434
-
435
- Producers should usually use `const queue = yield* AvatarQueue` and then call `queue.send(...)`, `queue.sendBatch(...)`, or `queue.metrics()`. The static `AvatarQueue.send(...)` helpers remain available for concise one-off calls.
436
-
437
- Queue handlers run inline failures through Cloudflare's normal retry path. If background work scheduled with `WorkerContext.waitUntil(...)` should also make the batch retry, use `WorkerContext.waitUntilPropagating(...)` or `waitUntil(..., { mode: "propagate" })`; the default `waitUntil` mode observes and logs failures without rejecting the native `waitUntil` promise.
438
-
439
- effect-cf automatically schedules best-effort OTLP flushes only for Worker
440
- fetch/RPC and Durable Object alarm/RPC handlers. Those internal flushes are
441
- capped at two seconds. Queue handlers do not flush telemetry automatically;
442
- applications that explicitly flush in a queue handler own that flush's timeout
443
- and failure policy.
444
-
445
- ## Cache Example
446
-
447
- `Cache.layer` exposes Cloudflare's global Cache API as an Effect service. Cache misses are represented by `Option.none()`, and named caches are available through `open(...)`.
448
-
449
- ```ts
450
- import { Effect, Option } from "effect";
451
- import { Cache, Worker } from "effect-cf";
452
-
453
- export default Worker.make(Cache.layer, {
454
- fetch: Effect.gen(function* () {
455
- const request = yield* Worker.NativeRequest;
456
- const storage = yield* Cache.CacheStorage;
457
- const cached = yield* storage.default.match(request);
458
-
459
- if (Option.isSome(cached)) {
460
- return cached.value;
461
- }
462
-
463
- const response = new Response("fresh", {
464
- headers: { "Cache-Control": "public, max-age=300" },
465
- });
466
-
467
- const context = yield* Worker.WorkerContext;
468
- yield* context.waitUntil(storage.default.put(request, response.clone()));
469
-
470
- return response;
471
- }),
472
- });
473
- ```
474
-
475
- Use `const cache = yield* storage.open("api-cache")` for a named cache. `match`, `put`, `delete`, and `open` failures are reported as `CacheOperationError` values.
476
-
477
- ## R2 Example
478
-
479
- R2 bucket tags expose Cloudflare object operations as Effects and map nullable reads to `Option`.
480
-
481
- ```ts
482
- import { Effect, Layer, Option } from "effect";
483
- import { R2 } from "effect-cf";
484
-
485
- class ArtifactBucket extends R2.Tag<ArtifactBucket>()("ArtifactBucket") {}
486
-
487
- export const ArtifactBucketLayer = ArtifactBucket.layer({ binding: "ARTIFACT_BUCKET" });
488
-
489
- export const writeArtifact = (key: string, body: string) =>
490
- Effect.gen(function* () {
491
- const bucket = yield* ArtifactBucket;
492
- yield* bucket.put(key, body, {
493
- httpMetadata: { contentType: "application/json" },
494
- });
495
- });
496
-
497
- export const readArtifact = (key: string) =>
498
- Effect.gen(function* () {
499
- const bucket = yield* ArtifactBucket;
500
- const object = yield* bucket.get(key);
501
- return Option.isSome(object) ? yield* Effect.promise(() => object.value.text()) : undefined;
502
- });
503
- ```
504
-
505
- Use `createMultipartUpload(...)` or `resumeMultipartUpload(...)` for large objects; returned upload handles wrap `uploadPart`, `complete`, and `abort` in Effect.
506
-
507
- ## Artifacts Example
508
-
509
- Artifacts tags expose every namespace and repository-handle binding operation as an Effect. Configure the binding under Wrangler's `artifacts` field:
510
-
511
- ```jsonc
512
- {
513
- "artifacts": [
514
- {
515
- "binding": "ARTIFACTS",
516
- "namespace": "default",
517
- // Optional in local development:
518
- "remote": true,
519
- },
520
- ],
521
- }
522
- ```
523
-
524
- ```ts
525
- import { Effect } from "effect";
526
- import { Artifacts } from "effect-cf";
527
-
528
- class Repositories extends Artifacts.Tag<Repositories>()("Repositories") {}
529
-
530
- export const RepositoriesLayer = Repositories.layer({ binding: "ARTIFACTS" });
531
-
532
- export const createWorkspace = Effect.gen(function* () {
533
- const artifacts = yield* Repositories;
534
- const created = yield* artifacts.create("agent-workspace", {
535
- description: "Versioned agent workspace",
536
- setDefaultBranch: "main",
537
- });
538
- const repo = yield* artifacts.get(created.name);
539
- const readToken = yield* repo.createToken("read", 3600);
540
-
541
- return {
542
- remote: created.remote,
543
- token: readToken.plaintext,
544
- history: yield* repo.log({ ref: "main", limit: 10 }),
545
- };
546
- });
547
- ```
548
-
549
- `create`, `import`, and `fork` return an initial plaintext token. Treat returned tokens as secrets and avoid logging or persisting them unnecessarily.
550
-
551
- ## Computer Workspace Example
552
16
 
553
- `ComputerWorkspace` exposes the complete `@cloudflare/computer`
554
- `WorkspaceClient` as Effects. The workspace client and every runtime execution
555
- handle are owned by an Effect `Scope`, so RPC stubs are disposed when their
556
- scope closes. The host mixin is a separate optional-dependency entrypoint:
557
-
558
- ```ts
559
- import shellCurl from "@cloudflare/computer/shell/curl";
560
- import { Effect, Schema as S } from "effect";
561
- import { DurableObjectDefinition } from "effect-cf";
562
- import * as ComputerWorkspace from "effect-cf/computer-workspace";
563
- import { withComputerWorkspace, WorkspaceServiceProxy } from "effect-cf/computer-workspace-host";
564
-
565
- export { WorkspaceServiceProxy };
566
-
567
- const ResearchWorkspace = DurableObjectDefinition.make("ResearchWorkspace", {
568
- inspect: DurableObjectDefinition.method({ success: S.String }),
569
- });
570
-
571
- const ResearchWorkspaceLive = ResearchWorkspace.make(ComputerWorkspace.ComputerWorkspace.layer, {
572
- rpc: {
573
- inspect: () =>
574
- Effect.gen(function* () {
575
- const workspace = yield* ComputerWorkspace.ComputerWorkspace;
576
-
577
- // Keep reads bounded when returning their contents over RPC.
578
- const readme = yield* workspace.readFile("/repo/README.md", {
579
- byteLength: 256 * 1024,
580
- });
581
- const commits = yield* workspace.git.log({ maxCount: 20 });
582
- const run = yield* workspace.execCollect("git status --short", {
583
- cwd: "/repo",
584
- timeoutMillis: 30_000,
585
- });
586
-
587
- return `${commits.length}:${run.exitCode}:${readme.length}`;
588
- }),
589
- },
590
- });
591
-
592
- const HostedResearchWorkspace = withComputerWorkspace(ResearchWorkspaceLive, (_, state, env) => ({
593
- storage: state.storage,
594
- sessionId: state.id.toString(),
595
- gitIdentity: { name: "Research Agent", email: "agent@example.test" },
596
- artifacts: { binding: env.ARTIFACTS },
597
- shell: {
598
- loader: env.WORKER_LOADER,
599
- hostBinding: "RESEARCH_WORKSPACE",
600
- hostId: state.id.toString(),
601
- executionContext: state,
602
- commands: [shellCurl],
603
- egress: { mode: "none" },
604
- },
605
- }));
606
-
607
- export class ResearchWorkspaceDurableObject extends HostedResearchWorkspace {}
608
- ```
609
-
610
- Serialize checkout mutations such as `clone`, `fetch`, and reset/checkout
611
- sequences with an Effect `Semaphore` when several RPC methods can update the
612
- same working tree concurrently. Reads can remain concurrent.
613
-
614
- The corresponding Wrangler configuration needs SQLite-backed Durable Object
615
- storage. Worker-shell execution additionally requires the `experimental` flag,
616
- a Worker Loader binding, the loopback namespace binding named by `hostBinding`,
617
- and the exact `WorkspaceServiceProxy` export shown above:
618
-
619
- ```jsonc
620
- {
621
- "compatibility_flags": ["nodejs_compat", "experimental"],
622
- "worker_loaders": [{ "binding": "WORKER_LOADER" }],
623
- "artifacts": [{ "binding": "ARTIFACTS", "namespace": "default" }],
624
- "durable_objects": {
625
- "bindings": [
626
- {
627
- "name": "RESEARCH_WORKSPACE",
628
- "class_name": "ResearchWorkspaceDurableObject",
629
- },
630
- ],
631
- },
632
- "migrations": [
633
- {
634
- "tag": "v1",
635
- "new_sqlite_classes": ["ResearchWorkspaceDurableObject"],
636
- },
637
- ],
638
- }
639
- ```
640
-
641
- Omit `shell` and the Worker Loader configuration when only the SQLite
642
- filesystem and Git client are needed. Supplying `artifacts` also installs the
643
- session-scoped client at `workspace.artifacts` and makes the in-shell
644
- `artifacts` command use the same upstream repository-prefixing convention.
645
-
646
- Workers can use that session isolation without a Durable Object or workspace:
647
-
648
- ```ts
649
- import { Effect } from "effect";
650
- import { Artifacts } from "effect-cf";
651
- import * as ComputerArtifacts from "effect-cf/computer-artifacts";
652
-
653
- export const createSessionRepo = (binding: Artifacts.ArtifactsBinding, sessionId: string) =>
654
- Effect.gen(function* () {
655
- const artifacts = yield* ComputerArtifacts.makeClient(binding, sessionId);
656
- const created = yield* artifacts.create("build-cache", {
657
- description: "CI artifacts",
658
- });
659
- const token = yield* artifacts.createToken("build-cache", "read", 3600);
660
-
661
- return { remote: created.remote, token: token.plaintext };
662
- });
663
- ```
664
-
665
- Repository prefixing, list filtering, and the argv API are delegated directly
666
- to `@cloudflare/computer/artifacts`, keeping this API and the in-shell command
667
- in agreement.
668
-
669
- ## Hyperdrive Example
670
-
671
- Hyperdrive tags expose the binding `connectionString` directly on the yielded service.
672
-
673
- ```ts
674
- import { Effect } from "effect";
675
- import { Hyperdrive } from "effect-cf";
676
-
677
- class AppDatabase extends Hyperdrive.Tag<AppDatabase>()("AppDatabase") {}
678
-
679
- export const AppDatabaseLayer = AppDatabase.layer({ binding: "HYPERDRIVE" });
680
-
681
- export const databaseUrl = Effect.gen(function* () {
682
- const hyperdrive = yield* AppDatabase;
683
- return hyperdrive.connectionString;
684
- });
685
- ```
686
-
687
- If your Worker uses Postgres via `@effect/sql-pg`, install that driver and use the optional subpath integration:
688
-
689
- ```ts
690
- import { Hyperdrive } from "effect-cf";
691
- import * as HyperdrivePg from "effect-cf/hyperdrive-pg";
692
-
693
- class AppDatabase extends Hyperdrive.Tag<AppDatabase>()("AppDatabase") {}
694
-
695
- export const SqlLive = HyperdrivePg.layer(AppDatabase, { binding: "HYPERDRIVE" });
696
- ```
697
-
698
- The Postgres integration builds an Effect `PgClient` from Hyperdrive's generated connection string with `PgClient.makeClient`. It intentionally does not expose app-side pool options; Hyperdrive manages the underlying database pool.
699
-
700
- ## Images Example
701
-
702
- Images tags expose `info`, `input`, optional hosted image operations, and composable transform/draw steps.
703
-
704
- ```ts
705
- import { Effect } from "effect";
706
- import { Images } from "effect-cf";
707
-
708
- class AvatarImages extends Images.Tag<AvatarImages>()("AvatarImages") {}
709
-
710
- export const AvatarImagesLayer = AvatarImages.layer({ binding: "IMAGES" });
711
-
712
- export const resizeAvatar = (image: Images.ImageInputValue) =>
713
- Effect.gen(function* () {
714
- const images = yield* AvatarImages;
715
- const result = yield* images.process(
716
- Images.transform(Images.empty, { width: 256, height: 256 }),
717
- {
718
- stream: image,
719
- outputOptions: { format: "image/webp" },
720
- },
721
- );
722
-
723
- return yield* result.response();
724
- });
725
- ```
726
-
727
- ## Cloudflare runtime pin
728
-
729
- `effect-cf` develops and tests against a fixed Cloudflare Workers runtime stack. Keep consumer
730
- `compatibility_date` and optional `@cloudflare/workers-types` peers at or above this pin:
731
-
732
- | Pin | Version |
733
- | ------------------------------------------- | -------------- |
734
- | workerd | `1.20260820.1` |
735
- | `@cloudflare/workers-types` (optional peer) | `5.20260820.1` |
736
- | wrangler (repo catalog / examples) | `4.125.0` |
737
- | Recommended `compatibility_date` | `2026-08-20` |
738
-
739
- The date in the `workerd` / `@cloudflare/workers-types` version is the API surface this package wraps.
740
- Repo catalog pins live in the root `package.json` `catalog` field (`@cloudflare/workers-types`, `wrangler`).
741
- `wrangler@4.125.0` pulls `workerd@1.20260820.1` and peers `@cloudflare/workers-types@^5.20260820.1`.
742
-
743
- ## Email Example
744
-
745
- Email tags expose Cloudflare Email Service `send_email` bindings as Effect-wrapped `send(...)` operations. Structured messages are validated against documented Email Sending limits before reaching the binding, and Cloudflare's `E_*` error codes are surfaced on `EmailOperationError.code`.
746
-
747
- ```ts
748
- import { Effect } from "effect";
749
- import { Email } from "effect-cf";
750
-
751
- class TransactionalEmail extends Email.Tag<TransactionalEmail>()("TransactionalEmail") {}
752
-
753
- export const TransactionalEmailLayer = TransactionalEmail.layer({ binding: "EMAIL" });
754
-
755
- export const sendWelcomeEmail = (to: string) =>
756
- Effect.gen(function* () {
757
- const email = yield* TransactionalEmail;
758
-
759
- return yield* email.send({
760
- from: { name: "Example", email: "team@example.com" },
761
- to,
762
- subject: "Welcome to Example",
763
- text: "Welcome to Example",
764
- html: "<p>Welcome to Example</p>",
765
- });
766
- });
767
- ```
768
-
769
- Onboard the sending domain under **Compute > Email Service > Email Sending**, then declare the binding in the consuming Worker's `wrangler.jsonc`:
770
-
771
- ```jsonc
772
- {
773
- "send_email": [{ "name": "EMAIL" }],
774
- }
775
- ```
776
-
777
- Messages that violate a documented limit fail with `EmailValidationError` carrying every violation, without calling the binding. Use `layer({ binding, send: { validate: false } })` to skip validation and let Cloudflare reject the message instead.
778
-
779
- Cloudflare's total message size limit is exposed as `Email.sendLimits.maxMessageBytes` but is not enforced, because the encoded MIME size is only known once Cloudflare composes the message. Oversized messages fail at the binding with `E_CONTENT_TOO_LARGE`.
780
-
781
- See [`examples/email/README.md`](../../examples/email/README.md) for domain onboarding, binding restrictions, and typed recovery from `EmailValidationError` and `EmailOperationError`.
782
-
783
- ## Analytics Engine Example
784
-
785
- Analytics Engine tags expose Cloudflare dataset bindings as Effect-wrapped `writeDataPoint(...)` operations. Writes use Cloudflare's native non-blocking runtime API and are validated against Workers Analytics Engine limits before reaching the binding.
786
-
787
- ```ts
788
- import { Effect } from "effect";
789
- import { AnalyticsEngine } from "effect-cf";
790
-
791
- class RequestAnalytics extends AnalyticsEngine.Tag<RequestAnalytics>()("RequestAnalytics") {}
792
-
793
- export const RequestAnalyticsLayer = RequestAnalytics.layer({
794
- binding: "REQUEST_ANALYTICS",
795
- write: {
796
- onInvalid: "error",
797
- },
17
+ export default Worker.make(Layer.empty, {
18
+ fetch: Effect.sync(() => new Response("Hello")),
798
19
  });
799
-
800
- export const recordPageView = (request: Request) =>
801
- Effect.gen(function* () {
802
- const analytics = yield* RequestAnalytics;
803
- const url = new URL(request.url);
804
-
805
- yield* analytics.writeDataPoint({
806
- indexes: [url.hostname],
807
- blobs: [url.pathname, request.headers.get("cf-connecting-country") ?? "unknown"],
808
- doubles: [1],
809
- });
810
-
811
- yield* analytics.writeDataPoints(
812
- [
813
- {
814
- indexes: [url.hostname],
815
- blobs: [url.pathname, "page-view"],
816
- doubles: [1],
817
- },
818
- ],
819
- { onInvalid: "drop" },
820
- );
821
- });
822
20
  ```
823
21
 
824
- Define the dataset binding in the consuming Worker's `wrangler.jsonc` with `analytics_engine_datasets`, then provide `RequestAnalytics.layer({ binding: "REQUEST_ANALYTICS" })` from the Worker layer. Invalid writes fail with `AnalyticsEngineWriteValidationError` by default. Use `write: { onInvalid: "drop" }` on the layer or `{ onInvalid: "drop" }` per call when dropping invalid points is preferable.
825
-
826
- Analytics Engine query tags wrap Cloudflare's HTTP SQL API. Configuration can stay in Effect `Config`, including a redacted API token, the outbound transport is an Effect `HttpClient` dependency, and rows can be decoded with Effect schemas at the query boundary. Use `layerFetchConfig(...)` as shorthand when the platform fetch-backed client is enough.
22
+ `Worker.make` owns the Effect runtime. Pass application services as its layer; use `Worker.NativeRequest` inside the handler to read the request.
827
23
 
828
- ```ts
829
- import { Effect, Layer, Schema as S } from "effect";
830
- import { FetchHttpClient } from "effect/unstable/http";
831
- import { AnalyticsEngine, WorkerConfig } from "effect-cf";
832
-
833
- class AnalyticsQuery extends AnalyticsEngine.QueryTag<AnalyticsQuery>()("AnalyticsQuery") {}
834
-
835
- export const AnalyticsQueryLayer = AnalyticsQuery.layerConfig(
836
- AnalyticsEngine.queryConfig({
837
- accountId: WorkerConfig.string("CLOUDFLARE_ACCOUNT_ID"),
838
- apiToken: WorkerConfig.redacted("CLOUDFLARE_API_TOKEN"),
839
- }),
840
- ).pipe(Layer.provide(FetchHttpClient.layer));
841
-
842
- const PageView = S.Struct({
843
- path: S.String,
844
- views: S.Number,
845
- });
846
-
847
- export const topPages = Effect.gen(function* () {
848
- const analytics = yield* AnalyticsQuery;
849
-
850
- return yield* analytics.queryRows(
851
- PageView,
852
- `
853
- SELECT blob1 AS path, SUM(_sample_interval) AS views
854
- FROM request_metrics
855
- GROUP BY path
856
- ORDER BY views DESC
857
- LIMIT 10
858
- `,
859
- );
860
- });
861
- ```
862
-
863
- ## Workflow Example
24
+ ## Bindings
864
25
 
865
- Workflow definitions type the payload and result. Runtime handlers can access `Workflow.WorkflowEvent`, use durable `Workflow.step(...)`, and use normal binding services inside steps.
26
+ Define a service, connect it to a Wrangler binding, then yield it in your program.
866
27
 
867
28
  ```ts
868
- import { Effect, Layer, Schema as S } from "effect";
869
- import { Workflow } from "effect-cf";
29
+ import { Effect, Schema } from "effect";
30
+ import { Kv } from "effect-cf";
870
31
 
871
- class ExportWorkflow extends Workflow.Tag<ExportWorkflow>()("ExportWorkflow", {
872
- payload: S.Struct({ segmentId: S.String }),
873
- result: S.Struct({ objectKey: S.String }),
32
+ class Settings extends Kv.Tag<Settings>()("Settings", {
33
+ key: Schema.String,
34
+ value: Schema.String,
874
35
  }) {}
875
36
 
876
- export const ExportWorkflowEntrypoint = ExportWorkflow.make(Layer.empty, {
877
- run: (payload) =>
878
- Effect.gen(function* () {
879
- const objectKey = yield* Workflow.step(
880
- "write-export",
881
- Effect.succeed(`exports/${payload.segmentId}.json`),
882
- );
883
-
884
- return { objectKey };
885
- }),
886
- });
887
-
888
- export const ExportWorkflowLayer = ExportWorkflow.layer({ binding: "EXPORT_WORKFLOW" });
889
- ```
890
-
891
- Provide `ExportWorkflow.layer({ binding: "EXPORT_WORKFLOW" })`, then use `const workflow = yield* ExportWorkflow` or the static `ExportWorkflow.create(...)`, `createBatch(...)`, and `get(...)` helpers to start and inspect instances.
892
-
893
- In definition-backed workflows, the `payload` argument is the typed decoded payload and is the source of truth. `Workflow.WorkflowEvent.payload` is also re-provided decoded for convenience; `Workflow.WorkflowEvent.raw.payload` remains the native Cloudflare event payload.
894
-
895
- ## Durable Object WebSockets
896
-
897
- Use `initialize` for work that should run each time Cloudflare loads a Durable
898
- Object instance into memory. Yield `state.blockConcurrencyWhile(...)` when
899
- later events should wait for initialize to finish. If work should happen only once
900
- for a Durable Object id, store a sentinel in Durable Object storage.
901
-
902
- ```ts
903
- export const RoomLive = DurableObject.make(layer, {
904
- initialize: Effect.gen(function* () {
905
- const state = yield* DurableObjectState.DurableObjectState;
906
- yield* state.blockConcurrencyWhile(
907
- Effect.gen(function* () {
908
- yield* state.storage.put("loadedAt", Date.now());
909
- }),
910
- );
911
- }),
912
- fetch,
913
- });
914
- ```
915
-
916
- Durable Object application sockets should use the hibernation-compatible state API. Accept sockets with `DurableObjectWebSocket.acceptUpgrade(...)`; do not call `server.accept()` or attach native `message` listeners in application code.
917
-
918
- ```ts
919
- import { Effect, Schema as S } from "effect";
920
- import { DurableObject, DurableObjectState, DurableObjectWebSocket, Worker } from "effect-cf";
921
-
922
- const ConnectionAttachment = S.Struct({
923
- id: S.String,
924
- roomId: S.String,
925
- });
926
-
927
- const Attachments = DurableObjectWebSocket.attachment(ConnectionAttachment);
928
-
929
- export const fetch = Effect.gen(function* () {
930
- const request = yield* Worker.NativeRequest;
931
-
932
- if (!Worker.isWebSocketUpgrade(request)) {
933
- return new Response("Expected WebSocket upgrade", { status: 426 });
934
- }
935
-
936
- const upgrade = yield* DurableObjectWebSocket.acceptUpgrade({ tags: ["room:general"] });
937
- yield* Attachments.serialize(upgrade.server, {
938
- id: crypto.randomUUID(),
939
- roomId: "general",
940
- });
941
- yield* upgrade.server.send(JSON.stringify({ type: "ready" }));
942
-
943
- return upgrade.response;
944
- });
945
- ```
946
-
947
- `DurableWebSocket` keeps the native socket available as `socket.raw`, while `send`, `close`, `serializeAttachment`, and `deserializeAttachment` return typed `Effect` failures. Use `state.getWebSockets(tag)` to retrieve wrapped sockets for broadcast and rehydration.
948
-
949
- `DurableObject.make` lifecycle handlers receive wrapped sockets automatically:
950
-
951
- ```ts
952
- export const RoomLive = DurableObject.make(layer, {
953
- webSocketMessage: (socket, message) =>
954
- Effect.gen(function* () {
955
- yield* socket.send(message);
956
- }),
957
- });
958
- ```
959
-
960
- Schema-backed attachments can rehydrate hibernated sockets:
961
-
962
- ```text
963
- const restored = yield* Attachments.rehydrate({
964
- tag: "room:general",
965
- onInvalid: "ignore-and-close",
966
- });
967
-
968
- for (const { socket, attachment } of restored) {
969
- yield* socket.send(`restored:${attachment.id}`).pipe(Effect.ignore);
970
- }
971
- ```
972
-
973
- Worker-to-Durable-Object forwarding should stay native so WebSocket upgrade responses are preserved:
974
-
975
- ```text
976
- if (Worker.isWebSocketUpgrade(request)) {
977
- const rooms = yield* ChatRoom;
978
- return yield* rooms.byName(roomId).fetch(request);
979
- }
980
- ```
981
-
982
- ### Hibernation-aware Effect RPC WebSockets
983
-
984
- `DurableObjectRpcWebSocket.layer(...)` adapts Effect RPC to Cloudflare's hibernatable WebSocket API. Build the layer in the Durable Object runtime and forward all three WebSocket lifecycle events:
985
-
986
- ```ts
987
- import { Effect, Layer } from "effect";
988
- import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
989
- import { DurableObject, DurableObjectRpcWebSocket, Worker } from "effect-cf";
990
-
991
- const RpcLive = RpcServer.layer(MyRpcs).pipe(
992
- Layer.provideMerge(DurableObjectRpcWebSocket.layer({ tag: "my-rpc" })),
993
- Layer.provide(MyRpcHandlers),
994
- Layer.provide(RpcSerialization.layerJson),
995
- );
996
-
997
- const MyObjectLive = DurableObject.make(RpcLive, {
998
- fetch: Effect.gen(function* () {
999
- const request = yield* Worker.NativeRequest;
1000
-
1001
- if (!Worker.isWebSocketUpgrade(request)) {
1002
- return new Response("Expected WebSocket upgrade", { status: 426 });
1003
- }
1004
-
1005
- const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
1006
- const upgrade = yield* transport.acceptUpgrade({
1007
- tags: ["room:general"],
1008
- attachment: { roomId: "general" },
1009
- });
1010
-
1011
- return upgrade.response;
1012
- }),
1013
- webSocketMessage: (socket, message) =>
1014
- Effect.gen(function* () {
1015
- const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
1016
-
1017
- yield* transport.message(socket, message);
1018
- }),
1019
- webSocketClose: (socket) =>
1020
- Effect.gen(function* () {
1021
- const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
37
+ const SettingsLive = Settings.layer({ binding: "SETTINGS" });
1022
38
 
1023
- yield* transport.close(socket);
1024
- }),
1025
- webSocketError: (socket, cause) =>
1026
- Effect.gen(function* () {
1027
- const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
39
+ const greeting = Effect.gen(function* () {
40
+ const settings = yield* Settings;
1028
41
 
1029
- yield* transport.error(socket, cause);
1030
- }),
42
+ return yield* settings.get("greeting");
1031
43
  });
1032
-
1033
- export class MyDurableObject extends MyObjectLive {}
1034
44
  ```
1035
45
 
1036
- `DurableObject.make` builds a new managed runtime whenever Cloudflare constructs or reconstructs the object. During that build, the RPC layer calls `getWebSockets(tag)` and registers tagged sockets before the triggering lifecycle handler runs. A completed finite call can therefore be followed by idle hibernation and another finite call on the same physical socket.
1037
-
1038
- This is a hibernation-aware RPC transport, not a durable RPC runtime. The adapter stores versioned state under the `attachmentKey` namespace, which defaults to `effectCloudflareRpcClientId`, and shallow-merges it with application-owned object fields. Its RPC tag is added alongside application tags. Application code that later replaces an attachment must preserve the reserved namespace; the adapter requires object-shaped attachments. Keep resume descriptors compact because Cloudflare caps the entire serialized attachment, including application fields, at 16,384 bytes. Legacy numeric client-ID metadata is migrated when read.
1039
-
1040
- Each non-notification request stays marked pending until its terminal `Exit` is sent or the request is interrupted. If a new constructor finds a pending non-resumable request, it quarantines the socket and closes it with WebSocket code `1012` before dispatching the event that woke the object. Effect's stock socket protocol reports this as an `RpcClientError` containing `SocketCloseError`, so the affected call or stream fails instead of hanging. The socket transport may reconnect for later calls, but the adapter never replays a non-resumable request. Retrying a mutation requires application-level idempotency or deduplication. Notifications are also not durable and have no delivery acknowledgement.
1041
-
1042
- The default `heartbeat: "auto-response"` policy installs Cloudflare's exact text Ping/Pong auto-response while at least one RPC socket exists and no non-resumable RPC operation is active. The stock Effect client's five-second Ping therefore does not run Durable Object JavaScript while the socket is idle or while an explicitly resumable subscription waits for data. The adapter creates no timer. A non-resumable call or stream removes the global auto-response so a Ping wakes a newly reconstructed object and triggers its pending-operation reset. Cloudflare provides only one auto-response pair per Durable Object, across every accepted socket. Use `"passthrough"` if unrelated WebSocket protocols share the same object.
1043
-
1044
- Use `heartbeat: "passthrough"` for a non-text serialization or when the application owns a different auto-response pair. That mode does not alter the pair; the application is responsible for ensuring a lost pending operation still wakes and resets. With the default policy, a conflicting application pair fails layer construction instead of being overwritten.
46
+ Declare `SETTINGS` in `wrangler.jsonc` and pass `SettingsLive` to `Worker.make`. Other bindings use the same tag/layer pattern.
1045
47
 
1046
- Streams are non-resumable unless their RPC tag matches a declaration passed in `resumableStreams`. Constructor recreation resets an ordinary active stream with code `1012`.
48
+ The [counter example](https://github.com/danieljvdm/effect-cf/tree/main/examples/counter) shows `DurableObject.Tag`, storage, a typed RPC call, and the complete Wrangler configuration.
1047
49
 
1048
- An explicitly resumable stream persists its original Effect request ID, RPC tag, application subscription key, compact resume descriptor, and last application checkpoint in the socket attachment. Each declaration supplies `resumeDescriptorSchema` and `checkpointSchema`; a new constructor validates both persisted values before it rebuilds anything. Invalid state resets that socket with code `1012` instead of reaching application reconstruction. Valid state becomes an encoded request written through the new `RpcServer.Protocol` with the original request ID. Effect runs request decoding, middleware, authorization, and the handler again. The physical socket stays open, so the stock Effect client routes later `Chunk` messages into the existing client `Stream`.
50
+ ## Native RPC tracing
1049
51
 
1050
- This reconstructs a declared logical subscription. It does not restore the original Effect fiber, scope, stream, queue, or middleware context. The application must keep a durable event log and rebuild the stream after its cursor. Delivery is at least once. If hibernation lands after a chunk reaches the client but before its checkpoint is stored, the rebuilt handler emits that chunk again. Apply events idempotently by cursor, then call a finite checkpoint RPC. Retention, cursor expiry, snapshots, and resync remain application concerns. This transport does not provide exactly-once delivery or generic stream resumption.
52
+ `call()`, `scopedCall()`, and definition methods create one CLIENT span named `binding/method`, covering argument encoding, the native RPC wait, and success decoding. Raw `rpc()` retains Cloudflare's pipelined result without creating a span. Wrap its complete lifetime with `RpcTracing.withRpcClientSpan` when tracing raw calls.
1051
53
 
1052
- The declaration also gives the transport a stable token for each encoded chunk value. A cursor/token must identify one logical event for the full lifetime of that subscription. Distinct events must not share it; a replay of the same event reuses it. The adapter ignores stock request-ID-only stream `Ack` messages for resumable requests. It persists the matching explicit checkpoint before releasing the current server chunk latch, and an acknowledgement left over from an earlier activation cannot release a later chunk. If attachment serialization fails, `checkpoint` fails with `DurableWebSocketAttachmentError` and the transport resets the socket with code `1012`; it never releases or sends a chunk whose replay state was not stored. Interrupt and terminal `Exit` remove the persisted descriptor.
1053
-
1054
- The compiled [`ResumableEventLog.example.ts`](../../examples/todo-rpc-ws/durable-objects/todo-store/src/ResumableEventLog.example.ts) defines a standard `RpcGroup`, cursor-backed Durable Object SQL event log, `RpcServer.layer`, resumable declaration, checkpoint handler using `Rpc.ServerClient.id`, and complete constructor/event wiring. Its client-side ordering is the important part:
1055
-
1056
- ```ts
1057
- import { Effect, Stream } from "effect";
1058
-
1059
- client.SubscribeEvents({ subscriptionKey, topic, after }).pipe(
1060
- Stream.runForEach((event) =>
1061
- applyEventIfNew(event).pipe(
1062
- Effect.andThen(
1063
- client.CheckpointSubscription({
1064
- subscriptionKey,
1065
- cursor: event.cursor,
1066
- }),
1067
- ),
1068
- ),
1069
- ),
1070
- );
1071
- ```
54
+ Live parent propagation requires both `Contract.layer({ binding: "COUNTERS", rpcTracing: true })` on the client and `rpcTracing: { service: "COUNTERS" }` in the receiver's `make` options. Namespace and service binding clients accept the same boolean option. Enable receivers before clients. Disabled clients preserve argument lists exactly; receivers strip only a valid trailing `effect-cf/RpcTraceContext/v1` argument after opting in. Opting in reserves that complete tagged shape in the final argument position, so do not use it there as a domain argument, including during receiver-first rollout.
1072
55
 
1073
- Checkpoint only after `applyEventIfNew` commits. A replayed cursor must make that function a no-op. `CheckpointSubscription` is an ordinary non-resumable finite RPC, so make it idempotent and safe for the client to retry. Map `DurableWebSocketAttachmentError` into that RPC's declared error channel. The checkpoint call must be able to run while stream handlers are active. Keep Effect's default unbounded RPC server concurrency, or set a bound above the maximum number of simultaneously open streams so checkpoint calls retain capacity. If the activation disappears after checkpoint persistence but before that RPC sends its `Exit`, the socket resets with `1012`. The client reconnects and starts a new subscription after its durably applied cursor. Register the same declaration on every constructor activation and keep its `id`, schemas, and checkpoint token semantics stable for stored attachments.
56
+ Applications own SERVER spans. Override the exported `DurableObject.RunSymbol` or `Worker.RunSymbol`, wrap the effect with `RpcTracing.withRpcServerSpan(effect, options.rpc)` when `options.rpc` exists, and call `super` with the original options. See the [typed receiver example](https://github.com/danieljvdm/effect-cf/blob/main/packages/effect-cf/tests/fixtures/durable-object-consumer.ts). The receiver installs the validated parent before instrumentation and event-layer setup. No additional layer input is required.
1074
57
 
1075
- `accept(socket, tags)` remains available for a manually created, not-yet-accepted server socket. Prefer `transport.acceptUpgrade(...)` for new upgrades so the socket is accepted exactly once. Use the general `DurableObjectWebSocket` module instead for rooms, presence, broadcasts, or other non-RPC protocols.
58
+ `RunOptions.event` identifies the native event before work starts. `RunOptions.rpc` includes `service`, `method`, native `args`, and the validated `parent`. Its `decodedArgs` becomes available after definition decoding succeeds, before the handler runs. Never log these arguments. Span helpers record stable RPC attributes and failure status without error payloads; original typed failures still reach the caller.
1076
59
 
1077
- ## WebTransport and HTTP/3
60
+ This metadata belongs only to the live native call. Do not store it in domain envelopes, alarms, queues, or WebSocket attachments, or reuse it for resumed work. Sampling and exporter configuration remain application choices.
1078
61
 
1079
- Cloudflare Workers cannot accept inbound WebTransport sessions today: workerd contains no QUIC/HTTP-3 stack, there is no `WebSocketPair`-style session API, and the maintainers state the feature "is not currently on our priority list" ([cloudflare/workerd#6451](https://github.com/cloudflare/workerd/issues/6451), [discussion #6454](https://github.com/cloudflare/workerd/discussions/6454)). Inbound HTTP/3 is a zone-level setting — the edge terminates QUIC and the Worker still receives an ordinary `fetch` Request. Workers and Durable Objects can accept inbound WebSocket connections; Durable Objects additionally support the hibernation WebSocket API.
1080
-
1081
- The `WebTransport` module gives that boundary a typed shape rather than pretending otherwise:
1082
-
1083
- ```ts
1084
- import { Effect, Option } from "effect";
1085
- import { WebTransport } from "effect-cf";
1086
-
1087
- const fetch = Effect.fn("fetch")(function* (request: Request) {
1088
- // Decoded metadata about the browser→edge hop (HTTP/3 does not reach you).
1089
- const transport = WebTransport.inboundTransport(request);
1090
- const viaHttp3 = Option.match(transport, {
1091
- onNone: () => false,
1092
- onSome: WebTransport.isHttp3,
1093
- });
1094
-
1095
- // Feature-detected runtime capabilities: { inboundSessions: false, ... }.
1096
- const capabilities = yield* WebTransport.capabilities;
1097
-
1098
- // Explicit typed boundary where a future inbound session API would slot in.
1099
- // yield* WebTransport.inboundSessionsUnsupported;
1100
-
1101
- return Response.json({ viaHttp3, capabilities });
1102
- });
1103
- ```
62
+ ## API
1104
63
 
1105
- For clients that prefer WebTransport where it exists and fall back to Cloudflare-supported WebSockets, see the companion [`effect-webtransport`](../effect-webtransport) package's `Fallback` module — `examples/todo-rpc-ws` wires it up end to end.
64
+ See the [exports](src/index.ts) and [tests](https://github.com/danieljvdm/effect-cf/tree/main/packages/effect-cf/tests) for the remaining APIs.
1106
65
 
1107
- ## License
66
+ Optional integrations have separate imports: `effect-cf/hyperdrive-pg`, `effect-cf/computer-workspace`, `effect-cf/computer-artifacts`, `effect-cf/computer-workspace-host`, `effect-cf/sandbox`, and `effect-cf/vitest`. Install the matching SDK or driver listed in [peerDependencies](package.json). Computer Git operations also require `@platformatic/vfs`.
1108
67
 
1109
- MIT
68
+ [Changelog](CHANGELOG.md) · [MIT license](LICENSE)