cojson 0.19.19 → 0.19.20

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.
Files changed (72) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/config.d.ts +6 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +10 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/exports.d.ts +7 -1
  7. package/dist/exports.d.ts.map +1 -1
  8. package/dist/exports.js +4 -1
  9. package/dist/exports.js.map +1 -1
  10. package/dist/queue/IncomingMessagesQueue.d.ts +6 -7
  11. package/dist/queue/IncomingMessagesQueue.d.ts.map +1 -1
  12. package/dist/queue/IncomingMessagesQueue.js +7 -30
  13. package/dist/queue/IncomingMessagesQueue.js.map +1 -1
  14. package/dist/queue/LinkedList.d.ts +1 -1
  15. package/dist/queue/LinkedList.d.ts.map +1 -1
  16. package/dist/queue/LinkedList.js.map +1 -1
  17. package/dist/queue/StorageStreamingQueue.d.ts +43 -0
  18. package/dist/queue/StorageStreamingQueue.d.ts.map +1 -0
  19. package/dist/queue/StorageStreamingQueue.js +70 -0
  20. package/dist/queue/StorageStreamingQueue.js.map +1 -0
  21. package/dist/storage/storageSync.d.ts +8 -2
  22. package/dist/storage/storageSync.d.ts.map +1 -1
  23. package/dist/storage/storageSync.js +56 -44
  24. package/dist/storage/storageSync.js.map +1 -1
  25. package/dist/storage/types.d.ts +2 -0
  26. package/dist/storage/types.d.ts.map +1 -1
  27. package/dist/sync.d.ts +17 -0
  28. package/dist/sync.d.ts.map +1 -1
  29. package/dist/sync.js +74 -5
  30. package/dist/sync.js.map +1 -1
  31. package/dist/tests/IncomingMessagesQueue.test.js +4 -150
  32. package/dist/tests/IncomingMessagesQueue.test.js.map +1 -1
  33. package/dist/tests/StorageStreamingQueue.test.d.ts +2 -0
  34. package/dist/tests/StorageStreamingQueue.test.d.ts.map +1 -0
  35. package/dist/tests/StorageStreamingQueue.test.js +213 -0
  36. package/dist/tests/StorageStreamingQueue.test.js.map +1 -0
  37. package/dist/tests/SyncManager.processQueues.test.d.ts +2 -0
  38. package/dist/tests/SyncManager.processQueues.test.d.ts.map +1 -0
  39. package/dist/tests/SyncManager.processQueues.test.js +208 -0
  40. package/dist/tests/SyncManager.processQueues.test.js.map +1 -0
  41. package/dist/tests/setup.d.ts +2 -0
  42. package/dist/tests/setup.d.ts.map +1 -0
  43. package/dist/tests/setup.js +4 -0
  44. package/dist/tests/setup.js.map +1 -0
  45. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  46. package/dist/tests/sync.mesh.test.js +19 -19
  47. package/dist/tests/sync.storage.test.js +176 -20
  48. package/dist/tests/sync.storage.test.js.map +1 -1
  49. package/dist/tests/sync.test.js +1 -1
  50. package/dist/tests/sync.test.js.map +1 -1
  51. package/dist/tests/testUtils.d.ts +2 -2
  52. package/dist/tests/testUtils.js +2 -2
  53. package/dist/tests/testUtils.js.map +1 -1
  54. package/package.json +4 -4
  55. package/src/config.ts +13 -0
  56. package/src/exports.ts +6 -0
  57. package/src/queue/IncomingMessagesQueue.ts +7 -39
  58. package/src/queue/LinkedList.ts +1 -1
  59. package/src/queue/StorageStreamingQueue.ts +96 -0
  60. package/src/storage/storageSync.ts +99 -55
  61. package/src/storage/types.ts +3 -0
  62. package/src/sync.ts +84 -5
  63. package/src/tests/IncomingMessagesQueue.test.ts +4 -206
  64. package/src/tests/StorageStreamingQueue.test.ts +276 -0
  65. package/src/tests/SyncManager.processQueues.test.ts +287 -0
  66. package/src/tests/setup.ts +4 -0
  67. package/src/tests/sync.garbageCollection.test.ts +1 -3
  68. package/src/tests/sync.mesh.test.ts +19 -19
  69. package/src/tests/sync.storage.test.ts +224 -32
  70. package/src/tests/sync.test.ts +1 -9
  71. package/src/tests/testUtils.ts +2 -2
  72. package/vitest.config.ts +1 -0
package/src/sync.ts CHANGED
@@ -3,6 +3,7 @@ import { Histogram, ValueType, metrics } from "@opentelemetry/api";
3
3
  import { PeerState } from "./PeerState.js";
4
4
  import { SyncStateManager } from "./SyncStateManager.js";
5
5
  import { UnsyncedCoValuesTracker } from "./UnsyncedCoValuesTracker.js";
6
+ import { SYNC_SCHEDULER_CONFIG } from "./config.js";
6
7
  import {
7
8
  getContenDebugInfo,
8
9
  getNewTransactionsFromContentMessage,
@@ -19,6 +20,7 @@ import { logger } from "./logger.js";
19
20
  import { CoValuePriority } from "./priority.js";
20
21
  import { IncomingMessagesQueue } from "./queue/IncomingMessagesQueue.js";
21
22
  import { LocalTransactionsSyncQueue } from "./queue/LocalTransactionsSyncQueue.js";
23
+ import type { StorageStreamingQueue } from "./queue/StorageStreamingQueue.js";
22
24
  import {
23
25
  CoValueKnownState,
24
26
  knownStateFrom,
@@ -426,17 +428,87 @@ export class SyncManager {
426
428
  }
427
429
  }
428
430
 
429
- messagesQueue = new IncomingMessagesQueue();
431
+ messagesQueue = new IncomingMessagesQueue(() => this.processQueues());
432
+ private processing = false;
433
+
430
434
  pushMessage(incoming: SyncMessage, peer: PeerState) {
431
435
  this.messagesQueue.push(incoming, peer);
436
+ }
437
+
438
+ /**
439
+ * Get the storage streaming queue if available.
440
+ * Returns undefined if storage doesn't have a streaming queue.
441
+ */
442
+ private getStorageStreamingQueue(): StorageStreamingQueue | undefined {
443
+ const storage = this.local.storage;
444
+ if (storage && "streamingQueue" in storage) {
445
+ return storage.streamingQueue as StorageStreamingQueue;
446
+ }
447
+ return undefined;
448
+ }
432
449
 
433
- if (this.messagesQueue.processing) {
450
+ /**
451
+ * Unified queue processing that coordinates both incoming messages
452
+ * and storage streaming entries.
453
+ *
454
+ * Processes items from both queues with priority ordering:
455
+ * - Incoming messages are processed via round-robin across peers
456
+ * - Storage streaming entries are processed by priority (MEDIUM before LOW)
457
+ *
458
+ * Implements time budget scheduling to avoid blocking the main thread.
459
+ */
460
+ private async processQueues() {
461
+ if (this.processing) {
434
462
  return;
435
463
  }
436
464
 
437
- this.messagesQueue.processQueue((msg, peer) => {
438
- this.handleSyncMessage(msg, peer);
439
- });
465
+ this.processing = true;
466
+ let lastTimer = performance.now();
467
+
468
+ const streamingQueue = this.getStorageStreamingQueue();
469
+
470
+ while (true) {
471
+ // First, try to pull from incoming messages queue
472
+ const messageEntry = this.messagesQueue.pull();
473
+ if (messageEntry) {
474
+ try {
475
+ this.handleSyncMessage(messageEntry.msg, messageEntry.peer);
476
+ } catch (err) {
477
+ logger.error("Error processing message", { err });
478
+ }
479
+ }
480
+
481
+ // Then, try to pull from storage streaming queue
482
+ const pushStreamingContent = streamingQueue?.pull();
483
+ if (pushStreamingContent) {
484
+ try {
485
+ // Invoke the pushContent callback to stream the content
486
+ pushStreamingContent();
487
+ } catch (err) {
488
+ logger.error("Error processing storage streaming entry", {
489
+ err,
490
+ });
491
+ }
492
+ }
493
+
494
+ // If both queues are empty, we're done
495
+ if (!messageEntry && !pushStreamingContent) {
496
+ break;
497
+ }
498
+
499
+ // Check if we have blocked the main thread for too long
500
+ // and if so, yield to the event loop
501
+ const currentTimer = performance.now();
502
+ if (
503
+ currentTimer - lastTimer >
504
+ SYNC_SCHEDULER_CONFIG.INCOMING_MESSAGES_TIME_BUDGET
505
+ ) {
506
+ await new Promise<void>((resolve) => setTimeout(resolve));
507
+ lastTimer = performance.now();
508
+ }
509
+ }
510
+
511
+ this.processing = false;
440
512
  }
441
513
 
442
514
  addPeer(peer: Peer, skipReconciliation: boolean = false) {
@@ -1053,6 +1125,13 @@ export class SyncManager {
1053
1125
 
1054
1126
  setStorage(storage: StorageAPI) {
1055
1127
  this.unsyncedTracker.setStorage(storage);
1128
+
1129
+ const storageStreamingQueue = this.getStorageStreamingQueue();
1130
+ if (storageStreamingQueue) {
1131
+ storageStreamingQueue.setListener(() => {
1132
+ this.processQueues();
1133
+ });
1134
+ }
1056
1135
  }
1057
1136
 
1058
1137
  removeStorage() {
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
2
  import { PeerState } from "../PeerState.js";
3
3
  import { IncomingMessagesQueue } from "../queue/IncomingMessagesQueue.js";
4
4
  import { ConnectedPeerChannel } from "../streamUtils.js";
@@ -8,13 +8,6 @@ import {
8
8
  tearDownTestMetricReader,
9
9
  } from "./testUtils.js";
10
10
 
11
- // Mock performance.now for consistent timing tests
12
- let mockPerformanceNow = vi.spyOn(performance, "now");
13
-
14
- beforeEach(() => {
15
- vi.clearAllMocks();
16
- });
17
-
18
11
  function createMockPeer(id: string): Peer {
19
12
  return {
20
13
  id,
@@ -60,18 +53,15 @@ function createMockSyncMessage(
60
53
  }
61
54
 
62
55
  function setup() {
56
+ const processQueues = vi.fn();
63
57
  const metricReader = createTestMetricReader();
64
- const queue = new IncomingMessagesQueue();
58
+ const queue = new IncomingMessagesQueue(processQueues);
65
59
  const peer1 = createMockPeerState("peer1");
66
60
  const peer2 = createMockPeerState("peer2");
67
61
 
68
- return { queue, peer1, peer2, metricReader };
62
+ return { queue, peer1, peer2, metricReader, processQueues };
69
63
  }
70
64
 
71
- beforeEach(() => {
72
- mockPerformanceNow.mockReturnValue(0);
73
- });
74
-
75
65
  afterEach(() => {
76
66
  tearDownTestMetricReader();
77
67
  });
@@ -82,7 +72,6 @@ describe("IncomingMessagesQueue", () => {
82
72
  const { queue } = setup();
83
73
  expect(queue["queues"]).toEqual([]);
84
74
  expect(queue.currentQueue).toBe(0);
85
- expect(queue.processing).toBe(false);
86
75
  });
87
76
  });
88
77
 
@@ -217,142 +206,6 @@ describe("IncomingMessagesQueue", () => {
217
206
  });
218
207
  });
219
208
 
220
- describe("processQueue", () => {
221
- test("should process all messages in queue", async () => {
222
- const { queue, peer1, peer2 } = setup();
223
- const msg1 = createMockSyncMessage("test1");
224
- const msg2 = createMockSyncMessage("test2");
225
- const msg3 = createMockSyncMessage("test3");
226
-
227
- queue.push(msg1, peer1);
228
- queue.push(msg2, peer1);
229
- queue.push(msg3, peer2);
230
-
231
- const processedMessages: Array<{ msg: SyncMessage; peer: PeerState }> =
232
- [];
233
-
234
- await queue.processQueue((msg, peer) => {
235
- processedMessages.push({ msg, peer });
236
- });
237
-
238
- expect(processedMessages).toEqual([
239
- { msg: msg1, peer: peer1 },
240
- { msg: msg3, peer: peer2 },
241
- { msg: msg2, peer: peer1 },
242
- ]);
243
- expect(queue.processing).toBe(false);
244
- });
245
-
246
- test("should set processing flag during execution", async () => {
247
- const { queue, peer1 } = setup();
248
- const msg = createMockSyncMessage("test");
249
- queue.push(msg, peer1);
250
-
251
- let processingFlagDuringExecution = false;
252
- const processingPromise = queue.processQueue(() => {
253
- processingFlagDuringExecution = queue.processing;
254
- });
255
-
256
- await processingPromise;
257
- expect(processingFlagDuringExecution).toBe(true);
258
- expect(queue.processing).toBe(false);
259
- });
260
-
261
- test("should handle empty queue", async () => {
262
- const { queue } = setup();
263
- const callback = vi.fn();
264
-
265
- await queue.processQueue(callback);
266
-
267
- expect(callback).not.toHaveBeenCalled();
268
- expect(queue.processing).toBe(false);
269
- });
270
-
271
- test("should yield to event loop when processing takes too long", async () => {
272
- const { queue, peer1 } = setup();
273
- const msg1 = createMockSyncMessage("test1");
274
- const msg2 = createMockSyncMessage("test2");
275
-
276
- queue.push(msg1, peer1);
277
- queue.push(msg2, peer1);
278
-
279
- // Mock timing to simulate long processing
280
- mockPerformanceNow
281
- .mockReturnValueOnce(0) // Initial time
282
- .mockReturnValueOnce(60); // After first message (60ms > 50ms threshold)
283
-
284
- const setTimeoutSpy = vi.spyOn(global, "setTimeout");
285
-
286
- await queue.processQueue(() => {
287
- // Simulate some processing time
288
- });
289
-
290
- expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 0);
291
- });
292
-
293
- test("should not yield to event loop when processing is fast", async () => {
294
- const { queue, peer1 } = setup();
295
- const msg = createMockSyncMessage("test");
296
- queue.push(msg, peer1);
297
-
298
- // Mock timing to simulate fast processing
299
- mockPerformanceNow
300
- .mockReturnValueOnce(0) // Initial time
301
- .mockReturnValueOnce(30); // After message (30ms < 50ms threshold)
302
-
303
- const setTimeoutSpy = vi.spyOn(global, "setTimeout");
304
-
305
- await queue.processQueue(() => {
306
- // Simulate some processing time
307
- });
308
-
309
- expect(setTimeoutSpy).not.toHaveBeenCalled();
310
- });
311
-
312
- test("should handle callback errors gracefully", async () => {
313
- const { queue, peer1 } = setup();
314
- const msg = createMockSyncMessage("test");
315
- queue.push(msg, peer1);
316
-
317
- const error = new Error("Callback error");
318
-
319
- await queue.processQueue(() => {
320
- throw error;
321
- });
322
-
323
- // The processing flag should be reset even when an error occurs
324
- expect(queue.processing).toBe(false);
325
- });
326
-
327
- test("should process messages in correct round-robin order", async () => {
328
- const { queue, peer1, peer2 } = setup();
329
- const msg1 = createMockSyncMessage("test1");
330
- const msg2 = createMockSyncMessage("test2");
331
- const msg3 = createMockSyncMessage("test3");
332
- const msg4 = createMockSyncMessage("test4");
333
-
334
- queue.push(msg1, peer1);
335
- queue.push(msg2, peer1);
336
- queue.push(msg3, peer2);
337
- queue.push(msg4, peer2);
338
-
339
- const processedMessages: Array<{ msg: SyncMessage; peer: PeerState }> =
340
- [];
341
-
342
- await queue.processQueue((msg, peer) => {
343
- processedMessages.push({ msg, peer });
344
- });
345
-
346
- // Should process in round-robin: peer1, peer2, peer1, peer2
347
- expect(processedMessages).toEqual([
348
- { msg: msg1, peer: peer1 },
349
- { msg: msg3, peer: peer2 },
350
- { msg: msg2, peer: peer1 },
351
- { msg: msg4, peer: peer2 },
352
- ]);
353
- });
354
- });
355
-
356
209
  describe("edge cases", () => {
357
210
  test("should handle peer with multiple messages correctly", () => {
358
211
  const { queue, peer1 } = setup();
@@ -411,35 +264,6 @@ describe("IncomingMessagesQueue", () => {
411
264
  });
412
265
  });
413
266
 
414
- describe("concurrent operations", () => {
415
- test("should prevent multiple concurrent processQueue calls", async () => {
416
- const { queue, peer1 } = setup();
417
- const msg = createMockSyncMessage("test");
418
- queue.push(msg, peer1);
419
-
420
- const firstProcessSpy = vi.fn();
421
-
422
- const firstProcess = queue.processQueue((msg, peer) => {
423
- firstProcessSpy(msg, peer);
424
- });
425
-
426
- const secondProcessSpy = vi.fn();
427
-
428
- // Second process should not interfere
429
- const secondProcess = queue.processQueue(() => {
430
- secondProcessSpy();
431
- });
432
-
433
- await firstProcess;
434
- await secondProcess;
435
-
436
- expect(firstProcessSpy).toHaveBeenCalled();
437
- expect(secondProcessSpy).not.toHaveBeenCalled();
438
-
439
- expect(queue.processing).toBe(false);
440
- });
441
- });
442
-
443
267
  describe("metrics", () => {
444
268
  test("should increment push counter when pushing messages", async () => {
445
269
  const { queue, peer1, metricReader } = setup();
@@ -594,31 +418,5 @@ describe("IncomingMessagesQueue", () => {
594
418
  expect(clientPullValue).toBe(0);
595
419
  expect(serverPullValue).toBe(0);
596
420
  });
597
-
598
- test("should track metrics during processQueue execution", async () => {
599
- const { queue, peer1, metricReader } = setup();
600
- const msg1 = createMockSyncMessage("test1");
601
- const msg2 = createMockSyncMessage("test2");
602
-
603
- queue.push(msg1, peer1);
604
- queue.push(msg2, peer1);
605
-
606
- await queue.processQueue(() => {
607
- // Process messages
608
- });
609
-
610
- const pushValue = await metricReader.getMetricValue(
611
- "jazz.messagequeue.incoming.pushed",
612
- { peerRole: "client" },
613
- );
614
-
615
- const pullValue = await metricReader.getMetricValue(
616
- "jazz.messagequeue.incoming.pulled",
617
- { peerRole: "client" },
618
- );
619
-
620
- expect(pushValue).toBe(2);
621
- expect(pullValue).toBe(2);
622
- });
623
421
  });
624
422
  });
@@ -0,0 +1,276 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { CO_VALUE_PRIORITY } from "../priority.js";
3
+ import { StorageStreamingQueue } from "../queue/StorageStreamingQueue.js";
4
+
5
+ describe("StorageStreamingQueue", () => {
6
+ describe("constructor", () => {
7
+ test("should initialize with empty queues", () => {
8
+ const queue = new StorageStreamingQueue();
9
+ expect(queue.isEmpty()).toBe(true);
10
+ });
11
+ });
12
+
13
+ describe("push", () => {
14
+ test("should add MEDIUM priority entry to queue", () => {
15
+ const queue = new StorageStreamingQueue();
16
+
17
+ queue.push(() => {}, CO_VALUE_PRIORITY.MEDIUM);
18
+
19
+ expect(queue.isEmpty()).toBe(false);
20
+ });
21
+
22
+ test("should add LOW priority entry to queue", () => {
23
+ const queue = new StorageStreamingQueue();
24
+
25
+ queue.push(() => {}, CO_VALUE_PRIORITY.LOW);
26
+
27
+ expect(queue.isEmpty()).toBe(false);
28
+ });
29
+
30
+ test("should add HIGH priority entry to queue", () => {
31
+ const queue = new StorageStreamingQueue();
32
+
33
+ queue.push(() => {}, CO_VALUE_PRIORITY.HIGH);
34
+
35
+ expect(queue.isEmpty()).toBe(false);
36
+ });
37
+
38
+ test("should accept multiple entries", () => {
39
+ const queue = new StorageStreamingQueue();
40
+ const entry1 = () => {};
41
+ const entry2 = () => {};
42
+
43
+ queue.push(entry1, CO_VALUE_PRIORITY.MEDIUM);
44
+ queue.push(entry2, CO_VALUE_PRIORITY.MEDIUM);
45
+
46
+ expect(queue.pull()).toBe(entry1);
47
+ expect(queue.pull()).toBe(entry2);
48
+ });
49
+ });
50
+
51
+ describe("pull", () => {
52
+ test("should return undefined for empty queue", () => {
53
+ const queue = new StorageStreamingQueue();
54
+ expect(queue.pull()).toBeUndefined();
55
+ });
56
+
57
+ test("should return and remove entry from queue", () => {
58
+ const queue = new StorageStreamingQueue();
59
+ const entry = () => {};
60
+
61
+ queue.push(entry, CO_VALUE_PRIORITY.MEDIUM);
62
+ const pulled = queue.pull();
63
+
64
+ expect(pulled).toBe(entry);
65
+ expect(queue.isEmpty()).toBe(true);
66
+ });
67
+
68
+ test("should pull HIGH priority before MEDIUM and LOW", () => {
69
+ const queue = new StorageStreamingQueue();
70
+ const lowEntry = () => {};
71
+ const mediumEntry = () => {};
72
+ const highEntry = () => {};
73
+
74
+ // Push in reverse order
75
+ queue.push(lowEntry, CO_VALUE_PRIORITY.LOW);
76
+ queue.push(mediumEntry, CO_VALUE_PRIORITY.MEDIUM);
77
+ queue.push(highEntry, CO_VALUE_PRIORITY.HIGH);
78
+
79
+ // Should pull HIGH first, then MEDIUM, then LOW
80
+ expect(queue.pull()).toBe(highEntry);
81
+ expect(queue.pull()).toBe(mediumEntry);
82
+ expect(queue.pull()).toBe(lowEntry);
83
+ });
84
+
85
+ test("should pull MEDIUM priority before LOW priority", () => {
86
+ const queue = new StorageStreamingQueue();
87
+ const lowEntry = () => {};
88
+ const mediumEntry = () => {};
89
+
90
+ // Push LOW first, then MEDIUM
91
+ queue.push(lowEntry, CO_VALUE_PRIORITY.LOW);
92
+ queue.push(mediumEntry, CO_VALUE_PRIORITY.MEDIUM);
93
+
94
+ // Should pull MEDIUM first
95
+ expect(queue.pull()).toBe(mediumEntry);
96
+ expect(queue.pull()).toBe(lowEntry);
97
+ });
98
+
99
+ test("should pull entries in FIFO order within same priority", () => {
100
+ const queue = new StorageStreamingQueue();
101
+ const entry1 = () => {};
102
+ const entry2 = () => {};
103
+ const entry3 = () => {};
104
+
105
+ queue.push(entry1, CO_VALUE_PRIORITY.MEDIUM);
106
+ queue.push(entry2, CO_VALUE_PRIORITY.MEDIUM);
107
+ queue.push(entry3, CO_VALUE_PRIORITY.MEDIUM);
108
+
109
+ expect(queue.pull()).toBe(entry1);
110
+ expect(queue.pull()).toBe(entry2);
111
+ expect(queue.pull()).toBe(entry3);
112
+ });
113
+
114
+ test("should handle interleaved priorities correctly", () => {
115
+ const queue = new StorageStreamingQueue();
116
+ const low1 = () => {};
117
+ const medium1 = () => {};
118
+ const high1 = () => {};
119
+ const low2 = () => {};
120
+ const medium2 = () => {};
121
+ const high2 = () => {};
122
+
123
+ queue.push(low1, CO_VALUE_PRIORITY.LOW);
124
+ queue.push(medium1, CO_VALUE_PRIORITY.MEDIUM);
125
+ queue.push(high1, CO_VALUE_PRIORITY.HIGH);
126
+ queue.push(low2, CO_VALUE_PRIORITY.LOW);
127
+ queue.push(medium2, CO_VALUE_PRIORITY.MEDIUM);
128
+ queue.push(high2, CO_VALUE_PRIORITY.HIGH);
129
+
130
+ // All HIGH should come first, in order
131
+ expect(queue.pull()).toBe(high1);
132
+ expect(queue.pull()).toBe(high2);
133
+ // Then all MEDIUM, in order
134
+ expect(queue.pull()).toBe(medium1);
135
+ expect(queue.pull()).toBe(medium2);
136
+ // Then all LOW, in order
137
+ expect(queue.pull()).toBe(low1);
138
+ expect(queue.pull()).toBe(low2);
139
+ });
140
+ });
141
+
142
+ describe("isEmpty", () => {
143
+ test("should return true for empty queue", () => {
144
+ const queue = new StorageStreamingQueue();
145
+ expect(queue.isEmpty()).toBe(true);
146
+ });
147
+
148
+ test("should return false when MEDIUM queue has entries", () => {
149
+ const queue = new StorageStreamingQueue();
150
+ queue.push(() => {}, CO_VALUE_PRIORITY.MEDIUM);
151
+ expect(queue.isEmpty()).toBe(false);
152
+ });
153
+
154
+ test("should return false when LOW queue has entries", () => {
155
+ const queue = new StorageStreamingQueue();
156
+ queue.push(() => {}, CO_VALUE_PRIORITY.LOW);
157
+ expect(queue.isEmpty()).toBe(false);
158
+ });
159
+
160
+ test("should return false when HIGH queue has entries", () => {
161
+ const queue = new StorageStreamingQueue();
162
+ queue.push(() => {}, CO_VALUE_PRIORITY.HIGH);
163
+ expect(queue.isEmpty()).toBe(false);
164
+ });
165
+
166
+ test("should return true after all entries are pulled", () => {
167
+ const queue = new StorageStreamingQueue();
168
+ queue.push(() => {}, CO_VALUE_PRIORITY.MEDIUM);
169
+ queue.push(() => {}, CO_VALUE_PRIORITY.LOW);
170
+
171
+ queue.pull();
172
+ queue.pull();
173
+
174
+ expect(queue.isEmpty()).toBe(true);
175
+ });
176
+ });
177
+
178
+ describe("callback invocation", () => {
179
+ test("should not invoke callback when pushed", () => {
180
+ const queue = new StorageStreamingQueue();
181
+ const callback = vi.fn();
182
+
183
+ queue.push(callback, CO_VALUE_PRIORITY.MEDIUM);
184
+
185
+ expect(callback).not.toHaveBeenCalled();
186
+ });
187
+
188
+ test("should not invoke callback when pulled", () => {
189
+ const queue = new StorageStreamingQueue();
190
+ const callback = vi.fn();
191
+
192
+ queue.push(callback, CO_VALUE_PRIORITY.MEDIUM);
193
+ queue.pull();
194
+
195
+ expect(callback).not.toHaveBeenCalled();
196
+ });
197
+
198
+ test("should allow caller to invoke callback after pull", () => {
199
+ const queue = new StorageStreamingQueue();
200
+ const callback = vi.fn();
201
+
202
+ queue.push(callback, CO_VALUE_PRIORITY.MEDIUM);
203
+ const pulled = queue.pull();
204
+
205
+ expect(callback).not.toHaveBeenCalled();
206
+
207
+ pulled?.();
208
+
209
+ expect(callback).toHaveBeenCalledTimes(1);
210
+ });
211
+ });
212
+
213
+ describe("setListener and emit", () => {
214
+ test("should call listener when emit is called", () => {
215
+ const queue = new StorageStreamingQueue();
216
+ const listener = vi.fn();
217
+
218
+ queue.setListener(listener);
219
+ queue.emit();
220
+
221
+ expect(listener).toHaveBeenCalledTimes(1);
222
+ });
223
+
224
+ test("should not throw when emit is called without listener", () => {
225
+ const queue = new StorageStreamingQueue();
226
+
227
+ expect(() => queue.emit()).not.toThrow();
228
+ });
229
+
230
+ test("should call listener multiple times on multiple emits", () => {
231
+ const queue = new StorageStreamingQueue();
232
+ const listener = vi.fn();
233
+
234
+ queue.setListener(listener);
235
+ queue.emit();
236
+ queue.emit();
237
+ queue.emit();
238
+
239
+ expect(listener).toHaveBeenCalledTimes(3);
240
+ });
241
+
242
+ test("should use latest listener when setListener is called multiple times", () => {
243
+ const queue = new StorageStreamingQueue();
244
+ const listener1 = vi.fn();
245
+ const listener2 = vi.fn();
246
+
247
+ queue.setListener(listener1);
248
+ queue.setListener(listener2);
249
+ queue.emit();
250
+
251
+ expect(listener1).not.toHaveBeenCalled();
252
+ expect(listener2).toHaveBeenCalledTimes(1);
253
+ });
254
+ });
255
+
256
+ describe("edge cases", () => {
257
+ test("should handle alternating push and pull operations", () => {
258
+ const queue = new StorageStreamingQueue();
259
+
260
+ const entry1 = () => {};
261
+ const entry2 = () => {};
262
+ const entry3 = () => {};
263
+
264
+ queue.push(entry1, CO_VALUE_PRIORITY.MEDIUM);
265
+ expect(queue.pull()).toBe(entry1);
266
+
267
+ queue.push(entry2, CO_VALUE_PRIORITY.LOW);
268
+ expect(queue.pull()).toBe(entry2);
269
+
270
+ expect(queue.isEmpty()).toBe(true);
271
+
272
+ queue.push(entry3, CO_VALUE_PRIORITY.MEDIUM);
273
+ expect(queue.pull()).toBe(entry3);
274
+ });
275
+ });
276
+ });