@uploadista/event-emitter-websocket 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 +0 -0
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/test-generic.d.ts +36 -0
- package/dist/test-generic.d.ts.map +1 -0
- package/dist/test-generic.js +12 -0
- package/dist/websocket-event-emitter.d.ts +7 -0
- package/dist/websocket-event-emitter.d.ts.map +1 -0
- package/dist/websocket-event-emitter.js +57 -0
- package/dist/websocket-manager.d.ts +27 -0
- package/dist/websocket-manager.d.ts.map +1 -0
- package/dist/websocket-manager.js +97 -0
- package/dist/websocket-server.d.ts +10 -0
- package/dist/websocket-server.d.ts.map +1 -0
- package/dist/websocket-server.js +56 -0
- package/package.json +28 -0
- package/src/index.ts +11 -0
- package/src/websocket-event-emitter.ts +83 -0
- package/src/websocket-manager.ts +146 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
|
File without changes
|
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,441 @@
|
|
|
1
|
+
# @uploadista/event-emitter-websocket
|
|
2
|
+
|
|
3
|
+
WebSocket-based event emitter for Uploadista. Sends real-time events to connected clients via persistent WebSocket connections.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The WebSocket event emitter broadcasts events to connected clients in real-time. Perfect for:
|
|
8
|
+
|
|
9
|
+
- **Real-Time Progress**: Stream upload progress to browsers
|
|
10
|
+
- **Live Notifications**: Immediate status updates for users
|
|
11
|
+
- **Client Subscriptions**: Clients subscribe to specific events via WebSocket
|
|
12
|
+
- **Single-Server Deployments**: Works seamlessly with memory broadcaster
|
|
13
|
+
- **Browser Clients**: Native WebSocket support in all browsers
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @uploadista/event-emitter-websocket
|
|
19
|
+
# or
|
|
20
|
+
pnpm add @uploadista/event-emitter-websocket
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Prerequisites
|
|
24
|
+
|
|
25
|
+
- Node.js 18+ with WebSocket support
|
|
26
|
+
- An event broadcaster layer (memory, redis, or ioredis)
|
|
27
|
+
- WebSocket client library in browser (built-in)
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
33
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
34
|
+
import { Effect } from "effect";
|
|
35
|
+
|
|
36
|
+
const program = Effect.gen(function* () {
|
|
37
|
+
// Event emitter is automatically available
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
Effect.runSync(
|
|
41
|
+
program.pipe(
|
|
42
|
+
Effect.provide(webSocketEventEmitter(memoryEventBroadcaster)),
|
|
43
|
+
// ... other layers
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- ✅ **Real-Time Events**: Sub-millisecond event delivery to browser
|
|
51
|
+
- ✅ **Browser Native**: Uses standard WebSocket API
|
|
52
|
+
- ✅ **Connection Management**: Automatic cleanup on disconnect
|
|
53
|
+
- ✅ **Event Routing**: Route events to specific subscribers
|
|
54
|
+
- ✅ **Type Safe**: Full TypeScript support
|
|
55
|
+
- ✅ **Broadcaster Agnostic**: Works with any broadcaster
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### Main Exports
|
|
60
|
+
|
|
61
|
+
#### `webSocketEventEmitter(broadcaster: Layer): Layer<BaseEventEmitterService>`
|
|
62
|
+
|
|
63
|
+
Creates an Effect layer combining WebSocket emitter with a broadcaster.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
67
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
68
|
+
|
|
69
|
+
const layer = webSocketEventEmitter(memoryEventBroadcaster);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### `WebSocketManager: Service`
|
|
73
|
+
|
|
74
|
+
Manages WebSocket connections and subscriptions.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { WebSocketManager } from "@uploadista/event-emitter-websocket";
|
|
78
|
+
import { Effect } from "effect";
|
|
79
|
+
|
|
80
|
+
const program = Effect.gen(function* () {
|
|
81
|
+
const manager = yield* WebSocketManager;
|
|
82
|
+
|
|
83
|
+
// Add connection
|
|
84
|
+
manager.addConnection("conn-1", connection);
|
|
85
|
+
|
|
86
|
+
// Subscribe to events
|
|
87
|
+
manager.subscribeToEvents("upload:123", "conn-1");
|
|
88
|
+
|
|
89
|
+
// Emit to subscribers
|
|
90
|
+
manager.emitToEvents("upload:123", "Upload complete");
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Available Operations
|
|
95
|
+
|
|
96
|
+
#### `emit(eventKey: string, message: string): Effect<void>`
|
|
97
|
+
|
|
98
|
+
Emit event to all subscribers.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
const program = Effect.gen(function* () {
|
|
102
|
+
yield* emitter.emit("upload:123", JSON.stringify({
|
|
103
|
+
status: "completed",
|
|
104
|
+
duration: 5000,
|
|
105
|
+
}));
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### `subscribe(eventKey: string, connection: WebSocketConnection): Effect<void>`
|
|
110
|
+
|
|
111
|
+
Subscribe a WebSocket to events.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const program = Effect.gen(function* () {
|
|
115
|
+
yield* emitter.subscribe("upload:123", wsConnection);
|
|
116
|
+
// Client now receives all events for upload:123
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### `unsubscribe(eventKey: string): Effect<void>`
|
|
121
|
+
|
|
122
|
+
Unsubscribe from events.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const program = Effect.gen(function* () {
|
|
126
|
+
yield* emitter.unsubscribe("upload:123");
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
### With Memory Broadcaster
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
136
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
137
|
+
import { uploadServer } from "@uploadista/server";
|
|
138
|
+
import { Effect } from "effect";
|
|
139
|
+
|
|
140
|
+
const program = Effect.gen(function* () {
|
|
141
|
+
const server = yield* uploadServer;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
Effect.runSync(
|
|
145
|
+
program.pipe(
|
|
146
|
+
Effect.provide(
|
|
147
|
+
webSocketEventEmitter(memoryEventBroadcaster)
|
|
148
|
+
),
|
|
149
|
+
// ... other layers
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### With Redis Broadcaster
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
158
|
+
import { redisEventBroadcaster } from "@uploadista/event-broadcaster-redis";
|
|
159
|
+
import { createClient } from "@redis/client";
|
|
160
|
+
|
|
161
|
+
const redis = createClient({ url: "redis://localhost:6379" });
|
|
162
|
+
const subscriberRedis = createClient({ url: "redis://localhost:6379" });
|
|
163
|
+
|
|
164
|
+
await redis.connect();
|
|
165
|
+
await subscriberRedis.connect();
|
|
166
|
+
|
|
167
|
+
const layer = webSocketEventEmitter(
|
|
168
|
+
redisEventBroadcaster({ redis, subscriberRedis })
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
### Example 1: Upload Progress to Browser
|
|
175
|
+
|
|
176
|
+
Server-side:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
180
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
181
|
+
|
|
182
|
+
// Subscribe client to upload events
|
|
183
|
+
const subscribeToUpload = (uploadId: string, wsConnection: WebSocket) =>
|
|
184
|
+
Effect.gen(function* () {
|
|
185
|
+
yield* emitter.subscribe(`upload:${uploadId}`, wsConnection);
|
|
186
|
+
console.log(`Client subscribed to ${uploadId}`);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Emit progress updates
|
|
190
|
+
const sendProgress = (uploadId: string, progress: number) =>
|
|
191
|
+
Effect.gen(function* () {
|
|
192
|
+
yield* emitter.emit(`upload:${uploadId}`, JSON.stringify({
|
|
193
|
+
type: "progress",
|
|
194
|
+
progress,
|
|
195
|
+
bytesReceived: Math.floor(100 * 1024 * 1024 * progress),
|
|
196
|
+
}));
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Client-side (browser):
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const ws = new WebSocket("ws://localhost:3000/uploads/abc123");
|
|
204
|
+
|
|
205
|
+
// Subscribe to upload
|
|
206
|
+
ws.send(JSON.stringify({
|
|
207
|
+
type: "subscribe",
|
|
208
|
+
uploadId: "abc123",
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
// Receive updates
|
|
212
|
+
ws.onmessage = (event) => {
|
|
213
|
+
const message = JSON.parse(event.data);
|
|
214
|
+
|
|
215
|
+
if (message.type === "progress") {
|
|
216
|
+
updateProgressBar(message.progress);
|
|
217
|
+
console.log(`Downloaded: ${message.bytesReceived} bytes`);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Example 2: Real-Time Flow Status
|
|
223
|
+
|
|
224
|
+
Server broadcasts flow job status:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const broadcastFlowStatus = (jobId: string, status: string) =>
|
|
228
|
+
Effect.gen(function* () {
|
|
229
|
+
yield* emitter.emit(`flow:${jobId}`, JSON.stringify({
|
|
230
|
+
status,
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
}));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const trackFlowJob = (jobId: string) =>
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
yield* broadcastFlowStatus(jobId, "queued");
|
|
238
|
+
yield* Effect.sleep("2 seconds");
|
|
239
|
+
|
|
240
|
+
yield* broadcastFlowStatus(jobId, "processing");
|
|
241
|
+
yield* Effect.sleep("5 seconds");
|
|
242
|
+
|
|
243
|
+
yield* broadcastFlowStatus(jobId, "completed");
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Example 3: Multiple Concurrent Uploads
|
|
248
|
+
|
|
249
|
+
Each client receives only its own upload events:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// Client 1 subscribes to upload A
|
|
253
|
+
yield* emitter.subscribe("upload:a", clientA_ws);
|
|
254
|
+
|
|
255
|
+
// Client 2 subscribes to upload B
|
|
256
|
+
yield* emitter.subscribe("upload:b", clientB_ws);
|
|
257
|
+
|
|
258
|
+
// Events are routed appropriately
|
|
259
|
+
yield* emitter.emit("upload:a", JSON.stringify({ progress: 0.5 }));
|
|
260
|
+
// Only clientA receives this
|
|
261
|
+
|
|
262
|
+
yield* emitter.emit("upload:b", JSON.stringify({ progress: 0.3 }));
|
|
263
|
+
// Only clientB receives this
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Message Format
|
|
267
|
+
|
|
268
|
+
Standard message structure for events:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
interface WebSocketMessage {
|
|
272
|
+
type: string; // Event type
|
|
273
|
+
payload?: any; // Event data
|
|
274
|
+
timestamp: string; // ISO timestamp
|
|
275
|
+
uploadId?: string; // Associated upload
|
|
276
|
+
flowId?: string; // Associated flow
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Example messages:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"type": "progress",
|
|
285
|
+
"payload": { "progress": 0.75, "speed": "2.5 MB/s" },
|
|
286
|
+
"timestamp": "2025-10-21T12:30:45Z",
|
|
287
|
+
"uploadId": "upl_123"
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Performance
|
|
292
|
+
|
|
293
|
+
| Operation | Latency | Throughput |
|
|
294
|
+
|-----------|---------|-----------|
|
|
295
|
+
| emit() | ~1ms | 1000+ events/sec per connection |
|
|
296
|
+
| subscribe() | ~100μs | N/A |
|
|
297
|
+
| Message delivery | ~5-10ms | Depends on network |
|
|
298
|
+
|
|
299
|
+
Each WebSocket connection can handle 1000+ events per second.
|
|
300
|
+
|
|
301
|
+
## Connection Lifecycle
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
Client connects
|
|
305
|
+
↓
|
|
306
|
+
subscribe(uploadId, wsConnection)
|
|
307
|
+
↓
|
|
308
|
+
Client receives events via WebSocket
|
|
309
|
+
↓
|
|
310
|
+
unsubscribe(uploadId) or disconnect
|
|
311
|
+
↓
|
|
312
|
+
Cleanup
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Best Practices
|
|
316
|
+
|
|
317
|
+
### 1. Use Specific Event Keys
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Good: Specific IDs
|
|
321
|
+
`upload:${uploadId}`
|
|
322
|
+
`flow:${jobId}`
|
|
323
|
+
`user:${userId}`
|
|
324
|
+
|
|
325
|
+
// Avoid: Generic
|
|
326
|
+
"updates", "events", "status"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 2. Include Type in Message
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Always include type
|
|
333
|
+
yield* emitter.emit(`upload:123`, JSON.stringify({
|
|
334
|
+
type: "progress",
|
|
335
|
+
progress: 0.5,
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
// Client can route based on type
|
|
339
|
+
if (msg.type === "progress") {
|
|
340
|
+
updateProgressBar(msg.progress);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 3. Handle Disconnects
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// Server
|
|
348
|
+
ws.addEventListener("close", () => {
|
|
349
|
+
// Cleanup subscriptions
|
|
350
|
+
yield* emitter.unsubscribe(`upload:${uploadId}`);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Client
|
|
354
|
+
ws.addEventListener("close", () => {
|
|
355
|
+
console.log("Lost connection, retrying...");
|
|
356
|
+
reconnectWebSocket();
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Deployment
|
|
361
|
+
|
|
362
|
+
### Express + Hono Example
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { createServer } from "http";
|
|
366
|
+
import { WebSocketServer } from "ws";
|
|
367
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
368
|
+
|
|
369
|
+
const server = createServer();
|
|
370
|
+
const wss = new WebSocketServer({ server });
|
|
371
|
+
|
|
372
|
+
wss.on("connection", (ws) => {
|
|
373
|
+
ws.on("message", (data) => {
|
|
374
|
+
const message = JSON.parse(data.toString());
|
|
375
|
+
|
|
376
|
+
if (message.type === "subscribe") {
|
|
377
|
+
const eventKey = `upload:${message.uploadId}`;
|
|
378
|
+
// Subscribe ws to events
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
server.listen(3000);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Troubleshooting
|
|
387
|
+
|
|
388
|
+
### Clients Not Receiving Events
|
|
389
|
+
|
|
390
|
+
Verify subscription before emitting:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// ✅ Correct order
|
|
394
|
+
yield* emitter.subscribe("upload:123", ws);
|
|
395
|
+
yield* emitter.emit("upload:123", message);
|
|
396
|
+
|
|
397
|
+
// ❌ Wrong
|
|
398
|
+
yield* emitter.emit("upload:123", message);
|
|
399
|
+
yield* emitter.subscribe("upload:123", ws); // Misses event
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### High Memory Usage
|
|
403
|
+
|
|
404
|
+
Clean up disconnected connections:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
ws.addEventListener("close", () => {
|
|
408
|
+
// Unsubscribe and cleanup
|
|
409
|
+
yield* emitter.unsubscribe(eventKey);
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Slow Message Delivery
|
|
414
|
+
|
|
415
|
+
Check broadcaster performance:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// With Redis broadcaster
|
|
419
|
+
redis-cli LATENCY LATEST
|
|
420
|
+
|
|
421
|
+
// With memory broadcaster
|
|
422
|
+
// Should be < 1ms
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Related Packages
|
|
426
|
+
|
|
427
|
+
- [@uploadista/core](../../core) - Core types
|
|
428
|
+
- [@uploadista/event-broadcaster-memory](../event-broadcasters/memory) - Memory broadcaster
|
|
429
|
+
- [@uploadista/event-broadcaster-redis](../event-broadcasters/redis) - Redis broadcaster
|
|
430
|
+
- [@uploadista/server](../../servers/server) - Upload server
|
|
431
|
+
- [@uploadista/adapters-hono](../../servers/adapters-hono) - Hono WebSocket adapter
|
|
432
|
+
|
|
433
|
+
## License
|
|
434
|
+
|
|
435
|
+
See [LICENSE](../../../LICENSE) in the main repository.
|
|
436
|
+
|
|
437
|
+
## See Also
|
|
438
|
+
|
|
439
|
+
- [EVENT_SYSTEM.md](./EVENT_SYSTEM.md) - Architecture overview
|
|
440
|
+
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) - Browser WebSocket
|
|
441
|
+
- [Server Setup Guide](../../../SERVER_SETUP.md#websockets) - WebSocket deployment
|
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,EACL,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
declare const uploadEventEmitter: import("@uploadista/core").EventEmitter<{
|
|
2
|
+
type: import("@uploadista/core").UploadEventType.UPLOAD_STARTED | import("@uploadista/core").UploadEventType.UPLOAD_COMPLETE;
|
|
3
|
+
data: {
|
|
4
|
+
id: string;
|
|
5
|
+
offset: number;
|
|
6
|
+
storage: {
|
|
7
|
+
id: string;
|
|
8
|
+
type: string;
|
|
9
|
+
path?: string | undefined;
|
|
10
|
+
uploadId?: string | undefined;
|
|
11
|
+
bucket?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
size?: number | undefined;
|
|
14
|
+
metadata?: Record<string, string> | undefined;
|
|
15
|
+
creationDate?: string | undefined;
|
|
16
|
+
url?: string | undefined;
|
|
17
|
+
sizeIsDeferred?: boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
} | {
|
|
20
|
+
type: import("@uploadista/core").UploadEventType.UPLOAD_PROGRESS;
|
|
21
|
+
data: {
|
|
22
|
+
id: string;
|
|
23
|
+
progress: number;
|
|
24
|
+
total: number;
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
type: import("@uploadista/core").UploadEventType.UPLOAD_FAILED;
|
|
28
|
+
data: {
|
|
29
|
+
id: string;
|
|
30
|
+
error: string;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
declare const flowEventEmitter: import("@uploadista/core").EventEmitter<FlowEvent>;
|
|
34
|
+
declare const combinedEventEmitter: import("@uploadista/core").EventEmitter<any>;
|
|
35
|
+
export { uploadEventEmitter, flowEventEmitter, combinedEventEmitter };
|
|
36
|
+
//# sourceMappingURL=test-generic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-generic.d.ts","sourceRoot":"","sources":["../src/test-generic.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EACgC,CAAC;AACzD,QAAA,MAAM,gBAAgB,oDAAqD,CAAC;AAC5E,QAAA,MAAM,oBAAoB,8CAER,CAAC;AAGnB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { webSocketEventEmitterStore } from "./websocket-event-emitter";
|
|
2
|
+
import { createWebSocketManager } from "./websocket-manager";
|
|
3
|
+
// Test that we can create websocket managers for both event types
|
|
4
|
+
const uploadManager = createWebSocketManager();
|
|
5
|
+
const flowManager = createWebSocketManager();
|
|
6
|
+
const combinedManager = createWebSocketManager();
|
|
7
|
+
// Test that we can create event emitters for both types
|
|
8
|
+
const uploadEventEmitter = webSocketEventEmitterStore(uploadManager);
|
|
9
|
+
const flowEventEmitter = webSocketEventEmitterStore(flowManager);
|
|
10
|
+
const combinedEventEmitter = webSocketEventEmitterStore(combinedManager);
|
|
11
|
+
// This file just tests compilation - no runtime code needed
|
|
12
|
+
export { uploadEventEmitter, flowEventEmitter, combinedEventEmitter };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type BaseEventEmitter, BaseEventEmitterService, type EventBroadcasterService } from "@uploadista/core/types";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
import { type WebSocketManager, WebSocketManagerService } from "./websocket-manager";
|
|
4
|
+
export declare function webSocketBaseEventEmitter(webSocketManager: WebSocketManager): BaseEventEmitter;
|
|
5
|
+
export declare const makeBaseEventEmitter: Effect.Effect<BaseEventEmitter, never, WebSocketManagerService>;
|
|
6
|
+
export declare const webSocketEventEmitter: (eventBroadcaster: Layer.Layer<EventBroadcasterService>) => Layer.Layer<BaseEventEmitterService, never, never>;
|
|
7
|
+
//# sourceMappingURL=websocket-event-emitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-event-emitter.d.ts","sourceRoot":"","sources":["../src/websocket-event-emitter.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,gBAAgB,EACrB,uBAAuB,EACvB,KAAK,uBAAuB,EAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EACL,KAAK,gBAAgB,EACrB,uBAAuB,EAExB,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,yBAAyB,CACvC,gBAAgB,EAAE,gBAAgB,GACjC,gBAAgB,CAiDlB;AAGD,eAAO,MAAM,oBAAoB,iEAG/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,GAChC,kBAAkB,KAAK,CAAC,KAAK,CAAC,uBAAuB,CAAC,uDAKrD,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { BaseEventEmitterService, } from "@uploadista/core/types";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
import { WebSocketManagerService, webSocketManager, } from "./websocket-manager";
|
|
5
|
+
export function webSocketBaseEventEmitter(webSocketManager) {
|
|
6
|
+
return {
|
|
7
|
+
emit: (eventKey, message) => {
|
|
8
|
+
return Effect.try({
|
|
9
|
+
try: () => {
|
|
10
|
+
webSocketManager.emitToEvents(eventKey, message);
|
|
11
|
+
},
|
|
12
|
+
catch: (cause) => {
|
|
13
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
subscribe: (eventKey, connection) => {
|
|
18
|
+
return Effect.try({
|
|
19
|
+
try: () => {
|
|
20
|
+
webSocketManager.addConnection(connection.id, connection);
|
|
21
|
+
webSocketManager.subscribeToEvents(eventKey, connection.id);
|
|
22
|
+
// Send confirmation message
|
|
23
|
+
connection.send(JSON.stringify({
|
|
24
|
+
type: "subscribed",
|
|
25
|
+
payload: { eventKey },
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
}));
|
|
28
|
+
},
|
|
29
|
+
catch: (cause) => {
|
|
30
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
unsubscribe: (eventKey) => {
|
|
35
|
+
return Effect.try({
|
|
36
|
+
try: () => {
|
|
37
|
+
// Note: We need connectionId for proper cleanup, but the interface only provides eventKey
|
|
38
|
+
// For now, we'll remove all connections for this eventKey
|
|
39
|
+
// This could be improved by tracking connection mapping
|
|
40
|
+
const connections = webSocketManager.getConnections();
|
|
41
|
+
for (const [connectionId] of connections) {
|
|
42
|
+
webSocketManager.unsubscribeFromEvents(eventKey, connectionId);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
catch: (cause) => {
|
|
46
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Effect-based implementation using webSocketManagerService
|
|
53
|
+
export const makeBaseEventEmitter = Effect.gen(function* () {
|
|
54
|
+
const webSocketManager = yield* WebSocketManagerService;
|
|
55
|
+
return webSocketBaseEventEmitter(webSocketManager);
|
|
56
|
+
});
|
|
57
|
+
export const webSocketEventEmitter = (eventBroadcaster) => Layer.effect(BaseEventEmitterService, makeBaseEventEmitter).pipe(Layer.provide(webSocketManager), Layer.provide(eventBroadcaster));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WebSocketConnection } from "@uploadista/core/types";
|
|
2
|
+
import { EventBroadcasterService } from "@uploadista/core/types";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
export interface WebSocketManager {
|
|
5
|
+
readonly getConnection: (key: string) => WebSocketConnection | null;
|
|
6
|
+
readonly getConnections: () => Map<string, WebSocketConnection>;
|
|
7
|
+
readonly addConnection: (key: string, connection: WebSocketConnection) => void;
|
|
8
|
+
readonly removeConnection: (key: string) => void;
|
|
9
|
+
readonly subscribeToEvents: (eventKey: string, connectionId: string) => void;
|
|
10
|
+
readonly unsubscribeFromEvents: (eventKey: string, connectionId: string) => void;
|
|
11
|
+
readonly emitToEvents: (eventKey: string, event: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare const makeWebSocketManager: Effect.Effect<{
|
|
14
|
+
getConnection: (key: string) => WebSocketConnection | null;
|
|
15
|
+
getConnections: () => Map<string, WebSocketConnection>;
|
|
16
|
+
addConnection: (key: string, connection: WebSocketConnection) => void;
|
|
17
|
+
removeConnection: (key: string) => void;
|
|
18
|
+
subscribeToEvents: (eventKey: string, connectionId: string) => void;
|
|
19
|
+
unsubscribeFromEvents: (eventKey: string, connectionId: string) => void;
|
|
20
|
+
emitToEvents: (eventKey: string, message: string) => void;
|
|
21
|
+
}, never, EventBroadcasterService>;
|
|
22
|
+
declare const WebSocketManagerService_base: Context.TagClass<WebSocketManagerService, "BaseWebSocketManagerService", WebSocketManager>;
|
|
23
|
+
export declare class WebSocketManagerService extends WebSocketManagerService_base {
|
|
24
|
+
}
|
|
25
|
+
export declare const webSocketManager: Layer.Layer<WebSocketManagerService, never, EventBroadcasterService>;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=websocket-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-manager.d.ts","sourceRoot":"","sources":["../src/websocket-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGhD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,mBAAmB,GAAG,IAAI,CAAC;IACpE,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAChE,QAAQ,CAAC,aAAa,EAAE,CACtB,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,mBAAmB,KAC5B,IAAI,CAAC;IACV,QAAQ,CAAC,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7E,QAAQ,CAAC,qBAAqB,EAAE,CAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,KACjB,IAAI,CAAC;IACV,QAAQ,CAAC,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE;AAED,eAAO,MAAM,oBAAoB;yBA8CH,MAAM,KAAG,mBAAmB,GAAG,IAAI;0BAIpC,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC;yBAKpD,MAAM,cACC,mBAAmB,KAC9B,IAAI;4BAIwB,MAAM,KAAG,IAAI;kCAWP,MAAM,gBAAgB,MAAM,KAAG,IAAI;sCAQ5D,MAAM,gBACF,MAAM,KACnB,IAAI;6BAUyB,MAAM,WAAW,MAAM,KAAG,IAAI;kCAqB9D,CAAC;;AAGH,qBAAa,uBAAwB,SAAQ,4BAEC;CAAG;AAGjD,eAAO,MAAM,gBAAgB,sEAG5B,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { EventBroadcasterService } from "@uploadista/core/types";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
export const makeWebSocketManager = Effect.gen(function* () {
|
|
4
|
+
const broadcaster = yield* EventBroadcasterService;
|
|
5
|
+
const connections = new Map();
|
|
6
|
+
const subscriptions = new Map(); // eventKey -> Set<connectionId>
|
|
7
|
+
// Helper to send messages to local connections for a given eventKey
|
|
8
|
+
const emitToLocalConnections = (eventKey, message) => {
|
|
9
|
+
const connectionIds = subscriptions.get(eventKey);
|
|
10
|
+
if (!connectionIds)
|
|
11
|
+
return;
|
|
12
|
+
for (const connectionId of connectionIds) {
|
|
13
|
+
const connection = connections.get(connectionId);
|
|
14
|
+
if (connection && connection.readyState === 1) {
|
|
15
|
+
try {
|
|
16
|
+
connection.send(message);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.warn(`Failed to send message to connection ${connectionId}:`, error);
|
|
20
|
+
removeConnection(connectionId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
removeConnection(connectionId);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
// Subscribe to broadcast events from other instances
|
|
29
|
+
yield* broadcaster
|
|
30
|
+
.subscribe("uploadista:events", (broadcastMessage) => {
|
|
31
|
+
try {
|
|
32
|
+
const { eventKey, message } = JSON.parse(broadcastMessage);
|
|
33
|
+
emitToLocalConnections(eventKey, message);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.warn("Failed to parse broadcast message:", error);
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.pipe(Effect.catchAll((error) => {
|
|
40
|
+
console.error("Failed to subscribe to broadcast events:", error);
|
|
41
|
+
return Effect.void;
|
|
42
|
+
}));
|
|
43
|
+
const getConnection = (key) => {
|
|
44
|
+
return connections.get(key) || null;
|
|
45
|
+
};
|
|
46
|
+
const getConnections = () => {
|
|
47
|
+
return connections;
|
|
48
|
+
};
|
|
49
|
+
const addConnection = (key, connection) => {
|
|
50
|
+
connections.set(key, connection);
|
|
51
|
+
};
|
|
52
|
+
const removeConnection = (key) => {
|
|
53
|
+
connections.delete(key);
|
|
54
|
+
// Clean up subscriptions
|
|
55
|
+
for (const [eventKey, connectionIds] of subscriptions.entries()) {
|
|
56
|
+
connectionIds.delete(key);
|
|
57
|
+
if (connectionIds.size === 0) {
|
|
58
|
+
subscriptions.delete(eventKey);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const subscribeToEvents = (eventKey, connectionId) => {
|
|
63
|
+
if (!subscriptions.has(eventKey)) {
|
|
64
|
+
subscriptions.set(eventKey, new Set());
|
|
65
|
+
}
|
|
66
|
+
subscriptions.get(eventKey)?.add(connectionId);
|
|
67
|
+
};
|
|
68
|
+
const unsubscribeFromEvents = (eventKey, connectionId) => {
|
|
69
|
+
const connectionIds = subscriptions.get(eventKey);
|
|
70
|
+
if (connectionIds) {
|
|
71
|
+
connectionIds.delete(connectionId);
|
|
72
|
+
if (connectionIds.size === 0) {
|
|
73
|
+
subscriptions.delete(eventKey);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const emitToEvents = (eventKey, message) => {
|
|
78
|
+
// Publish to broadcaster so all instances (including this one) receive it
|
|
79
|
+
Effect.runPromise(broadcaster.publish("uploadista:events", JSON.stringify({ eventKey, message }))).catch((error) => {
|
|
80
|
+
console.error("Failed to publish event to broadcaster:", error);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
getConnection,
|
|
85
|
+
getConnections,
|
|
86
|
+
addConnection,
|
|
87
|
+
removeConnection,
|
|
88
|
+
subscribeToEvents,
|
|
89
|
+
unsubscribeFromEvents,
|
|
90
|
+
emitToEvents,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
// Context tags
|
|
94
|
+
export class WebSocketManagerService extends Context.Tag("BaseWebSocketManagerService")() {
|
|
95
|
+
}
|
|
96
|
+
// Base layer
|
|
97
|
+
export const webSocketManager = Layer.effect(WebSocketManagerService, makeWebSocketManager);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
export interface WebSocketServer {
|
|
3
|
+
handleConnection(key: string, websocket: WebSocket): Effect.Effect<void, never>;
|
|
4
|
+
broadcastEvent(event: unknown): Effect.Effect<void, never>;
|
|
5
|
+
getConnectionCount(): number;
|
|
6
|
+
}
|
|
7
|
+
export declare function webSocketServer(): WebSocketServer;
|
|
8
|
+
export declare const WebSocketServer: Context.Tag<WebSocketServer, WebSocketServer>;
|
|
9
|
+
export declare const WebSocketServerLayer: Layer.Layer<WebSocketServer, never, never>;
|
|
10
|
+
//# sourceMappingURL=websocket-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-server.d.ts","sourceRoot":"","sources":["../src/websocket-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGhD,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CACd,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,SAAS,GACnB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC9B,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3D,kBAAkB,IAAI,MAAM,CAAC;CAC9B;AAED,wBAAgB,eAAe,IAAI,eAAe,CAgEjD;AAED,eAAO,MAAM,eAAe,+CAC4B,CAAC;AAEzD,eAAO,MAAM,oBAAoB,4CAGhC,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { createWebSocketManager } from "./websocket-event-emitter";
|
|
3
|
+
export function webSocketServer() {
|
|
4
|
+
const webSocketManager = createWebSocketManager();
|
|
5
|
+
const handleConnection = (key, websocket) => {
|
|
6
|
+
return Effect.sync(() => {
|
|
7
|
+
const connection = {
|
|
8
|
+
id: key,
|
|
9
|
+
send: (data) => websocket.send(data),
|
|
10
|
+
close: (code, reason) => websocket.close(code, reason),
|
|
11
|
+
readyState: websocket.readyState,
|
|
12
|
+
};
|
|
13
|
+
webSocketManager.addConnection(key, connection);
|
|
14
|
+
websocket.addEventListener("close", () => {
|
|
15
|
+
webSocketManager.removeConnection(key);
|
|
16
|
+
});
|
|
17
|
+
websocket.addEventListener("error", () => {
|
|
18
|
+
webSocketManager.removeConnection(key);
|
|
19
|
+
});
|
|
20
|
+
// Send a welcome message
|
|
21
|
+
websocket.send(JSON.stringify({
|
|
22
|
+
type: "connection",
|
|
23
|
+
message: "WebSocket connection established",
|
|
24
|
+
key,
|
|
25
|
+
}));
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const broadcastEvent = (event) => {
|
|
29
|
+
return Effect.sync(() => {
|
|
30
|
+
// Since we removed the generic broadcast method, we can use emitToUpload for specific upload events
|
|
31
|
+
// Or add a generic broadcast method if needed
|
|
32
|
+
const connections = webSocketManager.getConnections();
|
|
33
|
+
const message = JSON.stringify(event);
|
|
34
|
+
for (const [_key, connection] of connections) {
|
|
35
|
+
if (connection.readyState === 1) {
|
|
36
|
+
try {
|
|
37
|
+
connection.send(message);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.warn(`Failed to send broadcast message:`, error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const getConnectionCount = () => {
|
|
47
|
+
return webSocketManager.getConnections().size ?? 0;
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
handleConnection,
|
|
51
|
+
broadcastEvent,
|
|
52
|
+
getConnectionCount,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export const WebSocketServer = Context.GenericTag("WebSocketServer");
|
|
56
|
+
export const WebSocketServerLayer = Layer.effect(WebSocketServer, Effect.succeed(webSocketServer()));
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadista/event-emitter-websocket",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.3",
|
|
5
|
+
"description": "WebSocket 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
|
+
"effect": "3.18.4",
|
|
17
|
+
"@uploadista/core": "0.0.3"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@uploadista/typescript-config": "0.0.3"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -b",
|
|
24
|
+
"format": "biome format --write ./src",
|
|
25
|
+
"lint": "biome lint --write ./src",
|
|
26
|
+
"check": "biome check --write ./src"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import type {
|
|
3
|
+
WebSocketConnection,
|
|
4
|
+
WebSocketMessage,
|
|
5
|
+
} from "@uploadista/core/types";
|
|
6
|
+
import {
|
|
7
|
+
type BaseEventEmitter,
|
|
8
|
+
BaseEventEmitterService,
|
|
9
|
+
type EventBroadcasterService,
|
|
10
|
+
} from "@uploadista/core/types";
|
|
11
|
+
import { Effect, Layer } from "effect";
|
|
12
|
+
import {
|
|
13
|
+
type WebSocketManager,
|
|
14
|
+
WebSocketManagerService,
|
|
15
|
+
webSocketManager,
|
|
16
|
+
} from "./websocket-manager";
|
|
17
|
+
|
|
18
|
+
export function webSocketBaseEventEmitter(
|
|
19
|
+
webSocketManager: WebSocketManager,
|
|
20
|
+
): BaseEventEmitter {
|
|
21
|
+
return {
|
|
22
|
+
emit: (eventKey: string, message: string) => {
|
|
23
|
+
return Effect.try({
|
|
24
|
+
try: () => {
|
|
25
|
+
webSocketManager.emitToEvents(eventKey, message);
|
|
26
|
+
},
|
|
27
|
+
catch: (cause) => {
|
|
28
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
subscribe: (eventKey: string, connection: WebSocketConnection) => {
|
|
33
|
+
return Effect.try({
|
|
34
|
+
try: () => {
|
|
35
|
+
webSocketManager.addConnection(connection.id, connection);
|
|
36
|
+
webSocketManager.subscribeToEvents(eventKey, connection.id);
|
|
37
|
+
|
|
38
|
+
// Send confirmation message
|
|
39
|
+
connection.send(
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
type: "subscribed",
|
|
42
|
+
payload: { eventKey },
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
} satisfies WebSocketMessage),
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
catch: (cause) => {
|
|
48
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
unsubscribe: (eventKey: string) => {
|
|
53
|
+
return Effect.try({
|
|
54
|
+
try: () => {
|
|
55
|
+
// Note: We need connectionId for proper cleanup, but the interface only provides eventKey
|
|
56
|
+
// For now, we'll remove all connections for this eventKey
|
|
57
|
+
// This could be improved by tracking connection mapping
|
|
58
|
+
const connections = webSocketManager.getConnections();
|
|
59
|
+
for (const [connectionId] of connections) {
|
|
60
|
+
webSocketManager.unsubscribeFromEvents(eventKey, connectionId);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
catch: (cause) => {
|
|
64
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", { cause });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Effect-based implementation using webSocketManagerService
|
|
72
|
+
export const makeBaseEventEmitter = Effect.gen(function* () {
|
|
73
|
+
const webSocketManager = yield* WebSocketManagerService;
|
|
74
|
+
return webSocketBaseEventEmitter(webSocketManager);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const webSocketEventEmitter = (
|
|
78
|
+
eventBroadcaster: Layer.Layer<EventBroadcasterService>,
|
|
79
|
+
) =>
|
|
80
|
+
Layer.effect(BaseEventEmitterService, makeBaseEventEmitter).pipe(
|
|
81
|
+
Layer.provide(webSocketManager),
|
|
82
|
+
Layer.provide(eventBroadcaster),
|
|
83
|
+
);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { WebSocketConnection } from "@uploadista/core/types";
|
|
2
|
+
import { EventBroadcasterService } from "@uploadista/core/types";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
// Base untyped WebSocketManager - works with raw string messages
|
|
6
|
+
export interface WebSocketManager {
|
|
7
|
+
readonly getConnection: (key: string) => WebSocketConnection | null;
|
|
8
|
+
readonly getConnections: () => Map<string, WebSocketConnection>;
|
|
9
|
+
readonly addConnection: (
|
|
10
|
+
key: string,
|
|
11
|
+
connection: WebSocketConnection,
|
|
12
|
+
) => void;
|
|
13
|
+
readonly removeConnection: (key: string) => void;
|
|
14
|
+
readonly subscribeToEvents: (eventKey: string, connectionId: string) => void;
|
|
15
|
+
readonly unsubscribeFromEvents: (
|
|
16
|
+
eventKey: string,
|
|
17
|
+
connectionId: string,
|
|
18
|
+
) => void;
|
|
19
|
+
readonly emitToEvents: (eventKey: string, event: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const makeWebSocketManager = Effect.gen(function* () {
|
|
23
|
+
const broadcaster = yield* EventBroadcasterService;
|
|
24
|
+
|
|
25
|
+
const connections = new Map<string, WebSocketConnection>();
|
|
26
|
+
const subscriptions = new Map<string, Set<string>>(); // eventKey -> Set<connectionId>
|
|
27
|
+
|
|
28
|
+
// Helper to send messages to local connections for a given eventKey
|
|
29
|
+
const emitToLocalConnections = (eventKey: string, message: string): void => {
|
|
30
|
+
const connectionIds = subscriptions.get(eventKey);
|
|
31
|
+
if (!connectionIds) return;
|
|
32
|
+
|
|
33
|
+
for (const connectionId of connectionIds) {
|
|
34
|
+
const connection = connections.get(connectionId);
|
|
35
|
+
if (connection && connection.readyState === 1) {
|
|
36
|
+
try {
|
|
37
|
+
connection.send(message);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn(
|
|
40
|
+
`Failed to send message to connection ${connectionId}:`,
|
|
41
|
+
error,
|
|
42
|
+
);
|
|
43
|
+
removeConnection(connectionId);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
removeConnection(connectionId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Subscribe to broadcast events from other instances
|
|
52
|
+
yield* broadcaster
|
|
53
|
+
.subscribe("uploadista:events", (broadcastMessage) => {
|
|
54
|
+
try {
|
|
55
|
+
const { eventKey, message } = JSON.parse(broadcastMessage);
|
|
56
|
+
emitToLocalConnections(eventKey, message);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn("Failed to parse broadcast message:", error);
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.pipe(
|
|
62
|
+
Effect.catchAll((error) => {
|
|
63
|
+
console.error("Failed to subscribe to broadcast events:", error);
|
|
64
|
+
return Effect.void;
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const getConnection = (key: string): WebSocketConnection | null => {
|
|
69
|
+
return connections.get(key) || null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const getConnections = (): Map<string, WebSocketConnection> => {
|
|
73
|
+
return connections;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const addConnection = (
|
|
77
|
+
key: string,
|
|
78
|
+
connection: WebSocketConnection,
|
|
79
|
+
): void => {
|
|
80
|
+
connections.set(key, connection);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const removeConnection = (key: string): void => {
|
|
84
|
+
connections.delete(key);
|
|
85
|
+
// Clean up subscriptions
|
|
86
|
+
for (const [eventKey, connectionIds] of subscriptions.entries()) {
|
|
87
|
+
connectionIds.delete(key);
|
|
88
|
+
if (connectionIds.size === 0) {
|
|
89
|
+
subscriptions.delete(eventKey);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const subscribeToEvents = (eventKey: string, connectionId: string): void => {
|
|
95
|
+
if (!subscriptions.has(eventKey)) {
|
|
96
|
+
subscriptions.set(eventKey, new Set());
|
|
97
|
+
}
|
|
98
|
+
subscriptions.get(eventKey)?.add(connectionId);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const unsubscribeFromEvents = (
|
|
102
|
+
eventKey: string,
|
|
103
|
+
connectionId: string,
|
|
104
|
+
): void => {
|
|
105
|
+
const connectionIds = subscriptions.get(eventKey);
|
|
106
|
+
if (connectionIds) {
|
|
107
|
+
connectionIds.delete(connectionId);
|
|
108
|
+
if (connectionIds.size === 0) {
|
|
109
|
+
subscriptions.delete(eventKey);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const emitToEvents = (eventKey: string, message: string): void => {
|
|
115
|
+
// Publish to broadcaster so all instances (including this one) receive it
|
|
116
|
+
Effect.runPromise(
|
|
117
|
+
broadcaster.publish(
|
|
118
|
+
"uploadista:events",
|
|
119
|
+
JSON.stringify({ eventKey, message }),
|
|
120
|
+
),
|
|
121
|
+
).catch((error) => {
|
|
122
|
+
console.error("Failed to publish event to broadcaster:", error);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
getConnection,
|
|
128
|
+
getConnections,
|
|
129
|
+
addConnection,
|
|
130
|
+
removeConnection,
|
|
131
|
+
subscribeToEvents,
|
|
132
|
+
unsubscribeFromEvents,
|
|
133
|
+
emitToEvents,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Context tags
|
|
138
|
+
export class WebSocketManagerService extends Context.Tag(
|
|
139
|
+
"BaseWebSocketManagerService",
|
|
140
|
+
)<WebSocketManagerService, WebSocketManager>() {}
|
|
141
|
+
|
|
142
|
+
// Base layer
|
|
143
|
+
export const webSocketManager = Layer.effect(
|
|
144
|
+
WebSocketManagerService,
|
|
145
|
+
makeWebSocketManager,
|
|
146
|
+
);
|
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
|
+
"typeRoots": ["../../../../node_modules/@types"],
|
|
11
|
+
"lib": ["ES2022", "WebWorker"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts","./src/websocket-event-emitter.ts","./src/websocket-manager.ts"],"version":"5.9.3"}
|