cojson-storage-indexeddb 0.19.17 → 0.19.19
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 +13 -0
- package/dist/CoJsonIDBTransaction.d.ts +8 -4
- package/dist/CoJsonIDBTransaction.d.ts.map +1 -1
- package/dist/CoJsonIDBTransaction.js +17 -11
- package/dist/CoJsonIDBTransaction.js.map +1 -1
- package/dist/idbClient.d.ts +38 -3
- package/dist/idbClient.d.ts.map +1 -1
- package/dist/idbClient.js +78 -3
- package/dist/idbClient.js.map +1 -1
- package/dist/idbNode.d.ts.map +1 -1
- package/dist/idbNode.js +11 -1
- package/dist/idbNode.js.map +1 -1
- package/dist/tests/CoJsonIDBTransaction.test.js +49 -0
- package/dist/tests/CoJsonIDBTransaction.test.js.map +1 -1
- package/dist/tests/storage.indexeddb.test.js +101 -27
- package/dist/tests/storage.indexeddb.test.js.map +1 -1
- package/dist/tests/testUtils.d.ts +8 -1
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +50 -1
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +2 -2
- package/src/CoJsonIDBTransaction.ts +21 -19
- package/src/idbClient.ts +122 -2
- package/src/idbNode.ts +15 -1
- package/src/tests/CoJsonIDBTransaction.test.ts +87 -0
- package/src/tests/storage.indexeddb.test.ts +146 -95
- package/src/tests/testUtils.ts +83 -2
- package/vitest.config.ts +6 -1
package/src/idbNode.ts
CHANGED
|
@@ -9,7 +9,7 @@ export function internal_setDatabaseName(name: string) {
|
|
|
9
9
|
|
|
10
10
|
export async function getIndexedDBStorage(name = DATABASE_NAME) {
|
|
11
11
|
const dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
12
|
-
const request = indexedDB.open(name,
|
|
12
|
+
const request = indexedDB.open(name, 5);
|
|
13
13
|
request.onerror = () => {
|
|
14
14
|
reject(request.error);
|
|
15
15
|
};
|
|
@@ -47,6 +47,20 @@ export async function getIndexedDBStorage(name = DATABASE_NAME) {
|
|
|
47
47
|
keyPath: ["ses", "idx"],
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
+
if (ev.oldVersion <= 4) {
|
|
51
|
+
const unsyncedCoValues = db.createObjectStore("unsyncedCoValues", {
|
|
52
|
+
autoIncrement: true,
|
|
53
|
+
keyPath: "rowID",
|
|
54
|
+
});
|
|
55
|
+
unsyncedCoValues.createIndex("byCoValueId", "coValueId");
|
|
56
|
+
unsyncedCoValues.createIndex(
|
|
57
|
+
"uniqueUnsyncedCoValues",
|
|
58
|
+
["coValueId", "peerId"],
|
|
59
|
+
{
|
|
60
|
+
unique: true,
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
}
|
|
50
64
|
};
|
|
51
65
|
});
|
|
52
66
|
|
|
@@ -23,6 +23,17 @@ describe("CoJsonIDBTransaction", () => {
|
|
|
23
23
|
});
|
|
24
24
|
db.createObjectStore("transactions", { keyPath: "id" });
|
|
25
25
|
db.createObjectStore("signatureAfter", { keyPath: "id" });
|
|
26
|
+
const unsyncedCoValues = db.createObjectStore("unsyncedCoValues", {
|
|
27
|
+
keyPath: "rowID",
|
|
28
|
+
});
|
|
29
|
+
unsyncedCoValues.createIndex("byCoValueId", "coValueId");
|
|
30
|
+
unsyncedCoValues.createIndex(
|
|
31
|
+
"uniqueUnsyncedCoValues",
|
|
32
|
+
["coValueId", "peerId"],
|
|
33
|
+
{
|
|
34
|
+
unique: true,
|
|
35
|
+
},
|
|
36
|
+
);
|
|
26
37
|
};
|
|
27
38
|
|
|
28
39
|
request.onsuccess = () => {
|
|
@@ -167,4 +178,80 @@ describe("CoJsonIDBTransaction", () => {
|
|
|
167
178
|
|
|
168
179
|
expect(badTx.failed).toBe(true);
|
|
169
180
|
});
|
|
181
|
+
|
|
182
|
+
test("transaction with custom stores only includes specified stores", async () => {
|
|
183
|
+
const tx = new CoJsonIDBTransaction(db, ["coValues", "sessions"]);
|
|
184
|
+
|
|
185
|
+
// Should work with included stores
|
|
186
|
+
await tx.handleRequest((tx) =>
|
|
187
|
+
tx.getObjectStore("coValues").put({
|
|
188
|
+
id: "test1",
|
|
189
|
+
value: "hello",
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await tx.handleRequest((tx) =>
|
|
194
|
+
tx.getObjectStore("sessions").put({
|
|
195
|
+
id: "session1",
|
|
196
|
+
data: "session data",
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Should fail when trying to access a store not included in transaction
|
|
201
|
+
await expect(
|
|
202
|
+
tx.handleRequest((tx) =>
|
|
203
|
+
tx.getObjectStore("transactions").put({
|
|
204
|
+
id: "tx1",
|
|
205
|
+
data: "tx data",
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
).rejects.toThrow(
|
|
209
|
+
"Failed to execute 'objectStore' on 'IDBTransaction': The specified object store was not found.",
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("if no custom stores are provided, transaction uses default stores", async () => {
|
|
214
|
+
const tx = new CoJsonIDBTransaction(db);
|
|
215
|
+
|
|
216
|
+
await tx.handleRequest((tx) =>
|
|
217
|
+
tx.getObjectStore("coValues").put({
|
|
218
|
+
id: "test1",
|
|
219
|
+
value: "hello",
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await tx.handleRequest((tx) =>
|
|
224
|
+
tx.getObjectStore("sessions").put({
|
|
225
|
+
id: "session1",
|
|
226
|
+
data: "session data",
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await tx.handleRequest((tx) =>
|
|
231
|
+
tx.getObjectStore("transactions").put({
|
|
232
|
+
id: "tx1",
|
|
233
|
+
data: "tx data",
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await tx.handleRequest((tx) =>
|
|
238
|
+
tx.getObjectStore("signatureAfter").put({
|
|
239
|
+
id: "sig1",
|
|
240
|
+
data: "sig data",
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Should fail when trying to access unsyncedCoValues (not in default)
|
|
245
|
+
await expect(
|
|
246
|
+
tx.handleRequest((tx) =>
|
|
247
|
+
tx.getObjectStore("unsyncedCoValues").put({
|
|
248
|
+
rowID: 1,
|
|
249
|
+
coValueId: "coValue1",
|
|
250
|
+
peerId: "peer1",
|
|
251
|
+
}),
|
|
252
|
+
),
|
|
253
|
+
).rejects.toThrow(
|
|
254
|
+
"Failed to execute 'objectStore' on 'IDBTransaction': The specified object store was not found.",
|
|
255
|
+
);
|
|
256
|
+
});
|
|
170
257
|
});
|
|
@@ -1,29 +1,37 @@
|
|
|
1
|
-
import { LocalNode, StorageApiAsync } from "cojson";
|
|
1
|
+
import { LocalNode, StorageApiAsync, cojsonInternals } from "cojson";
|
|
2
2
|
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
|
3
|
-
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
4
|
-
import { getIndexedDBStorage } from "../index.js";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { getIndexedDBStorage, internal_setDatabaseName } from "../index.js";
|
|
5
5
|
import { toSimplifiedMessages } from "./messagesTestUtils.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
clearObjectStore,
|
|
8
|
+
connectToSyncServer,
|
|
9
|
+
createTestNode,
|
|
10
|
+
trackMessages,
|
|
11
|
+
waitFor,
|
|
12
|
+
} from "./testUtils.js";
|
|
7
13
|
|
|
8
14
|
const Crypto = await WasmCrypto.create();
|
|
9
15
|
let syncMessages: ReturnType<typeof trackMessages>;
|
|
10
16
|
|
|
17
|
+
const DATABASE_NAME = "jazz-storage";
|
|
18
|
+
internal_setDatabaseName(DATABASE_NAME);
|
|
19
|
+
|
|
11
20
|
beforeEach(() => {
|
|
12
21
|
syncMessages = trackMessages();
|
|
22
|
+
cojsonInternals.setSyncStateTrackingBatchDelay(0);
|
|
23
|
+
cojsonInternals.setCoValueLoadingRetryDelay(10);
|
|
13
24
|
});
|
|
14
25
|
|
|
15
|
-
afterEach(() => {
|
|
26
|
+
afterEach(async () => {
|
|
16
27
|
syncMessages.restore();
|
|
28
|
+
cojsonInternals.setSyncStateTrackingBatchDelay(1000);
|
|
29
|
+
|
|
30
|
+
await clearObjectStore(DATABASE_NAME, "unsyncedCoValues");
|
|
17
31
|
});
|
|
18
32
|
|
|
19
33
|
test("should sync and load data from storage", async () => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const node1 = new LocalNode(
|
|
23
|
-
agentSecret,
|
|
24
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
25
|
-
Crypto,
|
|
26
|
-
);
|
|
34
|
+
const node1 = createTestNode();
|
|
27
35
|
node1.setStorage(await getIndexedDBStorage());
|
|
28
36
|
|
|
29
37
|
const group = node1.createGroup();
|
|
@@ -51,12 +59,7 @@ test("should sync and load data from storage", async () => {
|
|
|
51
59
|
node1.gracefulShutdown();
|
|
52
60
|
syncMessages.clear();
|
|
53
61
|
|
|
54
|
-
const node2 =
|
|
55
|
-
agentSecret,
|
|
56
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
57
|
-
Crypto,
|
|
58
|
-
);
|
|
59
|
-
|
|
62
|
+
const node2 = createTestNode({ secret: node1.agentSecret });
|
|
60
63
|
node2.setStorage(await getIndexedDBStorage());
|
|
61
64
|
|
|
62
65
|
const map2 = await node2.load(map.id);
|
|
@@ -84,13 +87,7 @@ test("should sync and load data from storage", async () => {
|
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
test("should send an empty content message if there is no content", async () => {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const node1 = new LocalNode(
|
|
90
|
-
agentSecret,
|
|
91
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
92
|
-
Crypto,
|
|
93
|
-
);
|
|
90
|
+
const node1 = createTestNode();
|
|
94
91
|
|
|
95
92
|
node1.setStorage(await getIndexedDBStorage());
|
|
96
93
|
|
|
@@ -117,11 +114,7 @@ test("should send an empty content message if there is no content", async () =>
|
|
|
117
114
|
syncMessages.clear();
|
|
118
115
|
node1.gracefulShutdown();
|
|
119
116
|
|
|
120
|
-
const node2 =
|
|
121
|
-
agentSecret,
|
|
122
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
123
|
-
Crypto,
|
|
124
|
-
);
|
|
117
|
+
const node2 = createTestNode({ secret: node1.agentSecret });
|
|
125
118
|
|
|
126
119
|
node2.setStorage(await getIndexedDBStorage());
|
|
127
120
|
|
|
@@ -148,13 +141,7 @@ test("should send an empty content message if there is no content", async () =>
|
|
|
148
141
|
});
|
|
149
142
|
|
|
150
143
|
test("should load dependencies correctly (group inheritance)", async () => {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const node1 = new LocalNode(
|
|
154
|
-
agentSecret,
|
|
155
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
156
|
-
Crypto,
|
|
157
|
-
);
|
|
144
|
+
const node1 = createTestNode();
|
|
158
145
|
|
|
159
146
|
node1.setStorage(await getIndexedDBStorage());
|
|
160
147
|
const group = node1.createGroup();
|
|
@@ -189,11 +176,7 @@ test("should load dependencies correctly (group inheritance)", async () => {
|
|
|
189
176
|
syncMessages.clear();
|
|
190
177
|
node1.gracefulShutdown();
|
|
191
178
|
|
|
192
|
-
const node2 =
|
|
193
|
-
agentSecret,
|
|
194
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
195
|
-
Crypto,
|
|
196
|
-
);
|
|
179
|
+
const node2 = createTestNode({ secret: node1.agentSecret });
|
|
197
180
|
|
|
198
181
|
node2.setStorage(await getIndexedDBStorage());
|
|
199
182
|
|
|
@@ -223,13 +206,7 @@ test("should load dependencies correctly (group inheritance)", async () => {
|
|
|
223
206
|
});
|
|
224
207
|
|
|
225
208
|
test("should not send the same dependency value twice", async () => {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const node1 = new LocalNode(
|
|
229
|
-
agentSecret,
|
|
230
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
231
|
-
Crypto,
|
|
232
|
-
);
|
|
209
|
+
const node1 = createTestNode();
|
|
233
210
|
|
|
234
211
|
node1.setStorage(await getIndexedDBStorage());
|
|
235
212
|
|
|
@@ -250,11 +227,7 @@ test("should not send the same dependency value twice", async () => {
|
|
|
250
227
|
syncMessages.clear();
|
|
251
228
|
node1.gracefulShutdown();
|
|
252
229
|
|
|
253
|
-
const node2 =
|
|
254
|
-
agentSecret,
|
|
255
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
256
|
-
Crypto,
|
|
257
|
-
);
|
|
230
|
+
const node2 = createTestNode({ secret: node1.agentSecret });
|
|
258
231
|
|
|
259
232
|
node2.setStorage(await getIndexedDBStorage());
|
|
260
233
|
|
|
@@ -289,13 +262,7 @@ test("should not send the same dependency value twice", async () => {
|
|
|
289
262
|
});
|
|
290
263
|
|
|
291
264
|
test("should recover from data loss", async () => {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const node1 = new LocalNode(
|
|
295
|
-
agentSecret,
|
|
296
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
297
|
-
Crypto,
|
|
298
|
-
);
|
|
265
|
+
const node1 = createTestNode();
|
|
299
266
|
|
|
300
267
|
const storage = await getIndexedDBStorage();
|
|
301
268
|
node1.setStorage(storage);
|
|
@@ -347,11 +314,7 @@ test("should recover from data loss", async () => {
|
|
|
347
314
|
syncMessages.clear();
|
|
348
315
|
node1.gracefulShutdown();
|
|
349
316
|
|
|
350
|
-
const node2 =
|
|
351
|
-
agentSecret,
|
|
352
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
353
|
-
Crypto,
|
|
354
|
-
);
|
|
317
|
+
const node2 = createTestNode({ secret: node1.agentSecret });
|
|
355
318
|
|
|
356
319
|
node2.setStorage(await getIndexedDBStorage());
|
|
357
320
|
|
|
@@ -386,13 +349,7 @@ test("should recover from data loss", async () => {
|
|
|
386
349
|
});
|
|
387
350
|
|
|
388
351
|
test("should sync multiple sessions in a single content message", async () => {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
const node1 = new LocalNode(
|
|
392
|
-
agentSecret,
|
|
393
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
394
|
-
Crypto,
|
|
395
|
-
);
|
|
352
|
+
const node1 = createTestNode();
|
|
396
353
|
|
|
397
354
|
node1.setStorage(await getIndexedDBStorage());
|
|
398
355
|
|
|
@@ -406,11 +363,7 @@ test("should sync multiple sessions in a single content message", async () => {
|
|
|
406
363
|
|
|
407
364
|
node1.gracefulShutdown();
|
|
408
365
|
|
|
409
|
-
const node2 =
|
|
410
|
-
agentSecret,
|
|
411
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
412
|
-
Crypto,
|
|
413
|
-
);
|
|
366
|
+
const node2 = createTestNode({ secret: node1.agentSecret });
|
|
414
367
|
|
|
415
368
|
node2.setStorage(await getIndexedDBStorage());
|
|
416
369
|
|
|
@@ -427,11 +380,7 @@ test("should sync multiple sessions in a single content message", async () => {
|
|
|
427
380
|
|
|
428
381
|
node2.gracefulShutdown();
|
|
429
382
|
|
|
430
|
-
const node3 =
|
|
431
|
-
agentSecret,
|
|
432
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
433
|
-
Crypto,
|
|
434
|
-
);
|
|
383
|
+
const node3 = createTestNode({ secret: node1.agentSecret });
|
|
435
384
|
|
|
436
385
|
syncMessages.clear();
|
|
437
386
|
|
|
@@ -462,13 +411,7 @@ test("should sync multiple sessions in a single content message", async () => {
|
|
|
462
411
|
});
|
|
463
412
|
|
|
464
413
|
test("large coValue upload streaming", async () => {
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
const node1 = new LocalNode(
|
|
468
|
-
agentSecret,
|
|
469
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
470
|
-
Crypto,
|
|
471
|
-
);
|
|
414
|
+
const node1 = createTestNode();
|
|
472
415
|
|
|
473
416
|
node1.setStorage(await getIndexedDBStorage());
|
|
474
417
|
|
|
@@ -494,11 +437,7 @@ test("large coValue upload streaming", async () => {
|
|
|
494
437
|
|
|
495
438
|
node1.gracefulShutdown();
|
|
496
439
|
|
|
497
|
-
const node2 =
|
|
498
|
-
agentSecret,
|
|
499
|
-
Crypto.newRandomSessionID(Crypto.getAgentID(agentSecret)),
|
|
500
|
-
Crypto,
|
|
501
|
-
);
|
|
440
|
+
const node2 = createTestNode({ secret: node1.agentSecret });
|
|
502
441
|
|
|
503
442
|
syncMessages.clear();
|
|
504
443
|
|
|
@@ -603,3 +542,115 @@ test("should sync and load accounts from storage", async () => {
|
|
|
603
542
|
|
|
604
543
|
expect(node2.getCoValue(accountID).isAvailable()).toBeTruthy();
|
|
605
544
|
});
|
|
545
|
+
|
|
546
|
+
describe("sync state persistence", () => {
|
|
547
|
+
test("unsynced coValues are asynchronously persisted to storage", async () => {
|
|
548
|
+
// Client is not connected to a sync server, so sync will not be completed
|
|
549
|
+
const client = createTestNode();
|
|
550
|
+
client.setStorage(await getIndexedDBStorage());
|
|
551
|
+
|
|
552
|
+
const group = client.createGroup();
|
|
553
|
+
const map = group.createMap();
|
|
554
|
+
map.set("key", "value");
|
|
555
|
+
|
|
556
|
+
// Wait for the unsynced coValues to be persisted to storage
|
|
557
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 500));
|
|
558
|
+
|
|
559
|
+
const unsyncedCoValueIDs = await new Promise((resolve) =>
|
|
560
|
+
client.storage?.getUnsyncedCoValueIDs(resolve),
|
|
561
|
+
);
|
|
562
|
+
expect(unsyncedCoValueIDs).toHaveLength(2);
|
|
563
|
+
expect(unsyncedCoValueIDs).toContain(map.id);
|
|
564
|
+
expect(unsyncedCoValueIDs).toContain(group.id);
|
|
565
|
+
|
|
566
|
+
await client.gracefulShutdown();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("synced coValues are removed from storage", async () => {
|
|
570
|
+
const syncServer = createTestNode();
|
|
571
|
+
const client = createTestNode();
|
|
572
|
+
client.setStorage(await getIndexedDBStorage());
|
|
573
|
+
|
|
574
|
+
connectToSyncServer(client, syncServer);
|
|
575
|
+
|
|
576
|
+
const group = client.createGroup();
|
|
577
|
+
const map = group.createMap();
|
|
578
|
+
map.set("key", "value");
|
|
579
|
+
|
|
580
|
+
// Wait enough time for the coValue to be synced
|
|
581
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 500));
|
|
582
|
+
|
|
583
|
+
const unsyncedCoValueIDs = await new Promise((resolve) =>
|
|
584
|
+
client.storage?.getUnsyncedCoValueIDs(resolve),
|
|
585
|
+
);
|
|
586
|
+
expect(unsyncedCoValueIDs).toHaveLength(0);
|
|
587
|
+
expect(client.syncManager.unsyncedTracker.has(map.id)).toBe(false);
|
|
588
|
+
|
|
589
|
+
await client.gracefulShutdown();
|
|
590
|
+
await syncServer.gracefulShutdown();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("unsynced coValues are persisted to storage when the node is shutdown", async () => {
|
|
594
|
+
const client = createTestNode();
|
|
595
|
+
client.setStorage(await getIndexedDBStorage());
|
|
596
|
+
|
|
597
|
+
const group = client.createGroup();
|
|
598
|
+
const map = group.createMap();
|
|
599
|
+
map.set("key", "value");
|
|
600
|
+
|
|
601
|
+
// Wait for local transaction to trigger sync
|
|
602
|
+
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
603
|
+
|
|
604
|
+
await client.gracefulShutdown();
|
|
605
|
+
|
|
606
|
+
const unsyncedCoValueIDs = await new Promise((resolve) =>
|
|
607
|
+
client.storage?.getUnsyncedCoValueIDs(resolve),
|
|
608
|
+
);
|
|
609
|
+
expect(unsyncedCoValueIDs).toHaveLength(2);
|
|
610
|
+
expect(unsyncedCoValueIDs).toContain(map.id);
|
|
611
|
+
expect(unsyncedCoValueIDs).toContain(group.id);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe("sync resumption", () => {
|
|
616
|
+
test("unsynced coValues are resumed when the node is restarted", async () => {
|
|
617
|
+
// Client is not connected to a sync server, so sync will not be completed
|
|
618
|
+
const node1 = createTestNode();
|
|
619
|
+
const storage = await getIndexedDBStorage();
|
|
620
|
+
node1.setStorage(storage);
|
|
621
|
+
|
|
622
|
+
const getUnsyncedCoValueIDsFromStorage = async () =>
|
|
623
|
+
new Promise<string[]>((resolve) =>
|
|
624
|
+
node1.storage?.getUnsyncedCoValueIDs(resolve),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const group = node1.createGroup();
|
|
628
|
+
const map = group.createMap();
|
|
629
|
+
map.set("key", "value");
|
|
630
|
+
|
|
631
|
+
// Wait for the unsynced coValues to be persisted to storage
|
|
632
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 100));
|
|
633
|
+
|
|
634
|
+
const unsyncedTracker = node1.syncManager.unsyncedTracker;
|
|
635
|
+
expect(unsyncedTracker.has(map.id)).toBe(true);
|
|
636
|
+
expect(await getUnsyncedCoValueIDsFromStorage()).toHaveLength(2);
|
|
637
|
+
|
|
638
|
+
node1.gracefulShutdown();
|
|
639
|
+
|
|
640
|
+
// Create second node with the same storage
|
|
641
|
+
const node2 = createTestNode();
|
|
642
|
+
node2.setStorage(storage);
|
|
643
|
+
|
|
644
|
+
// Connect to sync server
|
|
645
|
+
const syncServer = createTestNode();
|
|
646
|
+
connectToSyncServer(node2, syncServer);
|
|
647
|
+
|
|
648
|
+
await node2.syncManager.waitForAllCoValuesSync();
|
|
649
|
+
// Wait for sync to resume & complete
|
|
650
|
+
await waitFor(
|
|
651
|
+
async () => (await getUnsyncedCoValueIDsFromStorage()).length === 0,
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
await node2.gracefulShutdown();
|
|
655
|
+
});
|
|
656
|
+
});
|
package/src/tests/testUtils.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type { RawCoID, SyncMessage } from "cojson";
|
|
2
|
-
import {
|
|
1
|
+
import type { AgentSecret, RawCoID, SessionID, SyncMessage } from "cojson";
|
|
2
|
+
import {
|
|
3
|
+
cojsonInternals,
|
|
4
|
+
ControlledAgent,
|
|
5
|
+
LocalNode,
|
|
6
|
+
StorageApiAsync,
|
|
7
|
+
} from "cojson";
|
|
8
|
+
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
|
3
9
|
import { onTestFinished } from "vitest";
|
|
4
10
|
|
|
5
11
|
export function trackMessages() {
|
|
@@ -115,3 +121,78 @@ export function waitFor(
|
|
|
115
121
|
}, 100);
|
|
116
122
|
});
|
|
117
123
|
}
|
|
124
|
+
|
|
125
|
+
export async function clearObjectStore(
|
|
126
|
+
dbName: string,
|
|
127
|
+
storeName: string,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const openReq = indexedDB.open(dbName);
|
|
131
|
+
|
|
132
|
+
openReq.onerror = () => reject(openReq.error);
|
|
133
|
+
|
|
134
|
+
openReq.onsuccess = () => {
|
|
135
|
+
const db = openReq.result;
|
|
136
|
+
|
|
137
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
138
|
+
db.close();
|
|
139
|
+
resolve();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
144
|
+
const store = tx.objectStore(storeName);
|
|
145
|
+
|
|
146
|
+
const clearReq = store.clear();
|
|
147
|
+
|
|
148
|
+
clearReq.onerror = () => reject(clearReq.error);
|
|
149
|
+
|
|
150
|
+
tx.oncomplete = () => {
|
|
151
|
+
db.close();
|
|
152
|
+
resolve();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
tx.onerror = () => {
|
|
156
|
+
db.close();
|
|
157
|
+
reject(tx.error);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
tx.onabort = () => {
|
|
161
|
+
db.close();
|
|
162
|
+
reject(tx.error || new Error("Transaction aborted"));
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const Crypto = await WasmCrypto.create();
|
|
169
|
+
|
|
170
|
+
export function getAgentAndSessionID(
|
|
171
|
+
secret: AgentSecret = Crypto.newRandomAgentSecret(),
|
|
172
|
+
): [ControlledAgent, SessionID] {
|
|
173
|
+
const sessionID = Crypto.newRandomSessionID(Crypto.getAgentID(secret));
|
|
174
|
+
return [new ControlledAgent(secret, Crypto), sessionID];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function createTestNode(opts?: { secret?: AgentSecret }) {
|
|
178
|
+
const [admin, session] = getAgentAndSessionID(opts?.secret);
|
|
179
|
+
return new LocalNode(admin.agentSecret, session, Crypto);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function connectToSyncServer(
|
|
183
|
+
client: LocalNode,
|
|
184
|
+
syncServer: LocalNode,
|
|
185
|
+
): void {
|
|
186
|
+
const [clientPeer, serverPeer] = cojsonInternals.connectedPeers(
|
|
187
|
+
client.currentSessionID,
|
|
188
|
+
syncServer.currentSessionID,
|
|
189
|
+
{
|
|
190
|
+
peer1role: "client",
|
|
191
|
+
peer2role: "server",
|
|
192
|
+
persistent: true,
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
client.syncManager.addPeer(serverPeer);
|
|
197
|
+
syncServer.syncManager.addPeer(clientPeer);
|
|
198
|
+
}
|
package/vitest.config.ts
CHANGED
|
@@ -7,7 +7,12 @@ export default defineProject({
|
|
|
7
7
|
browser: {
|
|
8
8
|
enabled: true,
|
|
9
9
|
provider: playwright(),
|
|
10
|
-
instances: [
|
|
10
|
+
instances: [
|
|
11
|
+
{
|
|
12
|
+
headless: process.env.HEADLESS !== "false",
|
|
13
|
+
browser: "chromium",
|
|
14
|
+
},
|
|
15
|
+
],
|
|
11
16
|
},
|
|
12
17
|
include: ["src/**/*.test.ts"],
|
|
13
18
|
},
|