effect-cf 0.1.0 → 0.3.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 +27 -0
- package/README.md +139 -1
- package/dist/index.d.mts +904 -311
- package/dist/index.mjs +616 -127
- package/package.json +3 -2
- package/src/DurableObject.ts +9 -6
- package/src/DurableObjectRpcWebSocket.ts +21 -17
- package/src/DurableObjectState.ts +12 -10
- package/src/DurableObjectWebSocket.ts +203 -6
- package/src/Queue.ts +116 -0
- package/src/QueueBinding.ts +131 -0
- package/src/QueueDefinition.ts +129 -0
- package/src/Worker.ts +57 -68
- package/src/WorkerDefinition.ts +6 -0
- package/src/Workflow.ts +279 -0
- package/src/WorkflowBinding.ts +254 -0
- package/src/WorkflowDefinition.ts +167 -0
- package/src/index.ts +6 -0
- package/src/internal/WorkerContext.ts +83 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# effect-cf
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10](https://github.com/danieljvdm/effect-cf/pull/10) [`a0e3f43`](https://github.com/danieljvdm/effect-cf/commit/a0e3f436df695b8ea3908fbb7813efea099ccd13) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Type Durable Object websocket lifecycle handlers with `DurableWebSocket` instead of raw `WebSocket`, so handlers can use the Effect-native durable socket API without manually wrapping Cloudflare sockets.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#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.
|
|
14
|
+
|
|
15
|
+
`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.
|
|
16
|
+
|
|
17
|
+
- [#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.
|
|
18
|
+
|
|
19
|
+
- [#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)`.
|
|
20
|
+
|
|
21
|
+
## 0.1.0
|
|
22
|
+
|
|
23
|
+
Initial public release.
|
|
24
|
+
|
|
25
|
+
- Add Effect-native Worker and Durable Object entrypoint helpers.
|
|
26
|
+
- Add typed Worker service binding and Durable Object namespace helpers.
|
|
27
|
+
- Add KV, Durable Object state/storage, RPC, and WebSocket primitives.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
-
Effect-native Cloudflare primitives for Workers, Durable Objects, bindings, KV, and Durable Object storage.
|
|
3
|
+
Effect-native Cloudflare primitives for Workers, Durable Objects, bindings, KV, Queues, Workflows, and Durable Object storage.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -31,6 +31,8 @@ Runtime creation belongs at Cloudflare entrypoints, not inside binding helpers.
|
|
|
31
31
|
- `DurableObjectState` / `DurableObjectStorage` - Effect wrappers for state, alarms, SQL, and embedded KV
|
|
32
32
|
- `DurableObjectWebSocket` - WebSocket upgrade helpers for Durable Objects
|
|
33
33
|
- `Kv` - typed KV namespace helper
|
|
34
|
+
- `Queue` / `QueueBinding` - typed Queue producer bindings and consumer handlers
|
|
35
|
+
- `Workflow` / `WorkflowBinding` - typed Workflow entrypoints, steps, and starter bindings
|
|
34
36
|
- `Rpc` - Cloudflare RPC type helpers and scoped disposal utilities
|
|
35
37
|
- `WorkerConfig` - Effect `Config` helpers backed by Cloudflare `env`
|
|
36
38
|
|
|
@@ -71,6 +73,142 @@ export const CounterDurableObject = Counter.make(Layer.empty, {
|
|
|
71
73
|
|
|
72
74
|
Define Wrangler bindings and migrations in the consuming application.
|
|
73
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 { DurableObject, 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
|
+
`DurableObject.make` lifecycle handlers receive wrapped sockets automatically:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
export const RoomLive = DurableObject.make(layer, {
|
|
180
|
+
webSocketMessage: (socket, message) =>
|
|
181
|
+
Effect.gen(function* () {
|
|
182
|
+
yield* socket.send(message);
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Schema-backed attachments can rehydrate hibernated sockets:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const restored =
|
|
191
|
+
yield *
|
|
192
|
+
Attachments.rehydrate({
|
|
193
|
+
tag: "room:general",
|
|
194
|
+
onInvalid: "ignore-and-close",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
for (const { socket, attachment } of restored) {
|
|
198
|
+
yield * socket.send(`restored:${attachment.id}`).pipe(Effect.ignore);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Worker-to-Durable-Object forwarding should stay native so WebSocket upgrade responses are preserved:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
if (Worker.isWebSocketUpgrade(request)) {
|
|
206
|
+
return yield * ChatRooms.byName(roomId).fetch(request);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
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.
|
|
211
|
+
|
|
74
212
|
## License
|
|
75
213
|
|
|
76
214
|
MIT
|