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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/exports.d.ts +7 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +4 -1
- package/dist/exports.js.map +1 -1
- package/dist/queue/IncomingMessagesQueue.d.ts +6 -7
- package/dist/queue/IncomingMessagesQueue.d.ts.map +1 -1
- package/dist/queue/IncomingMessagesQueue.js +7 -30
- package/dist/queue/IncomingMessagesQueue.js.map +1 -1
- package/dist/queue/LinkedList.d.ts +1 -1
- package/dist/queue/LinkedList.d.ts.map +1 -1
- package/dist/queue/LinkedList.js.map +1 -1
- package/dist/queue/StorageStreamingQueue.d.ts +43 -0
- package/dist/queue/StorageStreamingQueue.d.ts.map +1 -0
- package/dist/queue/StorageStreamingQueue.js +70 -0
- package/dist/queue/StorageStreamingQueue.js.map +1 -0
- package/dist/storage/storageSync.d.ts +8 -2
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +56 -44
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +2 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +17 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +74 -5
- package/dist/sync.js.map +1 -1
- package/dist/tests/IncomingMessagesQueue.test.js +4 -150
- package/dist/tests/IncomingMessagesQueue.test.js.map +1 -1
- package/dist/tests/StorageStreamingQueue.test.d.ts +2 -0
- package/dist/tests/StorageStreamingQueue.test.d.ts.map +1 -0
- package/dist/tests/StorageStreamingQueue.test.js +213 -0
- package/dist/tests/StorageStreamingQueue.test.js.map +1 -0
- package/dist/tests/SyncManager.processQueues.test.d.ts +2 -0
- package/dist/tests/SyncManager.processQueues.test.d.ts.map +1 -0
- package/dist/tests/SyncManager.processQueues.test.js +208 -0
- package/dist/tests/SyncManager.processQueues.test.js.map +1 -0
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +4 -0
- package/dist/tests/setup.js.map +1 -0
- package/dist/tests/sync.garbageCollection.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +19 -19
- package/dist/tests/sync.storage.test.js +176 -20
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.test.js +1 -1
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/testUtils.d.ts +2 -2
- package/dist/tests/testUtils.js +2 -2
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/config.ts +13 -0
- package/src/exports.ts +6 -0
- package/src/queue/IncomingMessagesQueue.ts +7 -39
- package/src/queue/LinkedList.ts +1 -1
- package/src/queue/StorageStreamingQueue.ts +96 -0
- package/src/storage/storageSync.ts +99 -55
- package/src/storage/types.ts +3 -0
- package/src/sync.ts +84 -5
- package/src/tests/IncomingMessagesQueue.test.ts +4 -206
- package/src/tests/StorageStreamingQueue.test.ts +276 -0
- package/src/tests/SyncManager.processQueues.test.ts +287 -0
- package/src/tests/setup.ts +4 -0
- package/src/tests/sync.garbageCollection.test.ts +1 -3
- package/src/tests/sync.mesh.test.ts +19 -19
- package/src/tests/sync.storage.test.ts +224 -32
- package/src/tests/sync.test.ts +1 -9
- package/src/tests/testUtils.ts +2 -2
- 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
|
-
|
|
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.
|
|
438
|
-
|
|
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,
|
|
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
|
+
});
|