@instantdb/core 0.22.57 → 0.22.58
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/__tests__/src/Reactor.test.js +97 -21
- package/__tests__/src/utils/PersistedObject.test.ts +383 -0
- package/dist/commonjs/InMemoryStorage.d.ts +11 -0
- package/dist/commonjs/InMemoryStorage.d.ts.map +1 -0
- package/dist/commonjs/InMemoryStorage.js +48 -0
- package/dist/commonjs/InMemoryStorage.js.map +1 -0
- package/dist/commonjs/IndexedDBStorage.d.ts +10 -3
- package/dist/commonjs/IndexedDBStorage.d.ts.map +1 -1
- package/dist/commonjs/IndexedDBStorage.js +254 -8
- package/dist/commonjs/IndexedDBStorage.js.map +1 -1
- package/dist/commonjs/Reactor.d.ts +7 -16
- package/dist/commonjs/Reactor.d.ts.map +1 -1
- package/dist/commonjs/Reactor.js +199 -149
- package/dist/commonjs/Reactor.js.map +1 -1
- package/dist/commonjs/SyncTable.d.ts +3 -1
- package/dist/commonjs/SyncTable.d.ts.map +1 -1
- package/dist/commonjs/SyncTable.js +194 -168
- package/dist/commonjs/SyncTable.js.map +1 -1
- package/dist/commonjs/index.d.ts +2 -1
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js +3 -1
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/utils/PersistedObject.d.ts +73 -29
- package/dist/commonjs/utils/PersistedObject.d.ts.map +1 -1
- package/dist/commonjs/utils/PersistedObject.js +366 -77
- package/dist/commonjs/utils/PersistedObject.js.map +1 -1
- package/dist/esm/InMemoryStorage.d.ts +11 -0
- package/dist/esm/InMemoryStorage.d.ts.map +1 -0
- package/dist/esm/InMemoryStorage.js +45 -0
- package/dist/esm/InMemoryStorage.js.map +1 -0
- package/dist/esm/IndexedDBStorage.d.ts +10 -3
- package/dist/esm/IndexedDBStorage.d.ts.map +1 -1
- package/dist/esm/IndexedDBStorage.js +254 -8
- package/dist/esm/IndexedDBStorage.js.map +1 -1
- package/dist/esm/Reactor.d.ts +7 -16
- package/dist/esm/Reactor.d.ts.map +1 -1
- package/dist/esm/Reactor.js +199 -149
- package/dist/esm/Reactor.js.map +1 -1
- package/dist/esm/SyncTable.d.ts +3 -1
- package/dist/esm/SyncTable.d.ts.map +1 -1
- package/dist/esm/SyncTable.js +194 -168
- package/dist/esm/SyncTable.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/PersistedObject.d.ts +73 -29
- package/dist/esm/utils/PersistedObject.d.ts.map +1 -1
- package/dist/esm/utils/PersistedObject.js +364 -76
- package/dist/esm/utils/PersistedObject.js.map +1 -1
- package/dist/standalone/index.js +1978 -1567
- package/dist/standalone/index.umd.cjs +3 -3
- package/package.json +2 -2
- package/src/InMemoryStorage.ts +34 -0
- package/src/IndexedDBStorage.ts +281 -9
- package/src/Reactor.js +212 -184
- package/src/SyncTable.ts +220 -191
- package/src/index.ts +8 -0
- package/src/utils/PersistedObject.ts +480 -109
- package/src/InMemoryStorage.js +0 -14
|
@@ -12,12 +12,21 @@ import * as instatx from '../../src/instatx';
|
|
|
12
12
|
import zenecaAttrs from './data/zeneca/attrs.json';
|
|
13
13
|
import zenecaTriples from './data/zeneca/triples.json';
|
|
14
14
|
import uuid from '../../src/utils/uuid';
|
|
15
|
+
import { weakHash } from '../../src';
|
|
15
16
|
|
|
16
17
|
const zenecaIdToAttr = zenecaAttrs.reduce((res, x) => {
|
|
17
18
|
res[x.id] = x;
|
|
18
19
|
return res;
|
|
19
20
|
}, {});
|
|
20
21
|
|
|
22
|
+
async function waitForLoaded(reactor) {
|
|
23
|
+
await reactor.querySubs.waitForMetaToLoad();
|
|
24
|
+
await reactor.kv.waitForMetaToLoad();
|
|
25
|
+
await reactor.kv.waitForKeyToLoad('pendingMutations');
|
|
26
|
+
await reactor.querySubs.flush();
|
|
27
|
+
await reactor.kv.flush();
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
test('querySubs round-trips', async () => {
|
|
22
31
|
const appId = uuid();
|
|
23
32
|
const reactor = new Reactor({ appId });
|
|
@@ -25,7 +34,7 @@ test('querySubs round-trips', async () => {
|
|
|
25
34
|
reactor._setAttrs(zenecaAttrs);
|
|
26
35
|
const q = { users: {} };
|
|
27
36
|
|
|
28
|
-
await
|
|
37
|
+
await waitForLoaded(reactor);
|
|
29
38
|
|
|
30
39
|
const resultOne = new Promise((resolve, reject) => {
|
|
31
40
|
reactor.subscribeQuery(q, (res) => {
|
|
@@ -63,14 +72,15 @@ test('querySubs round-trips', async () => {
|
|
|
63
72
|
'nicolegf',
|
|
64
73
|
]);
|
|
65
74
|
|
|
66
|
-
await reactor
|
|
75
|
+
await waitForLoaded(reactor);
|
|
67
76
|
|
|
68
77
|
// Create a new reactor
|
|
69
78
|
const reactor2 = new Reactor({ appId });
|
|
70
79
|
reactor2._initStorage(IndexedDBStorage);
|
|
71
80
|
reactor2._setAttrs(zenecaAttrs);
|
|
72
81
|
|
|
73
|
-
await
|
|
82
|
+
await waitForLoaded(reactor2);
|
|
83
|
+
await reactor2.querySubs.waitForKeyToLoad(weakHash(q));
|
|
74
84
|
|
|
75
85
|
// Check that it pull the data from indexedDB
|
|
76
86
|
const res = await new Promise((resolve, reject) => {
|
|
@@ -165,7 +175,7 @@ test('rewrite mutations works with multiple transactions', () => {
|
|
|
165
175
|
op: 'transact',
|
|
166
176
|
'tx-steps': steps,
|
|
167
177
|
};
|
|
168
|
-
reactor.
|
|
178
|
+
reactor._updatePendingMutations((prev) => {
|
|
169
179
|
prev.set(k, mut);
|
|
170
180
|
return prev;
|
|
171
181
|
});
|
|
@@ -174,7 +184,7 @@ test('rewrite mutations works with multiple transactions', () => {
|
|
|
174
184
|
// rewrite them with the new server attributes
|
|
175
185
|
const rewrittenMutations = reactor._rewriteMutations(
|
|
176
186
|
zenecaIdToAttr,
|
|
177
|
-
reactor.
|
|
187
|
+
reactor._pendingMutations(),
|
|
178
188
|
);
|
|
179
189
|
|
|
180
190
|
const serverSteps = instaml.transform({ attrs: zenecaIdToAttr }, ops);
|
|
@@ -191,7 +201,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
191
201
|
const q = { users: {} };
|
|
192
202
|
const joe_id = 'ce942051-2d74-404a-9c7d-4aa3f2d54ae4';
|
|
193
203
|
|
|
194
|
-
await
|
|
204
|
+
await waitForLoaded(reactor);
|
|
195
205
|
|
|
196
206
|
let data = null;
|
|
197
207
|
|
|
@@ -216,7 +226,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
216
226
|
],
|
|
217
227
|
});
|
|
218
228
|
|
|
219
|
-
await reactor.querySubs.
|
|
229
|
+
await reactor.querySubs.flush();
|
|
220
230
|
|
|
221
231
|
expect(data.data.users.map((x) => x.handle)).toEqual([
|
|
222
232
|
'joe',
|
|
@@ -234,7 +244,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
234
244
|
|
|
235
245
|
reactor.pushTx(ops2);
|
|
236
246
|
|
|
237
|
-
await reactor.querySubs.
|
|
247
|
+
await reactor.querySubs.flush();
|
|
238
248
|
|
|
239
249
|
expect(data.data.users.map((x) => x.handle)).toEqual([
|
|
240
250
|
'joe2',
|
|
@@ -243,7 +253,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
243
253
|
'nicolegf',
|
|
244
254
|
]);
|
|
245
255
|
|
|
246
|
-
const [eventId2] = reactor.
|
|
256
|
+
const [eventId2] = reactor._pendingMutations().keys();
|
|
247
257
|
|
|
248
258
|
// second optimistic
|
|
249
259
|
const ops3 = [
|
|
@@ -254,7 +264,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
254
264
|
|
|
255
265
|
reactor.pushTx(ops3);
|
|
256
266
|
|
|
257
|
-
await reactor.querySubs.
|
|
267
|
+
await reactor.querySubs.flush();
|
|
258
268
|
|
|
259
269
|
expect(data.data.users.map((x) => x.handle)).toEqual([
|
|
260
270
|
'joe3',
|
|
@@ -263,7 +273,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
263
273
|
'nicolegf',
|
|
264
274
|
]);
|
|
265
275
|
|
|
266
|
-
const [_, eventId3] = reactor.
|
|
276
|
+
const [_, eventId3] = reactor._pendingMutations().keys();
|
|
267
277
|
|
|
268
278
|
// confirmation from server for first optimistic
|
|
269
279
|
reactor._handleReceive(1, {
|
|
@@ -272,7 +282,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
272
282
|
'tx-id': 100,
|
|
273
283
|
});
|
|
274
284
|
|
|
275
|
-
await reactor
|
|
285
|
+
await waitForLoaded(reactor);
|
|
276
286
|
|
|
277
287
|
expect(data.data.users.map((x) => x.handle)).toEqual([
|
|
278
288
|
'joe3',
|
|
@@ -304,7 +314,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
304
314
|
});
|
|
305
315
|
|
|
306
316
|
// make sure it doesn’t override local results
|
|
307
|
-
await reactor
|
|
317
|
+
await waitForLoaded(reactor);
|
|
308
318
|
|
|
309
319
|
expect(data.data.users.map((x) => x.handle)).toEqual([
|
|
310
320
|
'joe3',
|
|
@@ -320,7 +330,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
320
330
|
'tx-id': 101,
|
|
321
331
|
});
|
|
322
332
|
|
|
323
|
-
await reactor
|
|
333
|
+
await waitForLoaded(reactor);
|
|
324
334
|
|
|
325
335
|
expect(data.data.users.map((x) => x.handle)).toEqual([
|
|
326
336
|
'joe3',
|
|
@@ -330,7 +340,7 @@ test('optimisticTx is not overwritten by refresh-ok', async () => {
|
|
|
330
340
|
]);
|
|
331
341
|
|
|
332
342
|
// make sure it still doesn’t override local results
|
|
333
|
-
await reactor
|
|
343
|
+
await waitForLoaded(reactor);
|
|
334
344
|
|
|
335
345
|
expect(data.data.users.map((x) => x.handle)).toEqual([
|
|
336
346
|
'joe3',
|
|
@@ -353,7 +363,7 @@ test("we don't cleanup mutations we're still waiting on", async () => {
|
|
|
353
363
|
const q = { users: {} };
|
|
354
364
|
const joe_id = 'ce942051-2d74-404a-9c7d-4aa3f2d54ae4';
|
|
355
365
|
|
|
356
|
-
await
|
|
366
|
+
await waitForLoaded(reactor);
|
|
357
367
|
|
|
358
368
|
// Add two transactions
|
|
359
369
|
const ops1 = [
|
|
@@ -372,9 +382,9 @@ test("we don't cleanup mutations we're still waiting on", async () => {
|
|
|
372
382
|
|
|
373
383
|
reactor.pushTx(ops2);
|
|
374
384
|
|
|
375
|
-
await reactor.
|
|
385
|
+
await reactor.kv.flush();
|
|
376
386
|
|
|
377
|
-
const [ev1, ev2] = reactor.
|
|
387
|
+
const [ev1, ev2] = reactor._pendingMutations().keys();
|
|
378
388
|
|
|
379
389
|
// Mark one as received
|
|
380
390
|
reactor._handleReceive(1, {
|
|
@@ -389,9 +399,75 @@ test("we don't cleanup mutations we're still waiting on", async () => {
|
|
|
389
399
|
// Cleanup shouldn't remove the mutation we're still waiting on
|
|
390
400
|
await reactor._cleanupPendingMutationsTimeout();
|
|
391
401
|
|
|
392
|
-
const remainingKeys = new Array(
|
|
393
|
-
...reactor.pendingMutations.currentValue.keys(),
|
|
394
|
-
);
|
|
402
|
+
const remainingKeys = new Array(...reactor._pendingMutations().keys());
|
|
395
403
|
|
|
396
404
|
expect(remainingKeys).toStrictEqual([ev2]);
|
|
397
405
|
});
|
|
406
|
+
|
|
407
|
+
test('getLocalId always returns the same id', async () => {
|
|
408
|
+
const appId = uuid();
|
|
409
|
+
|
|
410
|
+
const idbs = new Map();
|
|
411
|
+
|
|
412
|
+
// Class that reuses the same underlying storage for indexeddb.
|
|
413
|
+
// This will make fake-indexeddb act more like the browser, otherwise
|
|
414
|
+
// we get a fresh in-memory store each time we call indexedDB.open
|
|
415
|
+
class IDBStorage {
|
|
416
|
+
constructor(appId, dbName) {
|
|
417
|
+
if (idbs.get(dbName)) {
|
|
418
|
+
return idbs.get(dbName);
|
|
419
|
+
}
|
|
420
|
+
const idb = new IndexedDBStorage(appId, dbName);
|
|
421
|
+
idbs.set(dbName, idb);
|
|
422
|
+
return idb;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const reactor = new Reactor({
|
|
427
|
+
appId,
|
|
428
|
+
pendingTxCleanupTimeout: 1,
|
|
429
|
+
pendingMutationCleanupThreshold: 0,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
reactor._initStorage(IDBStorage);
|
|
433
|
+
|
|
434
|
+
const promises = [];
|
|
435
|
+
|
|
436
|
+
const ids = new Set();
|
|
437
|
+
|
|
438
|
+
for (const _ of [...new Array(1000)]) {
|
|
439
|
+
const p = reactor.getLocalId('id').then((id) => ids.add(id));
|
|
440
|
+
promises.push(p);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await Promise.all(promises);
|
|
444
|
+
|
|
445
|
+
expect(ids.size).toBe(1);
|
|
446
|
+
await reactor.querySubs.flush();
|
|
447
|
+
|
|
448
|
+
await reactor.kv.flush();
|
|
449
|
+
|
|
450
|
+
const reactor2 = new Reactor({
|
|
451
|
+
appId,
|
|
452
|
+
pendingTxCleanupTimeout: 1,
|
|
453
|
+
pendingMutationCleanupThreshold: 0,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
async function idbSnapshot(idb, { includeMeta }) {
|
|
457
|
+
const keys = await idb.getAllKeys();
|
|
458
|
+
const res = {};
|
|
459
|
+
for (const key of keys) {
|
|
460
|
+
if (key === '__meta' && !includeMeta) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
res[key] = await idb.getItem(key);
|
|
464
|
+
}
|
|
465
|
+
return res;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
reactor2._initStorage(IDBStorage);
|
|
469
|
+
|
|
470
|
+
const id = await reactor2.getLocalId('id');
|
|
471
|
+
|
|
472
|
+
expect(id).toStrictEqual([...ids][0]);
|
|
473
|
+
});
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import 'fake-indexeddb/auto';
|
|
2
|
+
import { test, expect, describe } from 'vitest';
|
|
3
|
+
import { PersistedObject } from '../../../src/utils/PersistedObject';
|
|
4
|
+
import { IndexedDBStorage } from '../../../src';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import createLogger from '../../../src/utils/log';
|
|
7
|
+
|
|
8
|
+
async function idbSnapshot(idb, { includeMeta }) {
|
|
9
|
+
const keys = await idb.getAllKeys();
|
|
10
|
+
const res = {};
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
if (key === '__meta' && !includeMeta) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
res[key] = await idb.getItem(key);
|
|
16
|
+
}
|
|
17
|
+
return res;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const devNullLogger = createLogger(false, () => ({}));
|
|
21
|
+
|
|
22
|
+
test('PersistedObject saves values to storage', async () => {
|
|
23
|
+
const idb = new IndexedDBStorage(randomUUID(), 'querySubs');
|
|
24
|
+
const PO = new PersistedObject<string, string, string>({
|
|
25
|
+
persister: idb,
|
|
26
|
+
merge: (_k, storage, memory) => storage || memory || 'none',
|
|
27
|
+
serialize: (_k, x) => x,
|
|
28
|
+
parse: (_k, x) => x,
|
|
29
|
+
objectSize: (_v) => 0,
|
|
30
|
+
logger: devNullLogger,
|
|
31
|
+
saveThrottleMs: 0,
|
|
32
|
+
gc: {
|
|
33
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
34
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
35
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
PO.updateInPlace((prev) => {
|
|
40
|
+
prev.a = 'b';
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await PO.flush();
|
|
44
|
+
|
|
45
|
+
const snapshot = await idbSnapshot(idb, { includeMeta: false });
|
|
46
|
+
|
|
47
|
+
expect(snapshot).toStrictEqual({ a: 'b' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('PersistedObject merges existing values', async () => {
|
|
51
|
+
const idb = new IndexedDBStorage(randomUUID(), 'querySubs');
|
|
52
|
+
let storageV;
|
|
53
|
+
let memoryV;
|
|
54
|
+
const PO = new PersistedObject<string, string, string>({
|
|
55
|
+
persister: idb,
|
|
56
|
+
merge: (_k, storage, memory) => {
|
|
57
|
+
storageV = storage;
|
|
58
|
+
memoryV = memory;
|
|
59
|
+
return 'merged-value';
|
|
60
|
+
},
|
|
61
|
+
serialize: (_k, x) => x,
|
|
62
|
+
parse: (_k, x) => x,
|
|
63
|
+
objectSize: (_v) => 0,
|
|
64
|
+
logger: devNullLogger,
|
|
65
|
+
saveThrottleMs: 0,
|
|
66
|
+
gc: {
|
|
67
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
68
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
69
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
PO.updateInPlace((prev) => {
|
|
74
|
+
prev.a = 'b';
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await PO.flush();
|
|
78
|
+
|
|
79
|
+
// We don't run merge if there was nothing in the store
|
|
80
|
+
expect(storageV).toBeUndefined();
|
|
81
|
+
expect(memoryV).toBeUndefined();
|
|
82
|
+
expect(PO.currentValue.a).toEqual('b');
|
|
83
|
+
|
|
84
|
+
const snapshot = await idbSnapshot(idb, { includeMeta: false });
|
|
85
|
+
|
|
86
|
+
expect(snapshot).toStrictEqual({ a: 'b' });
|
|
87
|
+
|
|
88
|
+
let storageV2;
|
|
89
|
+
let memoryV2;
|
|
90
|
+
const PO2 = new PersistedObject<string, string, string>({
|
|
91
|
+
persister: idb,
|
|
92
|
+
merge: (_k, storage, memory) => {
|
|
93
|
+
storageV2 = storage;
|
|
94
|
+
memoryV2 = memory;
|
|
95
|
+
return 'merged-value-2';
|
|
96
|
+
},
|
|
97
|
+
serialize: (_k, x) => x,
|
|
98
|
+
parse: (_k, x) => x,
|
|
99
|
+
objectSize: (_v) => 0,
|
|
100
|
+
logger: devNullLogger,
|
|
101
|
+
saveThrottleMs: 0,
|
|
102
|
+
gc: {
|
|
103
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
104
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
105
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
PO2.updateInPlace((prev) => {
|
|
110
|
+
prev.a = 'c';
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await PO2.flush();
|
|
114
|
+
|
|
115
|
+
expect(storageV2).toEqual('b');
|
|
116
|
+
expect(memoryV2).toEqual('c');
|
|
117
|
+
expect(PO2.currentValue.a).toEqual('merged-value-2');
|
|
118
|
+
|
|
119
|
+
const snapshot2 = await idbSnapshot(idb, { includeMeta: false });
|
|
120
|
+
|
|
121
|
+
expect(snapshot2).toStrictEqual({ a: 'merged-value-2' });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('PersistedObject notifies you when it loads a key from storage', async () => {
|
|
125
|
+
const idb = new IndexedDBStorage(randomUUID(), 'querySubs');
|
|
126
|
+
const PO = new PersistedObject({
|
|
127
|
+
persister: idb,
|
|
128
|
+
merge: (_k, _storage, _memory) => 'merged',
|
|
129
|
+
serialize: (_k, x) => x,
|
|
130
|
+
parse: (_k, x) => x,
|
|
131
|
+
objectSize: () => 0,
|
|
132
|
+
logger: devNullLogger,
|
|
133
|
+
saveThrottleMs: 0,
|
|
134
|
+
gc: {
|
|
135
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
136
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
137
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('PersistedObject garbage collects when we exceed max items', async () => {
|
|
143
|
+
const idb = new IndexedDBStorage(randomUUID(), 'querySubs');
|
|
144
|
+
const PO = new PersistedObject({
|
|
145
|
+
persister: idb,
|
|
146
|
+
merge: (_k, storage, memory) => storage || memory || 'new',
|
|
147
|
+
serialize: (_k, x) => x,
|
|
148
|
+
parse: (_k, x) => x,
|
|
149
|
+
objectSize: () => 0,
|
|
150
|
+
logger: devNullLogger,
|
|
151
|
+
saveThrottleMs: 0,
|
|
152
|
+
gc: {
|
|
153
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
154
|
+
maxEntries: 3,
|
|
155
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
for (const [i, k] of Object.entries(['a', 'b', 'c', 'd', 'e'])) {
|
|
160
|
+
PO.updateInPlace((prev) => {
|
|
161
|
+
prev[k] = parseInt(i, 10) + 1;
|
|
162
|
+
});
|
|
163
|
+
await PO.flush();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// @ts-expect-error: allow access to private fields for test
|
|
167
|
+
await PO._gc();
|
|
168
|
+
|
|
169
|
+
const snapshot = await idbSnapshot(idb, { includeMeta: false });
|
|
170
|
+
|
|
171
|
+
// We don't get rid of live keys
|
|
172
|
+
expect(PO.currentValue).toStrictEqual({ a: 1, b: 2, c: 3, d: 4, e: 5 });
|
|
173
|
+
expect(snapshot).toStrictEqual({
|
|
174
|
+
a: 1,
|
|
175
|
+
b: 2,
|
|
176
|
+
c: 3,
|
|
177
|
+
d: 4,
|
|
178
|
+
e: 5,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
PO.unloadKey('e');
|
|
182
|
+
|
|
183
|
+
await PO.flush();
|
|
184
|
+
|
|
185
|
+
// @ts-expect-error: allow access to private fields for test
|
|
186
|
+
await PO._gc();
|
|
187
|
+
|
|
188
|
+
const snapshot2 = await idbSnapshot(idb, { includeMeta: false });
|
|
189
|
+
|
|
190
|
+
// It does get rid of unloaded keys
|
|
191
|
+
expect(PO.currentValue).toStrictEqual({ a: 1, b: 2, c: 3, d: 4 });
|
|
192
|
+
expect(snapshot2).toStrictEqual({ a: 1, b: 2, c: 3, d: 4 });
|
|
193
|
+
|
|
194
|
+
// Simulate a reload of the page
|
|
195
|
+
const PO2 = new PersistedObject({
|
|
196
|
+
persister: idb,
|
|
197
|
+
merge: (_k, storage, memory) => storage || memory || 'new',
|
|
198
|
+
serialize: (_k, x) => x,
|
|
199
|
+
parse: (_k, x) => x,
|
|
200
|
+
objectSize: () => 0,
|
|
201
|
+
logger: devNullLogger,
|
|
202
|
+
saveThrottleMs: 0,
|
|
203
|
+
gc: {
|
|
204
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
205
|
+
maxEntries: 3,
|
|
206
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await PO2.waitForMetaToLoad();
|
|
211
|
+
|
|
212
|
+
// @ts-expect-error: allow access to private fields for test
|
|
213
|
+
await PO2._gc();
|
|
214
|
+
const snapshot3 = await idbSnapshot(idb, { includeMeta: false });
|
|
215
|
+
|
|
216
|
+
expect(snapshot3).toStrictEqual({ b: 2, c: 3, d: 4 });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('PersistedObject garbage collects when we exceed max size', async () => {
|
|
220
|
+
const idb = new IndexedDBStorage(randomUUID(), 'querySubs');
|
|
221
|
+
const PO = new PersistedObject<string, number, number>({
|
|
222
|
+
persister: idb,
|
|
223
|
+
merge: (_k, storage, memory) => storage || memory || 0,
|
|
224
|
+
serialize: (_k, x) => x,
|
|
225
|
+
parse: (_k, x) => x,
|
|
226
|
+
objectSize: (v) => v,
|
|
227
|
+
logger: devNullLogger,
|
|
228
|
+
saveThrottleMs: 0,
|
|
229
|
+
gc: {
|
|
230
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
231
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
232
|
+
maxSize: 100,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
for (const [i, k] of [
|
|
237
|
+
[10, 'a'],
|
|
238
|
+
[20, 'b'],
|
|
239
|
+
[50, 'c'],
|
|
240
|
+
[50, 'd'],
|
|
241
|
+
[50, 'e'],
|
|
242
|
+
]) {
|
|
243
|
+
PO.updateInPlace((prev) => {
|
|
244
|
+
prev[k] = i as number;
|
|
245
|
+
});
|
|
246
|
+
await PO.flush();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// @ts-expect-error: allow access to private fields for test
|
|
250
|
+
await PO._gc();
|
|
251
|
+
|
|
252
|
+
const snapshot = await idbSnapshot(idb, { includeMeta: false });
|
|
253
|
+
|
|
254
|
+
// We don't get rid of live keys
|
|
255
|
+
expect(PO.currentValue).toStrictEqual({ a: 10, b: 20, c: 50, d: 50, e: 50 });
|
|
256
|
+
expect(snapshot).toStrictEqual({
|
|
257
|
+
a: 10,
|
|
258
|
+
b: 20,
|
|
259
|
+
c: 50,
|
|
260
|
+
d: 50,
|
|
261
|
+
e: 50,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
PO.unloadKey('e');
|
|
265
|
+
|
|
266
|
+
await PO.flush();
|
|
267
|
+
|
|
268
|
+
// @ts-expect-error: allow access to private fields for test
|
|
269
|
+
await PO._gc();
|
|
270
|
+
|
|
271
|
+
const snapshot2 = await idbSnapshot(idb, { includeMeta: false });
|
|
272
|
+
|
|
273
|
+
// It does get rid of unloaded keys
|
|
274
|
+
expect(PO.currentValue).toStrictEqual({ a: 10, b: 20, c: 50, d: 50 });
|
|
275
|
+
expect(snapshot2).toStrictEqual({ a: 10, b: 20, c: 50, d: 50 });
|
|
276
|
+
|
|
277
|
+
// Simulate a reload of the page
|
|
278
|
+
const PO2 = new PersistedObject<string, number, number>({
|
|
279
|
+
persister: idb,
|
|
280
|
+
merge: (_k, storage, memory) => storage || memory || 0,
|
|
281
|
+
serialize: (_k, x) => x,
|
|
282
|
+
parse: (_k, x) => x,
|
|
283
|
+
objectSize: (v) => v,
|
|
284
|
+
logger: devNullLogger,
|
|
285
|
+
saveThrottleMs: 0,
|
|
286
|
+
gc: {
|
|
287
|
+
maxAgeMs: Number.MAX_SAFE_INTEGER,
|
|
288
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
289
|
+
maxSize: 100,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await PO2.waitForMetaToLoad();
|
|
294
|
+
|
|
295
|
+
// @ts-expect-error: allow access to private fields for test
|
|
296
|
+
await PO2._gc();
|
|
297
|
+
const snapshot3 = await idbSnapshot(idb, { includeMeta: false });
|
|
298
|
+
|
|
299
|
+
expect(snapshot3).toStrictEqual({ c: 50, d: 50 });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('PersistedObject garbage collects when we exceed max age', async () => {
|
|
303
|
+
const idb = new IndexedDBStorage(randomUUID(), 'querySubs');
|
|
304
|
+
const PO = new PersistedObject<string, number, number>({
|
|
305
|
+
persister: idb,
|
|
306
|
+
merge: (_k, storage, memory) => storage || memory || 0,
|
|
307
|
+
serialize: (_k, x) => x,
|
|
308
|
+
parse: (_k, x) => x,
|
|
309
|
+
objectSize: (v) => v,
|
|
310
|
+
logger: devNullLogger,
|
|
311
|
+
saveThrottleMs: 0,
|
|
312
|
+
gc: {
|
|
313
|
+
maxAgeMs: 0,
|
|
314
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
315
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
for (const [i, k] of [
|
|
320
|
+
[10, 'a'],
|
|
321
|
+
[20, 'b'],
|
|
322
|
+
[50, 'c'],
|
|
323
|
+
[50, 'd'],
|
|
324
|
+
[50, 'e'],
|
|
325
|
+
]) {
|
|
326
|
+
PO.updateInPlace((prev) => {
|
|
327
|
+
prev[k] = i as number;
|
|
328
|
+
});
|
|
329
|
+
await PO.flush();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// @ts-expect-error: allow access to private fields for test
|
|
333
|
+
await PO._gc();
|
|
334
|
+
|
|
335
|
+
const snapshot = await idbSnapshot(idb, { includeMeta: false });
|
|
336
|
+
|
|
337
|
+
// We don't get rid of live keys
|
|
338
|
+
expect(PO.currentValue).toStrictEqual({ a: 10, b: 20, c: 50, d: 50, e: 50 });
|
|
339
|
+
expect(snapshot).toStrictEqual({
|
|
340
|
+
a: 10,
|
|
341
|
+
b: 20,
|
|
342
|
+
c: 50,
|
|
343
|
+
d: 50,
|
|
344
|
+
e: 50,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
PO.unloadKey('e');
|
|
348
|
+
|
|
349
|
+
await PO.flush();
|
|
350
|
+
|
|
351
|
+
// @ts-expect-error: allow access to private fields for test
|
|
352
|
+
await PO._gc();
|
|
353
|
+
|
|
354
|
+
const snapshot2 = await idbSnapshot(idb, { includeMeta: false });
|
|
355
|
+
|
|
356
|
+
// It does get rid of unloaded keys
|
|
357
|
+
expect(PO.currentValue).toStrictEqual({ a: 10, b: 20, c: 50, d: 50 });
|
|
358
|
+
expect(snapshot2).toStrictEqual({ a: 10, b: 20, c: 50, d: 50 });
|
|
359
|
+
|
|
360
|
+
// Simulate a reload of the page
|
|
361
|
+
const PO2 = new PersistedObject<string, number, number>({
|
|
362
|
+
persister: idb,
|
|
363
|
+
merge: (_k, storage, memory) => storage || memory || 0,
|
|
364
|
+
serialize: (_k, x) => x,
|
|
365
|
+
parse: (_k, x) => x,
|
|
366
|
+
objectSize: (v) => v,
|
|
367
|
+
logger: devNullLogger,
|
|
368
|
+
saveThrottleMs: 0,
|
|
369
|
+
gc: {
|
|
370
|
+
maxAgeMs: 0,
|
|
371
|
+
maxEntries: Number.MAX_SAFE_INTEGER,
|
|
372
|
+
maxSize: Number.MAX_SAFE_INTEGER,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await PO2.waitForMetaToLoad();
|
|
377
|
+
|
|
378
|
+
// @ts-expect-error: allow access to private fields for test
|
|
379
|
+
await PO2._gc();
|
|
380
|
+
const snapshot3 = await idbSnapshot(idb, { includeMeta: false });
|
|
381
|
+
|
|
382
|
+
expect(snapshot3).toStrictEqual({});
|
|
383
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StorageInterface, StorageInterfaceStoreName } from './utils/PersistedObject.js';
|
|
2
|
+
export default class InMemoryStorage extends StorageInterface {
|
|
3
|
+
private store;
|
|
4
|
+
constructor(appId: string, dbName: StorageInterfaceStoreName);
|
|
5
|
+
getItem(k: any): Promise<any>;
|
|
6
|
+
setItem(k: any, v: any): Promise<void>;
|
|
7
|
+
getAllKeys(): Promise<string[]>;
|
|
8
|
+
multiSet(keyValuePairs: Array<[string, any]>): Promise<void>;
|
|
9
|
+
removeItem(key: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=InMemoryStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InMemoryStorage.d.ts","sourceRoot":"","sources":["../../src/InMemoryStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,yBAAyB,EAC1B,MAAM,4BAA4B,CAAC;AAEpC,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,gBAAgB;IAC3D,OAAO,CAAC,KAAK,CAAmB;gBACpB,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB;IAKtD,OAAO,CAAC,CAAC,KAAA;IAIT,OAAO,CAAC,CAAC,KAAA,EAAE,CAAC,KAAA;IAIZ,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAI/B,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG7C"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const PersistedObject_js_1 = require("./utils/PersistedObject.js");
|
|
13
|
+
class InMemoryStorage extends PersistedObject_js_1.StorageInterface {
|
|
14
|
+
constructor(appId, dbName) {
|
|
15
|
+
super(appId, dbName);
|
|
16
|
+
this.store = new Map();
|
|
17
|
+
}
|
|
18
|
+
getItem(k) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
var _a;
|
|
21
|
+
return (_a = this.store.get(k)) !== null && _a !== void 0 ? _a : null;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
setItem(k, v) {
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
this.store.set(k, v);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
getAllKeys() {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
return [...this.store.keys()];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
multiSet(keyValuePairs) {
|
|
35
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
for (const [k, v] of keyValuePairs) {
|
|
37
|
+
this.setItem(k, v);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
removeItem(key) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
this.store.delete(key);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.default = InMemoryStorage;
|
|
48
|
+
//# sourceMappingURL=InMemoryStorage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InMemoryStorage.js","sourceRoot":"","sources":["../../src/InMemoryStorage.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,mEAGoC;AAEpC,MAAqB,eAAgB,SAAQ,qCAAgB;IAE3D,YAAY,KAAa,EAAE,MAAiC;QAC1D,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;IACzB,CAAC;IAEK,OAAO,CAAC,CAAC;;;YACb,OAAO,MAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,mCAAI,IAAI,CAAC;QACnC,CAAC;KAAA;IAEK,OAAO,CAAC,CAAC,EAAE,CAAC;;YAChB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvB,CAAC;KAAA;IAEK,UAAU;;YACd,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAChC,CAAC;KAAA;IAEK,QAAQ,CAAC,aAAmC;;YAChD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC;gBACnC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;KAAA;IAEK,UAAU,CAAC,GAAW;;YAC1B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC;KAAA;CACF;AA5BD,kCA4BC","sourcesContent":["import {\n StorageInterface,\n StorageInterfaceStoreName,\n} from './utils/PersistedObject.js';\n\nexport default class InMemoryStorage extends StorageInterface {\n private store: Map<string, any>;\n constructor(appId: string, dbName: StorageInterfaceStoreName) {\n super(appId, dbName);\n this.store = new Map();\n }\n\n async getItem(k) {\n return this.store.get(k) ?? null;\n }\n\n async setItem(k, v) {\n this.store.set(k, v);\n }\n\n async getAllKeys(): Promise<string[]> {\n return [...this.store.keys()];\n }\n\n async multiSet(keyValuePairs: Array<[string, any]>): Promise<void> {\n for (const [k, v] of keyValuePairs) {\n this.setItem(k, v);\n }\n }\n\n async removeItem(key: string): Promise<void> {\n this.store.delete(key);\n }\n}\n"]}
|