@uploadista/client-core 0.0.13 → 0.0.14

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/client-core",
3
3
  "type": "module",
4
- "version": "0.0.13",
4
+ "version": "0.0.14",
5
5
  "description": "Platform-agnostic core upload client logic for Uploadista",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -21,17 +21,25 @@
21
21
  "./upload": {
22
22
  "types": "./dist/upload/index.d.mts",
23
23
  "import": "./dist/upload/index.mjs"
24
+ },
25
+ "./managers": {
26
+ "types": "./dist/managers/index.d.mts",
27
+ "import": "./dist/managers/index.mjs"
28
+ },
29
+ "./testing": {
30
+ "types": "./dist/testing/index.d.mts",
31
+ "import": "./dist/testing/index.mjs"
24
32
  }
25
33
  },
26
34
  "dependencies": {
27
35
  "js-base64": "3.7.8",
28
36
  "zod": "4.1.12",
29
- "@uploadista/core": "0.0.13"
37
+ "@uploadista/core": "0.0.14"
30
38
  },
31
39
  "devDependencies": {
32
- "tsdown": "0.16.3",
40
+ "tsdown": "0.16.4",
33
41
  "vitest": "4.0.8",
34
- "@uploadista/typescript-config": "0.0.13"
42
+ "@uploadista/typescript-config": "0.0.14"
35
43
  },
36
44
  "scripts": {
37
45
  "build": "tsdown",
package/src/index.ts CHANGED
@@ -11,3 +11,5 @@ export * from "./services";
11
11
  export * from "./storage";
12
12
  // Core types
13
13
  export * from "./types";
14
+ // Managers
15
+ export * from "./managers";
@@ -0,0 +1,566 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ type EventSource,
4
+ EventSubscriptionManager,
5
+ type GenericEvent,
6
+ type SubscriptionEventHandler,
7
+ } from "../event-subscription-manager";
8
+
9
+ describe("EventSubscriptionManager", () => {
10
+ interface TestEvent extends GenericEvent {
11
+ type: string;
12
+ data?: {
13
+ id?: string;
14
+ priority?: string;
15
+ value?: number;
16
+ };
17
+ }
18
+
19
+ let mockEventSource: EventSource<TestEvent>;
20
+ let eventHandlers: SubscriptionEventHandler<TestEvent>[];
21
+
22
+ beforeEach(() => {
23
+ eventHandlers = [];
24
+
25
+ mockEventSource = {
26
+ subscribe: vi.fn((handler: SubscriptionEventHandler<TestEvent>) => {
27
+ eventHandlers.push(handler);
28
+ return () => {
29
+ const index = eventHandlers.indexOf(handler);
30
+ if (index !== -1) {
31
+ eventHandlers.splice(index, 1);
32
+ }
33
+ };
34
+ }),
35
+ };
36
+ });
37
+
38
+ const emitEvent = (event: TestEvent) => {
39
+ for (const handler of eventHandlers) {
40
+ handler(event);
41
+ }
42
+ };
43
+
44
+ describe("constructor", () => {
45
+ it("should create manager with event source", () => {
46
+ const manager = new EventSubscriptionManager(mockEventSource);
47
+ expect(manager).toBeInstanceOf(EventSubscriptionManager);
48
+ });
49
+
50
+ it("should start with zero subscriptions", () => {
51
+ const manager = new EventSubscriptionManager(mockEventSource);
52
+ expect(manager.getSubscriptionCount()).toBe(0);
53
+ expect(manager.hasSubscriptions()).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe("subscribe", () => {
58
+ it("should subscribe to event source", () => {
59
+ const manager = new EventSubscriptionManager(mockEventSource);
60
+ const handler = vi.fn();
61
+
62
+ manager.subscribe(handler);
63
+
64
+ expect(mockEventSource.subscribe).toHaveBeenCalledWith(
65
+ expect.any(Function),
66
+ );
67
+ });
68
+
69
+ it("should call handler when event occurs", () => {
70
+ const manager = new EventSubscriptionManager(mockEventSource);
71
+ const handler = vi.fn();
72
+
73
+ manager.subscribe(handler);
74
+
75
+ const event: TestEvent = { type: "UPLOAD_PROGRESS", data: { value: 50 } };
76
+ emitEvent(event);
77
+
78
+ expect(handler).toHaveBeenCalledWith(event);
79
+ });
80
+
81
+ it("should return unsubscribe function", () => {
82
+ const manager = new EventSubscriptionManager(mockEventSource);
83
+ const handler = vi.fn();
84
+
85
+ const unsubscribe = manager.subscribe(handler);
86
+
87
+ expect(typeof unsubscribe).toBe("function");
88
+ });
89
+
90
+ it("should increment subscription count", () => {
91
+ const manager = new EventSubscriptionManager(mockEventSource);
92
+
93
+ expect(manager.getSubscriptionCount()).toBe(0);
94
+
95
+ manager.subscribe(vi.fn());
96
+ expect(manager.getSubscriptionCount()).toBe(1);
97
+
98
+ manager.subscribe(vi.fn());
99
+ expect(manager.getSubscriptionCount()).toBe(2);
100
+ });
101
+
102
+ it("should track multiple subscriptions independently", () => {
103
+ const manager = new EventSubscriptionManager(mockEventSource);
104
+ const handler1 = vi.fn();
105
+ const handler2 = vi.fn();
106
+
107
+ manager.subscribe(handler1);
108
+ manager.subscribe(handler2);
109
+
110
+ const event: TestEvent = { type: "TEST" };
111
+ emitEvent(event);
112
+
113
+ expect(handler1).toHaveBeenCalledWith(event);
114
+ expect(handler2).toHaveBeenCalledWith(event);
115
+ });
116
+ });
117
+
118
+ describe("event filtering", () => {
119
+ describe("by event type", () => {
120
+ it("should filter events by type", () => {
121
+ const manager = new EventSubscriptionManager(mockEventSource);
122
+ const handler = vi.fn();
123
+
124
+ manager.subscribe(handler, { eventType: "UPLOAD_PROGRESS" });
125
+
126
+ emitEvent({ type: "UPLOAD_PROGRESS" });
127
+ emitEvent({ type: "UPLOAD_ERROR" });
128
+ emitEvent({ type: "UPLOAD_PROGRESS" });
129
+
130
+ expect(handler).toHaveBeenCalledTimes(2);
131
+ expect(handler).toHaveBeenCalledWith({ type: "UPLOAD_PROGRESS" });
132
+ });
133
+
134
+ it("should pass all events when no type filter", () => {
135
+ const manager = new EventSubscriptionManager(mockEventSource);
136
+ const handler = vi.fn();
137
+
138
+ manager.subscribe(handler);
139
+
140
+ emitEvent({ type: "TYPE_A" });
141
+ emitEvent({ type: "TYPE_B" });
142
+ emitEvent({ type: "TYPE_C" });
143
+
144
+ expect(handler).toHaveBeenCalledTimes(3);
145
+ });
146
+ });
147
+
148
+ describe("by upload ID", () => {
149
+ it("should filter events by upload ID", () => {
150
+ const manager = new EventSubscriptionManager(mockEventSource);
151
+ const handler = vi.fn();
152
+
153
+ manager.subscribe(handler, { uploadId: "upload-123" });
154
+
155
+ emitEvent({ type: "PROGRESS", data: { id: "upload-123" } });
156
+ emitEvent({ type: "PROGRESS", data: { id: "upload-456" } });
157
+ emitEvent({ type: "PROGRESS", data: { id: "upload-123" } });
158
+
159
+ expect(handler).toHaveBeenCalledTimes(2);
160
+ expect(handler).toHaveBeenCalledWith({
161
+ type: "PROGRESS",
162
+ data: { id: "upload-123" },
163
+ });
164
+ });
165
+
166
+ it("should filter for null ID (events without ID)", () => {
167
+ const manager = new EventSubscriptionManager(mockEventSource);
168
+ const handler = vi.fn();
169
+
170
+ manager.subscribe(handler, { uploadId: null });
171
+
172
+ emitEvent({ type: "GENERAL" });
173
+ emitEvent({ type: "UPLOAD", data: { id: "upload-123" } });
174
+ emitEvent({ type: "GENERAL", data: {} });
175
+
176
+ // Should only get events without an ID
177
+ expect(handler).toHaveBeenCalledTimes(2);
178
+ });
179
+
180
+ it("should handle missing data gracefully", () => {
181
+ const manager = new EventSubscriptionManager(mockEventSource);
182
+ const handler = vi.fn();
183
+
184
+ manager.subscribe(handler, { uploadId: "upload-123" });
185
+
186
+ emitEvent({ type: "EVENT", data: undefined });
187
+ emitEvent({ type: "EVENT" });
188
+
189
+ expect(handler).not.toHaveBeenCalled();
190
+ });
191
+ });
192
+
193
+ describe("by custom filter", () => {
194
+ it("should apply custom filter function", () => {
195
+ const manager = new EventSubscriptionManager(mockEventSource);
196
+ const handler = vi.fn();
197
+
198
+ manager.subscribe(handler, {
199
+ customFilter: (event) =>
200
+ (event.data as { priority?: string })?.priority === "high",
201
+ });
202
+
203
+ emitEvent({ type: "TASK", data: { priority: "high" } });
204
+ emitEvent({ type: "TASK", data: { priority: "low" } });
205
+ emitEvent({ type: "TASK", data: { priority: "high" } });
206
+
207
+ expect(handler).toHaveBeenCalledTimes(2);
208
+ });
209
+
210
+ it("should combine event type and custom filter", () => {
211
+ const manager = new EventSubscriptionManager(mockEventSource);
212
+ const handler = vi.fn();
213
+
214
+ manager.subscribe(handler, {
215
+ eventType: "PROGRESS",
216
+ customFilter: (event) =>
217
+ ((event.data as { value?: number })?.value ?? 0) > 50,
218
+ });
219
+
220
+ emitEvent({ type: "PROGRESS", data: { value: 75 } }); // pass
221
+ emitEvent({ type: "PROGRESS", data: { value: 25 } }); // fail custom
222
+ emitEvent({ type: "ERROR", data: { value: 75 } }); // fail type
223
+ emitEvent({ type: "PROGRESS", data: { value: 100 } }); // pass
224
+
225
+ expect(handler).toHaveBeenCalledTimes(2);
226
+ });
227
+
228
+ it("should apply all filters together", () => {
229
+ const manager = new EventSubscriptionManager(mockEventSource);
230
+ const handler = vi.fn();
231
+
232
+ manager.subscribe(handler, {
233
+ eventType: "PROGRESS",
234
+ uploadId: "upload-123",
235
+ customFilter: (event) =>
236
+ ((event.data as { value?: number })?.value ?? 0) > 50,
237
+ });
238
+
239
+ // All pass
240
+ emitEvent({
241
+ type: "PROGRESS",
242
+ data: { id: "upload-123", value: 75 },
243
+ });
244
+
245
+ // Fail type
246
+ emitEvent({ type: "ERROR", data: { id: "upload-123", value: 75 } });
247
+
248
+ // Fail ID
249
+ emitEvent({
250
+ type: "PROGRESS",
251
+ data: { id: "upload-456", value: 75 },
252
+ });
253
+
254
+ // Fail custom filter
255
+ emitEvent({
256
+ type: "PROGRESS",
257
+ data: { id: "upload-123", value: 25 },
258
+ });
259
+
260
+ expect(handler).toHaveBeenCalledTimes(1);
261
+ });
262
+ });
263
+ });
264
+
265
+ describe("unsubscribe", () => {
266
+ it("should stop receiving events after unsubscribe", () => {
267
+ const manager = new EventSubscriptionManager(mockEventSource);
268
+ const handler = vi.fn();
269
+
270
+ const unsubscribe = manager.subscribe(handler);
271
+
272
+ emitEvent({ type: "TEST" });
273
+ expect(handler).toHaveBeenCalledTimes(1);
274
+
275
+ unsubscribe();
276
+
277
+ emitEvent({ type: "TEST" });
278
+ expect(handler).toHaveBeenCalledTimes(1); // Still 1, not called again
279
+ });
280
+
281
+ it("should decrement subscription count", () => {
282
+ const manager = new EventSubscriptionManager(mockEventSource);
283
+
284
+ const unsub1 = manager.subscribe(vi.fn());
285
+ const unsub2 = manager.subscribe(vi.fn());
286
+
287
+ expect(manager.getSubscriptionCount()).toBe(2);
288
+
289
+ unsub1();
290
+ expect(manager.getSubscriptionCount()).toBe(1);
291
+
292
+ unsub2();
293
+ expect(manager.getSubscriptionCount()).toBe(0);
294
+ });
295
+
296
+ it("should only affect specific subscription", () => {
297
+ const manager = new EventSubscriptionManager(mockEventSource);
298
+ const handler1 = vi.fn();
299
+ const handler2 = vi.fn();
300
+
301
+ const unsub1 = manager.subscribe(handler1);
302
+ manager.subscribe(handler2);
303
+
304
+ unsub1();
305
+
306
+ emitEvent({ type: "TEST" });
307
+
308
+ expect(handler1).not.toHaveBeenCalled();
309
+ expect(handler2).toHaveBeenCalled();
310
+ });
311
+
312
+ it("should be safe to call unsubscribe multiple times", () => {
313
+ const manager = new EventSubscriptionManager(mockEventSource);
314
+ const handler = vi.fn();
315
+
316
+ const unsubscribe = manager.subscribe(handler);
317
+
318
+ unsubscribe();
319
+ expect(() => unsubscribe()).not.toThrow();
320
+ expect(manager.getSubscriptionCount()).toBe(0);
321
+ });
322
+ });
323
+
324
+ describe("getSubscriptionCount", () => {
325
+ it("should return correct count", () => {
326
+ const manager = new EventSubscriptionManager(mockEventSource);
327
+
328
+ expect(manager.getSubscriptionCount()).toBe(0);
329
+
330
+ const unsub1 = manager.subscribe(vi.fn());
331
+ expect(manager.getSubscriptionCount()).toBe(1);
332
+
333
+ const unsub2 = manager.subscribe(vi.fn());
334
+ expect(manager.getSubscriptionCount()).toBe(2);
335
+
336
+ const unsub3 = manager.subscribe(vi.fn());
337
+ expect(manager.getSubscriptionCount()).toBe(3);
338
+
339
+ unsub2();
340
+ expect(manager.getSubscriptionCount()).toBe(2);
341
+
342
+ unsub1();
343
+ unsub3();
344
+ expect(manager.getSubscriptionCount()).toBe(0);
345
+ });
346
+ });
347
+
348
+ describe("hasSubscriptions", () => {
349
+ it("should return false when no subscriptions", () => {
350
+ const manager = new EventSubscriptionManager(mockEventSource);
351
+ expect(manager.hasSubscriptions()).toBe(false);
352
+ });
353
+
354
+ it("should return true when subscriptions exist", () => {
355
+ const manager = new EventSubscriptionManager(mockEventSource);
356
+
357
+ manager.subscribe(vi.fn());
358
+ expect(manager.hasSubscriptions()).toBe(true);
359
+ });
360
+
361
+ it("should return false after all unsubscribed", () => {
362
+ const manager = new EventSubscriptionManager(mockEventSource);
363
+
364
+ const unsub1 = manager.subscribe(vi.fn());
365
+ const unsub2 = manager.subscribe(vi.fn());
366
+
367
+ expect(manager.hasSubscriptions()).toBe(true);
368
+
369
+ unsub1();
370
+ expect(manager.hasSubscriptions()).toBe(true);
371
+
372
+ unsub2();
373
+ expect(manager.hasSubscriptions()).toBe(false);
374
+ });
375
+ });
376
+
377
+ describe("cleanup", () => {
378
+ it("should unsubscribe all subscriptions", () => {
379
+ const manager = new EventSubscriptionManager(mockEventSource);
380
+ const handler1 = vi.fn();
381
+ const handler2 = vi.fn();
382
+ const handler3 = vi.fn();
383
+
384
+ manager.subscribe(handler1);
385
+ manager.subscribe(handler2);
386
+ manager.subscribe(handler3);
387
+
388
+ expect(manager.getSubscriptionCount()).toBe(3);
389
+
390
+ manager.cleanup();
391
+
392
+ expect(manager.getSubscriptionCount()).toBe(0);
393
+ expect(manager.hasSubscriptions()).toBe(false);
394
+ });
395
+
396
+ it("should stop all handlers from receiving events", () => {
397
+ const manager = new EventSubscriptionManager(mockEventSource);
398
+ const handler1 = vi.fn();
399
+ const handler2 = vi.fn();
400
+
401
+ manager.subscribe(handler1);
402
+ manager.subscribe(handler2);
403
+
404
+ manager.cleanup();
405
+
406
+ emitEvent({ type: "TEST" });
407
+
408
+ expect(handler1).not.toHaveBeenCalled();
409
+ expect(handler2).not.toHaveBeenCalled();
410
+ });
411
+
412
+ it("should be safe to call multiple times", () => {
413
+ const manager = new EventSubscriptionManager(mockEventSource);
414
+
415
+ manager.subscribe(vi.fn());
416
+ manager.subscribe(vi.fn());
417
+
418
+ manager.cleanup();
419
+ expect(() => manager.cleanup()).not.toThrow();
420
+ expect(manager.getSubscriptionCount()).toBe(0);
421
+ });
422
+
423
+ it("should allow new subscriptions after cleanup", () => {
424
+ const manager = new EventSubscriptionManager(mockEventSource);
425
+ const handler1 = vi.fn();
426
+
427
+ manager.subscribe(handler1);
428
+ manager.cleanup();
429
+
430
+ const handler2 = vi.fn();
431
+ manager.subscribe(handler2);
432
+
433
+ emitEvent({ type: "TEST" });
434
+
435
+ expect(handler1).not.toHaveBeenCalled();
436
+ expect(handler2).toHaveBeenCalled();
437
+ expect(manager.getSubscriptionCount()).toBe(1);
438
+ });
439
+ });
440
+
441
+ describe("updateUploadIdFilter", () => {
442
+ it("should update upload ID in existing filters", () => {
443
+ const manager = new EventSubscriptionManager(mockEventSource);
444
+ const handler = vi.fn();
445
+
446
+ manager.subscribe(handler, { uploadId: "upload-old" });
447
+
448
+ emitEvent({ type: "PROGRESS", data: { id: "upload-old" } });
449
+ expect(handler).toHaveBeenCalledTimes(1);
450
+
451
+ handler.mockClear();
452
+ manager.updateUploadIdFilter("upload-new");
453
+
454
+ emitEvent({ type: "PROGRESS", data: { id: "upload-old" } });
455
+ emitEvent({ type: "PROGRESS", data: { id: "upload-new" } });
456
+
457
+ expect(handler).toHaveBeenCalledTimes(1);
458
+ expect(handler).toHaveBeenCalledWith({
459
+ type: "PROGRESS",
460
+ data: { id: "upload-new" },
461
+ });
462
+ });
463
+
464
+ it("should update multiple subscriptions with upload ID filter", () => {
465
+ const manager = new EventSubscriptionManager(mockEventSource);
466
+ const handler1 = vi.fn();
467
+ const handler2 = vi.fn();
468
+
469
+ manager.subscribe(handler1, { uploadId: "old" });
470
+ manager.subscribe(handler2, { uploadId: "old" });
471
+
472
+ manager.updateUploadIdFilter("new");
473
+
474
+ emitEvent({ type: "EVENT", data: { id: "new" } });
475
+
476
+ expect(handler1).toHaveBeenCalled();
477
+ expect(handler2).toHaveBeenCalled();
478
+ });
479
+
480
+ it("should not affect subscriptions without upload ID filter", () => {
481
+ const manager = new EventSubscriptionManager(mockEventSource);
482
+ const handler1 = vi.fn();
483
+ const handler2 = vi.fn();
484
+
485
+ manager.subscribe(handler1, { uploadId: "upload-1" });
486
+ manager.subscribe(handler2, { eventType: "PROGRESS" });
487
+
488
+ manager.updateUploadIdFilter("upload-2");
489
+
490
+ emitEvent({ type: "PROGRESS", data: { id: "upload-2" } });
491
+
492
+ // handler1 should get event (uploadId was updated)
493
+ expect(handler1).toHaveBeenCalled();
494
+
495
+ // handler2 should get event (no uploadId filter)
496
+ expect(handler2).toHaveBeenCalled();
497
+ });
498
+
499
+ it("should handle null upload ID", () => {
500
+ const manager = new EventSubscriptionManager(mockEventSource);
501
+ const handler = vi.fn();
502
+
503
+ manager.subscribe(handler, { uploadId: "upload-123" });
504
+
505
+ manager.updateUploadIdFilter(null);
506
+
507
+ emitEvent({ type: "EVENT" });
508
+ emitEvent({ type: "EVENT", data: { id: "upload-123" } });
509
+
510
+ // Should only get event without ID
511
+ expect(handler).toHaveBeenCalledTimes(1);
512
+ });
513
+ });
514
+
515
+ describe("edge cases", () => {
516
+ it("should handle events with no type", () => {
517
+ const manager = new EventSubscriptionManager(mockEventSource);
518
+ const handler = vi.fn();
519
+
520
+ manager.subscribe(handler);
521
+
522
+ // @ts-expect-error - Testing edge case
523
+ emitEvent({});
524
+
525
+ expect(handler).toHaveBeenCalled();
526
+ });
527
+
528
+ it("should handle rapid subscribe/unsubscribe", () => {
529
+ const manager = new EventSubscriptionManager(mockEventSource);
530
+
531
+ for (let i = 0; i < 100; i++) {
532
+ const unsub = manager.subscribe(vi.fn());
533
+ unsub();
534
+ }
535
+
536
+ expect(manager.getSubscriptionCount()).toBe(0);
537
+ });
538
+
539
+ it("should handle unsubscribe during event handling", () => {
540
+ const manager = new EventSubscriptionManager(mockEventSource);
541
+ let unsubscribe: (() => void) | null = null;
542
+
543
+ const handler = vi.fn(() => {
544
+ unsubscribe?.();
545
+ });
546
+
547
+ unsubscribe = manager.subscribe(handler);
548
+
549
+ expect(() => emitEvent({ type: "TEST" })).not.toThrow();
550
+ expect(handler).toHaveBeenCalledTimes(1);
551
+ });
552
+
553
+ it("should handle filter with undefined uploadId explicitly", () => {
554
+ const manager = new EventSubscriptionManager(mockEventSource);
555
+ const handler = vi.fn();
556
+
557
+ manager.subscribe(handler, { uploadId: undefined });
558
+
559
+ emitEvent({ type: "EVENT", data: { id: "upload-123" } });
560
+ emitEvent({ type: "EVENT" });
561
+
562
+ // uploadId: undefined should be treated as "no filter"
563
+ expect(handler).toHaveBeenCalledTimes(2);
564
+ });
565
+ });
566
+ });