@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
@@ -15,6 +15,10 @@
15
15
 
16
16
  // Uses `requestIdleCallback` if available, otherwise calls the
17
17
  // callback immediately
18
+
19
+ import { create } from 'mutative';
20
+ import type { Logger } from './log.ts';
21
+
18
22
  function safeIdleCallback(cb, timeout: number) {
19
23
  if (typeof requestIdleCallback === 'undefined') {
20
24
  cb();
@@ -23,115 +27,313 @@ function safeIdleCallback(cb, timeout: number) {
23
27
  }
24
28
  }
25
29
 
26
- export class PersistedObject<T> {
27
- _subs = [];
28
- _persister: Storage;
29
- _key: string;
30
- _onMerge: (fromStorage: T, inMemoryValue: T) => T | void;
31
- _loadedCbs: Array<() => void>;
32
- _isLoading: boolean;
33
- currentValue: T;
34
- serialize: (input: T) => any;
35
- parse: (input: any) => T;
36
- _saveThrottleMs: number;
37
- _idleCallbackMaxWaitMs: number;
38
- _pendingSaveCbs: Array<() => void>;
39
- _version: number;
40
- _nextSave: null | NodeJS.Timeout = null;
41
-
42
- constructor(
43
- persister: Storage,
44
- key: string,
45
- defaultValue: T,
46
- /**
47
- * Merges data from storage with in-memory value on load.
48
- * If a value is returned from onMerge, that value that will
49
- * become the current value.
50
- */
51
- onMerge: (fromStorage: T, inMemoryValue: T) => T | void,
52
- serialize = (x: T): any => {
53
- return x;
54
- },
55
- parse = (x: any): T => {
56
- return x;
57
- },
58
- saveThrottleMs = 100,
59
- idleCallbackMaxWaitMs = 1000,
60
- ) {
61
- this._persister = persister;
62
- this._key = key;
63
-
64
- this._onMerge = onMerge;
65
-
66
- this._loadedCbs = [];
67
- this._isLoading = true;
68
- this.currentValue = defaultValue;
69
- this.serialize = serialize;
70
- this.parse = parse;
71
- this._saveThrottleMs = saveThrottleMs;
72
- this._pendingSaveCbs = [];
73
- this._version = 0;
74
- this._idleCallbackMaxWaitMs = idleCallbackMaxWaitMs;
75
-
76
- this._load();
77
- }
78
-
79
- async _getFromStorage() {
30
+ export const META_KEY = '__meta';
31
+
32
+ export type ObjectMeta = { createdAt: number; updatedAt: number; size: number };
33
+
34
+ export type Meta<K extends string> = {
35
+ objects: Record<K, ObjectMeta>;
36
+ };
37
+
38
+ export type StorageInterfaceStoreName = 'kv' | 'querySubs' | 'syncSubs';
39
+
40
+ export abstract class StorageInterface {
41
+ constructor(appId: string, storeName: StorageInterfaceStoreName) {}
42
+ abstract getItem(key: string): Promise<any>;
43
+ abstract removeItem(key: string): Promise<void>;
44
+ abstract multiSet(keyValuePairs: Array<[string, any]>): Promise<void>;
45
+ abstract getAllKeys(): Promise<string[]>;
46
+ }
47
+
48
+ export type GCOpts = {
49
+ maxSize: number;
50
+ maxAgeMs: number;
51
+ maxEntries: number;
52
+ };
53
+
54
+ export type Opts<K, T, SerializedT> = {
55
+ persister: StorageInterface;
56
+ /**
57
+ * Merges data from storage with in-memory value on load.
58
+ * The value returned from merge will become the current value.
59
+ */
60
+ merge: (
61
+ key: K,
62
+ fromStorage: T | null | undefined,
63
+ inMemoryValue: T | null | undefined,
64
+ ) => T;
65
+ serialize: (key: K, input: T) => SerializedT;
66
+ parse: (key: K, value: SerializedT) => T;
67
+ objectSize: (x: SerializedT) => number;
68
+ logger: Logger;
69
+ gc: GCOpts | null | undefined;
70
+ saveThrottleMs?: number | null | undefined;
71
+ idleCallbackMaxWaitMs?: number | null | undefined;
72
+ preloadEntryCount?: number | null | undefined;
73
+ };
74
+
75
+ export class PersistedObject<K extends string, T, SerializedT> {
76
+ currentValue: Record<K, T>;
77
+ private _subs = [];
78
+ private _persister: StorageInterface;
79
+ private _merge: (key: K, fromStorage: T, inMemoryValue: T) => T;
80
+ private serialize: (key: K, input: T) => SerializedT;
81
+ private parse: (key: K, value: SerializedT) => T;
82
+ private _saveThrottleMs: number;
83
+ private _idleCallbackMaxWaitMs: number;
84
+ private _nextSave: null | NodeJS.Timeout = null;
85
+ private _nextGc: null | NodeJS.Timeout = null;
86
+ private _pendingSaveKeys: Set<K> = new Set();
87
+ private _loadedKeys: Set<K> = new Set();
88
+ private _loadingKeys: Record<K, Promise<T>>;
89
+ private _objectSize: (serializedObject: SerializedT) => number;
90
+ private _log: Logger;
91
+
92
+ onKeyLoaded: (key: string) => void | null | undefined;
93
+ private _version = 0;
94
+ private _meta: {
95
+ isLoading: boolean;
96
+ onLoadCbs: Array<() => void>;
97
+ value: null | Meta<K>;
98
+ error: null | Error;
99
+ attempts: number;
100
+ loadingPromise?: Promise<Meta<K>> | null | undefined;
101
+ } = {
102
+ isLoading: true,
103
+ onLoadCbs: [],
104
+ value: null,
105
+ error: null,
106
+ attempts: 0,
107
+ };
108
+ private _gcOpts: GCOpts;
109
+
110
+ constructor(opts: Opts<K, T, SerializedT>) {
111
+ this._persister = opts.persister;
112
+ this._merge = opts.merge;
113
+ this.serialize = opts.serialize;
114
+ this.parse = opts.parse;
115
+ this._objectSize = opts.objectSize;
116
+ this._log = opts.logger;
117
+ this._saveThrottleMs = opts.saveThrottleMs ?? 100;
118
+ this._idleCallbackMaxWaitMs = opts.idleCallbackMaxWaitMs ?? 1000;
119
+ this._gcOpts = opts.gc;
120
+ this.currentValue = {} as Record<K, T>;
121
+ this._loadedKeys = new Set();
122
+ this._loadingKeys = {} as Record<K, Promise<T>>;
123
+ this._initMeta();
124
+ if (opts.preloadEntryCount) {
125
+ this._preloadEntries(opts.preloadEntryCount);
126
+ }
127
+ }
128
+
129
+ private async _initMeta() {
130
+ if (this._meta.loadingPromise) {
131
+ await this._meta.loadingPromise;
132
+ }
80
133
  try {
81
- return this.parse(await this._persister.getItem(this._key));
134
+ const p = this._persister.getItem(META_KEY);
135
+ this._meta.loadingPromise = p;
136
+ const v = await p;
137
+ this._meta.isLoading = false;
138
+ this._meta.error = null;
139
+ this._meta.loadingPromise = null;
140
+ this._meta.attempts = 0;
141
+ const existingObjects = this._meta.value?.objects ?? {};
142
+ const value = v ?? {};
143
+ const objects = value.objects ?? {};
144
+ // Merge the values from storage with in-memory values
145
+ this._meta.value = {
146
+ ...value,
147
+ objects: { ...existingObjects, ...objects },
148
+ } as Meta<K>;
82
149
  } catch (e) {
83
- console.error(`Unable to read from storage for key=${this._key}`, e);
84
- return null;
150
+ this._meta.error = e;
151
+ this._meta.attempts++;
152
+ this._meta.loadingPromise = null;
85
153
  }
86
154
  }
87
155
 
88
- async _load() {
89
- const fromStorage = await this._getFromStorage();
90
- this._isLoading = false;
156
+ private async _getMeta(): Promise<Meta<K>> {
157
+ if (this._meta.value) {
158
+ return this._meta.value;
159
+ }
160
+ if (this._meta.loadingPromise) {
161
+ await this._meta.loadingPromise;
162
+ return this._meta.value;
163
+ }
164
+ this._initMeta();
165
+ await this._meta.loadingPromise;
166
+ return this._meta.value;
167
+ }
91
168
 
92
- const mergeResult = this._onMerge(fromStorage, this.currentValue);
93
- if (mergeResult) {
94
- this.currentValue = mergeResult;
169
+ private async _refreshMeta(): Promise<Meta<K>> {
170
+ await this._initMeta();
171
+ return this._meta.value;
172
+ }
173
+
174
+ private async _preloadEntries(n: number) {
175
+ const meta = await this.waitForMetaToLoad();
176
+ const entries = Object.entries(meta.objects) as Array<[K, ObjectMeta]>;
177
+ entries.sort(([_k_a, a_meta], [_k_b, b_meta]) => {
178
+ return b_meta.updatedAt - a_meta.updatedAt;
179
+ });
180
+ for (const [k] of entries.slice(0, n)) {
181
+ this._loadKey(k);
95
182
  }
96
- for (const cb of this._loadedCbs) {
97
- cb();
183
+ }
184
+
185
+ private async _getFromStorage(key: K) {
186
+ try {
187
+ const data = await this._persister.getItem(key);
188
+ if (!data) {
189
+ return data;
190
+ }
191
+ const parsed = this.parse(key, data as SerializedT);
192
+ return parsed;
193
+ } catch (e) {
194
+ console.error(`Unable to read from storage for key=${key}`, e);
195
+ return null;
98
196
  }
99
197
  }
100
198
 
101
- async waitForLoaded(): Promise<void> {
102
- if (!this._isLoading) {
103
- return;
199
+ public async waitForKeyToLoad(k: K) {
200
+ if (this._loadedKeys.has(k)) {
201
+ return this.currentValue[k];
104
202
  }
105
- const loadedPromise = new Promise<void>((resolve) => {
106
- this._loadedCbs.push(resolve);
107
- });
108
- await loadedPromise;
203
+ await (this._loadingKeys[k] || this._loadKey(k));
204
+ return this.currentValue[k];
109
205
  }
110
206
 
111
- isLoading() {
112
- return this._isLoading;
207
+ // Used for tests
208
+ public async waitForMetaToLoad() {
209
+ return this._getMeta();
113
210
  }
114
211
 
115
- version() {
116
- return this._version;
212
+ // Unloads the key so that it can be garbage collected, but does not
213
+ // delete it. Removes the key from currentValue.
214
+ public unloadKey(k: K) {
215
+ this._loadedKeys.delete(k);
216
+ delete this._loadingKeys[k];
217
+ delete this.currentValue[k];
117
218
  }
118
219
 
119
- async waitForSync() {
120
- if (!this._nextSave) {
121
- return;
220
+ private async _loadKey(k: K) {
221
+ if (this._loadedKeys.has(k) || k in this._loadingKeys) return;
222
+ const p = this._getFromStorage(k);
223
+ this._loadingKeys[k] = p;
224
+ const value = await p;
225
+ delete this._loadingKeys[k];
226
+ this._loadedKeys.add(k);
227
+
228
+ if (value) {
229
+ const merged = this._merge(k, value, this.currentValue[k]);
230
+ if (merged) {
231
+ this.currentValue[k] = merged;
232
+ }
122
233
  }
123
- const syncedPromise = new Promise<void>((resolve) => {
124
- this._pendingSaveCbs.push(resolve);
125
- });
126
- await syncedPromise;
234
+ this.onKeyLoaded && this.onKeyLoaded(k);
127
235
  }
128
236
 
129
- _writeToStorage() {
130
- this._persister.setItem(this._key, this.serialize(this.currentValue));
131
- for (const cb of this._pendingSaveCbs) {
132
- cb();
237
+ // Returns a promise with a number so that we can wait for flush
238
+ // to finish in the tests. The number is the number of operations
239
+ // it performed, but it's mostly there so that typescript will warn
240
+ // us if we forget to retun the promise from the function.
241
+ private _writeToStorage(opts?: {
242
+ skipGc?: boolean | null | undefined;
243
+ attempts?: number | null | undefined;
244
+ }): Promise<number> {
245
+ const promises: Promise<number>[] = [];
246
+ const skipGc = opts?.skipGc;
247
+ if (this._meta.isLoading) {
248
+ // Wait for meta to load and try again, give it a delay so that
249
+ // we don't spend too much time retrying
250
+ const p: Promise<number> = new Promise((resolve, reject) => {
251
+ setTimeout(
252
+ () =>
253
+ this._enqueuePersist(
254
+ opts
255
+ ? { ...opts, attempts: (opts.attempts || 0) + 1 }
256
+ : { attempts: 1 },
257
+ )
258
+ .then(resolve)
259
+ .catch(reject),
260
+ 10 + (opts?.attempts ?? 0) * 1000,
261
+ );
262
+ });
263
+ promises.push(p);
264
+ return Promise.all(promises).then((vs) =>
265
+ vs.reduce((acc, x) => acc + x, 0),
266
+ );
267
+ }
268
+ const metaValue = this._meta.value;
269
+ if (!metaValue) {
270
+ // If it's not loading and we don't have the data, then there
271
+ // must be an error and we're not going to be able to save until
272
+ // the error is resolved elsewhere.
273
+ return Promise.resolve(0);
274
+ }
275
+ const keysToDelete = [];
276
+ const keysToUpdate = [];
277
+ for (const k of this._pendingSaveKeys) {
278
+ if (!(k in this.currentValue)) {
279
+ keysToDelete.push(k);
280
+ delete metaValue.objects[k];
281
+ } else {
282
+ keysToUpdate.push(k);
283
+ }
284
+ }
285
+
286
+ for (const k of keysToDelete) {
287
+ const p = this._persister.removeItem(k);
288
+ promises.push(p.then(() => 1));
289
+ this._loadedKeys.delete(k);
290
+ this._pendingSaveKeys.delete(k);
291
+ }
292
+
293
+ const keysToLoad = [];
294
+
295
+ const kvPairs: Array<[string, any]> = [[META_KEY, metaValue]];
296
+ const metaObjects: Meta<K>['objects'] =
297
+ metaValue.objects ?? ({} as Meta<K>['objects']);
298
+ metaValue.objects = metaObjects;
299
+ for (const k of keysToUpdate) {
300
+ if (this._loadedKeys.has(k)) {
301
+ const serializedV = this.serialize(k, this.currentValue[k]);
302
+ kvPairs.push([k, serializedV]);
303
+ const size = this._objectSize(serializedV);
304
+ const m = metaObjects[k] ?? {
305
+ createdAt: Date.now(),
306
+ updatedAt: Date.now(),
307
+ size,
308
+ };
309
+ m.updatedAt = Date.now();
310
+ m.size = size;
311
+ metaObjects[k] = m;
312
+ this._pendingSaveKeys.delete(k);
313
+ } else {
314
+ keysToLoad.push(k);
315
+ }
316
+ }
317
+
318
+ const p = this._persister.multiSet(kvPairs);
319
+ promises.push(p.then(() => 1));
320
+
321
+ // For the keys that haven't loaded, load the key then try
322
+ // persisting again. We don't want to do any async work here
323
+ // or else we might end up saving older copies of the data to
324
+ // the store.
325
+ for (const k of keysToLoad) {
326
+ const p = this._loadKey(k).then(() => this._enqueuePersist(opts));
327
+ promises.push(p);
328
+ }
329
+
330
+ if (!skipGc) {
331
+ this.gc();
133
332
  }
134
- this._pendingSaveCbs.length = 0;
333
+
334
+ return Promise.all(promises).then((vs) => {
335
+ return vs.reduce((acc, x) => acc + x, 0);
336
+ });
135
337
  }
136
338
 
137
339
  async flush() {
@@ -139,39 +341,208 @@ export class PersistedObject<T> {
139
341
  return;
140
342
  }
141
343
  clearTimeout(this._nextSave);
142
- this._writeToStorage();
344
+ this._nextSave = null;
345
+ const p = this._writeToStorage();
346
+ return p;
143
347
  }
144
348
 
145
- _enqueuePersist(cb) {
146
- if (this._nextSave) {
147
- if (cb) {
148
- this._pendingSaveCbs.push(cb);
349
+ private async _gc() {
350
+ if (!this._gcOpts) {
351
+ return;
352
+ }
353
+ const keys = new Set(await this._persister.getAllKeys());
354
+ keys.delete(META_KEY);
355
+
356
+ // Keys we can't delete
357
+ const sacredKeys = new Set(Object.keys(this.currentValue));
358
+ for (const k of Object.keys(this._loadingKeys)) {
359
+ sacredKeys.add(k);
360
+ }
361
+ for (const k of this._loadedKeys) {
362
+ sacredKeys.add(k);
363
+ }
364
+
365
+ // Refresh meta from the store so that we're less likely to
366
+ // clobber data from other tabs
367
+ const meta = await this._refreshMeta();
368
+ if (!meta) {
369
+ this._log.info('Could not gc because we were not able to load meta');
370
+ return;
371
+ }
372
+
373
+ const promises = [];
374
+
375
+ const deets = {
376
+ gcOpts: this._gcOpts,
377
+ keys,
378
+ sacredKeys,
379
+ removed: [],
380
+ metaRemoved: [],
381
+ removedMissingCount: 0,
382
+ removedOldCount: 0,
383
+ removedThresholdCount: 0,
384
+ removedSizeCount: 0,
385
+ };
386
+
387
+ // First, remove all keys we don't know about
388
+ for (const key of keys) {
389
+ if (sacredKeys.has(key) || key in meta.objects) {
390
+ continue;
391
+ }
392
+ this._log.info('Lost track of key in meta', key);
393
+ promises.push(this._persister.removeItem(key));
394
+ deets.removed.push(key);
395
+ deets.removedMissingCount++;
396
+ }
397
+
398
+ // Remove anything over the max age
399
+ const now = Date.now();
400
+ for (const [k, m] of Object.entries(meta.objects)) {
401
+ if (
402
+ !sacredKeys.has(k) &&
403
+ (m as ObjectMeta).updatedAt < now - this._gcOpts.maxAgeMs
404
+ ) {
405
+ promises.push(this._persister.removeItem(k));
406
+ delete meta.objects[k];
407
+ deets.removed.push(k);
408
+ deets.removedOldCount++;
409
+ }
410
+ }
411
+
412
+ // Keep queries under max queries
413
+ const maxEntries = Object.entries(meta.objects) as Array<[K, ObjectMeta]>;
414
+ maxEntries.sort(([_k_a, a_meta], [_k_b, b_meta]) => {
415
+ return a_meta.updatedAt - b_meta.updatedAt;
416
+ });
417
+
418
+ const deletableMaxEntries = maxEntries.filter(([x]) => !sacredKeys.has(x));
419
+ if (maxEntries.length > this._gcOpts.maxEntries) {
420
+ for (const [k] of deletableMaxEntries.slice(
421
+ 0,
422
+ maxEntries.length - this._gcOpts.maxEntries,
423
+ )) {
424
+ promises.push(this._persister.removeItem(k));
425
+ delete meta.objects[k];
426
+ deets.removed.push(k);
427
+ deets.removedThresholdCount++;
428
+ }
429
+ }
430
+
431
+ // Remove oldest entries until we are under max size
432
+ const delEntries = Object.entries(meta.objects) as Array<[K, ObjectMeta]>;
433
+ delEntries.sort(([_k_a, a_meta], [_k_b, b_meta]) => {
434
+ return a_meta.updatedAt - b_meta.updatedAt;
435
+ });
436
+ const deletableDelEntries = delEntries.filter(([x]) => !sacredKeys.has(x));
437
+
438
+ let currentSize = delEntries.reduce((acc, [_k, m]) => {
439
+ return acc + m.size;
440
+ }, 0);
441
+
442
+ while (
443
+ currentSize > 0 &&
444
+ currentSize > this._gcOpts.maxSize &&
445
+ deletableDelEntries.length
446
+ ) {
447
+ const [[k, m]] = deletableDelEntries.splice(0, 1);
448
+ currentSize -= m.size;
449
+ promises.push(this._persister.removeItem(k));
450
+ delete meta.objects[k];
451
+ deets.removed.push(k);
452
+ deets.removedSizeCount++;
453
+ }
454
+
455
+ // Update meta to remove keys that are no longer in the store
456
+ for (const k of Object.keys(meta.objects)) {
457
+ if (!keys.has(k) && !sacredKeys.has(k)) {
458
+ delete meta.objects[k];
149
459
  }
460
+ }
461
+
462
+ if (deets.removed.length || deets.metaRemoved.length) {
463
+ // Trigger a flush of the meta
464
+ promises.push(this._enqueuePersist({ skipGc: true }));
465
+ }
466
+
467
+ this._log.info('Completed GC', deets);
468
+
469
+ await Promise.all(promises);
470
+ return deets;
471
+ }
472
+
473
+ // Schedules a GC to run in one minute (unless it is already scheduled)
474
+ gc() {
475
+ if (this._nextGc) {
150
476
  return;
151
477
  }
152
- this._nextSave = setTimeout(() => {
153
- safeIdleCallback(() => {
154
- this._nextSave = null;
155
- this._writeToStorage();
156
- }, this._idleCallbackMaxWaitMs);
157
- }, this._saveThrottleMs);
478
+ this._nextGc = setTimeout(
479
+ () => {
480
+ safeIdleCallback(() => {
481
+ this._nextGc = null;
482
+ this._gc();
483
+ }, 30 * 1000);
484
+ },
485
+ // 1 minute + some jitter to keep multiple tabs from running at same time
486
+ 1000 * 60 + Math.random() * 500,
487
+ );
488
+ }
489
+
490
+ private _enqueuePersist(opts?: {
491
+ skipGc?: boolean | null | undefined;
492
+ attempts?: number | null | undefined;
493
+ }): Promise<number> {
494
+ return new Promise((resolve, reject) => {
495
+ if (this._nextSave) {
496
+ resolve(0);
497
+ return;
498
+ }
499
+
500
+ this._nextSave = setTimeout(() => {
501
+ safeIdleCallback(() => {
502
+ this._nextSave = null;
503
+ this._writeToStorage(opts).then(resolve).catch(reject);
504
+ }, this._idleCallbackMaxWaitMs);
505
+ }, this._saveThrottleMs);
506
+ });
507
+ }
508
+
509
+ version() {
510
+ return this._version;
158
511
  }
159
512
 
160
- set(f: (prev: T) => T, cb?: () => void): T {
513
+ // Takes a function that updates the store in place.
514
+ // Uses `mutative` to get a list of keys that were changed
515
+ // so that we know which entries we need to persist to the store.
516
+ public updateInPlace(f: (prev: Record<string, T>) => void) {
161
517
  this._version++;
162
- this.currentValue = f(this.currentValue);
163
- if (this._isLoading) {
164
- this._loadedCbs.push(() => this._enqueuePersist(cb));
165
- } else {
166
- this._enqueuePersist(cb);
518
+ const [state, patches] = create(this.currentValue, f, {
519
+ enablePatches: true,
520
+ });
521
+ for (const patch of patches) {
522
+ const k = patch.path[0];
523
+ if (k && typeof k === 'string') {
524
+ this._pendingSaveKeys.add(k as K);
525
+ if (!this._loadedKeys.has(k as K)) {
526
+ this._loadKey(k as K);
527
+ }
528
+ }
529
+ }
530
+
531
+ for (const k of Object.keys(state)) {
532
+ if (!state[k]) {
533
+ debugger;
534
+ }
167
535
  }
168
- for (const sub of this._subs) {
169
- sub(this.currentValue);
536
+
537
+ this.currentValue = state;
538
+ this._enqueuePersist();
539
+ for (const cb of this._subs) {
540
+ cb(this.currentValue);
170
541
  }
171
- return this.currentValue;
542
+ return state;
172
543
  }
173
544
 
174
- subscribe(cb) {
545
+ public subscribe(cb: (value: Record<K, T>) => void) {
175
546
  this._subs.push(cb);
176
547
  cb(this.currentValue);
177
548
 
@@ -1,14 +0,0 @@
1
- export default class InMemoryStorage {
2
- constructor(dbName) {
3
- this.dbName = dbName;
4
- this.store = new Map();
5
- }
6
-
7
- async getItem(k) {
8
- return this.store.get(k) ?? null;
9
- }
10
-
11
- async setItem(k, v) {
12
- this.store.set(k, v);
13
- }
14
- }