effect-cf 0.0.4 → 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ # effect-cf
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#5](https://github.com/danieljvdm/effect-cf/pull/5) [`a17685f`](https://github.com/danieljvdm/effect-cf/commit/a17685fe3873c18994102fad6c6b4074f2b3b1e8) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add Effect-native Durable Object WebSocket APIs for hibernatable application sockets.
8
+
9
+ `DurableObjectWebSocket.acceptUpgrade` now returns a wrapped `DurableWebSocket` server socket with Effect-based `send`, `close`, and attachment helpers. `DurableObjectState.getWebSockets` and `acceptWebSocket` now use the same wrapper, and schema-backed attachment helpers support typed rehydration of hibernated sockets.
10
+
11
+ - [#7](https://github.com/danieljvdm/effect-cf/pull/7) [`2af014c`](https://github.com/danieljvdm/effect-cf/commit/2af014ca704bf0a170133cadebe4572ccc67e020) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add Effect-native Cloudflare Queue and Workflow primitives with typed definitions, producer/control bindings, runtime entrypoints, and runnable examples.
12
+
13
+ - [#3](https://github.com/danieljvdm/effect-cf/pull/3) [`219f568`](https://github.com/danieljvdm/effect-cf/commit/219f568639c324da9681de6c34e4e45189ac7972) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add a fetch-handler shorthand for `Worker.make(layer, effect)`.
14
+
15
+ ## 0.1.0
16
+
17
+ Initial public release.
18
+
19
+ - Add Effect-native Worker and Durable Object entrypoint helpers.
20
+ - Add typed Worker service binding and Durable Object namespace helpers.
21
+ - Add KV, Durable Object state/storage, RPC, and WebSocket primitives.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dan van der Merwe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,70 +1,58 @@
1
1
  # effect-cf
2
2
 
3
- Effect-native primitives for Cloudflare Workers and bindings.
3
+ Effect-native Cloudflare primitives for Workers, Durable Objects, bindings, KV, Queues, Workflows, and Durable Object storage.
4
4
 
5
- ## Design Goal
5
+ ## Install
6
6
 
7
- Cloudflare services should be described as `Context`, `Layer`, and `Effect` values and executed through package-owned Cloudflare entrypoint/runtime boundaries. Consumer code should stay in `Effect`; bindings and storage helpers wrap Promise-returning Cloudflare APIs so applications do not call `Effect.runPromise` or manually bridge Promises.
7
+ `effect-cf` currently targets Effect 4 beta.
8
8
 
9
- The low-level Worker entrypoint factory is exported as `Worker.make`. It returns a `WorkerEntrypoint` class with a `fetch` handler and optional typed RPC methods. Durable Object support starts with `DurableObject.make`, which routes Durable Object lifecycle methods and RPC handlers through one managed runtime for each Durable Object instance.
10
-
11
- Worker fetch handlers can return either web `Response` values or Effect `HttpServerResponse` values. `Worker.make` renders `HttpServerResponse` back to a web `Response` at the Cloudflare boundary, so application handlers can stay in Effect HTTP primitives.
12
-
13
- Request-scoped app services belong in the implementation layer that owns request semantics, such as `HttpRouter` middleware, RPC middleware, or explicit `Effect.provide(...)` inside a custom fetch effect. `Worker.make` stays focused on Cloudflare boundary services: `NativeRequest`, `HttpServerRequest`, `WorkerEnvironment`, `ExecutionContext`, and `WorkerContext`.
9
+ ```bash
10
+ bun add effect-cf "effect@^4.0.0-beta.65"
11
+ ```
14
12
 
15
- For modules that need an `ExportedHandler` object instead of a `WorkerEntrypoint` class, use `Worker.makeFetchHandler`. This helper is fetch-only and intentionally does not accept Worker RPC methods.
13
+ ```bash
14
+ pnpm add effect-cf "effect@^4.0.0-beta.65"
15
+ ```
16
16
 
17
- RPC contracts can be described as classes with `Worker.Tag` and `DurableObject.Tag`. Contract classes expose `.make(...)` for runtime entrypoint creation and `.Binding(...)` / `.Namespace(...)` for typed bindings. Bindings infer call types from the same code-owned contract classes. Definition-backed calls encode/decode arguments and success values with their Effect schemas at Worker and Durable Object boundaries.
17
+ ```bash
18
+ npm install effect-cf "effect@^4.0.0-beta.65"
19
+ ```
18
20
 
19
- Binding helpers expose two RPC paths:
21
+ ## Goal
20
22
 
21
- - `call(...)` awaits one RPC result inside `Effect`. Use it for ordinary structured-cloneable values.
22
- - `rpc(...)` returns Cloudflare's native RPC thenable/stub without awaiting it, preserving promise pipelining.
23
- - `scopedCall(...)` awaits the result and registers any `Symbol.dispose` disposer with the current Effect `Scope`.
23
+ 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.
24
24
 
25
- Client APIs use Cloudflare-style provider types, so object results expose pipelined property and method access. `ServerApi` remains available when a plain implementation-shaped `(...args) => Promise<A>` contract is needed.
25
+ Runtime creation belongs at Cloudflare entrypoints, not inside binding helpers.
26
26
 
27
- ## Current Exports
27
+ ## Exports
28
28
 
29
- - `Worker` - WorkerEntrypoint class factory, schema-backed RPC contract tags, and service binding helpers
30
- - `DurableObject` - Durable Object class factory, schema-backed RPC contract tags, and namespace helpers
31
- - `WorkerDefinition`, `DurableObjectDefinition` - compatibility exports for the lower-level definition modules
32
- - `DurableObjectNamespace` - typed Durable Object namespace binding helper
33
- - `DurableObjectState`, `DurableObjectStorage` - small Effect wrappers for Durable Object state, alarms, SQL, and embedded KV storage
34
- - `DurableObjectWebSocket` - Durable Object WebSocket upgrade helpers built on `WebSocketPair` and `acceptWebSocket`
35
- - `Binding` - low-level binding service helper
29
+ - `Worker` - Worker entrypoint factory, request services, and typed Worker bindings
30
+ - `DurableObject` - Durable Object entrypoint factory and typed namespace helpers
31
+ - `DurableObjectState` / `DurableObjectStorage` - Effect wrappers for state, alarms, SQL, and embedded KV
32
+ - `DurableObjectWebSocket` - WebSocket upgrade helpers for Durable Objects
36
33
  - `Kv` - typed KV namespace helper
37
- - `Rpc` - Cloudflare RPC type helpers, `RpcTarget` / `RpcStub` re-exports, and scoped disposal utilities
38
- - `ServiceBinding` - typed Worker-to-Worker service binding helper
39
- - `WorkerEnvironment` - request environment service
40
- - `WorkerConfig` - Effect `Config` helpers backed by Cloudflare worker `env`, including `layerWith` for custom env-to-config mapping
34
+ - `Queue` / `QueueBinding` - typed Queue producer bindings and consumer handlers
35
+ - `Workflow` / `WorkflowBinding` - typed Workflow entrypoints, steps, and starter bindings
36
+ - `Rpc` - Cloudflare RPC type helpers and scoped disposal utilities
37
+ - `WorkerConfig` - Effect `Config` helpers backed by Cloudflare `env`
41
38
 
42
- ## RPC Values
43
-
44
- Definition-backed RPC methods are intended for schema-backed structured values. If a Cloudflare RPC method returns a lifecycle-managed RPC object or stub, expose that through a lower-level binding API instead of a `WorkerDefinition` / `DurableObjectDefinition` method so disposal remains explicit.
45
-
46
- ## Durable Object WebSocket Example
39
+ ## Worker Example
47
40
 
48
41
  ```ts
49
- const request = yield * Worker.NativeRequest;
50
-
51
- if (!Worker.isWebSocketUpgrade(request)) {
52
- return new Response("Expected WebSocket upgrade", { status: 426 });
53
- }
42
+ import { HttpServerResponse } from "effect";
43
+ import { Worker } from "effect-cf";
54
44
 
55
- const upgrade =
56
- yield *
57
- DurableObjectWebSocket.acceptUpgrade({
58
- tags: ["room:general"],
59
- attachment: { roomId: "general" },
60
- });
61
-
62
- return upgrade.response;
45
+ export default Worker.make({
46
+ fetch: () => HttpServerResponse.text("ok"),
47
+ });
63
48
  ```
64
49
 
65
50
  ## Durable Object Example
66
51
 
67
52
  ```ts
53
+ import { Effect, Layer, Schema as S } from "effect";
54
+ import { DurableObject, DurableObjectState } from "effect-cf";
55
+
68
56
  export class Counter extends DurableObject.Tag<Counter>()("Counter", {
69
57
  get: DurableObject.method({ success: S.Number }),
70
58
  }) {}
@@ -83,4 +71,133 @@ export const CounterDurableObject = Counter.make(Layer.empty, {
83
71
  });
84
72
  ```
85
73
 
86
- Define the Wrangler binding and SQLite migration in the application. This package intentionally does not replace `wrangler.jsonc`.
74
+ Define Wrangler bindings and migrations in the consuming application.
75
+
76
+ ## Queue Example
77
+
78
+ Queues use the same definition + binding split as Workers and Durable Objects: define the message contract once, use `.binding(...)` or `.Binding(...)` to produce from any runtime with `WorkerEnvironment`, and use `.make(...)` for the consumer Worker entrypoint.
79
+
80
+ ```ts
81
+ import { Effect, Layer, Schema as S } from "effect";
82
+ import { Queue } from "effect-cf";
83
+
84
+ class AvatarJobs extends Queue.Tag<AvatarJobs>()("AvatarJobs", {
85
+ message: S.Struct({ userId: S.String, imageKey: S.String }),
86
+ }) {}
87
+
88
+ export const AvatarQueue = AvatarJobs.binding("AvatarQueue", {
89
+ binding: "AVATAR_QUEUE",
90
+ });
91
+
92
+ export const AvatarQueueConsumer = AvatarJobs.make(Layer.empty, {
93
+ queue: (batch) =>
94
+ Effect.gen(function* () {
95
+ for (const message of batch.messages) {
96
+ yield* Effect.logInfo("process avatar", message.body.userId);
97
+ yield* message.ack;
98
+ }
99
+ }),
100
+ });
101
+ ```
102
+
103
+ Producers can use `AvatarQueue.send(...)`, `AvatarQueue.sendBatch(...)`, and `AvatarQueue.metrics` from any Worker, Durable Object, or Workflow layer that provides `WorkerEnvironment`.
104
+
105
+ 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.
106
+
107
+ ## Workflow Example
108
+
109
+ Workflow definitions type the payload and result. Runtime handlers can access `Workflow.WorkflowEvent`, use durable `Workflow.step(...)`, and use normal binding services inside steps.
110
+
111
+ ```ts
112
+ import { Effect, Layer, Schema as S } from "effect";
113
+ import { Workflow } from "effect-cf";
114
+
115
+ class ExportWorkflow extends Workflow.Tag<ExportWorkflow>()("ExportWorkflow", {
116
+ payload: S.Struct({ segmentId: S.String }),
117
+ result: S.Struct({ objectKey: S.String }),
118
+ }) {}
119
+
120
+ export const ExportWorkflowEntrypoint = ExportWorkflow.make(Layer.empty, {
121
+ run: (payload) =>
122
+ Effect.gen(function* () {
123
+ const objectKey = yield* Workflow.step(
124
+ "write-export",
125
+ Effect.succeed(`exports/${payload.segmentId}.json`),
126
+ );
127
+
128
+ return { objectKey };
129
+ }),
130
+ });
131
+
132
+ export const ExportWorkflowBinding = ExportWorkflow.binding("ExportWorkflow", {
133
+ binding: "EXPORT_WORKFLOW",
134
+ });
135
+ ```
136
+
137
+ Use `ExportWorkflowBinding.create(...)`, `createBatch(...)`, and `get(...)` to start and inspect instances.
138
+
139
+ 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.
140
+
141
+ ## Durable Object WebSockets
142
+
143
+ 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.
144
+
145
+ ```ts
146
+ import { Effect, Schema as S } from "effect";
147
+ import { DurableObjectState, DurableObjectWebSocket, Worker } from "effect-cf";
148
+
149
+ const ConnectionAttachment = S.Struct({
150
+ id: S.String,
151
+ roomId: S.String,
152
+ });
153
+
154
+ const Attachments = DurableObjectWebSocket.attachment(ConnectionAttachment);
155
+
156
+ export const fetch = Effect.gen(function* () {
157
+ const request = yield* Worker.NativeRequest;
158
+
159
+ if (!Worker.isWebSocketUpgrade(request)) {
160
+ return new Response("Expected WebSocket upgrade", { status: 426 });
161
+ }
162
+
163
+ const upgrade = yield* DurableObjectWebSocket.acceptUpgrade({ tags: ["room:general"] });
164
+ yield* Attachments.serialize(upgrade.server, {
165
+ id: crypto.randomUUID(),
166
+ roomId: "general",
167
+ });
168
+ yield* upgrade.server.send(JSON.stringify({ type: "ready" }));
169
+
170
+ return upgrade.response;
171
+ });
172
+ ```
173
+
174
+ `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.
175
+
176
+ Schema-backed attachments can rehydrate hibernated sockets:
177
+
178
+ ```ts
179
+ const restored =
180
+ yield *
181
+ Attachments.rehydrate({
182
+ tag: "room:general",
183
+ onInvalid: "ignore-and-close",
184
+ });
185
+
186
+ for (const { socket, attachment } of restored) {
187
+ yield * socket.send(`restored:${attachment.id}`).pipe(Effect.ignore);
188
+ }
189
+ ```
190
+
191
+ Worker-to-Durable-Object forwarding should stay native so WebSocket upgrade responses are preserved:
192
+
193
+ ```ts
194
+ if (Worker.isWebSocketUpgrade(request)) {
195
+ return yield * ChatRooms.byName(roomId).fetch(request);
196
+ }
197
+ ```
198
+
199
+ Use `DurableObjectRpcWebSocket.layer(...)` for Effect RPC-over-WebSocket transports. It owns protocol parsing and RPC client bookkeeping; use `DurableWebSocket` for general application sockets, rooms, presence, and broadcast flows.
200
+
201
+ ## License
202
+
203
+ MIT