cojson 0.19.18 → 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/CHANGELOG.md +9 -0
- package/dist/PeerState.d.ts.map +1 -1
- package/dist/SyncStateManager.d.ts +5 -2
- package/dist/SyncStateManager.d.ts.map +1 -1
- package/dist/SyncStateManager.js +49 -12
- package/dist/SyncStateManager.js.map +1 -1
- package/dist/UnsyncedCoValuesTracker.d.ts +81 -0
- package/dist/UnsyncedCoValuesTracker.d.ts.map +1 -0
- package/dist/UnsyncedCoValuesTracker.js +209 -0
- package/dist/UnsyncedCoValuesTracker.js.map +1 -0
- 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 +11 -3
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +6 -1
- package/dist/exports.js.map +1 -1
- package/dist/localNode.d.ts +9 -5
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +12 -8
- package/dist/localNode.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/knownState.d.ts +1 -1
- package/dist/storage/knownState.js +4 -4
- package/dist/storage/sqlite/client.d.ts +8 -0
- package/dist/storage/sqlite/client.d.ts.map +1 -1
- package/dist/storage/sqlite/client.js +17 -0
- package/dist/storage/sqlite/client.js.map +1 -1
- package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
- package/dist/storage/sqlite/sqliteMigrations.js +9 -0
- package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
- package/dist/storage/sqliteAsync/client.d.ts +8 -0
- package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
- package/dist/storage/sqliteAsync/client.js +19 -0
- package/dist/storage/sqliteAsync/client.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +9 -2
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +9 -0
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +17 -4
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +67 -44
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/types.d.ts +35 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +38 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +181 -7
- 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/SyncStateManager.test.js +3 -3
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/coValueCore.loadFromStorage.test.js +3 -0
- package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
- 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/sync.tracking.test.d.ts +2 -0
- package/dist/tests/sync.tracking.test.d.ts.map +1 -0
- package/dist/tests/sync.tracking.test.js +261 -0
- package/dist/tests/sync.tracking.test.js.map +1 -0
- package/dist/tests/testUtils.d.ts +4 -3
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +4 -4
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/PeerState.ts +2 -2
- package/src/SyncStateManager.ts +63 -12
- package/src/UnsyncedCoValuesTracker.ts +272 -0
- package/src/config.ts +13 -0
- package/src/exports.ts +10 -1
- package/src/localNode.ts +15 -3
- package/src/queue/IncomingMessagesQueue.ts +7 -39
- package/src/queue/LinkedList.ts +1 -1
- package/src/queue/StorageStreamingQueue.ts +96 -0
- package/src/storage/knownState.ts +4 -4
- package/src/storage/sqlite/client.ts +31 -0
- package/src/storage/sqlite/sqliteMigrations.ts +9 -0
- package/src/storage/sqliteAsync/client.ts +35 -0
- package/src/storage/storageAsync.ts +18 -1
- package/src/storage/storageSync.ts +119 -56
- package/src/storage/types.ts +42 -0
- package/src/sync.ts +235 -8
- 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/SyncStateManager.test.ts +3 -0
- package/src/tests/coValueCore.loadFromStorage.test.ts +11 -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/sync.tracking.test.ts +396 -0
- package/src/tests/testUtils.ts +11 -5
- package/vitest.config.ts +1 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { CO_VALUE_PRIORITY } from "../priority.js";
|
|
3
|
+
import { StorageStreamingQueue } from "../queue/StorageStreamingQueue.js";
|
|
4
|
+
import {
|
|
5
|
+
SyncMessagesLog,
|
|
6
|
+
loadCoValueOrFail,
|
|
7
|
+
setupTestNode,
|
|
8
|
+
waitFor,
|
|
9
|
+
} from "./testUtils.js";
|
|
10
|
+
|
|
11
|
+
describe("SyncManager.processQueues", () => {
|
|
12
|
+
let jazzCloud: ReturnType<typeof setupTestNode>;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
SyncMessagesLog.clear();
|
|
16
|
+
jazzCloud = setupTestNode({
|
|
17
|
+
isSyncServer: true,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("incoming messages processing", () => {
|
|
22
|
+
test("should process incoming messages from peers", async () => {
|
|
23
|
+
const client = setupTestNode();
|
|
24
|
+
client.connectToSyncServer();
|
|
25
|
+
|
|
26
|
+
const group = jazzCloud.node.createGroup();
|
|
27
|
+
const map = group.createMap();
|
|
28
|
+
map.set("hello", "world", "trusting");
|
|
29
|
+
|
|
30
|
+
const mapOnClient = await loadCoValueOrFail(client.node, map.id);
|
|
31
|
+
expect(mapOnClient.get("hello")).toEqual("world");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("should process multiple messages in sequence", async () => {
|
|
35
|
+
const client = setupTestNode();
|
|
36
|
+
client.connectToSyncServer();
|
|
37
|
+
|
|
38
|
+
const group = jazzCloud.node.createGroup();
|
|
39
|
+
const map1 = group.createMap();
|
|
40
|
+
const map2 = group.createMap();
|
|
41
|
+
const map3 = group.createMap();
|
|
42
|
+
|
|
43
|
+
map1.set("key", "value1", "trusting");
|
|
44
|
+
map2.set("key", "value2", "trusting");
|
|
45
|
+
map3.set("key", "value3", "trusting");
|
|
46
|
+
|
|
47
|
+
const [loadedMap1, loadedMap2, loadedMap3] = await Promise.all([
|
|
48
|
+
loadCoValueOrFail(client.node, map1.id),
|
|
49
|
+
loadCoValueOrFail(client.node, map2.id),
|
|
50
|
+
loadCoValueOrFail(client.node, map3.id),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
expect(loadedMap1.get("key")).toEqual("value1");
|
|
54
|
+
expect(loadedMap2.get("key")).toEqual("value2");
|
|
55
|
+
expect(loadedMap3.get("key")).toEqual("value3");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("storage streaming processing", () => {
|
|
60
|
+
test("should process storage streaming callbacks", async () => {
|
|
61
|
+
const client = setupTestNode();
|
|
62
|
+
client.connectToSyncServer();
|
|
63
|
+
const { storage } = client.addStorage();
|
|
64
|
+
|
|
65
|
+
const group = jazzCloud.node.createGroup();
|
|
66
|
+
const map = group.createMap();
|
|
67
|
+
map.set("hello", "world", "trusting");
|
|
68
|
+
|
|
69
|
+
// First load to populate storage
|
|
70
|
+
await loadCoValueOrFail(client.node, map.id);
|
|
71
|
+
|
|
72
|
+
// Restart and load from storage
|
|
73
|
+
client.restart();
|
|
74
|
+
client.connectToSyncServer();
|
|
75
|
+
client.addStorage({ storage });
|
|
76
|
+
|
|
77
|
+
SyncMessagesLog.clear();
|
|
78
|
+
|
|
79
|
+
const mapOnClient = await loadCoValueOrFail(client.node, map.id);
|
|
80
|
+
expect(mapOnClient.get("hello")).toEqual("world");
|
|
81
|
+
|
|
82
|
+
// Verify content came from storage
|
|
83
|
+
const storageMessages = SyncMessagesLog.messages.filter(
|
|
84
|
+
(msg) => msg.from === "storage" || msg.to === "storage",
|
|
85
|
+
);
|
|
86
|
+
expect(storageMessages.length).toBeGreaterThan(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("should invoke streaming callbacks when pulled", async () => {
|
|
90
|
+
const client = setupTestNode();
|
|
91
|
+
const { storage } = client.addStorage();
|
|
92
|
+
|
|
93
|
+
const callback = vi.fn();
|
|
94
|
+
storage.streamingQueue?.push(callback, CO_VALUE_PRIORITY.MEDIUM);
|
|
95
|
+
storage.streamingQueue?.emit();
|
|
96
|
+
|
|
97
|
+
// Wait for processQueues to run
|
|
98
|
+
await waitFor(() => callback.mock.calls.length > 0);
|
|
99
|
+
|
|
100
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("should process MEDIUM priority before LOW priority", async () => {
|
|
104
|
+
const client = setupTestNode();
|
|
105
|
+
const { storage } = client.addStorage();
|
|
106
|
+
|
|
107
|
+
const order: string[] = [];
|
|
108
|
+
const lowCallback = () => order.push("low");
|
|
109
|
+
const mediumCallback = () => order.push("medium");
|
|
110
|
+
const highCallback = () => order.push("high");
|
|
111
|
+
|
|
112
|
+
// Push LOW first, then MEDIUM
|
|
113
|
+
storage.streamingQueue?.push(lowCallback, CO_VALUE_PRIORITY.LOW);
|
|
114
|
+
storage.streamingQueue?.push(mediumCallback, CO_VALUE_PRIORITY.MEDIUM);
|
|
115
|
+
storage.streamingQueue?.push(highCallback, CO_VALUE_PRIORITY.HIGH);
|
|
116
|
+
storage.streamingQueue?.emit();
|
|
117
|
+
|
|
118
|
+
// Wait for both to be processed
|
|
119
|
+
await waitFor(() => order.length === 3);
|
|
120
|
+
|
|
121
|
+
// MEDIUM should be processed first
|
|
122
|
+
expect(order).toEqual(["high", "medium", "low"]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("unified scheduling", () => {
|
|
127
|
+
test("should process both incoming messages and storage streaming", async () => {
|
|
128
|
+
const client = setupTestNode();
|
|
129
|
+
client.connectToSyncServer();
|
|
130
|
+
const { storage } = client.addStorage();
|
|
131
|
+
|
|
132
|
+
const group = jazzCloud.node.createGroup();
|
|
133
|
+
const map = group.createMap();
|
|
134
|
+
map.set("hello", "world", "trusting");
|
|
135
|
+
|
|
136
|
+
// Queue a storage streaming callback
|
|
137
|
+
const streamingCallback = vi.fn();
|
|
138
|
+
storage.streamingQueue?.push(streamingCallback, CO_VALUE_PRIORITY.MEDIUM);
|
|
139
|
+
storage.streamingQueue?.emit();
|
|
140
|
+
|
|
141
|
+
// Load from server (incoming messages)
|
|
142
|
+
const mapOnClient = await loadCoValueOrFail(client.node, map.id);
|
|
143
|
+
|
|
144
|
+
expect(mapOnClient.get("hello")).toEqual("world");
|
|
145
|
+
expect(streamingCallback).toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("should alternate between message queue and storage queue", async () => {
|
|
149
|
+
const client = setupTestNode();
|
|
150
|
+
const { storage } = client.addStorage();
|
|
151
|
+
|
|
152
|
+
const order: string[] = [];
|
|
153
|
+
|
|
154
|
+
// Push multiple storage callbacks
|
|
155
|
+
storage.streamingQueue?.push(
|
|
156
|
+
() => order.push("storage1"),
|
|
157
|
+
CO_VALUE_PRIORITY.MEDIUM,
|
|
158
|
+
);
|
|
159
|
+
storage.streamingQueue?.push(
|
|
160
|
+
() => order.push("storage2"),
|
|
161
|
+
CO_VALUE_PRIORITY.MEDIUM,
|
|
162
|
+
);
|
|
163
|
+
storage.streamingQueue?.emit();
|
|
164
|
+
|
|
165
|
+
// Wait for processing
|
|
166
|
+
await waitFor(() => order.length === 2);
|
|
167
|
+
|
|
168
|
+
expect(order).toContain("storage1");
|
|
169
|
+
expect(order).toContain("storage2");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("processing flag", () => {
|
|
174
|
+
test("should prevent concurrent processQueues calls", async () => {
|
|
175
|
+
const client = setupTestNode();
|
|
176
|
+
const { storage } = client.addStorage();
|
|
177
|
+
|
|
178
|
+
let concurrentCalls = 0;
|
|
179
|
+
let maxConcurrentCalls = 0;
|
|
180
|
+
|
|
181
|
+
const callback = () => {
|
|
182
|
+
concurrentCalls++;
|
|
183
|
+
maxConcurrentCalls = Math.max(maxConcurrentCalls, concurrentCalls);
|
|
184
|
+
// Simulate some work
|
|
185
|
+
for (let i = 0; i < 1000; i++) {
|
|
186
|
+
Math.random();
|
|
187
|
+
}
|
|
188
|
+
concurrentCalls--;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Push multiple callbacks
|
|
192
|
+
for (let i = 0; i < 10; i++) {
|
|
193
|
+
storage.streamingQueue?.push(callback, CO_VALUE_PRIORITY.MEDIUM);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Emit multiple times to trigger multiple processQueues calls
|
|
197
|
+
storage.streamingQueue?.emit();
|
|
198
|
+
storage.streamingQueue?.emit();
|
|
199
|
+
storage.streamingQueue?.emit();
|
|
200
|
+
|
|
201
|
+
// Wait for all to complete
|
|
202
|
+
await waitFor(() => storage.streamingQueue?.isEmpty());
|
|
203
|
+
|
|
204
|
+
// Should never have more than 1 concurrent call
|
|
205
|
+
expect(maxConcurrentCalls).toBe(1);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("error handling", () => {
|
|
210
|
+
test("should continue processing after storage callback error", async () => {
|
|
211
|
+
const client = setupTestNode();
|
|
212
|
+
const { storage } = client.addStorage();
|
|
213
|
+
|
|
214
|
+
const processed: string[] = [];
|
|
215
|
+
|
|
216
|
+
storage.streamingQueue?.push(() => {
|
|
217
|
+
processed.push("before");
|
|
218
|
+
}, CO_VALUE_PRIORITY.MEDIUM);
|
|
219
|
+
|
|
220
|
+
storage.streamingQueue?.push(() => {
|
|
221
|
+
throw new Error("Test error");
|
|
222
|
+
}, CO_VALUE_PRIORITY.MEDIUM);
|
|
223
|
+
|
|
224
|
+
storage.streamingQueue?.push(() => {
|
|
225
|
+
processed.push("after");
|
|
226
|
+
}, CO_VALUE_PRIORITY.MEDIUM);
|
|
227
|
+
|
|
228
|
+
storage.streamingQueue?.emit();
|
|
229
|
+
|
|
230
|
+
// Wait for processing to complete
|
|
231
|
+
await waitFor(() => storage.streamingQueue?.isEmpty());
|
|
232
|
+
|
|
233
|
+
// Both before and after should be processed despite error
|
|
234
|
+
expect(processed).toContain("before");
|
|
235
|
+
expect(processed).toContain("after");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("queue triggers", () => {
|
|
240
|
+
test("IncomingMessagesQueue.push should trigger processQueues", async () => {
|
|
241
|
+
const client = setupTestNode();
|
|
242
|
+
client.connectToSyncServer();
|
|
243
|
+
|
|
244
|
+
const group = jazzCloud.node.createGroup();
|
|
245
|
+
const map = group.createMap();
|
|
246
|
+
map.set("hello", "world", "trusting");
|
|
247
|
+
|
|
248
|
+
// Loading should trigger message processing automatically
|
|
249
|
+
const mapOnClient = await loadCoValueOrFail(client.node, map.id);
|
|
250
|
+
expect(mapOnClient.get("hello")).toEqual("world");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("StorageStreamingQueue.emit should trigger processQueues", async () => {
|
|
254
|
+
const client = setupTestNode();
|
|
255
|
+
const { storage } = client.addStorage();
|
|
256
|
+
|
|
257
|
+
const callback = vi.fn();
|
|
258
|
+
storage.streamingQueue?.push(callback, CO_VALUE_PRIORITY.MEDIUM);
|
|
259
|
+
|
|
260
|
+
// Before emit, callback should not be called
|
|
261
|
+
expect(callback).not.toHaveBeenCalled();
|
|
262
|
+
|
|
263
|
+
// After emit, processQueues should be triggered
|
|
264
|
+
storage.streamingQueue?.emit();
|
|
265
|
+
|
|
266
|
+
await waitFor(() => callback.mock.calls.length > 0);
|
|
267
|
+
expect(callback).toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("setStorage should connect queue listener", async () => {
|
|
271
|
+
const client = setupTestNode();
|
|
272
|
+
|
|
273
|
+
// Before adding storage, there's no queue
|
|
274
|
+
const queueBefore = (
|
|
275
|
+
client.node.syncManager as any
|
|
276
|
+
).getStorageStreamingQueue?.();
|
|
277
|
+
expect(queueBefore).toBeUndefined();
|
|
278
|
+
|
|
279
|
+
// After adding storage, queue should be available
|
|
280
|
+
const { storage } = client.addStorage();
|
|
281
|
+
const queueAfter = (
|
|
282
|
+
client.node.syncManager as any
|
|
283
|
+
).getStorageStreamingQueue?.();
|
|
284
|
+
expect(queueAfter).toBe(storage.streamingQueue);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -78,10 +78,12 @@ describe("SyncStateManager", () => {
|
|
|
78
78
|
const updateToStorageSpy: PeerSyncStateListenerCallback = vi.fn();
|
|
79
79
|
const unsubscribe1 = subscriptionManager.subscribeToPeerUpdates(
|
|
80
80
|
peerState.id,
|
|
81
|
+
map.core.id,
|
|
81
82
|
updateToJazzCloudSpy,
|
|
82
83
|
);
|
|
83
84
|
const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
|
|
84
85
|
serverPeer.id,
|
|
86
|
+
group.core.id,
|
|
85
87
|
updateToStorageSpy,
|
|
86
88
|
);
|
|
87
89
|
|
|
@@ -141,6 +143,7 @@ describe("SyncStateManager", () => {
|
|
|
141
143
|
const unsubscribe1 = subscriptionManager.subscribeToUpdates(anyUpdateSpy);
|
|
142
144
|
const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
|
|
143
145
|
peerState.id,
|
|
146
|
+
map.core.id,
|
|
144
147
|
anyUpdateSpy,
|
|
145
148
|
);
|
|
146
149
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { RawCoID } from "../ids";
|
|
3
|
+
import { PeerID } from "../sync";
|
|
3
4
|
import { StorageAPI } from "../storage/types";
|
|
4
5
|
import {
|
|
5
6
|
createTestMetricReader,
|
|
@@ -36,6 +37,13 @@ function createMockStorage(
|
|
|
36
37
|
store?: (data: any, correctionCallback: any) => void;
|
|
37
38
|
getKnownState?: (id: RawCoID) => any;
|
|
38
39
|
waitForSync?: (id: string, coValue: any) => Promise<void>;
|
|
40
|
+
trackCoValuesSyncState?: (
|
|
41
|
+
operations: Array<{ id: RawCoID; peerId: PeerID; synced: boolean }>,
|
|
42
|
+
) => void;
|
|
43
|
+
getUnsyncedCoValueIDs?: (
|
|
44
|
+
callback: (unsyncedCoValueIDs: RawCoID[]) => void,
|
|
45
|
+
) => void;
|
|
46
|
+
stopTrackingSyncState?: (id: RawCoID) => void;
|
|
39
47
|
close?: () => Promise<unknown> | undefined;
|
|
40
48
|
} = {},
|
|
41
49
|
): StorageAPI {
|
|
@@ -44,6 +52,9 @@ function createMockStorage(
|
|
|
44
52
|
store: opts.store || vi.fn(),
|
|
45
53
|
getKnownState: opts.getKnownState || vi.fn(),
|
|
46
54
|
waitForSync: opts.waitForSync || vi.fn().mockResolvedValue(undefined),
|
|
55
|
+
trackCoValuesSyncState: opts.trackCoValuesSyncState || vi.fn(),
|
|
56
|
+
getUnsyncedCoValueIDs: opts.getUnsyncedCoValueIDs || vi.fn(),
|
|
57
|
+
stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
|
|
47
58
|
close: opts.close || vi.fn().mockResolvedValue(undefined),
|
|
48
59
|
};
|
|
49
60
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { beforeEach, describe, expect, test } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { setGarbageCollectorMaxAge } from "../config";
|
|
4
|
-
import { emptyKnownState } from "../exports";
|
|
5
4
|
import {
|
|
6
5
|
SyncMessagesLog,
|
|
7
6
|
TEST_NODE_CONFIG,
|
|
8
7
|
loadCoValueOrFail,
|
|
9
8
|
setupTestNode,
|
|
10
|
-
waitFor,
|
|
11
9
|
} from "./testUtils";
|
|
12
10
|
|
|
13
11
|
// We want to simulate a real world communication that happens asynchronously
|