@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.
Files changed (60) hide show
  1. package/__tests__/src/Reactor.test.js +97 -21
  2. package/__tests__/src/utils/PersistedObject.test.ts +383 -0
  3. package/dist/commonjs/InMemoryStorage.d.ts +11 -0
  4. package/dist/commonjs/InMemoryStorage.d.ts.map +1 -0
  5. package/dist/commonjs/InMemoryStorage.js +48 -0
  6. package/dist/commonjs/InMemoryStorage.js.map +1 -0
  7. package/dist/commonjs/IndexedDBStorage.d.ts +10 -3
  8. package/dist/commonjs/IndexedDBStorage.d.ts.map +1 -1
  9. package/dist/commonjs/IndexedDBStorage.js +254 -8
  10. package/dist/commonjs/IndexedDBStorage.js.map +1 -1
  11. package/dist/commonjs/Reactor.d.ts +7 -16
  12. package/dist/commonjs/Reactor.d.ts.map +1 -1
  13. package/dist/commonjs/Reactor.js +199 -149
  14. package/dist/commonjs/Reactor.js.map +1 -1
  15. package/dist/commonjs/SyncTable.d.ts +3 -1
  16. package/dist/commonjs/SyncTable.d.ts.map +1 -1
  17. package/dist/commonjs/SyncTable.js +194 -168
  18. package/dist/commonjs/SyncTable.js.map +1 -1
  19. package/dist/commonjs/index.d.ts +2 -1
  20. package/dist/commonjs/index.d.ts.map +1 -1
  21. package/dist/commonjs/index.js +3 -1
  22. package/dist/commonjs/index.js.map +1 -1
  23. package/dist/commonjs/utils/PersistedObject.d.ts +73 -29
  24. package/dist/commonjs/utils/PersistedObject.d.ts.map +1 -1
  25. package/dist/commonjs/utils/PersistedObject.js +366 -77
  26. package/dist/commonjs/utils/PersistedObject.js.map +1 -1
  27. package/dist/esm/InMemoryStorage.d.ts +11 -0
  28. package/dist/esm/InMemoryStorage.d.ts.map +1 -0
  29. package/dist/esm/InMemoryStorage.js +45 -0
  30. package/dist/esm/InMemoryStorage.js.map +1 -0
  31. package/dist/esm/IndexedDBStorage.d.ts +10 -3
  32. package/dist/esm/IndexedDBStorage.d.ts.map +1 -1
  33. package/dist/esm/IndexedDBStorage.js +254 -8
  34. package/dist/esm/IndexedDBStorage.js.map +1 -1
  35. package/dist/esm/Reactor.d.ts +7 -16
  36. package/dist/esm/Reactor.d.ts.map +1 -1
  37. package/dist/esm/Reactor.js +199 -149
  38. package/dist/esm/Reactor.js.map +1 -1
  39. package/dist/esm/SyncTable.d.ts +3 -1
  40. package/dist/esm/SyncTable.d.ts.map +1 -1
  41. package/dist/esm/SyncTable.js +194 -168
  42. package/dist/esm/SyncTable.js.map +1 -1
  43. package/dist/esm/index.d.ts +2 -1
  44. package/dist/esm/index.d.ts.map +1 -1
  45. package/dist/esm/index.js +4 -1
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/utils/PersistedObject.d.ts +73 -29
  48. package/dist/esm/utils/PersistedObject.d.ts.map +1 -1
  49. package/dist/esm/utils/PersistedObject.js +364 -76
  50. package/dist/esm/utils/PersistedObject.js.map +1 -1
  51. package/dist/standalone/index.js +1978 -1567
  52. package/dist/standalone/index.umd.cjs +3 -3
  53. package/package.json +2 -2
  54. package/src/InMemoryStorage.ts +34 -0
  55. package/src/IndexedDBStorage.ts +281 -9
  56. package/src/Reactor.js +212 -184
  57. package/src/SyncTable.ts +220 -191
  58. package/src/index.ts +8 -0
  59. package/src/utils/PersistedObject.ts +480 -109
  60. 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 reactor.querySubs.waitForLoaded();
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.querySubs.waitForSync();
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 reactor2.querySubs.waitForLoaded();
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.pendingMutations.set((prev) => {
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.pendingMutations.currentValue,
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 reactor.querySubs.waitForLoaded();
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.waitForSync();
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.waitForSync();
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.pendingMutations.currentValue.keys();
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.waitForSync();
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.pendingMutations.currentValue.keys();
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.querySubs.waitForSync();
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.querySubs.waitForSync();
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.querySubs.waitForSync();
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.querySubs.waitForSync();
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 reactor.pendingMutations.waitForLoaded();
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.pendingMutations.waitForSync();
385
+ await reactor.kv.flush();
376
386
 
377
- const [ev1, ev2] = reactor.pendingMutations.currentValue.keys();
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"]}