@uploadista/server 0.0.13-beta.4 → 0.0.13
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/dist/auth/index.d.cts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +1 -1
- package/dist/{auth-BqArZeGK.mjs → auth-D2lKhlzK.mjs} +1 -1
- package/dist/{auth-BqArZeGK.mjs.map → auth-D2lKhlzK.mjs.map} +1 -1
- package/dist/{index-50KlDIjc.d.cts → index-BXLtlr98.d.mts} +1 -1
- package/dist/{index-50KlDIjc.d.cts.map → index-BXLtlr98.d.mts.map} +1 -1
- package/dist/{index--Lny6VJP.d.mts → index-mMP18lsw.d.cts} +1 -1
- package/dist/{index--Lny6VJP.d.mts.map → index-mMP18lsw.d.cts.map} +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +13 -11
- package/src/core/plugin-types.ts +5 -3
- package/src/plugins-typing.ts +1 -0
- package/{src/__tests__ → tests}/backward-compatibility.test.ts +23 -23
- package/{src → tests}/cache.test.ts +2 -2
- package/tests/core/http-handlers/flow-handlers.test.ts +495 -0
- package/tests/core/http-handlers/upload-handlers.test.ts +657 -0
- package/{src/core/__tests__ → tests/core}/plugin-validation.test.ts +59 -26
- package/tests/core/websocket-handlers/websocket-handlers.test.ts +659 -0
- package/{src → tests}/service.test.ts +2 -2
- package/type-tests/plugin-types.test-d.ts +56 -25
- package/vitest.config.ts +39 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for WebSocket Handlers
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - WebSocket connection authentication
|
|
6
|
+
* - Real-time upload progress updates
|
|
7
|
+
* - Flow execution event streaming
|
|
8
|
+
* - Connection lifecycle management
|
|
9
|
+
* - Error handling and reconnection
|
|
10
|
+
* - Message broadcasting
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { it } from "@effect/vitest";
|
|
14
|
+
import { Effect } from "effect";
|
|
15
|
+
import { describe, expect } from "vitest";
|
|
16
|
+
|
|
17
|
+
describe("WebSocket Handlers", () => {
|
|
18
|
+
describe("Connection Authentication", () => {
|
|
19
|
+
it.effect("should authenticate valid connection with token", () =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const mockAuthService = {
|
|
22
|
+
authenticateConnection: (token: string) =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
if (token === "valid-token") {
|
|
25
|
+
return {
|
|
26
|
+
authenticated: true,
|
|
27
|
+
clientId: "client-123",
|
|
28
|
+
permissions: ["upload", "read"],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return yield* Effect.fail(new Error("Invalid token"));
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result =
|
|
36
|
+
yield* mockAuthService.authenticateConnection("valid-token");
|
|
37
|
+
|
|
38
|
+
expect(result.authenticated).toBe(true);
|
|
39
|
+
expect(result.clientId).toBe("client-123");
|
|
40
|
+
expect(result.permissions).toContain("upload");
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it.effect("should reject invalid authentication tokens", () =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const mockAuthService = {
|
|
47
|
+
authenticateConnection: (token: string) =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
if (token !== "valid-token") {
|
|
50
|
+
return yield* Effect.fail(new Error("Invalid token"));
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
authenticated: true,
|
|
54
|
+
clientId: "client-123",
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const result = yield* Effect.either(
|
|
60
|
+
mockAuthService.authenticateConnection("invalid-token"),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(result._tag).toBe("Left");
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
it.effect("should handle anonymous connections when allowed", () =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const mockAuthService = {
|
|
70
|
+
authenticateConnection: (
|
|
71
|
+
token: string | null,
|
|
72
|
+
allowAnonymous: boolean,
|
|
73
|
+
) =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
if (token === null && allowAnonymous) {
|
|
76
|
+
return {
|
|
77
|
+
authenticated: true,
|
|
78
|
+
clientId: null,
|
|
79
|
+
anonymous: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (token === null) {
|
|
83
|
+
return yield* Effect.fail(new Error("Authentication required"));
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
authenticated: true,
|
|
87
|
+
clientId: "client-123",
|
|
88
|
+
anonymous: false,
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const result = yield* mockAuthService.authenticateConnection(
|
|
94
|
+
null,
|
|
95
|
+
true,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.authenticated).toBe(true);
|
|
99
|
+
expect(result.anonymous).toBe(true);
|
|
100
|
+
expect(result.clientId).toBeNull();
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
it.effect("should validate connection permissions", () =>
|
|
105
|
+
Effect.gen(function* () {
|
|
106
|
+
const mockAuthService = {
|
|
107
|
+
validatePermission: (clientId: string, requiredPermission: string) =>
|
|
108
|
+
Effect.gen(function* () {
|
|
109
|
+
const clientPermissions: Record<string, string[]> = {
|
|
110
|
+
"client-123": ["upload", "read"],
|
|
111
|
+
"client-456": ["read"],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const permissions = clientPermissions[clientId] || [];
|
|
115
|
+
if (!permissions.includes(requiredPermission)) {
|
|
116
|
+
return yield* Effect.fail(new Error("Permission denied"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { hasPermission: true };
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Client with upload permission
|
|
124
|
+
const result1 = yield* mockAuthService.validatePermission(
|
|
125
|
+
"client-123",
|
|
126
|
+
"upload",
|
|
127
|
+
);
|
|
128
|
+
expect(result1.hasPermission).toBe(true);
|
|
129
|
+
|
|
130
|
+
// Client without upload permission
|
|
131
|
+
const result2 = yield* Effect.either(
|
|
132
|
+
mockAuthService.validatePermission("client-456", "upload"),
|
|
133
|
+
);
|
|
134
|
+
expect(result2._tag).toBe("Left");
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("Real-Time Upload Progress", () => {
|
|
140
|
+
it.effect("should stream upload progress events", () =>
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
const progressEvents: Array<{ uploadId: string; progress: number }> =
|
|
143
|
+
[];
|
|
144
|
+
|
|
145
|
+
const mockProgressService = {
|
|
146
|
+
streamProgress: (uploadId: string, progress: number) =>
|
|
147
|
+
Effect.sync(() => {
|
|
148
|
+
progressEvents.push({ uploadId, progress });
|
|
149
|
+
return {
|
|
150
|
+
type: "progress" as const,
|
|
151
|
+
uploadId,
|
|
152
|
+
progress,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
}),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
yield* mockProgressService.streamProgress("upload-123", 25);
|
|
159
|
+
yield* mockProgressService.streamProgress("upload-123", 50);
|
|
160
|
+
yield* mockProgressService.streamProgress("upload-123", 75);
|
|
161
|
+
yield* mockProgressService.streamProgress("upload-123", 100);
|
|
162
|
+
|
|
163
|
+
expect(progressEvents).toHaveLength(4);
|
|
164
|
+
expect(progressEvents[0]?.progress).toBe(25);
|
|
165
|
+
expect(progressEvents[3]?.progress).toBe(100);
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
it.effect("should broadcast progress to multiple clients", () =>
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
const clientUpdates: Record<string, number[]> = {};
|
|
172
|
+
|
|
173
|
+
const mockBroadcastService = {
|
|
174
|
+
broadcastProgress: (
|
|
175
|
+
_uploadId: string,
|
|
176
|
+
progress: number,
|
|
177
|
+
clientIds: string[],
|
|
178
|
+
) =>
|
|
179
|
+
Effect.sync(() => {
|
|
180
|
+
for (const clientId of clientIds) {
|
|
181
|
+
if (!clientUpdates[clientId]) {
|
|
182
|
+
clientUpdates[clientId] = [];
|
|
183
|
+
}
|
|
184
|
+
clientUpdates[clientId].push(progress);
|
|
185
|
+
}
|
|
186
|
+
return { sent: clientIds.length };
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
yield* mockBroadcastService.broadcastProgress("upload-123", 50, [
|
|
191
|
+
"client-1",
|
|
192
|
+
"client-2",
|
|
193
|
+
"client-3",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect(Object.keys(clientUpdates)).toHaveLength(3);
|
|
197
|
+
expect(clientUpdates["client-1"]).toContain(50);
|
|
198
|
+
expect(clientUpdates["client-2"]).toContain(50);
|
|
199
|
+
expect(clientUpdates["client-3"]).toContain(50);
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
it.effect("should handle upload completion events", () =>
|
|
204
|
+
Effect.gen(function* () {
|
|
205
|
+
const mockProgressService = {
|
|
206
|
+
sendCompletionEvent: (uploadId: string, fileUrl: string) =>
|
|
207
|
+
Effect.succeed({
|
|
208
|
+
type: "completed" as const,
|
|
209
|
+
uploadId,
|
|
210
|
+
fileUrl,
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const result = yield* mockProgressService.sendCompletionEvent(
|
|
216
|
+
"upload-123",
|
|
217
|
+
"https://storage.example.com/files/upload-123",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(result.type).toBe("completed");
|
|
221
|
+
expect(result.uploadId).toBe("upload-123");
|
|
222
|
+
expect(result.fileUrl).toContain("upload-123");
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
it.effect("should handle upload error events", () =>
|
|
227
|
+
Effect.gen(function* () {
|
|
228
|
+
const mockProgressService = {
|
|
229
|
+
sendErrorEvent: (uploadId: string, error: string) =>
|
|
230
|
+
Effect.succeed({
|
|
231
|
+
type: "error" as const,
|
|
232
|
+
uploadId,
|
|
233
|
+
error,
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
}),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const result = yield* mockProgressService.sendErrorEvent(
|
|
239
|
+
"upload-123",
|
|
240
|
+
"Upload failed: Network error",
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(result.type).toBe("error");
|
|
244
|
+
expect(result.uploadId).toBe("upload-123");
|
|
245
|
+
expect(result.error).toContain("Network error");
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("Flow Execution Events", () => {
|
|
251
|
+
it.effect("should stream flow execution start events", () =>
|
|
252
|
+
Effect.gen(function* () {
|
|
253
|
+
const mockFlowService = {
|
|
254
|
+
sendFlowStartEvent: (flowId: string, jobId: string) =>
|
|
255
|
+
Effect.succeed({
|
|
256
|
+
type: "flow-started" as const,
|
|
257
|
+
flowId,
|
|
258
|
+
jobId,
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
}),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const result = yield* mockFlowService.sendFlowStartEvent(
|
|
264
|
+
"flow-123",
|
|
265
|
+
"job-456",
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(result.type).toBe("flow-started");
|
|
269
|
+
expect(result.flowId).toBe("flow-123");
|
|
270
|
+
expect(result.jobId).toBe("job-456");
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
it.effect("should stream node execution events", () =>
|
|
275
|
+
Effect.gen(function* () {
|
|
276
|
+
const nodeEvents: Array<{ nodeId: string; status: string }> = [];
|
|
277
|
+
|
|
278
|
+
const mockFlowService = {
|
|
279
|
+
sendNodeEvent: (
|
|
280
|
+
nodeId: string,
|
|
281
|
+
status: "started" | "completed" | "failed",
|
|
282
|
+
) =>
|
|
283
|
+
Effect.sync(() => {
|
|
284
|
+
nodeEvents.push({ nodeId, status });
|
|
285
|
+
return {
|
|
286
|
+
type: "node-event" as const,
|
|
287
|
+
nodeId,
|
|
288
|
+
status,
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
};
|
|
291
|
+
}),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
yield* mockFlowService.sendNodeEvent("node-1", "started");
|
|
295
|
+
yield* mockFlowService.sendNodeEvent("node-1", "completed");
|
|
296
|
+
yield* mockFlowService.sendNodeEvent("node-2", "started");
|
|
297
|
+
yield* mockFlowService.sendNodeEvent("node-2", "completed");
|
|
298
|
+
|
|
299
|
+
expect(nodeEvents).toHaveLength(4);
|
|
300
|
+
expect(nodeEvents[0]?.status).toBe("started");
|
|
301
|
+
expect(nodeEvents[1]?.status).toBe("completed");
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
it.effect("should stream flow completion events", () =>
|
|
306
|
+
Effect.gen(function* () {
|
|
307
|
+
const mockFlowService = {
|
|
308
|
+
sendFlowCompletionEvent: (
|
|
309
|
+
flowId: string,
|
|
310
|
+
jobId: string,
|
|
311
|
+
result: unknown,
|
|
312
|
+
) =>
|
|
313
|
+
Effect.succeed({
|
|
314
|
+
type: "flow-completed" as const,
|
|
315
|
+
flowId,
|
|
316
|
+
jobId,
|
|
317
|
+
result,
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
}),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const result = yield* mockFlowService.sendFlowCompletionEvent(
|
|
323
|
+
"flow-123",
|
|
324
|
+
"job-456",
|
|
325
|
+
{
|
|
326
|
+
outputFile: "processed-file.jpg",
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(result.type).toBe("flow-completed");
|
|
331
|
+
expect(result.flowId).toBe("flow-123");
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
it.effect("should stream flow error events", () =>
|
|
336
|
+
Effect.gen(function* () {
|
|
337
|
+
const mockFlowService = {
|
|
338
|
+
sendFlowErrorEvent: (flowId: string, jobId: string, error: string) =>
|
|
339
|
+
Effect.succeed({
|
|
340
|
+
type: "flow-error" as const,
|
|
341
|
+
flowId,
|
|
342
|
+
jobId,
|
|
343
|
+
error,
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const result = yield* mockFlowService.sendFlowErrorEvent(
|
|
349
|
+
"flow-123",
|
|
350
|
+
"job-456",
|
|
351
|
+
"Node execution failed",
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
expect(result.type).toBe("flow-error");
|
|
355
|
+
expect(result.error).toContain("failed");
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("Connection Lifecycle", () => {
|
|
361
|
+
it.effect("should handle connection open event", () =>
|
|
362
|
+
Effect.gen(function* () {
|
|
363
|
+
const mockConnectionService = {
|
|
364
|
+
handleOpen: (connectionId: string, clientId: string) =>
|
|
365
|
+
Effect.succeed({
|
|
366
|
+
event: "connection-open" as const,
|
|
367
|
+
connectionId,
|
|
368
|
+
clientId,
|
|
369
|
+
timestamp: Date.now(),
|
|
370
|
+
}),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const result = yield* mockConnectionService.handleOpen(
|
|
374
|
+
"conn-123",
|
|
375
|
+
"client-456",
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(result.event).toBe("connection-open");
|
|
379
|
+
expect(result.connectionId).toBe("conn-123");
|
|
380
|
+
expect(result.clientId).toBe("client-456");
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
it.effect("should handle connection close event", () =>
|
|
385
|
+
Effect.gen(function* () {
|
|
386
|
+
const mockConnectionService = {
|
|
387
|
+
handleClose: (connectionId: string, code: number, reason: string) =>
|
|
388
|
+
Effect.succeed({
|
|
389
|
+
event: "connection-close" as const,
|
|
390
|
+
connectionId,
|
|
391
|
+
code,
|
|
392
|
+
reason,
|
|
393
|
+
timestamp: Date.now(),
|
|
394
|
+
}),
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const result = yield* mockConnectionService.handleClose(
|
|
398
|
+
"conn-123",
|
|
399
|
+
1000,
|
|
400
|
+
"Normal closure",
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(result.event).toBe("connection-close");
|
|
404
|
+
expect(result.code).toBe(1000);
|
|
405
|
+
expect(result.reason).toBe("Normal closure");
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
it.effect("should track active connections", () =>
|
|
410
|
+
Effect.gen(function* () {
|
|
411
|
+
const activeConnections = new Set<string>();
|
|
412
|
+
|
|
413
|
+
const mockConnectionService = {
|
|
414
|
+
addConnection: (connectionId: string) =>
|
|
415
|
+
Effect.sync(() => {
|
|
416
|
+
activeConnections.add(connectionId);
|
|
417
|
+
return { count: activeConnections.size };
|
|
418
|
+
}),
|
|
419
|
+
removeConnection: (connectionId: string) =>
|
|
420
|
+
Effect.sync(() => {
|
|
421
|
+
activeConnections.delete(connectionId);
|
|
422
|
+
return { count: activeConnections.size };
|
|
423
|
+
}),
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
yield* mockConnectionService.addConnection("conn-1");
|
|
427
|
+
yield* mockConnectionService.addConnection("conn-2");
|
|
428
|
+
const result1 = yield* mockConnectionService.addConnection("conn-3");
|
|
429
|
+
expect(result1.count).toBe(3);
|
|
430
|
+
|
|
431
|
+
const result2 = yield* mockConnectionService.removeConnection("conn-2");
|
|
432
|
+
expect(result2.count).toBe(2);
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
it.effect("should handle connection heartbeat/ping", () =>
|
|
437
|
+
Effect.gen(function* () {
|
|
438
|
+
const mockConnectionService = {
|
|
439
|
+
handlePing: (connectionId: string) =>
|
|
440
|
+
Effect.succeed({
|
|
441
|
+
type: "pong" as const,
|
|
442
|
+
connectionId,
|
|
443
|
+
timestamp: Date.now(),
|
|
444
|
+
}),
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const result = yield* mockConnectionService.handlePing("conn-123");
|
|
448
|
+
|
|
449
|
+
expect(result.type).toBe("pong");
|
|
450
|
+
expect(result.connectionId).toBe("conn-123");
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe("Error Handling and Reconnection", () => {
|
|
456
|
+
it.effect("should handle connection errors gracefully", () =>
|
|
457
|
+
Effect.gen(function* () {
|
|
458
|
+
const mockErrorHandler = {
|
|
459
|
+
handleConnectionError: (connectionId: string, error: string) =>
|
|
460
|
+
Effect.succeed({
|
|
461
|
+
event: "connection-error" as const,
|
|
462
|
+
connectionId,
|
|
463
|
+
error,
|
|
464
|
+
shouldReconnect: true,
|
|
465
|
+
timestamp: Date.now(),
|
|
466
|
+
}),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const result = yield* mockErrorHandler.handleConnectionError(
|
|
470
|
+
"conn-123",
|
|
471
|
+
"Network timeout",
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(result.event).toBe("connection-error");
|
|
475
|
+
expect(result.shouldReconnect).toBe(true);
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
it.effect("should implement reconnection logic with backoff", () =>
|
|
480
|
+
Effect.gen(function* () {
|
|
481
|
+
let reconnectAttempts = 0;
|
|
482
|
+
const delays: number[] = [];
|
|
483
|
+
|
|
484
|
+
const mockReconnectService = {
|
|
485
|
+
attemptReconnect: (connectionId: string, attemptNumber: number) =>
|
|
486
|
+
Effect.gen(function* () {
|
|
487
|
+
reconnectAttempts++;
|
|
488
|
+
const delay = Math.min(1000 * 2 ** attemptNumber, 30000);
|
|
489
|
+
delays.push(delay);
|
|
490
|
+
|
|
491
|
+
if (attemptNumber < 3) {
|
|
492
|
+
return yield* Effect.fail(new Error("Reconnection failed"));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
success: true,
|
|
497
|
+
connectionId,
|
|
498
|
+
attempts: reconnectAttempts,
|
|
499
|
+
};
|
|
500
|
+
}),
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Simulate reconnection attempts
|
|
504
|
+
yield* Effect.either(
|
|
505
|
+
mockReconnectService.attemptReconnect("conn-123", 0),
|
|
506
|
+
);
|
|
507
|
+
yield* Effect.either(
|
|
508
|
+
mockReconnectService.attemptReconnect("conn-123", 1),
|
|
509
|
+
);
|
|
510
|
+
yield* Effect.either(
|
|
511
|
+
mockReconnectService.attemptReconnect("conn-123", 2),
|
|
512
|
+
);
|
|
513
|
+
const result = yield* mockReconnectService.attemptReconnect(
|
|
514
|
+
"conn-123",
|
|
515
|
+
3,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(result.success).toBe(true);
|
|
519
|
+
expect(reconnectAttempts).toBe(4);
|
|
520
|
+
// Delays should be: 1s, 2s, 4s, 8s
|
|
521
|
+
expect(delays[0]).toBe(1000);
|
|
522
|
+
expect(delays[1]).toBe(2000);
|
|
523
|
+
expect(delays[2]).toBe(4000);
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
it.effect("should limit maximum reconnection attempts", () =>
|
|
528
|
+
Effect.gen(function* () {
|
|
529
|
+
const MAX_ATTEMPTS = 5;
|
|
530
|
+
let attempts = 0;
|
|
531
|
+
|
|
532
|
+
const mockReconnectService = {
|
|
533
|
+
attemptReconnect: (_connectionId: string) =>
|
|
534
|
+
Effect.gen(function* () {
|
|
535
|
+
attempts++;
|
|
536
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
537
|
+
return {
|
|
538
|
+
success: false,
|
|
539
|
+
reason: "Max reconnection attempts reached",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return yield* Effect.fail(new Error("Reconnection failed"));
|
|
543
|
+
}),
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// Attempt reconnections until max attempts
|
|
547
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
548
|
+
yield* Effect.either(
|
|
549
|
+
mockReconnectService.attemptReconnect("conn-123"),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const finalResult =
|
|
554
|
+
yield* mockReconnectService.attemptReconnect("conn-123");
|
|
555
|
+
expect(finalResult.success).toBe(false);
|
|
556
|
+
expect(finalResult.reason).toContain("Max reconnection attempts");
|
|
557
|
+
}),
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe("Message Broadcasting", () => {
|
|
562
|
+
it.effect("should broadcast message to all connected clients", () =>
|
|
563
|
+
Effect.gen(function* () {
|
|
564
|
+
const deliveredTo: string[] = [];
|
|
565
|
+
|
|
566
|
+
const mockBroadcastService = {
|
|
567
|
+
broadcastToAll: (_message: unknown, excludeConnectionId?: string) =>
|
|
568
|
+
Effect.sync(() => {
|
|
569
|
+
const connections = ["conn-1", "conn-2", "conn-3"];
|
|
570
|
+
for (const connId of connections) {
|
|
571
|
+
if (connId !== excludeConnectionId) {
|
|
572
|
+
deliveredTo.push(connId);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return { delivered: deliveredTo.length };
|
|
576
|
+
}),
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const result = yield* mockBroadcastService.broadcastToAll({
|
|
580
|
+
type: "announcement",
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
expect(result.delivered).toBe(3);
|
|
584
|
+
expect(deliveredTo).toHaveLength(3);
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
it.effect("should broadcast to specific channel/room", () =>
|
|
589
|
+
Effect.gen(function* () {
|
|
590
|
+
const channels: Record<string, string[]> = {
|
|
591
|
+
"upload-123": ["conn-1", "conn-2"],
|
|
592
|
+
"upload-456": ["conn-3"],
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const mockBroadcastService = {
|
|
596
|
+
broadcastToChannel: (channelId: string, _message: unknown) =>
|
|
597
|
+
Effect.succeed({
|
|
598
|
+
delivered: (channels[channelId] || []).length,
|
|
599
|
+
connectionIds: channels[channelId] || [],
|
|
600
|
+
}),
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const result = yield* mockBroadcastService.broadcastToChannel(
|
|
604
|
+
"upload-123",
|
|
605
|
+
{
|
|
606
|
+
progress: 50,
|
|
607
|
+
},
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
expect(result.delivered).toBe(2);
|
|
611
|
+
expect(result.connectionIds).toContain("conn-1");
|
|
612
|
+
expect(result.connectionIds).toContain("conn-2");
|
|
613
|
+
}),
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
it.effect("should handle subscription to channels", () =>
|
|
617
|
+
Effect.gen(function* () {
|
|
618
|
+
const subscriptions: Record<string, Set<string>> = {};
|
|
619
|
+
|
|
620
|
+
const mockSubscriptionService = {
|
|
621
|
+
subscribe: (connectionId: string, channelId: string) =>
|
|
622
|
+
Effect.sync(() => {
|
|
623
|
+
if (!subscriptions[channelId]) {
|
|
624
|
+
subscriptions[channelId] = new Set();
|
|
625
|
+
}
|
|
626
|
+
subscriptions[channelId].add(connectionId);
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
channelId,
|
|
630
|
+
subscriberCount: subscriptions[channelId].size,
|
|
631
|
+
};
|
|
632
|
+
}),
|
|
633
|
+
unsubscribe: (connectionId: string, channelId: string) =>
|
|
634
|
+
Effect.sync(() => {
|
|
635
|
+
subscriptions[channelId]?.delete(connectionId);
|
|
636
|
+
return {
|
|
637
|
+
success: true,
|
|
638
|
+
channelId,
|
|
639
|
+
subscriberCount: subscriptions[channelId]?.size || 0,
|
|
640
|
+
};
|
|
641
|
+
}),
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
yield* mockSubscriptionService.subscribe("conn-1", "upload-123");
|
|
645
|
+
const result1 = yield* mockSubscriptionService.subscribe(
|
|
646
|
+
"conn-2",
|
|
647
|
+
"upload-123",
|
|
648
|
+
);
|
|
649
|
+
expect(result1.subscriberCount).toBe(2);
|
|
650
|
+
|
|
651
|
+
const result2 = yield* mockSubscriptionService.unsubscribe(
|
|
652
|
+
"conn-1",
|
|
653
|
+
"upload-123",
|
|
654
|
+
);
|
|
655
|
+
expect(result2.subscriberCount).toBe(1);
|
|
656
|
+
}),
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
AuthContextService,
|
|
5
5
|
AuthContextServiceLive,
|
|
6
6
|
NoAuthContextServiceLive,
|
|
7
|
-
} from "
|
|
8
|
-
import type { AuthContext } from "
|
|
7
|
+
} from "../src/service";
|
|
8
|
+
import type { AuthContext } from "../src/types";
|
|
9
9
|
|
|
10
10
|
describe("AuthContextService", () => {
|
|
11
11
|
describe("AuthContextServiceLive", () => {
|