@uploadista/event-emitter-durable-object 0.0.3
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +478 -0
- package/dist/do-event-emitter.d.ts +9 -0
- package/dist/do-event-emitter.d.ts.map +1 -0
- package/dist/do-event-emitter.js +48 -0
- package/dist/durable-object-impl.d.ts +2 -0
- package/dist/durable-object-impl.d.ts.map +1 -0
- package/dist/durable-object-impl.js +53 -0
- package/dist/event-emitter-durable-object.d.ts +9 -0
- package/dist/event-emitter-durable-object.d.ts.map +1 -0
- package/dist/event-emitter-durable-object.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +29 -0
- package/src/do-event-emitter.ts +73 -0
- package/src/durable-object-impl.ts +58 -0
- package/src/event-emitter-durable-object.ts +13 -0
- package/src/index.ts +3 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/worker-configuration.d.ts +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 uploadista
|
|
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
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# @uploadista/event-emitter-durable-object
|
|
2
|
+
|
|
3
|
+
Cloudflare Durable Objects-based event emitter for Uploadista. Provides globally consistent event emission with real-time WebSocket coordination.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Durable Objects event emitter uses Cloudflare Durable Objects for strongly-consistent event emission. Perfect for:
|
|
8
|
+
|
|
9
|
+
- **Edge Deployment**: Events coordinated globally at Cloudflare edge
|
|
10
|
+
- **Real-Time Coordination**: Strong consistency across all operations
|
|
11
|
+
- **WebSocket Integration**: Native persistent connection support
|
|
12
|
+
- **Global Subscribers**: Serve events to clients worldwide
|
|
13
|
+
- **Transactional Events**: ACID guarantees for complex workflows
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @uploadista/event-emitter-durable-object
|
|
19
|
+
# or
|
|
20
|
+
pnpm add @uploadista/event-emitter-durable-object
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Prerequisites
|
|
24
|
+
|
|
25
|
+
- Cloudflare Workers with Durable Objects enabled
|
|
26
|
+
- `@cloudflare/workers-types` for type definitions
|
|
27
|
+
- Durable Objects bindings in `wrangler.toml`
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { uploadEventEmitterDurableObjectStore } from "@uploadista/event-emitter-durable-object";
|
|
33
|
+
import type { UploadEvent } from "@uploadista/core/types";
|
|
34
|
+
import { Effect } from "effect";
|
|
35
|
+
|
|
36
|
+
export interface Env {
|
|
37
|
+
EVENT_EMITTER_DO: DurableObjectNamespace;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
async fetch(request: Request, env: Env) {
|
|
42
|
+
const program = Effect.gen(function* () {
|
|
43
|
+
// Event emitter is available
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return Effect.runPromise(
|
|
47
|
+
program.pipe(
|
|
48
|
+
Effect.provide(
|
|
49
|
+
uploadEventEmitterDurableObjectStore({
|
|
50
|
+
durableObject: env.EVENT_EMITTER_DO,
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- ✅ **Strong Consistency**: ACID properties for events
|
|
62
|
+
- ✅ **Global Edge**: Events coordinated at 300+ edge locations
|
|
63
|
+
- ✅ **WebSocket Native**: Built-in persistent connections
|
|
64
|
+
- ✅ **Real-Time**: Sub-10ms event delivery globally
|
|
65
|
+
- ✅ **Transactional**: Multiple operations as one unit
|
|
66
|
+
- ✅ **Durable**: Events persisted automatically
|
|
67
|
+
|
|
68
|
+
## API Reference
|
|
69
|
+
|
|
70
|
+
### Main Exports
|
|
71
|
+
|
|
72
|
+
#### `uploadEventEmitterDurableObjectStore(config): Layer<UploadEventEmitter>`
|
|
73
|
+
|
|
74
|
+
Creates an Effect layer for emitting upload events via Durable Objects.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { uploadEventEmitterDurableObjectStore } from "@uploadista/event-emitter-durable-object";
|
|
78
|
+
|
|
79
|
+
const layer = uploadEventEmitterDurableObjectStore({
|
|
80
|
+
durableObject: env.EVENT_EMITTER_DO,
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### `makeEventEmitterDurableObjectStore<T>(config): EventEmitter<T>`
|
|
85
|
+
|
|
86
|
+
Factory function for typed event emitter.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const emitter = makeEventEmitterDurableObjectStore<CustomEvent>({
|
|
90
|
+
durableObject: env.EVENT_EMITTER_DO,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Available Operations
|
|
95
|
+
|
|
96
|
+
#### `emit(key: string, event: T): Effect<void>`
|
|
97
|
+
|
|
98
|
+
Emit event to all subscribers globally.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const program = Effect.gen(function* () {
|
|
102
|
+
yield* emitter.emit("upload:abc123", {
|
|
103
|
+
type: "completed",
|
|
104
|
+
duration: 45000,
|
|
105
|
+
size: 1048576,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### `subscribe(key: string, connection): Effect<void>`
|
|
111
|
+
|
|
112
|
+
Subscribe a WebSocket connection to events.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const program = Effect.gen(function* () {
|
|
116
|
+
yield* emitter.subscribe("upload:abc123", wsConnection);
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### `unsubscribe(key: string): Effect<void>`
|
|
121
|
+
|
|
122
|
+
Unsubscribe from events.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const program = Effect.gen(function* () {
|
|
126
|
+
yield* emitter.unsubscribe("upload:abc123");
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
### Basic Setup in wrangler.toml
|
|
133
|
+
|
|
134
|
+
```toml
|
|
135
|
+
name = "uploadista-worker"
|
|
136
|
+
main = "src/index.ts"
|
|
137
|
+
|
|
138
|
+
[[durable_objects.bindings]]
|
|
139
|
+
name = "EVENT_EMITTER_DO"
|
|
140
|
+
class_name = "EventEmitterDurableObject"
|
|
141
|
+
|
|
142
|
+
[env.production]
|
|
143
|
+
durable_objects = { bindings = [{name = "EVENT_EMITTER_DO", class_name = "EventEmitterDurableObject"}] }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Worker Environment
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { uploadEventEmitterDurableObjectStore } from "@uploadista/event-emitter-durable-object";
|
|
150
|
+
|
|
151
|
+
export interface Env {
|
|
152
|
+
EVENT_EMITTER_DO: DurableObjectNamespace;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default {
|
|
156
|
+
async fetch(request: Request, env: Env) {
|
|
157
|
+
const program = Effect.gen(function* () {
|
|
158
|
+
// Use event emitter
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return Effect.runPromise(
|
|
162
|
+
program.pipe(
|
|
163
|
+
Effect.provide(
|
|
164
|
+
uploadEventEmitterDurableObjectStore({
|
|
165
|
+
durableObject: env.EVENT_EMITTER_DO,
|
|
166
|
+
})
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Examples
|
|
175
|
+
|
|
176
|
+
### Example 1: Real-Time Upload Tracking
|
|
177
|
+
|
|
178
|
+
Server emits progress, clients receive globally:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { uploadEventEmitterDurableObjectStore } from "@uploadista/event-emitter-durable-object";
|
|
182
|
+
import type { UploadEvent } from "@uploadista/core/types";
|
|
183
|
+
|
|
184
|
+
const trackUploadProgress = (uploadId: string) =>
|
|
185
|
+
Effect.gen(function* () {
|
|
186
|
+
// Emit start event
|
|
187
|
+
yield* emitter.emit(uploadId, {
|
|
188
|
+
type: "started",
|
|
189
|
+
timestamp: new Date().toISOString(),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Simulate progress
|
|
193
|
+
for (let i = 0; i <= 100; i += 25) {
|
|
194
|
+
yield* Effect.sleep("1 second");
|
|
195
|
+
|
|
196
|
+
yield* emitter.emit(uploadId, {
|
|
197
|
+
type: "progress",
|
|
198
|
+
progress: i / 100,
|
|
199
|
+
bytesReceived: Math.floor((i / 100) * 1048576),
|
|
200
|
+
timestamp: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Emit completion
|
|
205
|
+
yield* emitter.emit(uploadId, {
|
|
206
|
+
type: "completed",
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
duration: 4000,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Client-side (browser, anywhere globally):
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const ws = new WebSocket("wss://uploadista.example.com/events");
|
|
217
|
+
|
|
218
|
+
ws.onmessage = (event) => {
|
|
219
|
+
const message = JSON.parse(event.data);
|
|
220
|
+
|
|
221
|
+
if (message.type === "progress") {
|
|
222
|
+
updateProgressBar(message.progress);
|
|
223
|
+
} else if (message.type === "completed") {
|
|
224
|
+
showSuccess("Upload complete!");
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Example 2: Coordinated Multi-Step Workflow
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const executeUploadWorkflow = (uploadId: string) =>
|
|
233
|
+
Effect.gen(function* () {
|
|
234
|
+
// Step 1: Validate
|
|
235
|
+
yield* emitter.emit(uploadId, {
|
|
236
|
+
type: "workflow",
|
|
237
|
+
step: "validating",
|
|
238
|
+
details: "Checking file format...",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Step 2: Process
|
|
242
|
+
yield* emitter.emit(uploadId, {
|
|
243
|
+
type: "workflow",
|
|
244
|
+
step: "processing",
|
|
245
|
+
details: "Resizing images...",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Step 3: Store
|
|
249
|
+
yield* emitter.emit(uploadId, {
|
|
250
|
+
type: "workflow",
|
|
251
|
+
step: "storing",
|
|
252
|
+
details: "Uploading to R2...",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Completion
|
|
256
|
+
yield* emitter.emit(uploadId, {
|
|
257
|
+
type: "workflow",
|
|
258
|
+
step: "completed",
|
|
259
|
+
details: "All done!",
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Example 3: Global WebSocket Broadcast
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Upgrade HTTP to WebSocket
|
|
268
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
269
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
270
|
+
|
|
271
|
+
const uploadId = new URL(request.url).searchParams.get("uploadId");
|
|
272
|
+
|
|
273
|
+
const program = Effect.gen(function* () {
|
|
274
|
+
// Subscribe this client
|
|
275
|
+
yield* emitter.subscribe(uploadId, server);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await Effect.runPromise(program);
|
|
279
|
+
|
|
280
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Performance
|
|
285
|
+
|
|
286
|
+
| Operation | Latency | Range |
|
|
287
|
+
|-----------|---------|-------|
|
|
288
|
+
| emit() | 5-10ms | Global |
|
|
289
|
+
| subscribe() | 1-3ms | Global |
|
|
290
|
+
| unsubscribe() | 1-3ms | Global |
|
|
291
|
+
| Message delivery | 10-50ms | Any edge location |
|
|
292
|
+
|
|
293
|
+
Strong consistency: All subscribers see same events in same order.
|
|
294
|
+
|
|
295
|
+
## Limits & Quotas
|
|
296
|
+
|
|
297
|
+
| Limit | Value |
|
|
298
|
+
|-------|-------|
|
|
299
|
+
| Events per object | Unlimited |
|
|
300
|
+
| Simultaneous WebSockets | 128 per object |
|
|
301
|
+
| Storage | 128 MB per object |
|
|
302
|
+
| Event size | 512 KB recommended |
|
|
303
|
+
|
|
304
|
+
Partition large event streams across multiple objects if needed.
|
|
305
|
+
|
|
306
|
+
## Best Practices
|
|
307
|
+
|
|
308
|
+
### 1. Use Consistent Event Keys
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Good: Specific upload ID
|
|
312
|
+
"upload:abc123"
|
|
313
|
+
"flow:job:xyz"
|
|
314
|
+
"user:upload:456"
|
|
315
|
+
|
|
316
|
+
// Avoid: Generic
|
|
317
|
+
"events", "status", "updates"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 2. Handle WebSocket Lifecycle
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Client connects
|
|
324
|
+
yield* emitter.subscribe(uploadId, wsConnection);
|
|
325
|
+
|
|
326
|
+
// Client disconnects
|
|
327
|
+
ws.addEventListener("close", () => {
|
|
328
|
+
yield* emitter.unsubscribe(uploadId);
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 3. Structure Events Clearly
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
interface UploadEvent {
|
|
336
|
+
type: "started" | "progress" | "completed" | "error";
|
|
337
|
+
timestamp: string;
|
|
338
|
+
progress?: number;
|
|
339
|
+
error?: string;
|
|
340
|
+
metadata?: Record<string, any>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Deployment
|
|
345
|
+
|
|
346
|
+
### Cloudflare Workers Deployment
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
# Deploy to Cloudflare
|
|
350
|
+
wrangler publish
|
|
351
|
+
|
|
352
|
+
# To specific environment
|
|
353
|
+
wrangler publish --env production
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### GitHub Actions
|
|
357
|
+
|
|
358
|
+
```yaml
|
|
359
|
+
name: Deploy
|
|
360
|
+
|
|
361
|
+
on:
|
|
362
|
+
push:
|
|
363
|
+
branches: [main]
|
|
364
|
+
|
|
365
|
+
jobs:
|
|
366
|
+
deploy:
|
|
367
|
+
runs-on: ubuntu-latest
|
|
368
|
+
steps:
|
|
369
|
+
- uses: actions/checkout@v3
|
|
370
|
+
- uses: cloudflare/wrangler-action@v3
|
|
371
|
+
with:
|
|
372
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Monitoring
|
|
376
|
+
|
|
377
|
+
### Track Event Emission
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
const emitWithMetrics = (key: string, event: UploadEvent) =>
|
|
381
|
+
Effect.gen(function* () {
|
|
382
|
+
const start = Date.now();
|
|
383
|
+
|
|
384
|
+
yield* emitter.emit(key, event);
|
|
385
|
+
|
|
386
|
+
const duration = Date.now() - start;
|
|
387
|
+
console.log(`Event emitted: ${key} (${duration}ms)`);
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Monitor Durable Objects
|
|
392
|
+
|
|
393
|
+
Use Cloudflare Dashboard:
|
|
394
|
+
- "Durable Objects" in Workers analytics
|
|
395
|
+
- Monitor storage usage
|
|
396
|
+
- Track request rate
|
|
397
|
+
|
|
398
|
+
## Troubleshooting
|
|
399
|
+
|
|
400
|
+
### "Durable Object not found"
|
|
401
|
+
|
|
402
|
+
Ensure binding defined in `wrangler.toml`:
|
|
403
|
+
|
|
404
|
+
```toml
|
|
405
|
+
[[durable_objects.bindings]]
|
|
406
|
+
name = "EVENT_EMITTER_DO"
|
|
407
|
+
class_name = "EventEmitterDurableObject"
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### WebSocket Disconnections
|
|
411
|
+
|
|
412
|
+
Implement reconnect logic:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
const reconnectWebSocket = () => {
|
|
416
|
+
ws = new WebSocket(`wss://${host}/events?uploadId=${uploadId}`);
|
|
417
|
+
|
|
418
|
+
ws.onopen = () => {
|
|
419
|
+
console.log("Reconnected");
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
ws.onclose = () => {
|
|
423
|
+
setTimeout(reconnectWebSocket, 1000);
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Storage Exceeds 128MB
|
|
429
|
+
|
|
430
|
+
Partition events across multiple objects:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
const getObjectId = (uploadId: string) => {
|
|
434
|
+
// Deterministic partitioning
|
|
435
|
+
const hash = uploadId.charCodeAt(0);
|
|
436
|
+
return `events-${hash % 10}`;
|
|
437
|
+
};
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Integration Patterns
|
|
441
|
+
|
|
442
|
+
### With KV Cache
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// Emit to Durable Objects
|
|
446
|
+
yield* emitter.emit(uploadId, event);
|
|
447
|
+
|
|
448
|
+
// Also cache in KV
|
|
449
|
+
yield* kv.put(`event:${uploadId}:latest`, JSON.stringify(event));
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### With R2 Storage
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// Emit event
|
|
456
|
+
yield* emitter.emit(uploadId, { type: "completed" });
|
|
457
|
+
|
|
458
|
+
// Store result in R2
|
|
459
|
+
await env.R2.put(`uploads/${uploadId}/result.json`, resultData);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## Related Packages
|
|
463
|
+
|
|
464
|
+
- [@uploadista/core](../../core) - Core types
|
|
465
|
+
- [@uploadista/kv-store-cloudflare-do](../../kv-stores/cloudflare-do) - Durable Objects KV
|
|
466
|
+
- [@uploadista/adapters-hono](../../servers/adapters-hono) - Hono for Workers
|
|
467
|
+
- [@uploadista/event-emitter-websocket](../websocket) - WebSocket emitter
|
|
468
|
+
|
|
469
|
+
## License
|
|
470
|
+
|
|
471
|
+
See [LICENSE](../../../LICENSE) in the main repository.
|
|
472
|
+
|
|
473
|
+
## See Also
|
|
474
|
+
|
|
475
|
+
- [EVENT_SYSTEM.md](./EVENT_SYSTEM.md) - Architecture guide
|
|
476
|
+
- [Cloudflare Durable Objects](https://developers.cloudflare.com/workers/runtime-apis/durable-objects/) - Official docs
|
|
477
|
+
- [Server Setup Guide](../../../SERVER_SETUP.md#cloudflare) - Deployment guide
|
|
478
|
+
- [WebSocket Emitter](../websocket/README.md) - Alternative for single-region
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type EventEmitter, type UploadEvent, UploadEventEmitter } from "@uploadista/core/types";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
import type { EventEmitterDurableObject } from "./event-emitter-durable-object";
|
|
4
|
+
export type UploadEventEmitterDurableObjectStoreConfig = {
|
|
5
|
+
durableObject: EventEmitterDurableObject<UploadEvent>;
|
|
6
|
+
};
|
|
7
|
+
export declare function makeEventEmitterDurableObjectStore<T>({ durableObject, }: UploadEventEmitterDurableObjectStoreConfig): EventEmitter<T>;
|
|
8
|
+
export declare const uploadEventEmitterDurableObjectStore: (config: UploadEventEmitterDurableObjectStoreConfig) => Layer.Layer<UploadEventEmitter, never, never>;
|
|
9
|
+
//# sourceMappingURL=do-event-emitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"do-event-emitter.d.ts","sourceRoot":"","sources":["../src/do-event-emitter.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,gCAAgC,CAAC;AAExC,MAAM,MAAM,0CAA0C,GAAG;IACvD,aAAa,EAAE,yBAAyB,CAAC,WAAW,CAAC,CAAC;CACvD,CAAC;AAEF,wBAAgB,kCAAkC,CAAC,CAAC,EAAE,EACpD,aAAa,GACd,EAAE,0CAA0C,GAAG,YAAY,CAAC,CAAC,CAAC,CA8C9D;AAED,eAAO,MAAM,oCAAoC,GAC/C,QAAQ,0CAA0C,kDAKjD,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { UploadEventEmitter, } from "@uploadista/core/types";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
export function makeEventEmitterDurableObjectStore({ durableObject, }) {
|
|
5
|
+
function getStub(key) {
|
|
6
|
+
const id = durableObject.idFromName(key);
|
|
7
|
+
return durableObject.get(id);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
emit: (key, event) => {
|
|
11
|
+
const stub = getStub(key);
|
|
12
|
+
return Effect.tryPromise({
|
|
13
|
+
try: async () => {
|
|
14
|
+
await stub.emit(event);
|
|
15
|
+
return;
|
|
16
|
+
},
|
|
17
|
+
catch: (cause) => {
|
|
18
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
subscribe: (key, connection) => {
|
|
23
|
+
return Effect.tryPromise({
|
|
24
|
+
try: async () => {
|
|
25
|
+
const stub = getStub(key);
|
|
26
|
+
await stub.subscribe(connection);
|
|
27
|
+
return;
|
|
28
|
+
},
|
|
29
|
+
catch: (cause) => {
|
|
30
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
unsubscribe: (key) => {
|
|
35
|
+
return Effect.tryPromise({
|
|
36
|
+
try: async () => {
|
|
37
|
+
const stub = getStub(key);
|
|
38
|
+
await stub.unsubscribe();
|
|
39
|
+
return;
|
|
40
|
+
},
|
|
41
|
+
catch: (cause) => {
|
|
42
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export const uploadEventEmitterDurableObjectStore = (config) => Layer.succeed(UploadEventEmitter, makeEventEmitterDurableObjectStore(config));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-object-impl.d.ts","sourceRoot":"","sources":["../src/durable-object-impl.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
// import { WebSocketPair } from "@cloudflare/workers-types";
|
|
3
|
+
// import type { UploadEvent } from "@uploadista/core/types";
|
|
4
|
+
export {};
|
|
5
|
+
// export class UploadEventDurableObject extends DurableObject {
|
|
6
|
+
// async fetch(_request: Request): Promise<Response> {
|
|
7
|
+
// // Creates two ends of a WebSocket connection.
|
|
8
|
+
// const webSocketPair = new WebSocketPair();
|
|
9
|
+
// const [client, server] = Object.values(webSocketPair);
|
|
10
|
+
// // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
|
|
11
|
+
// // request within the Durable Object. It has the effect of "accepting" the connection,
|
|
12
|
+
// // and allowing the WebSocket to send and receive messages.
|
|
13
|
+
// // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
|
|
14
|
+
// // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
|
|
15
|
+
// // the connection is open. During periods of inactivity, the Durable Object can be evicted
|
|
16
|
+
// // from memory, but the WebSocket connection will remain open. If at some later point the
|
|
17
|
+
// // WebSocket receives a message, the runtime will recreate the Durable Object
|
|
18
|
+
// // (run the `constructor`) and deliver the message to the appropriate handler.
|
|
19
|
+
// this.ctx.acceptWebSocket(server);
|
|
20
|
+
// return new Response(null, {
|
|
21
|
+
// status: 101,
|
|
22
|
+
// webSocket: client,
|
|
23
|
+
// });
|
|
24
|
+
// }
|
|
25
|
+
// emit(event: UploadEvent) {
|
|
26
|
+
// for (const ws of this.ctx.getWebSockets()) {
|
|
27
|
+
// ws.send(JSON.stringify(event satisfies UploadEvent));
|
|
28
|
+
// }
|
|
29
|
+
// }
|
|
30
|
+
// async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
|
|
31
|
+
// // Upon receiving a message from the client, the server replies with the same message,
|
|
32
|
+
// // and the total number of connections with the "[Durable Object]: " prefix
|
|
33
|
+
// ws.send(
|
|
34
|
+
// `[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`,
|
|
35
|
+
// );
|
|
36
|
+
// }
|
|
37
|
+
// async webSocketClose(
|
|
38
|
+
// ws: WebSocket,
|
|
39
|
+
// code: number,
|
|
40
|
+
// _reason: string,
|
|
41
|
+
// _wasClean: boolean,
|
|
42
|
+
// ) {
|
|
43
|
+
// // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
|
|
44
|
+
// // Don't try to close an already closed WebSocket or use reserved close codes
|
|
45
|
+
// if (ws.readyState === WebSocket.OPEN) {
|
|
46
|
+
// // Use a valid close code instead of the potentially reserved one from the client
|
|
47
|
+
// // 1000 = Normal Closure, 1001 = Going Away are safe codes to use
|
|
48
|
+
// const validCloseCode =
|
|
49
|
+
// code === 1006 || code < 1000 || code > 4999 ? 1000 : code;
|
|
50
|
+
// ws.close(validCloseCode, "Durable Object is closing WebSocket");
|
|
51
|
+
// }
|
|
52
|
+
// }
|
|
53
|
+
// }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
2
|
+
import type { WebSocketConnection } from "@uploadista/core/types";
|
|
3
|
+
export type EventEmitterDurableObjectBranded<T> = Rpc.DurableObjectBranded & {
|
|
4
|
+
emit: (event: T) => Promise<void>;
|
|
5
|
+
subscribe: (connection: WebSocketConnection) => Promise<void>;
|
|
6
|
+
unsubscribe: () => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export type EventEmitterDurableObject<T> = DurableObjectNamespace<EventEmitterDurableObjectBranded<T>>;
|
|
9
|
+
//# sourceMappingURL=event-emitter-durable-object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-emitter-durable-object.d.ts","sourceRoot":"","sources":["../src/event-emitter-durable-object.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,MAAM,MAAM,gCAAgC,CAAC,CAAC,IAAI,GAAG,CAAC,oBAAoB,GAAG;IAC3E,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,SAAS,EAAE,CAAC,UAAU,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC,CAAC;AAGF,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,sBAAsB,CAC/D,gCAAgC,CAAC,CAAC,CAAC,CACpC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAC;AAE1E,YAAY,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { uploadEventEmitterDurableObjectStore } from "./do-event-emitter";
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadista/event-emitter-durable-object",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.3",
|
|
5
|
+
"description": "Durable Object event emitter for Uploadista",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Uploadista",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@cloudflare/workers-types": "4.20251011.0",
|
|
17
|
+
"effect": "3.18.4",
|
|
18
|
+
"@uploadista/core": "0.0.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@uploadista/typescript-config": "0.0.3"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b",
|
|
25
|
+
"format": "biome format --write ./src",
|
|
26
|
+
"lint": "biome lint --write ./src",
|
|
27
|
+
"check": "biome check --write ./src"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import {
|
|
3
|
+
type EventEmitter,
|
|
4
|
+
type UploadEvent,
|
|
5
|
+
UploadEventEmitter,
|
|
6
|
+
} from "@uploadista/core/types";
|
|
7
|
+
import { Effect, Layer } from "effect";
|
|
8
|
+
import type {
|
|
9
|
+
EventEmitterDurableObject,
|
|
10
|
+
EventEmitterDurableObjectBranded,
|
|
11
|
+
} from "./event-emitter-durable-object";
|
|
12
|
+
|
|
13
|
+
export type UploadEventEmitterDurableObjectStoreConfig = {
|
|
14
|
+
durableObject: EventEmitterDurableObject<UploadEvent>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function makeEventEmitterDurableObjectStore<T>({
|
|
18
|
+
durableObject,
|
|
19
|
+
}: UploadEventEmitterDurableObjectStoreConfig): EventEmitter<T> {
|
|
20
|
+
function getStub(key: string) {
|
|
21
|
+
const id = durableObject.idFromName(key);
|
|
22
|
+
return durableObject.get(
|
|
23
|
+
id,
|
|
24
|
+
) as unknown as EventEmitterDurableObjectBranded<T>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
emit: (key: string, event: T) => {
|
|
29
|
+
const stub = getStub(key);
|
|
30
|
+
return Effect.tryPromise({
|
|
31
|
+
try: async () => {
|
|
32
|
+
await stub.emit(event);
|
|
33
|
+
return;
|
|
34
|
+
},
|
|
35
|
+
catch: (cause) => {
|
|
36
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
subscribe: (key: string, connection) => {
|
|
41
|
+
return Effect.tryPromise({
|
|
42
|
+
try: async () => {
|
|
43
|
+
const stub = getStub(key);
|
|
44
|
+
await stub.subscribe(connection);
|
|
45
|
+
return;
|
|
46
|
+
},
|
|
47
|
+
catch: (cause) => {
|
|
48
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
unsubscribe: (key: string) => {
|
|
53
|
+
return Effect.tryPromise({
|
|
54
|
+
try: async () => {
|
|
55
|
+
const stub = getStub(key);
|
|
56
|
+
await stub.unsubscribe();
|
|
57
|
+
return;
|
|
58
|
+
},
|
|
59
|
+
catch: (cause) => {
|
|
60
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const uploadEventEmitterDurableObjectStore = (
|
|
68
|
+
config: UploadEventEmitterDurableObjectStoreConfig,
|
|
69
|
+
) =>
|
|
70
|
+
Layer.succeed(
|
|
71
|
+
UploadEventEmitter,
|
|
72
|
+
makeEventEmitterDurableObjectStore<UploadEvent>(config),
|
|
73
|
+
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
// import { WebSocketPair } from "@cloudflare/workers-types";
|
|
3
|
+
// import type { UploadEvent } from "@uploadista/core/types";
|
|
4
|
+
|
|
5
|
+
// export class UploadEventDurableObject extends DurableObject {
|
|
6
|
+
// async fetch(_request: Request): Promise<Response> {
|
|
7
|
+
// // Creates two ends of a WebSocket connection.
|
|
8
|
+
// const webSocketPair = new WebSocketPair();
|
|
9
|
+
// const [client, server] = Object.values(webSocketPair);
|
|
10
|
+
|
|
11
|
+
// // Calling `acceptWebSocket()` informs the runtime that this WebSocket is to begin terminating
|
|
12
|
+
// // request within the Durable Object. It has the effect of "accepting" the connection,
|
|
13
|
+
// // and allowing the WebSocket to send and receive messages.
|
|
14
|
+
// // Unlike `ws.accept()`, `state.acceptWebSocket(ws)` informs the Workers Runtime that the WebSocket
|
|
15
|
+
// // is "hibernatable", so the runtime does not need to pin this Durable Object to memory while
|
|
16
|
+
// // the connection is open. During periods of inactivity, the Durable Object can be evicted
|
|
17
|
+
// // from memory, but the WebSocket connection will remain open. If at some later point the
|
|
18
|
+
// // WebSocket receives a message, the runtime will recreate the Durable Object
|
|
19
|
+
// // (run the `constructor`) and deliver the message to the appropriate handler.
|
|
20
|
+
// this.ctx.acceptWebSocket(server);
|
|
21
|
+
|
|
22
|
+
// return new Response(null, {
|
|
23
|
+
// status: 101,
|
|
24
|
+
// webSocket: client,
|
|
25
|
+
// });
|
|
26
|
+
// }
|
|
27
|
+
|
|
28
|
+
// emit(event: UploadEvent) {
|
|
29
|
+
// for (const ws of this.ctx.getWebSockets()) {
|
|
30
|
+
// ws.send(JSON.stringify(event satisfies UploadEvent));
|
|
31
|
+
// }
|
|
32
|
+
// }
|
|
33
|
+
|
|
34
|
+
// async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
|
|
35
|
+
// // Upon receiving a message from the client, the server replies with the same message,
|
|
36
|
+
// // and the total number of connections with the "[Durable Object]: " prefix
|
|
37
|
+
// ws.send(
|
|
38
|
+
// `[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`,
|
|
39
|
+
// );
|
|
40
|
+
// }
|
|
41
|
+
|
|
42
|
+
// async webSocketClose(
|
|
43
|
+
// ws: WebSocket,
|
|
44
|
+
// code: number,
|
|
45
|
+
// _reason: string,
|
|
46
|
+
// _wasClean: boolean,
|
|
47
|
+
// ) {
|
|
48
|
+
// // If the client closes the connection, the runtime will invoke the webSocketClose() handler.
|
|
49
|
+
// // Don't try to close an already closed WebSocket or use reserved close codes
|
|
50
|
+
// if (ws.readyState === WebSocket.OPEN) {
|
|
51
|
+
// // Use a valid close code instead of the potentially reserved one from the client
|
|
52
|
+
// // 1000 = Normal Closure, 1001 = Going Away are safe codes to use
|
|
53
|
+
// const validCloseCode =
|
|
54
|
+
// code === 1006 || code < 1000 || code > 4999 ? 1000 : code;
|
|
55
|
+
// ws.close(validCloseCode, "Durable Object is closing WebSocket");
|
|
56
|
+
// }
|
|
57
|
+
// }
|
|
58
|
+
// }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
2
|
+
import type { WebSocketConnection } from "@uploadista/core/types";
|
|
3
|
+
|
|
4
|
+
export type EventEmitterDurableObjectBranded<T> = Rpc.DurableObjectBranded & {
|
|
5
|
+
emit: (event: T) => Promise<void>;
|
|
6
|
+
subscribe: (connection: WebSocketConnection) => Promise<void>;
|
|
7
|
+
unsubscribe: () => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Durable Object
|
|
11
|
+
export type EventEmitterDurableObject<T> = DurableObjectNamespace<
|
|
12
|
+
EventEmitterDurableObjectBranded<T>
|
|
13
|
+
>;
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@uploadista/typescript-config/server.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": "./",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/*": ["./src/*"]
|
|
7
|
+
},
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"types": ["./worker-configuration.d.ts"],
|
|
11
|
+
"lib": ["ES2022", "WebWorker"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "worker-configuration.d.ts"]
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/do-event-emitter.ts","./src/durable-object-impl.ts","./src/event-emitter-durable-object.ts","./src/index.ts","./worker-configuration.d.ts"],"version":"5.9.3"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare module "cloudflare:workers" {
|
|
2
|
+
abstract class DurableObject<Env = unknown>
|
|
3
|
+
implements Rpc.DurableObjectBranded
|
|
4
|
+
{
|
|
5
|
+
[Rpc.__DURABLE_OBJECT_BRAND]: never;
|
|
6
|
+
protected ctx: DurableObjectState;
|
|
7
|
+
protected env: Env;
|
|
8
|
+
constructor(ctx: DurableObjectState, env: Env);
|
|
9
|
+
fetch?(request: Request): Response | Promise<Response>;
|
|
10
|
+
alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise<void>;
|
|
11
|
+
webSocketMessage?(
|
|
12
|
+
ws: WebSocket,
|
|
13
|
+
message: string | ArrayBuffer,
|
|
14
|
+
): void | Promise<void>;
|
|
15
|
+
webSocketClose?(
|
|
16
|
+
ws: WebSocket,
|
|
17
|
+
code: number,
|
|
18
|
+
reason: string,
|
|
19
|
+
wasClean: boolean,
|
|
20
|
+
): void | Promise<void>;
|
|
21
|
+
webSocketError?(ws: WebSocket, error: unknown): void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
}
|