effect-cf 0.33.0 → 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.
- package/CHANGELOG.md +12 -0
- package/README.md +31 -1079
- package/dist/{Artifacts-DnHQA-Ax.d.mts → Artifacts-B3r3RuTB.d.mts} +1 -48
- package/dist/{Artifacts-CSIk1Oml.mjs → Artifacts-CBxWGeZB.mjs} +2 -7
- package/dist/{Binding-pfilNJmP.mjs → Binding-C82jRhdr.mjs} +1 -2
- package/dist/{Binding-xJJJPmiD.d.mts → Binding-DKcLrQCM.d.mts} +0 -45
- package/dist/ComputerArtifacts.d.mts +1 -3
- package/dist/ComputerArtifacts.mjs +1 -2
- package/dist/ComputerWorkspace.d.mts +1 -10
- package/dist/ComputerWorkspace.mjs +1 -2
- package/dist/ComputerWorkspaceHost.d.mts +1 -9
- package/dist/{ContainerNamespace-CFjiLw8b.d.mts → ContainerNamespace-DbLaeFhN.d.mts} +1 -14
- package/dist/{DurableObjectState-BpRjz6-q.d.mts → DurableObjectState-CrEunfQ-.d.mts} +82 -252
- package/dist/{DurableObjectState-CVLLse69.mjs → DurableObjectState-De4ZojhY.mjs} +1 -39
- package/dist/{ErrorMessage-BM0K-Nik.mjs → ErrorMessage-Byx2OgNd.mjs} +0 -1
- package/dist/{Hyperdrive-z-9fFz15.d.mts → Hyperdrive-HrwFlZ6x.d.mts} +1 -4
- package/dist/HyperdrivePg.d.mts +2 -11
- package/dist/HyperdrivePg.mjs +0 -8
- package/dist/Sandbox.d.mts +2 -32
- package/dist/Sandbox.mjs +2 -11
- package/dist/Vitest.d.mts +3 -31
- package/dist/Vitest.mjs +2 -17
- package/dist/{WorkflowBinding-lBR4_-op.d.mts → WorkflowBinding-BumVQTuW.d.mts} +2 -5
- package/dist/index.d.mts +34 -480
- package/dist/index.mjs +174 -312
- package/package.json +1 -1
- package/src/AiGateway.ts +0 -6
- package/src/AnalyticsEngine.ts +0 -9
- package/src/Artifacts.ts +0 -47
- package/src/Binding.ts +0 -8
- package/src/BrowserRendering.ts +0 -4
- package/src/Cache.ts +0 -19
- package/src/CloudflareOtlp.ts +3 -39
- package/src/ComputerArtifacts.ts +0 -2
- package/src/ComputerWorkspace.ts +0 -9
- package/src/ComputerWorkspaceHost.ts +0 -8
- package/src/ContainerNamespace.ts +0 -13
- package/src/D1.ts +0 -22
- package/src/DurableObject.ts +45 -25
- package/src/DurableObjectAlarm.ts +6 -210
- package/src/DurableObjectDefinition.ts +8 -20
- package/src/DurableObjectNamespace.ts +57 -112
- package/src/DurableObjectRpcWebSocket.ts +3 -19
- package/src/DurableObjectSqlite.ts +0 -12
- package/src/DurableObjectState.ts +0 -18
- package/src/DurableObjectStorage.ts +2 -35
- package/src/DurableObjectWebSocket.ts +0 -31
- package/src/Email.ts +0 -12
- package/src/Environment.ts +0 -37
- package/src/Hyperdrive.ts +0 -3
- package/src/HyperdrivePg.ts +0 -9
- package/src/Images.ts +0 -5
- package/src/Kv.ts +0 -15
- package/src/QueueBinding.ts +0 -2
- package/src/R2.ts +0 -5
- package/src/RpcDefinition.ts +0 -2
- package/src/RpcTracing.ts +133 -0
- package/src/Sandbox.ts +0 -30
- package/src/ServiceBinding.ts +48 -125
- package/src/Vectorize.ts +0 -5
- package/src/Vitest.ts +0 -28
- package/src/WebTransport.ts +0 -15
- package/src/Worker.ts +36 -57
- package/src/WorkerDefinition.ts +5 -14
- package/src/WorkersAi.ts +0 -4
- package/src/Workflow.ts +0 -5
- package/src/WorkflowBinding.ts +0 -3
- package/src/index.ts +1 -0
- package/src/internal/Entrypoint.ts +23 -4
- package/src/internal/ErrorMessage.ts +0 -1
- package/src/internal/RpcInvocation.ts +18 -1
- package/src/internal/Runtime.ts +12 -7
- package/src/internal/WorkerContext.ts +0 -11
package/README.md
CHANGED
|
@@ -1,1116 +1,68 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cloudflare entrypoints and bindings as Effect services.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`effect-cf` targets Effect `^4.0.0-beta.105` and Cloudflare workerd `1.20260825.1`
|
|
8
|
-
(`@cloudflare/workers-types@5.20260825.1`, recommended `compatibility_date` `2026-08-25`).
|
|
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
|
-
);
|
|
5
|
+
```sh
|
|
6
|
+
npm install effect-cf "effect@^4.0.0-rc.110"
|
|
130
7
|
```
|
|
131
8
|
|
|
132
|
-
|
|
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
|
-
});
|
|
9
|
+
The repository tests against workerd `1.20260825.1` and `@cloudflare/workers-types@5.20260825.1`. Use `compatibility_date: "2026-08-25"` in Wrangler.
|
|
151
10
|
|
|
152
|
-
|
|
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
|
-
);
|
|
222
|
-
```
|
|
223
|
-
|
|
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.
|
|
227
|
-
|
|
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
15
|
import { Worker } from "effect-cf";
|
|
234
16
|
|
|
235
|
-
export default Worker.make(Layer.empty,
|
|
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();
|
|
17
|
+
export default Worker.make(Layer.empty, {
|
|
18
|
+
fetch: Effect.sync(() => new Response("Hello")),
|
|
266
19
|
});
|
|
267
20
|
```
|
|
268
21
|
|
|
269
|
-
|
|
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`.
|
|
22
|
+
`Worker.make` owns the Effect runtime. Pass application services as its layer; use `Worker.NativeRequest` inside the handler to read the request.
|
|
276
23
|
|
|
277
|
-
|
|
278
|
-
Durable Object without retrying the alarm that triggered the reset. The option
|
|
279
|
-
has no effect outside an alarm handler and defaults to retrying the alarm.
|
|
24
|
+
## Bindings
|
|
280
25
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
Container entrypoints remain owned by `@cloudflare/containers`; `effect-cf`
|
|
284
|
-
wraps the Container namespace used by a calling Worker.
|
|
26
|
+
Define a service, connect it to a Wrangler binding, then yield it in your program.
|
|
285
27
|
|
|
286
28
|
```ts
|
|
287
|
-
import {
|
|
288
|
-
import {
|
|
289
|
-
import { ContainerNamespace, Worker } from "effect-cf";
|
|
290
|
-
|
|
291
|
-
export class RendererContainer extends Container {
|
|
292
|
-
defaultPort = 8080;
|
|
293
|
-
sleepAfter = "10m";
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
class Renderers extends ContainerNamespace.Tag<Renderers>()("Renderers") {}
|
|
297
|
-
|
|
298
|
-
const RenderersLive = Renderers.layer({ binding: "RENDERERS" });
|
|
29
|
+
import { Effect, Schema } from "effect";
|
|
30
|
+
import { Kv } from "effect-cf";
|
|
299
31
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const renderer = Renderers.byName(new URL(request.url).pathname);
|
|
304
|
-
|
|
305
|
-
yield* renderer.startAndWaitForPorts({ ports: 8080 });
|
|
306
|
-
return yield* renderer.fetch(switchPort(request, 8080));
|
|
307
|
-
}),
|
|
308
|
-
});
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
The instance client exposes `state`, `fetch`, `start`,
|
|
312
|
-
`startAndWaitForPorts`, `waitForPort`, `stop` (named or numeric signals),
|
|
313
|
-
`destroy`, the runtime host-policy operations (`setAllowedHosts`,
|
|
314
|
-
`setDeniedHosts`, `allowHost`, `denyHost`, `removeAllowedHost`,
|
|
315
|
-
`removeDeniedHost`), and the Effect-valued native `rawUnsafe` stub.
|
|
316
|
-
Host-policy operations only forward the native remote calls; applications
|
|
317
|
-
continue to own hostname policy and outbound handlers.
|
|
318
|
-
Responses are returned unchanged, including non-2xx and WebSocket upgrade
|
|
319
|
-
responses. If the Container uses outbound interception, also export
|
|
320
|
-
`ContainerProxy` from `@cloudflare/containers`.
|
|
321
|
-
|
|
322
|
-
`ContainerNamespace.Tag` accepts an optional second type parameter carrying
|
|
323
|
-
the exact native namespace type. `rawUnsafe` on the namespace and on named
|
|
324
|
-
instances then preserves that exact type, including extra subclass methods,
|
|
325
|
-
instead of the minimal structural shape:
|
|
326
|
-
|
|
327
|
-
```ts
|
|
328
|
-
class Sandboxes extends ContainerNamespace.Tag<Sandboxes, DurableObjectNamespace<CodexSandbox>>()(
|
|
329
|
-
"Sandboxes",
|
|
330
|
-
) {}
|
|
331
|
-
|
|
332
|
-
// Effect<DurableObjectStub<CodexSandbox>, ContainerOperationError, Sandboxes>
|
|
333
|
-
const stub = Sandboxes.byName("codex").rawUnsafe;
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
See [`examples/containers/README.md`](../../examples/containers/README.md) for
|
|
337
|
-
the corresponding Wrangler configuration and entrypoint responsibilities.
|
|
338
|
-
|
|
339
|
-
## Sandbox Example
|
|
340
|
-
|
|
341
|
-
`effect-cf/sandbox` wraps the [Cloudflare Sandbox SDK](https://github.com/cloudflare/sandbox-sdk)
|
|
342
|
-
1.0 API (`@cloudflare/sandbox@next`). The Sandbox Durable Object class remains
|
|
343
|
-
owned by `@cloudflare/sandbox`; `effect-cf` wraps the namespace binding used by
|
|
344
|
-
a calling Worker and loads the SDK lazily, so applications without the optional
|
|
345
|
-
peer are unaffected.
|
|
346
|
-
|
|
347
|
-
```ts
|
|
348
|
-
import { Effect, Option } from "effect";
|
|
349
|
-
import { Worker } from "effect-cf";
|
|
350
|
-
import * as Sandbox from "effect-cf/sandbox";
|
|
351
|
-
|
|
352
|
-
export { Sandbox as SandboxDurableObject } from "@cloudflare/sandbox";
|
|
353
|
-
|
|
354
|
-
class Sandboxes extends Sandbox.Tag<Sandboxes>()("Sandboxes") {}
|
|
355
|
-
|
|
356
|
-
const SandboxesLive = Sandboxes.layer({ binding: "Sandbox" });
|
|
357
|
-
|
|
358
|
-
export default Worker.make(SandboxesLive, {
|
|
359
|
-
fetch: Effect.gen(function* () {
|
|
360
|
-
const request = yield* Worker.NativeRequest;
|
|
361
|
-
|
|
362
|
-
// Route preview-URL traffic before any other routing.
|
|
363
|
-
const proxied = yield* Sandbox.proxyToSandbox(request);
|
|
364
|
-
if (Option.isSome(proxied)) {
|
|
365
|
-
return proxied.value;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const sandbox = yield* Sandboxes.get("my-sandbox");
|
|
369
|
-
const handle = yield* sandbox.exec(["python3", "-c", "print(2 + 2)"]);
|
|
370
|
-
const output = yield* handle.outputText();
|
|
371
|
-
|
|
372
|
-
return new Response(output.stdout);
|
|
373
|
-
}),
|
|
374
|
-
});
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
The instance client mirrors the complete `ISandbox` client contract plus the
|
|
378
|
-
lifecycle, port, tunnel, and backup operations the SDK client proxies to the
|
|
379
|
-
Sandbox Durable Object. `exec` returns a process handle whose `logs` is an
|
|
380
|
-
Effect `Stream` of typed log events; `watch` decodes the SDK's Server-Sent
|
|
381
|
-
Events into a `Stream` of file-watch events; `readFileStream` streams raw
|
|
382
|
-
bytes. Interruptible waits (`waitForExit`, `waitForLog`, `output`,
|
|
383
|
-
`waitForPort`) abort the underlying SDK call when the Effect is interrupted.
|
|
384
|
-
Failures carry a typed `SandboxOperationError` whose `cause` preserves the
|
|
385
|
-
SDK's rich error classes (`ProcessNotFoundError`, `PortNotExposedError`, and
|
|
386
|
-
friends) for inspection.
|
|
387
|
-
|
|
388
|
-
The corresponding `wrangler.jsonc` declares the container image and Durable
|
|
389
|
-
Object binding; note the SDK requires the exported Durable Object class to be
|
|
390
|
-
reachable under the configured `class_name`:
|
|
391
|
-
|
|
392
|
-
```jsonc
|
|
393
|
-
{
|
|
394
|
-
"containers": [
|
|
395
|
-
{
|
|
396
|
-
"class_name": "SandboxDurableObject",
|
|
397
|
-
"image": "./Dockerfile",
|
|
398
|
-
"instance_type": "lite",
|
|
399
|
-
},
|
|
400
|
-
],
|
|
401
|
-
"durable_objects": {
|
|
402
|
-
"bindings": [{ "class_name": "SandboxDurableObject", "name": "Sandbox" }],
|
|
403
|
-
},
|
|
404
|
-
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["SandboxDurableObject"] }],
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
## Queue Example
|
|
409
|
-
|
|
410
|
-
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.
|
|
411
|
-
|
|
412
|
-
```ts
|
|
413
|
-
import { Effect, Layer, Schema as S } from "effect";
|
|
414
|
-
import { Queue } from "effect-cf";
|
|
415
|
-
|
|
416
|
-
class AvatarQueue extends Queue.Tag<AvatarQueue>()("AvatarQueue", {
|
|
417
|
-
message: S.Struct({ userId: S.String, imageKey: S.String }),
|
|
32
|
+
class Settings extends Kv.Tag<Settings>()("Settings", {
|
|
33
|
+
key: Schema.String,
|
|
34
|
+
value: Schema.String,
|
|
418
35
|
}) {}
|
|
419
36
|
|
|
420
|
-
|
|
421
|
-
queue: (batch) =>
|
|
422
|
-
Effect.gen(function* () {
|
|
423
|
-
for (const message of batch.messages) {
|
|
424
|
-
yield* Effect.logInfo("process avatar", message.body.userId);
|
|
425
|
-
yield* message.ack;
|
|
426
|
-
}
|
|
427
|
-
}),
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
export const AvatarQueueLayer = AvatarQueue.layer({ binding: "AVATAR_QUEUE" });
|
|
431
|
-
|
|
432
|
-
export const enqueueAvatar = (userId: string, imageKey: string) =>
|
|
433
|
-
Effect.gen(function* () {
|
|
434
|
-
const queue = yield* AvatarQueue;
|
|
435
|
-
yield* queue.send({ userId, imageKey });
|
|
436
|
-
});
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
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.
|
|
440
|
-
|
|
441
|
-
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.
|
|
442
|
-
|
|
443
|
-
effect-cf automatically schedules best-effort OTLP flushes only for Worker
|
|
444
|
-
fetch/RPC and Durable Object alarm/RPC handlers. Those internal flushes are
|
|
445
|
-
capped at two seconds. Queue handlers do not flush telemetry automatically;
|
|
446
|
-
applications that explicitly flush in a queue handler own that flush's timeout
|
|
447
|
-
and failure policy.
|
|
448
|
-
|
|
449
|
-
## Cache Example
|
|
450
|
-
|
|
451
|
-
`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(...)`.
|
|
452
|
-
|
|
453
|
-
```ts
|
|
454
|
-
import { Effect, Option } from "effect";
|
|
455
|
-
import { Cache, Worker } from "effect-cf";
|
|
456
|
-
|
|
457
|
-
export default Worker.make(Cache.layer, {
|
|
458
|
-
fetch: Effect.gen(function* () {
|
|
459
|
-
const request = yield* Worker.NativeRequest;
|
|
460
|
-
const storage = yield* Cache.CacheStorage;
|
|
461
|
-
const cached = yield* storage.default.match(request);
|
|
462
|
-
|
|
463
|
-
if (Option.isSome(cached)) {
|
|
464
|
-
return cached.value;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const response = new Response("fresh", {
|
|
468
|
-
headers: { "Cache-Control": "public, max-age=300" },
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
const context = yield* Worker.WorkerContext;
|
|
472
|
-
yield* context.waitUntil(storage.default.put(request, response.clone()));
|
|
37
|
+
const SettingsLive = Settings.layer({ binding: "SETTINGS" });
|
|
473
38
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
});
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
Use `const cache = yield* storage.open("api-cache")` for a named cache. `match`, `put`, `delete`, and `open` failures are reported as `CacheOperationError` values.
|
|
480
|
-
|
|
481
|
-
## R2 Example
|
|
482
|
-
|
|
483
|
-
R2 bucket tags expose Cloudflare object operations as Effects and map nullable reads to `Option`.
|
|
484
|
-
|
|
485
|
-
```ts
|
|
486
|
-
import { Effect, Layer, Option } from "effect";
|
|
487
|
-
import { R2 } from "effect-cf";
|
|
488
|
-
|
|
489
|
-
class ArtifactBucket extends R2.Tag<ArtifactBucket>()("ArtifactBucket") {}
|
|
490
|
-
|
|
491
|
-
export const ArtifactBucketLayer = ArtifactBucket.layer({ binding: "ARTIFACT_BUCKET" });
|
|
492
|
-
|
|
493
|
-
export const writeArtifact = (key: string, body: string) =>
|
|
494
|
-
Effect.gen(function* () {
|
|
495
|
-
const bucket = yield* ArtifactBucket;
|
|
496
|
-
yield* bucket.put(key, body, {
|
|
497
|
-
httpMetadata: { contentType: "application/json" },
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
export const readArtifact = (key: string) =>
|
|
502
|
-
Effect.gen(function* () {
|
|
503
|
-
const bucket = yield* ArtifactBucket;
|
|
504
|
-
const object = yield* bucket.get(key);
|
|
505
|
-
return Option.isSome(object) ? yield* Effect.promise(() => object.value.text()) : undefined;
|
|
506
|
-
});
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
Use `createMultipartUpload(...)` or `resumeMultipartUpload(...)` for large objects; returned upload handles wrap `uploadPart`, `complete`, and `abort` in Effect.
|
|
510
|
-
|
|
511
|
-
## Artifacts Example
|
|
512
|
-
|
|
513
|
-
Artifacts tags expose every namespace and repository-handle binding operation as an Effect. Configure the binding under Wrangler's `artifacts` field:
|
|
514
|
-
|
|
515
|
-
```jsonc
|
|
516
|
-
{
|
|
517
|
-
"artifacts": [
|
|
518
|
-
{
|
|
519
|
-
"binding": "ARTIFACTS",
|
|
520
|
-
"namespace": "default",
|
|
521
|
-
// Optional in local development:
|
|
522
|
-
"remote": true,
|
|
523
|
-
},
|
|
524
|
-
],
|
|
525
|
-
}
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
```ts
|
|
529
|
-
import { Effect } from "effect";
|
|
530
|
-
import { Artifacts } from "effect-cf";
|
|
531
|
-
|
|
532
|
-
class Repositories extends Artifacts.Tag<Repositories>()("Repositories") {}
|
|
39
|
+
const greeting = Effect.gen(function* () {
|
|
40
|
+
const settings = yield* Settings;
|
|
533
41
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
export const createWorkspace = Effect.gen(function* () {
|
|
537
|
-
const artifacts = yield* Repositories;
|
|
538
|
-
const created = yield* artifacts.create("agent-workspace", {
|
|
539
|
-
description: "Versioned agent workspace",
|
|
540
|
-
setDefaultBranch: "main",
|
|
541
|
-
});
|
|
542
|
-
const repo = yield* artifacts.get(created.name);
|
|
543
|
-
const readToken = yield* repo.createToken("read", 3600);
|
|
544
|
-
|
|
545
|
-
return {
|
|
546
|
-
remote: created.remote,
|
|
547
|
-
token: readToken.plaintext,
|
|
548
|
-
history: yield* repo.log({ ref: "main", limit: 10 }),
|
|
549
|
-
};
|
|
42
|
+
return yield* settings.get("greeting");
|
|
550
43
|
});
|
|
551
44
|
```
|
|
552
45
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
## Computer Workspace Example
|
|
556
|
-
|
|
557
|
-
`ComputerWorkspace` exposes the complete `@cloudflare/computer`
|
|
558
|
-
`WorkspaceClient` as Effects. The workspace client and every runtime execution
|
|
559
|
-
handle are owned by an Effect `Scope`, so RPC stubs are disposed when their
|
|
560
|
-
scope closes. The host mixin is a separate optional-dependency entrypoint:
|
|
561
|
-
|
|
562
|
-
```ts
|
|
563
|
-
import shellCurl from "@cloudflare/computer/shell/curl";
|
|
564
|
-
import { Effect, Schema as S } from "effect";
|
|
565
|
-
import { DurableObjectDefinition } from "effect-cf";
|
|
566
|
-
import * as ComputerWorkspace from "effect-cf/computer-workspace";
|
|
567
|
-
import { withComputerWorkspace, WorkspaceServiceProxy } from "effect-cf/computer-workspace-host";
|
|
46
|
+
Declare `SETTINGS` in `wrangler.jsonc` and pass `SettingsLive` to `Worker.make`. Other bindings use the same tag/layer pattern.
|
|
568
47
|
|
|
569
|
-
|
|
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.
|
|
570
49
|
|
|
571
|
-
|
|
572
|
-
inspect: DurableObjectDefinition.method({ success: S.String }),
|
|
573
|
-
});
|
|
50
|
+
## Native RPC tracing
|
|
574
51
|
|
|
575
|
-
|
|
576
|
-
rpc: {
|
|
577
|
-
inspect: () =>
|
|
578
|
-
Effect.gen(function* () {
|
|
579
|
-
const workspace = yield* ComputerWorkspace.ComputerWorkspace;
|
|
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.
|
|
580
53
|
|
|
581
|
-
|
|
582
|
-
const readme = yield* workspace.readFile("/repo/README.md", {
|
|
583
|
-
byteLength: 256 * 1024,
|
|
584
|
-
});
|
|
585
|
-
const commits = yield* workspace.git.log({ maxCount: 20 });
|
|
586
|
-
const run = yield* workspace.execCollect("git status --short", {
|
|
587
|
-
cwd: "/repo",
|
|
588
|
-
timeoutMillis: 30_000,
|
|
589
|
-
});
|
|
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.
|
|
590
55
|
|
|
591
|
-
|
|
592
|
-
}),
|
|
593
|
-
},
|
|
594
|
-
});
|
|
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.
|
|
595
57
|
|
|
596
|
-
|
|
597
|
-
storage: state.storage,
|
|
598
|
-
sessionId: state.id.toString(),
|
|
599
|
-
gitIdentity: { name: "Research Agent", email: "agent@example.test" },
|
|
600
|
-
artifacts: { binding: env.ARTIFACTS },
|
|
601
|
-
shell: {
|
|
602
|
-
loader: env.WORKER_LOADER,
|
|
603
|
-
hostBinding: "RESEARCH_WORKSPACE",
|
|
604
|
-
hostId: state.id.toString(),
|
|
605
|
-
executionContext: state,
|
|
606
|
-
commands: [shellCurl],
|
|
607
|
-
egress: { mode: "none" },
|
|
608
|
-
},
|
|
609
|
-
}));
|
|
610
|
-
|
|
611
|
-
export class ResearchWorkspaceDurableObject extends HostedResearchWorkspace {}
|
|
612
|
-
```
|
|
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.
|
|
613
59
|
|
|
614
|
-
|
|
615
|
-
sequences with an Effect `Semaphore` when several RPC methods can update the
|
|
616
|
-
same working tree concurrently. Reads can remain concurrent.
|
|
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.
|
|
617
61
|
|
|
618
|
-
|
|
619
|
-
storage. Worker-shell execution additionally requires the `experimental` flag,
|
|
620
|
-
a Worker Loader binding, the loopback namespace binding named by `hostBinding`,
|
|
621
|
-
and the exact `WorkspaceServiceProxy` export shown above:
|
|
622
|
-
|
|
623
|
-
```jsonc
|
|
624
|
-
{
|
|
625
|
-
"compatibility_flags": ["nodejs_compat", "experimental"],
|
|
626
|
-
"worker_loaders": [{ "binding": "WORKER_LOADER" }],
|
|
627
|
-
"artifacts": [{ "binding": "ARTIFACTS", "namespace": "default" }],
|
|
628
|
-
"durable_objects": {
|
|
629
|
-
"bindings": [
|
|
630
|
-
{
|
|
631
|
-
"name": "RESEARCH_WORKSPACE",
|
|
632
|
-
"class_name": "ResearchWorkspaceDurableObject",
|
|
633
|
-
},
|
|
634
|
-
],
|
|
635
|
-
},
|
|
636
|
-
"migrations": [
|
|
637
|
-
{
|
|
638
|
-
"tag": "v1",
|
|
639
|
-
"new_sqlite_classes": ["ResearchWorkspaceDurableObject"],
|
|
640
|
-
},
|
|
641
|
-
],
|
|
642
|
-
}
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
Omit `shell` and the Worker Loader configuration when only the SQLite
|
|
646
|
-
filesystem and Git client are needed. Supplying `artifacts` also installs the
|
|
647
|
-
session-scoped client at `workspace.artifacts` and makes the in-shell
|
|
648
|
-
`artifacts` command use the same upstream repository-prefixing convention.
|
|
649
|
-
|
|
650
|
-
Workers can use that session isolation without a Durable Object or workspace:
|
|
651
|
-
|
|
652
|
-
```ts
|
|
653
|
-
import { Effect } from "effect";
|
|
654
|
-
import { Artifacts } from "effect-cf";
|
|
655
|
-
import * as ComputerArtifacts from "effect-cf/computer-artifacts";
|
|
656
|
-
|
|
657
|
-
export const createSessionRepo = (binding: Artifacts.ArtifactsBinding, sessionId: string) =>
|
|
658
|
-
Effect.gen(function* () {
|
|
659
|
-
const artifacts = yield* ComputerArtifacts.makeClient(binding, sessionId);
|
|
660
|
-
const created = yield* artifacts.create("build-cache", {
|
|
661
|
-
description: "CI artifacts",
|
|
662
|
-
});
|
|
663
|
-
const token = yield* artifacts.createToken("build-cache", "read", 3600);
|
|
664
|
-
|
|
665
|
-
return { remote: created.remote, token: token.plaintext };
|
|
666
|
-
});
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
Repository prefixing, list filtering, and the argv API are delegated directly
|
|
670
|
-
to `@cloudflare/computer/artifacts`, keeping this API and the in-shell command
|
|
671
|
-
in agreement.
|
|
672
|
-
|
|
673
|
-
## Hyperdrive Example
|
|
674
|
-
|
|
675
|
-
Hyperdrive tags expose the binding `connectionString` directly on the yielded service.
|
|
676
|
-
|
|
677
|
-
```ts
|
|
678
|
-
import { Effect } from "effect";
|
|
679
|
-
import { Hyperdrive } from "effect-cf";
|
|
680
|
-
|
|
681
|
-
class AppDatabase extends Hyperdrive.Tag<AppDatabase>()("AppDatabase") {}
|
|
682
|
-
|
|
683
|
-
export const AppDatabaseLayer = AppDatabase.layer({ binding: "HYPERDRIVE" });
|
|
684
|
-
|
|
685
|
-
export const databaseUrl = Effect.gen(function* () {
|
|
686
|
-
const hyperdrive = yield* AppDatabase;
|
|
687
|
-
return hyperdrive.connectionString;
|
|
688
|
-
});
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
If your Worker uses Postgres via `@effect/sql-pg`, install that driver and use the optional subpath integration:
|
|
692
|
-
|
|
693
|
-
```ts
|
|
694
|
-
import { Hyperdrive } from "effect-cf";
|
|
695
|
-
import * as HyperdrivePg from "effect-cf/hyperdrive-pg";
|
|
696
|
-
|
|
697
|
-
class AppDatabase extends Hyperdrive.Tag<AppDatabase>()("AppDatabase") {}
|
|
698
|
-
|
|
699
|
-
export const SqlLive = HyperdrivePg.layer(AppDatabase, { binding: "HYPERDRIVE" });
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
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.
|
|
703
|
-
|
|
704
|
-
## Images Example
|
|
705
|
-
|
|
706
|
-
Images tags expose `info`, `input`, optional hosted image operations, and composable transform/draw steps.
|
|
707
|
-
|
|
708
|
-
```ts
|
|
709
|
-
import { Effect } from "effect";
|
|
710
|
-
import { Images } from "effect-cf";
|
|
711
|
-
|
|
712
|
-
class AvatarImages extends Images.Tag<AvatarImages>()("AvatarImages") {}
|
|
713
|
-
|
|
714
|
-
export const AvatarImagesLayer = AvatarImages.layer({ binding: "IMAGES" });
|
|
715
|
-
|
|
716
|
-
export const resizeAvatar = (image: Images.ImageInputValue) =>
|
|
717
|
-
Effect.gen(function* () {
|
|
718
|
-
const images = yield* AvatarImages;
|
|
719
|
-
const result = yield* images.process(
|
|
720
|
-
Images.transform(Images.empty, { width: 256, height: 256 }),
|
|
721
|
-
{
|
|
722
|
-
stream: image,
|
|
723
|
-
outputOptions: { format: "image/webp" },
|
|
724
|
-
},
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
return yield* result.response();
|
|
728
|
-
});
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
## Cloudflare runtime pin
|
|
732
|
-
|
|
733
|
-
`effect-cf` develops and tests against a fixed Cloudflare Workers runtime stack. Keep consumer
|
|
734
|
-
`compatibility_date` and optional `@cloudflare/workers-types` peers at or above this pin:
|
|
735
|
-
|
|
736
|
-
| Pin | Version |
|
|
737
|
-
| ------------------------------------------- | -------------------- |
|
|
738
|
-
| workerd | `1.20260825.1` |
|
|
739
|
-
| `@cloudflare/workers-types` (optional peer) | `5.20260825.1` |
|
|
740
|
-
| miniflare (repo override / tests) | `5.20260825.0-alpha` |
|
|
741
|
-
| wrangler (repo catalog / examples) | `4.126.0` |
|
|
742
|
-
| Recommended `compatibility_date` | `2026-08-25` |
|
|
743
|
-
|
|
744
|
-
The date in the `workerd` / `@cloudflare/workers-types` version is the API surface this package wraps.
|
|
745
|
-
Repo catalog pins live in the root `package.json` `catalog` field (`@cloudflare/workers-types`, `wrangler`).
|
|
746
|
-
`wrangler@4.126.0` pulls `workerd@1.20260825.1` and peers `@cloudflare/workers-types@^5.20260825.1`.
|
|
747
|
-
The Miniflare and Wrangler overrides keep worker tests on the same runtime when
|
|
748
|
-
`@cloudflare/vitest-plugin` lags the repo catalog.
|
|
749
|
-
|
|
750
|
-
## Email Example
|
|
751
|
-
|
|
752
|
-
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`.
|
|
753
|
-
|
|
754
|
-
```ts
|
|
755
|
-
import { Effect } from "effect";
|
|
756
|
-
import { Email } from "effect-cf";
|
|
757
|
-
|
|
758
|
-
class TransactionalEmail extends Email.Tag<TransactionalEmail>()("TransactionalEmail") {}
|
|
759
|
-
|
|
760
|
-
export const TransactionalEmailLayer = TransactionalEmail.layer({ binding: "EMAIL" });
|
|
761
|
-
|
|
762
|
-
export const sendWelcomeEmail = (to: string) =>
|
|
763
|
-
Effect.gen(function* () {
|
|
764
|
-
const email = yield* TransactionalEmail;
|
|
765
|
-
|
|
766
|
-
return yield* email.send({
|
|
767
|
-
from: { name: "Example", email: "team@example.com" },
|
|
768
|
-
to,
|
|
769
|
-
subject: "Welcome to Example",
|
|
770
|
-
text: "Welcome to Example",
|
|
771
|
-
html: "<p>Welcome to Example</p>",
|
|
772
|
-
});
|
|
773
|
-
});
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
Onboard the sending domain under **Compute > Email Service > Email Sending**, then declare the binding in the consuming Worker's `wrangler.jsonc`:
|
|
777
|
-
|
|
778
|
-
```jsonc
|
|
779
|
-
{
|
|
780
|
-
"send_email": [{ "name": "EMAIL" }],
|
|
781
|
-
}
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
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.
|
|
785
|
-
|
|
786
|
-
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`.
|
|
787
|
-
|
|
788
|
-
See [`examples/email/README.md`](../../examples/email/README.md) for domain onboarding, binding restrictions, and typed recovery from `EmailValidationError` and `EmailOperationError`.
|
|
789
|
-
|
|
790
|
-
## Analytics Engine Example
|
|
791
|
-
|
|
792
|
-
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.
|
|
793
|
-
|
|
794
|
-
```ts
|
|
795
|
-
import { Effect } from "effect";
|
|
796
|
-
import { AnalyticsEngine } from "effect-cf";
|
|
797
|
-
|
|
798
|
-
class RequestAnalytics extends AnalyticsEngine.Tag<RequestAnalytics>()("RequestAnalytics") {}
|
|
799
|
-
|
|
800
|
-
export const RequestAnalyticsLayer = RequestAnalytics.layer({
|
|
801
|
-
binding: "REQUEST_ANALYTICS",
|
|
802
|
-
write: {
|
|
803
|
-
onInvalid: "error",
|
|
804
|
-
},
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
export const recordPageView = (request: Request) =>
|
|
808
|
-
Effect.gen(function* () {
|
|
809
|
-
const analytics = yield* RequestAnalytics;
|
|
810
|
-
const url = new URL(request.url);
|
|
811
|
-
|
|
812
|
-
yield* analytics.writeDataPoint({
|
|
813
|
-
indexes: [url.hostname],
|
|
814
|
-
blobs: [url.pathname, request.headers.get("cf-connecting-country") ?? "unknown"],
|
|
815
|
-
doubles: [1],
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
yield* analytics.writeDataPoints(
|
|
819
|
-
[
|
|
820
|
-
{
|
|
821
|
-
indexes: [url.hostname],
|
|
822
|
-
blobs: [url.pathname, "page-view"],
|
|
823
|
-
doubles: [1],
|
|
824
|
-
},
|
|
825
|
-
],
|
|
826
|
-
{ onInvalid: "drop" },
|
|
827
|
-
);
|
|
828
|
-
});
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
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.
|
|
832
|
-
|
|
833
|
-
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.
|
|
834
|
-
|
|
835
|
-
```ts
|
|
836
|
-
import { Effect, Layer, Schema as S } from "effect";
|
|
837
|
-
import { FetchHttpClient } from "effect/unstable/http";
|
|
838
|
-
import { AnalyticsEngine, WorkerConfig } from "effect-cf";
|
|
839
|
-
|
|
840
|
-
class AnalyticsQuery extends AnalyticsEngine.QueryTag<AnalyticsQuery>()("AnalyticsQuery") {}
|
|
841
|
-
|
|
842
|
-
export const AnalyticsQueryLayer = AnalyticsQuery.layerConfig(
|
|
843
|
-
AnalyticsEngine.queryConfig({
|
|
844
|
-
accountId: WorkerConfig.string("CLOUDFLARE_ACCOUNT_ID"),
|
|
845
|
-
apiToken: WorkerConfig.redacted("CLOUDFLARE_API_TOKEN"),
|
|
846
|
-
}),
|
|
847
|
-
).pipe(Layer.provide(FetchHttpClient.layer));
|
|
848
|
-
|
|
849
|
-
const PageView = S.Struct({
|
|
850
|
-
path: S.String,
|
|
851
|
-
views: S.Number,
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
export const topPages = Effect.gen(function* () {
|
|
855
|
-
const analytics = yield* AnalyticsQuery;
|
|
856
|
-
|
|
857
|
-
return yield* analytics.queryRows(
|
|
858
|
-
PageView,
|
|
859
|
-
`
|
|
860
|
-
SELECT blob1 AS path, SUM(_sample_interval) AS views
|
|
861
|
-
FROM request_metrics
|
|
862
|
-
GROUP BY path
|
|
863
|
-
ORDER BY views DESC
|
|
864
|
-
LIMIT 10
|
|
865
|
-
`,
|
|
866
|
-
);
|
|
867
|
-
});
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
## Workflow Example
|
|
871
|
-
|
|
872
|
-
Workflow definitions type the payload and result. Runtime handlers can access `Workflow.WorkflowEvent`, use durable `Workflow.step(...)`, and use normal binding services inside steps.
|
|
873
|
-
|
|
874
|
-
```ts
|
|
875
|
-
import { Effect, Layer, Schema as S } from "effect";
|
|
876
|
-
import { Workflow } from "effect-cf";
|
|
877
|
-
|
|
878
|
-
class ExportWorkflow extends Workflow.Tag<ExportWorkflow>()("ExportWorkflow", {
|
|
879
|
-
payload: S.Struct({ segmentId: S.String }),
|
|
880
|
-
result: S.Struct({ objectKey: S.String }),
|
|
881
|
-
}) {}
|
|
882
|
-
|
|
883
|
-
export const ExportWorkflowEntrypoint = ExportWorkflow.make(Layer.empty, {
|
|
884
|
-
run: (payload) =>
|
|
885
|
-
Effect.gen(function* () {
|
|
886
|
-
const objectKey = yield* Workflow.step(
|
|
887
|
-
"write-export",
|
|
888
|
-
Effect.succeed(`exports/${payload.segmentId}.json`),
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
return { objectKey };
|
|
892
|
-
}),
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
export const ExportWorkflowLayer = ExportWorkflow.layer({ binding: "EXPORT_WORKFLOW" });
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
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.
|
|
899
|
-
|
|
900
|
-
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.
|
|
901
|
-
|
|
902
|
-
## Durable Object WebSockets
|
|
903
|
-
|
|
904
|
-
Use `initialize` for work that should run each time Cloudflare loads a Durable
|
|
905
|
-
Object instance into memory. Yield `state.blockConcurrencyWhile(...)` when
|
|
906
|
-
later events should wait for initialize to finish. If work should happen only once
|
|
907
|
-
for a Durable Object id, store a sentinel in Durable Object storage.
|
|
908
|
-
|
|
909
|
-
```ts
|
|
910
|
-
export const RoomLive = DurableObject.make(layer, {
|
|
911
|
-
initialize: Effect.gen(function* () {
|
|
912
|
-
const state = yield* DurableObjectState.DurableObjectState;
|
|
913
|
-
yield* state.blockConcurrencyWhile(
|
|
914
|
-
Effect.gen(function* () {
|
|
915
|
-
yield* state.storage.put("loadedAt", Date.now());
|
|
916
|
-
}),
|
|
917
|
-
);
|
|
918
|
-
}),
|
|
919
|
-
fetch,
|
|
920
|
-
});
|
|
921
|
-
```
|
|
922
|
-
|
|
923
|
-
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.
|
|
924
|
-
|
|
925
|
-
```ts
|
|
926
|
-
import { Effect, Schema as S } from "effect";
|
|
927
|
-
import { DurableObject, DurableObjectState, DurableObjectWebSocket, Worker } from "effect-cf";
|
|
928
|
-
|
|
929
|
-
const ConnectionAttachment = S.Struct({
|
|
930
|
-
id: S.String,
|
|
931
|
-
roomId: S.String,
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
const Attachments = DurableObjectWebSocket.attachment(ConnectionAttachment);
|
|
935
|
-
|
|
936
|
-
export const fetch = Effect.gen(function* () {
|
|
937
|
-
const request = yield* Worker.NativeRequest;
|
|
938
|
-
|
|
939
|
-
if (!Worker.isWebSocketUpgrade(request)) {
|
|
940
|
-
return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
const upgrade = yield* DurableObjectWebSocket.acceptUpgrade({ tags: ["room:general"] });
|
|
944
|
-
yield* Attachments.serialize(upgrade.server, {
|
|
945
|
-
id: crypto.randomUUID(),
|
|
946
|
-
roomId: "general",
|
|
947
|
-
});
|
|
948
|
-
yield* upgrade.server.send(JSON.stringify({ type: "ready" }));
|
|
949
|
-
|
|
950
|
-
return upgrade.response;
|
|
951
|
-
});
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
`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.
|
|
955
|
-
|
|
956
|
-
`DurableObject.make` lifecycle handlers receive wrapped sockets automatically:
|
|
957
|
-
|
|
958
|
-
```ts
|
|
959
|
-
export const RoomLive = DurableObject.make(layer, {
|
|
960
|
-
webSocketMessage: (socket, message) =>
|
|
961
|
-
Effect.gen(function* () {
|
|
962
|
-
yield* socket.send(message);
|
|
963
|
-
}),
|
|
964
|
-
});
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
Schema-backed attachments can rehydrate hibernated sockets:
|
|
968
|
-
|
|
969
|
-
```text
|
|
970
|
-
const restored = yield* Attachments.rehydrate({
|
|
971
|
-
tag: "room:general",
|
|
972
|
-
onInvalid: "ignore-and-close",
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
for (const { socket, attachment } of restored) {
|
|
976
|
-
yield* socket.send(`restored:${attachment.id}`).pipe(Effect.ignore);
|
|
977
|
-
}
|
|
978
|
-
```
|
|
979
|
-
|
|
980
|
-
Worker-to-Durable-Object forwarding should stay native so WebSocket upgrade responses are preserved:
|
|
981
|
-
|
|
982
|
-
```text
|
|
983
|
-
if (Worker.isWebSocketUpgrade(request)) {
|
|
984
|
-
const rooms = yield* ChatRoom;
|
|
985
|
-
return yield* rooms.byName(roomId).fetch(request);
|
|
986
|
-
}
|
|
987
|
-
```
|
|
988
|
-
|
|
989
|
-
### Hibernation-aware Effect RPC WebSockets
|
|
990
|
-
|
|
991
|
-
`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:
|
|
992
|
-
|
|
993
|
-
```ts
|
|
994
|
-
import { Effect, Layer } from "effect";
|
|
995
|
-
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
996
|
-
import { DurableObject, DurableObjectRpcWebSocket, Worker } from "effect-cf";
|
|
997
|
-
|
|
998
|
-
const RpcLive = RpcServer.layer(MyRpcs).pipe(
|
|
999
|
-
Layer.provideMerge(DurableObjectRpcWebSocket.layer({ tag: "my-rpc" })),
|
|
1000
|
-
Layer.provide(MyRpcHandlers),
|
|
1001
|
-
Layer.provide(RpcSerialization.layerJson),
|
|
1002
|
-
);
|
|
1003
|
-
|
|
1004
|
-
const MyObjectLive = DurableObject.make(RpcLive, {
|
|
1005
|
-
fetch: Effect.gen(function* () {
|
|
1006
|
-
const request = yield* Worker.NativeRequest;
|
|
1007
|
-
|
|
1008
|
-
if (!Worker.isWebSocketUpgrade(request)) {
|
|
1009
|
-
return new Response("Expected WebSocket upgrade", { status: 426 });
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
|
|
1013
|
-
const upgrade = yield* transport.acceptUpgrade({
|
|
1014
|
-
tags: ["room:general"],
|
|
1015
|
-
attachment: { roomId: "general" },
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
return upgrade.response;
|
|
1019
|
-
}),
|
|
1020
|
-
webSocketMessage: (socket, message) =>
|
|
1021
|
-
Effect.gen(function* () {
|
|
1022
|
-
const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
|
|
1023
|
-
|
|
1024
|
-
yield* transport.message(socket, message);
|
|
1025
|
-
}),
|
|
1026
|
-
webSocketClose: (socket) =>
|
|
1027
|
-
Effect.gen(function* () {
|
|
1028
|
-
const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
|
|
1029
|
-
|
|
1030
|
-
yield* transport.close(socket);
|
|
1031
|
-
}),
|
|
1032
|
-
webSocketError: (socket, cause) =>
|
|
1033
|
-
Effect.gen(function* () {
|
|
1034
|
-
const transport = yield* DurableObjectRpcWebSocket.DurableObjectRpcWebSocket;
|
|
1035
|
-
|
|
1036
|
-
yield* transport.error(socket, cause);
|
|
1037
|
-
}),
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
export class MyDurableObject extends MyObjectLive {}
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
`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.
|
|
1044
|
-
|
|
1045
|
-
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.
|
|
1046
|
-
|
|
1047
|
-
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.
|
|
1048
|
-
|
|
1049
|
-
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.
|
|
1050
|
-
|
|
1051
|
-
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.
|
|
1052
|
-
|
|
1053
|
-
Streams are non-resumable unless their RPC tag matches a declaration passed in `resumableStreams`. Constructor recreation resets an ordinary active stream with code `1012`.
|
|
1054
|
-
|
|
1055
|
-
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`.
|
|
1056
|
-
|
|
1057
|
-
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.
|
|
1058
|
-
|
|
1059
|
-
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.
|
|
1060
|
-
|
|
1061
|
-
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:
|
|
1062
|
-
|
|
1063
|
-
```ts
|
|
1064
|
-
import { Effect, Stream } from "effect";
|
|
1065
|
-
|
|
1066
|
-
client.SubscribeEvents({ subscriptionKey, topic, after }).pipe(
|
|
1067
|
-
Stream.runForEach((event) =>
|
|
1068
|
-
applyEventIfNew(event).pipe(
|
|
1069
|
-
Effect.andThen(
|
|
1070
|
-
client.CheckpointSubscription({
|
|
1071
|
-
subscriptionKey,
|
|
1072
|
-
cursor: event.cursor,
|
|
1073
|
-
}),
|
|
1074
|
-
),
|
|
1075
|
-
),
|
|
1076
|
-
),
|
|
1077
|
-
);
|
|
1078
|
-
```
|
|
1079
|
-
|
|
1080
|
-
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.
|
|
1081
|
-
|
|
1082
|
-
`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.
|
|
1083
|
-
|
|
1084
|
-
## WebTransport and HTTP/3
|
|
1085
|
-
|
|
1086
|
-
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.
|
|
1087
|
-
|
|
1088
|
-
The `WebTransport` module gives that boundary a typed shape rather than pretending otherwise:
|
|
1089
|
-
|
|
1090
|
-
```ts
|
|
1091
|
-
import { Effect, Option } from "effect";
|
|
1092
|
-
import { WebTransport } from "effect-cf";
|
|
1093
|
-
|
|
1094
|
-
const fetch = Effect.fn("fetch")(function* (request: Request) {
|
|
1095
|
-
// Decoded metadata about the browser→edge hop (HTTP/3 does not reach you).
|
|
1096
|
-
const transport = WebTransport.inboundTransport(request);
|
|
1097
|
-
const viaHttp3 = Option.match(transport, {
|
|
1098
|
-
onNone: () => false,
|
|
1099
|
-
onSome: WebTransport.isHttp3,
|
|
1100
|
-
});
|
|
1101
|
-
|
|
1102
|
-
// Feature-detected runtime capabilities: { inboundSessions: false, ... }.
|
|
1103
|
-
const capabilities = yield* WebTransport.capabilities;
|
|
1104
|
-
|
|
1105
|
-
// Explicit typed boundary where a future inbound session API would slot in.
|
|
1106
|
-
// yield* WebTransport.inboundSessionsUnsupported;
|
|
1107
|
-
|
|
1108
|
-
return Response.json({ viaHttp3, capabilities });
|
|
1109
|
-
});
|
|
1110
|
-
```
|
|
62
|
+
## API
|
|
1111
63
|
|
|
1112
|
-
|
|
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.
|
|
1113
65
|
|
|
1114
|
-
|
|
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`.
|
|
1115
67
|
|
|
1116
|
-
MIT
|
|
68
|
+
[Changelog](CHANGELOG.md) · [MIT license](LICENSE)
|