@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
package/src/Reactor.js CHANGED
@@ -12,6 +12,7 @@ import * as flags from './utils/flags.ts';
12
12
  import { buildPresenceSlice, hasPresenceResponseChanged } from './presence.ts';
13
13
  import { Deferred } from './utils/Deferred.js';
14
14
  import { PersistedObject } from './utils/PersistedObject.ts';
15
+
15
16
  import { extractTriples } from './model/instaqlResult.js';
16
17
  import {
17
18
  areObjectsDeepEqual,
@@ -106,35 +107,56 @@ const ignoreLogging = {
106
107
  'patch-presence': true,
107
108
  };
108
109
 
109
- function querySubsFromStorage(x, useDateObjects) {
110
- const parsed = typeof x === 'string' ? JSON.parse(x) : x;
111
- for (const key in parsed) {
112
- const v = parsed[key];
113
- if (v?.result?.store) {
114
- const storeJSON = v.result.store;
115
- v.result.store = s.fromJSON({
116
- ...storeJSON,
117
- useDateObjects: useDateObjects,
118
- });
119
- }
110
+ function querySubFromStorage(x, useDateObjects) {
111
+ const v = typeof x === 'string' ? JSON.parse(x) : x;
112
+
113
+ if (v?.result?.store) {
114
+ const storeJSON = v.result.store;
115
+ v.result.store = s.fromJSON({
116
+ ...storeJSON,
117
+ useDateObjects: useDateObjects,
118
+ });
120
119
  }
121
- return parsed;
120
+
121
+ return v;
122
122
  }
123
123
 
124
- function querySubsToStorage(querySubs) {
125
- const jsonSubs = {};
126
- for (const key in querySubs) {
127
- const sub = querySubs[key];
128
- const jsonSub = { ...sub };
129
- if (sub.result?.store) {
130
- jsonSub.result = {
131
- ...sub.result,
132
- store: s.toJSON(sub.result.store),
133
- };
134
- }
135
- jsonSubs[key] = jsonSub;
124
+ function querySubToStorage(_key, sub) {
125
+ const jsonSub = { ...sub };
126
+ if (sub.result?.store) {
127
+ jsonSub.result = {
128
+ ...sub.result,
129
+ store: s.toJSON(sub.result.store),
130
+ };
131
+ }
132
+ return jsonSub;
133
+ }
134
+
135
+ function kvFromStorage(key, x) {
136
+ switch (key) {
137
+ case 'pendingMutations':
138
+ return new Map(typeof x === 'string' ? JSON.parse(x) : x);
139
+ default:
140
+ return x;
141
+ }
142
+ }
143
+
144
+ function kvToStorage(key, x) {
145
+ switch (key) {
146
+ case 'pendingMutations':
147
+ return [...x.entries()];
148
+ default:
149
+ return x;
150
+ }
151
+ }
152
+
153
+ function onMergeQuerySub(_k, storageSub, inMemorySub) {
154
+ const storageResult = storageSub?.result;
155
+ const memoryResult = inMemorySub?.result;
156
+ if (storageResult && !memoryResult && inMemorySub) {
157
+ inMemorySub.result = storageResult;
136
158
  }
137
- return jsonSubs;
159
+ return inMemorySub || storageSub;
138
160
  }
139
161
 
140
162
  function sortedMutationEntries(entries) {
@@ -161,8 +183,9 @@ export default class Reactor {
161
183
 
162
184
  /** @type {PersistedObject} */
163
185
  querySubs;
186
+
164
187
  /** @type {PersistedObject} */
165
- pendingMutations;
188
+ kv;
166
189
 
167
190
  /** @type {Record<string, Array<{ q: any, cb: (data: any) => any }>>} */
168
191
  queryCbs = {};
@@ -173,7 +196,6 @@ export default class Reactor {
173
196
  mutationErrorCbs = [];
174
197
  connectionStatusCbs = [];
175
198
  config;
176
- _persister;
177
199
  mutationDeferredStore = new Map();
178
200
  _reconnectTimeoutId = null;
179
201
  _reconnectTimeoutMs = 0;
@@ -275,7 +297,7 @@ export default class Reactor {
275
297
 
276
298
  this._syncTable = new SyncTable(
277
299
  this._trySendAuthed.bind(this),
278
- this._persister,
300
+ new Storage(this.config.appId, 'syncSubs'),
279
301
  {
280
302
  useDateObjects: this.config.useDateObjects,
281
303
  },
@@ -339,37 +361,57 @@ export default class Reactor {
339
361
  _reactorStats() {
340
362
  return {
341
363
  inFlightMutationCount: this._inFlightMutationEventIds.size,
342
- pendingMutationCount: this.pendingMutations.currentValue.size,
364
+ pendingMutationCount: this._pendingMutations().size,
343
365
  transportType: this._transportType,
344
366
  };
345
367
  }
346
368
 
369
+ _onQuerySubLoaded(hash) {
370
+ this.kv
371
+ .waitForKeyToLoad('pendingMutations')
372
+ .then(() => this.notifyOne(hash));
373
+ }
374
+
347
375
  _initStorage(Storage) {
348
- this._persister = new Storage(`instant_${this.config.appId}_5`);
349
- this.querySubs = new PersistedObject(
350
- this._persister,
351
- 'querySubs',
352
- {},
353
- this._onMergeQuerySubs,
354
- querySubsToStorage,
355
- (x) => querySubsFromStorage(x, this.config.useDateObjects),
356
- );
357
- this.pendingMutations = new PersistedObject(
358
- this._persister,
359
- 'pendingMutations',
360
- new Map(),
361
- this._onMergePendingMutations,
362
- (x) => {
363
- return [...x.entries()];
364
- },
365
- (x) => {
366
- return new Map(typeof x === 'string' ? JSON.parse(x) : x);
376
+ this.querySubs = new PersistedObject({
377
+ persister: new Storage(this.config.appId, 'querySubs'),
378
+ merge: onMergeQuerySub,
379
+ serialize: querySubToStorage,
380
+ parse: (_key, x) => querySubFromStorage(x, this.config.useDateObjects),
381
+ // objectSize
382
+ objectSize: (x) => x.result?.store?.triples?.length ?? 0,
383
+ logger: this._log,
384
+ preloadEntryCount: 10,
385
+ gc: {
386
+ maxAgeMs: 1000 * 60 * 60 * 24 * 7 * 52, // 1 year
387
+ maxEntries: 1000,
388
+ // Size of each query is the number of triples
389
+ maxSize: 1_000_000, // 1 million triples
367
390
  },
368
- 100, // throttle
369
- 100, // idlecallback timeout, flush more often for mutations
370
- );
391
+ });
392
+ this.querySubs.onKeyLoaded = (k) => this._onQuerySubLoaded(k);
393
+ this.kv = new PersistedObject({
394
+ persister: new Storage(this.config.appId, 'kv'),
395
+ merge: this._onMergeKv,
396
+ serialize: kvToStorage,
397
+ parse: kvFromStorage,
398
+ objectSize: () => 0,
399
+ logger: this._log,
400
+ saveThrottleMs: 100,
401
+ idleCallbackMaxWaitMs: 100,
402
+ // Don't GC the kv store
403
+ gc: null,
404
+ });
405
+ this.kv.onKeyLoaded = (k) => {
406
+ if (k === 'pendingMutations') {
407
+ this.notifyAll();
408
+ }
409
+ };
410
+ // Trigger immediate load for pendingMutations and currentUser
411
+ this.kv.waitForKeyToLoad('pendingMutations');
412
+ this.kv.waitForKeyToLoad(currentUserKey);
371
413
  this._beforeUnloadCbs.push(() => {
372
- this.pendingMutations.flush();
414
+ this.kv.flush();
373
415
  this.querySubs.flush();
374
416
  });
375
417
  }
@@ -423,72 +465,25 @@ export default class Reactor {
423
465
  this.notifyConnectionStatusSubs(status);
424
466
  }
425
467
 
426
- /**
427
- * merge querySubs from storage and in memory. Has the following side
428
- * effects:
429
- * - We notify all queryCbs because results may been added during merge
430
- */
431
- _onMergeQuerySubs = (_storageSubs, inMemorySubs) => {
432
- const storageSubs = _storageSubs || {};
433
- const ret = { ...inMemorySubs };
434
-
435
- // Consider an inMemorySub with no result;
436
- // If we have a result from storageSubs, let's add it
437
- Object.entries(inMemorySubs).forEach(([hash, querySub]) => {
438
- const storageResult = storageSubs?.[hash]?.result;
439
- const memoryResult = querySub.result;
440
- if (storageResult && !memoryResult) {
441
- ret[hash].result = storageResult;
442
- }
443
- });
444
-
445
- // Consider a storageSub with no corresponding inMemorySub
446
- // This means that at least at this point,
447
- // the user has not asked to subscribe to the query.
448
- // We may _still_ want to add it, because in just a
449
- // few milliseconds, the user will ask to subscribe to the
450
- // query.
451
- // For now, we can't really tell if the user will ask to subscribe
452
- // or not. So for now let's just add the first 10 queries from storage.
453
- // Eventually, we could be smarter about this. For example,
454
- // we can keep usage information about which queries are popular.
455
- const storageKsToAdd = Object.keys(storageSubs)
456
- .filter((k) => !inMemorySubs[k])
457
- .sort((a, b) => {
458
- // Sort by lastAccessed, newest first
459
- const aTime = storageSubs[a]?.lastAccessed || 0;
460
- const bTime = storageSubs[b]?.lastAccessed || 0;
461
- return bTime - aTime;
462
- })
463
- .slice(0, this.queryCacheLimit);
464
-
465
- storageKsToAdd.forEach((k) => {
466
- ret[k] = storageSubs[k];
467
- });
468
-
469
- // Okay, now we have merged our querySubs
470
- this.querySubs.set((_) => ret);
471
-
472
- this.loadedNotifyAll();
473
- };
474
-
475
- /**
476
- * merge pendingMutations from storage and in memory. Has a side effect of
477
- * sending mutations that were stored but not acked
478
- */
479
- _onMergePendingMutations = (storageMuts, inMemoryMuts) => {
480
- const ret = new Map([...storageMuts.entries(), ...inMemoryMuts.entries()]);
481
- this.pendingMutations.set((_) => ret);
482
- this.loadedNotifyAll();
483
- const rewrittenStorageMuts = this._rewriteMutationsSorted(
484
- this.attrs,
485
- storageMuts,
486
- );
487
- rewrittenStorageMuts.forEach(([k, mut]) => {
488
- if (!inMemoryMuts.has(k) && !mut['tx-id']) {
489
- this._sendMutation(k, mut);
468
+ _onMergeKv = (key, storageV, inMemoryV) => {
469
+ switch (key) {
470
+ case 'pendingMutations': {
471
+ const storageEntries = storageV?.entries() ?? [];
472
+ const inMemoryEntries = inMemoryV?.entries() ?? [];
473
+ const muts = new Map([...storageEntries, ...inMemoryEntries]);
474
+ const rewrittenStorageMuts = storageV
475
+ ? this._rewriteMutationsSorted(this.attrs, storageV)
476
+ : [];
477
+ rewrittenStorageMuts.forEach(([k, mut]) => {
478
+ if (!inMemoryV?.pendingMutations?.has(k) && !mut['tx-id']) {
479
+ this._sendMutation(k, mut);
480
+ }
481
+ });
482
+ return muts;
490
483
  }
491
- });
484
+ default:
485
+ return inMemoryV || storageV;
486
+ }
492
487
  };
493
488
 
494
489
  _flushEnqueuedRoomData(roomId) {
@@ -542,6 +537,9 @@ export default class Reactor {
542
537
  case 'add-query-ok': {
543
538
  const { q, result } = msg;
544
539
  const hash = weakHash(q);
540
+ if (!this._hasQueryListeners() && !this.querySubs.currentValue[hash]) {
541
+ break;
542
+ }
545
543
  const pageInfo = result?.[0]?.data?.['page-info'];
546
544
  const aggregate = result?.[0]?.data?.['aggregate'];
547
545
  const triples = extractTriples(result);
@@ -552,14 +550,17 @@ export default class Reactor {
552
550
  this._linkIndex,
553
551
  this.config.useDateObjects,
554
552
  );
555
- this.querySubs.set((prev) => {
553
+ this.querySubs.updateInPlace((prev) => {
554
+ if (!prev[hash]) {
555
+ this._log.info('Missing value in querySubs', { hash, q });
556
+ return;
557
+ }
556
558
  prev[hash].result = {
557
559
  store,
558
560
  pageInfo,
559
561
  aggregate,
560
562
  processedTxId: msg['processed-tx-id'],
561
563
  };
562
- return prev;
563
564
  });
564
565
  this._cleanupPendingMutationsQueries();
565
566
  this.notifyOne(hash);
@@ -594,15 +595,17 @@ export default class Reactor {
594
595
 
595
596
  const rewrittenMutations = this._rewriteMutations(
596
597
  this.attrs,
597
- this.pendingMutations.currentValue,
598
+ this._pendingMutations(),
598
599
  processedTxId,
599
600
  );
600
601
 
601
- if (rewrittenMutations !== this.pendingMutations.currentValue) {
602
+ if (rewrittenMutations !== this._pendingMutations()) {
602
603
  // We know we've changed the mutations to fix the attr ids and removed
603
604
  // processed attrs, so we'll persist those changes to prevent optimisticAttrs
604
605
  // from using old attr definitions
605
- this.pendingMutations.set(() => rewrittenMutations);
606
+ this.kv.updateInPlace((prev) => {
607
+ prev.pendingMutations = rewrittenMutations;
608
+ });
606
609
  }
607
610
 
608
611
  const mutations = sortedMutationEntries(rewrittenMutations.entries());
@@ -626,13 +629,16 @@ export default class Reactor {
626
629
  );
627
630
  const pageInfo = result?.[0]?.data?.['page-info'];
628
631
  const aggregate = result?.[0]?.data?.['aggregate'];
629
- return { hash, store: newStore, pageInfo, aggregate };
632
+ return { q, hash, store: newStore, pageInfo, aggregate };
630
633
  });
631
634
 
632
- updates.forEach(({ hash, store, pageInfo, aggregate }) => {
633
- this.querySubs.set((prev) => {
635
+ updates.forEach(({ hash, q, store, pageInfo, aggregate }) => {
636
+ this.querySubs.updateInPlace((prev) => {
637
+ if (!prev[hash]) {
638
+ this._log.error('Missing value in querySubs', { hash, q });
639
+ return;
640
+ }
634
641
  prev[hash].result = { store, pageInfo, aggregate, processedTxId };
635
- return prev;
636
642
  });
637
643
  });
638
644
 
@@ -650,7 +656,7 @@ export default class Reactor {
650
656
 
651
657
  const muts = this._rewriteMutations(
652
658
  this.attrs,
653
- this.pendingMutations.currentValue,
659
+ this._pendingMutations(),
654
660
  );
655
661
  const prevMutation = muts.get(eventId);
656
662
  if (!prevMutation) {
@@ -658,13 +664,12 @@ export default class Reactor {
658
664
  }
659
665
 
660
666
  // update pendingMutation with server-side tx-id
661
- this.pendingMutations.set((prev) => {
667
+ this._updatePendingMutations((prev) => {
662
668
  prev.set(eventId, {
663
669
  ...prev.get(eventId),
664
670
  'tx-id': txId,
665
671
  confirmed: Date.now(),
666
672
  });
667
- return prev;
668
673
  });
669
674
 
670
675
  const newAttrs = prevMutation['tx-steps']
@@ -740,16 +745,28 @@ export default class Reactor {
740
745
  }
741
746
  }
742
747
 
748
+ _pendingMutations() {
749
+ return this.kv.currentValue.pendingMutations ?? new Map();
750
+ }
751
+
752
+ _updatePendingMutations(f) {
753
+ this.kv.updateInPlace((prev) => {
754
+ const muts = prev.pendingMutations ?? new Map();
755
+ prev.pendingMutations = muts;
756
+ f(muts);
757
+ });
758
+ }
759
+
743
760
  /**
744
761
  * @param {'timeout' | 'error'} status
745
762
  * @param {string} eventId
746
763
  * @param {{message: string, type?: string, status?: number, hint?: unknown}} errorMsg
747
764
  */
748
765
  _handleMutationError(status, eventId, errorMsg) {
749
- const mut = this.pendingMutations.currentValue.get(eventId);
766
+ const mut = this._pendingMutations().get(eventId);
750
767
 
751
768
  if (mut && (status !== 'timeout' || !mut['tx-id'])) {
752
- this.pendingMutations.set((prev) => {
769
+ this._updatePendingMutations((prev) => {
753
770
  prev.delete(eventId);
754
771
  return prev;
755
772
  });
@@ -770,7 +787,7 @@ export default class Reactor {
770
787
  const eventId = msg['client-event-id'];
771
788
  // This might not be a mutation, but it can't hurt to delete it
772
789
  this._inFlightMutationEventIds.delete(eventId);
773
- const prevMutation = this.pendingMutations.currentValue.get(eventId);
790
+ const prevMutation = this._pendingMutations().get(eventId);
774
791
  const errorMessage = {
775
792
  message: msg.message || 'Uh-oh, something went wrong. Ping Joe & Stopa.',
776
793
  };
@@ -862,10 +879,9 @@ export default class Reactor {
862
879
 
863
880
  _startQuerySub(q, hash) {
864
881
  const eventId = uuid();
865
- this.querySubs.set((prev) => {
882
+ this.querySubs.updateInPlace((prev) => {
866
883
  prev[hash] = prev[hash] || { q, result: null, eventId };
867
884
  prev[hash].lastAccessed = Date.now();
868
- return prev;
869
885
  });
870
886
  this._trySendAuthed(eventId, { op: 'add-query', q });
871
887
 
@@ -971,14 +987,17 @@ export default class Reactor {
971
987
  this._cleanupQuery(q, hash);
972
988
  }
973
989
 
974
- _cleanupQuery(q, hash) {
975
- const hasListeners =
976
- this.queryCbs[hash]?.length || this.queryOnceDfds[hash]?.length;
990
+ _hasQueryListeners(hash) {
991
+ return !!(this.queryCbs[hash]?.length || this.queryOnceDfds[hash]?.length);
992
+ }
977
993
 
994
+ _cleanupQuery(q, hash) {
995
+ const hasListeners = this._hasQueryListeners(hash);
978
996
  if (hasListeners) return;
979
-
980
997
  delete this.queryCbs[hash];
981
998
  delete this.queryOnceDfds[hash];
999
+ delete this._dataForQueryCache[hash];
1000
+ this.querySubs.unloadKey(hash);
982
1001
 
983
1002
  this._trySendAuthed(uuid(), { op: 'remove-query', q });
984
1003
  }
@@ -996,6 +1015,7 @@ export default class Reactor {
996
1015
  // server attr-ids.
997
1016
  _rewriteMutations(attrs, muts, processedTxId) {
998
1017
  if (!attrs) return muts;
1018
+ if (!muts) return new Map();
999
1019
  const findExistingAttr = (attr) => {
1000
1020
  const [_, etype, label] = attr['forward-identity'];
1001
1021
  const existing = instaml.getAttrByFwdIdentName(attrs, etype, label);
@@ -1081,9 +1101,7 @@ export default class Reactor {
1081
1101
  // Transact
1082
1102
 
1083
1103
  optimisticAttrs() {
1084
- const pendingMutationSteps = [
1085
- ...this.pendingMutations.currentValue.values(),
1086
- ] // hack due to Map()
1104
+ const pendingMutationSteps = [...this._pendingMutations().values()] // hack due to Map()
1087
1105
  .flatMap((x) => x['tx-steps']);
1088
1106
 
1089
1107
  const deletedAttrIds = new Set(
@@ -1125,11 +1143,11 @@ export default class Reactor {
1125
1143
  return { error: errorMessage };
1126
1144
  }
1127
1145
  if (!this.querySubs) return;
1128
- if (!this.pendingMutations) return;
1146
+ if (!this.kv.currentValue.pendingMutations) return;
1129
1147
  const querySubVersion = this.querySubs.version();
1130
1148
  const querySubs = this.querySubs.currentValue;
1131
- const pendingMutationsVersion = this.pendingMutations.version();
1132
- const pendingMutations = this.pendingMutations.currentValue;
1149
+ const pendingMutationsVersion = this.kv.version();
1150
+ const pendingMutations = this._pendingMutations();
1133
1151
 
1134
1152
  const { q, result } = querySubs[hash] || {};
1135
1153
  if (!result) return;
@@ -1203,13 +1221,18 @@ export default class Reactor {
1203
1221
  /** Re-compute all subscriptions */
1204
1222
  notifyAll() {
1205
1223
  Object.keys(this.queryCbs).forEach((hash) => {
1206
- this.notifyOne(hash);
1224
+ this.querySubs
1225
+ .waitForKeyToLoad(hash)
1226
+ .then(() => this.notifyOne(hash))
1227
+ .catch(() => this.notifyOne(hash));
1207
1228
  });
1208
1229
  }
1209
1230
 
1210
1231
  loadedNotifyAll() {
1211
- if (this.pendingMutations.isLoading() || this.querySubs.isLoading()) return;
1212
- this.notifyAll();
1232
+ this.kv
1233
+ .waitForKeyToLoad('pendingMutations')
1234
+ .then(() => this.notifyAll())
1235
+ .catch(() => this.notifyAll());
1213
1236
  }
1214
1237
 
1215
1238
  /** Applies transactions locally and sends transact message to server */
@@ -1243,7 +1266,7 @@ export default class Reactor {
1243
1266
  */
1244
1267
  pushOps = (txSteps, error) => {
1245
1268
  const eventId = uuid();
1246
- const mutations = [...this.pendingMutations.currentValue.values()];
1269
+ const mutations = [...this._pendingMutations().values()];
1247
1270
  const order = Math.max(0, ...mutations.map((mut) => mut.order || 0)) + 1;
1248
1271
  const mutation = {
1249
1272
  op: 'transact',
@@ -1252,9 +1275,8 @@ export default class Reactor {
1252
1275
  error,
1253
1276
  order,
1254
1277
  };
1255
- this.pendingMutations.set((prev) => {
1278
+ this._updatePendingMutations((prev) => {
1256
1279
  prev.set(eventId, mutation);
1257
- return prev;
1258
1280
  });
1259
1281
 
1260
1282
  const dfd = new Deferred();
@@ -1295,7 +1317,7 @@ export default class Reactor {
1295
1317
  Math.min(
1296
1318
  this._inFlightMutationEventIds.size + 1,
1297
1319
  // Defensive code in case we don't clean up in flight mutation event ids
1298
- this.pendingMutations.currentValue.size + 1,
1320
+ this._pendingMutations().size + 1,
1299
1321
  ) * 6000,
1300
1322
  );
1301
1323
 
@@ -1341,7 +1363,7 @@ export default class Reactor {
1341
1363
 
1342
1364
  const muts = this._rewriteMutationsSorted(
1343
1365
  this.attrs,
1344
- this.pendingMutations.currentValue,
1366
+ this._pendingMutations(),
1345
1367
  );
1346
1368
  muts.forEach(([eventId, mut]) => {
1347
1369
  if (!mut['tx-id']) {
@@ -1363,13 +1385,12 @@ export default class Reactor {
1363
1385
  }
1364
1386
  }
1365
1387
 
1366
- this.pendingMutations.set((prev) => {
1388
+ this._updatePendingMutations((prev) => {
1367
1389
  for (const [eventId, mut] of Array.from(prev.entries())) {
1368
1390
  if (mut['tx-id'] && mut['tx-id'] <= minProcessedTxId) {
1369
1391
  prev.delete(eventId);
1370
1392
  }
1371
1393
  }
1372
- return prev;
1373
1394
  });
1374
1395
  }
1375
1396
 
@@ -1379,16 +1400,13 @@ export default class Reactor {
1379
1400
  * unaffected by this mutation and it’s safe to delete it from local queue
1380
1401
  */
1381
1402
  _cleanupPendingMutationsTimeout() {
1382
- if (
1383
- this.pendingMutations.currentValue.size <
1384
- this._pendingMutationCleanupThreshold
1385
- ) {
1403
+ if (this._pendingMutations().size < this._pendingMutationCleanupThreshold) {
1386
1404
  return;
1387
1405
  }
1388
1406
 
1389
1407
  const now = Date.now();
1390
1408
 
1391
- this.pendingMutations.set((prev) => {
1409
+ this._updatePendingMutations((prev) => {
1392
1410
  for (const [eventId, mut] of Array.from(prev.entries())) {
1393
1411
  if (
1394
1412
  mut.confirmed &&
@@ -1397,8 +1415,6 @@ export default class Reactor {
1397
1415
  prev.delete(eventId);
1398
1416
  }
1399
1417
  }
1400
-
1401
- return prev;
1402
1418
  });
1403
1419
  }
1404
1420
 
@@ -1616,21 +1632,23 @@ export default class Reactor {
1616
1632
  *
1617
1633
  * Note: If the user deletes their local storage, this id will change.
1618
1634
  *
1619
- * We use this._localIdPromises to ensure that we only generate a local
1620
- * id once, even if multiple callers call this function concurrently.
1621
1635
  */
1622
1636
  async getLocalId(name) {
1623
1637
  const k = `localToken_${name}`;
1624
- const id = await this._persister.getItem(k);
1625
- if (id) return id;
1626
- if (this._localIdPromises[k]) {
1627
- return this._localIdPromises[k];
1638
+ if (this.kv.currentValue[k]) {
1639
+ return this.kv.currentValue[k];
1640
+ }
1641
+
1642
+ const current = await this.kv.waitForKeyToLoad(k);
1643
+ if (current) {
1644
+ return current;
1628
1645
  }
1629
1646
  const newId = uuid();
1630
- this._localIdPromises[k] = this._persister
1631
- .setItem(k, newId)
1632
- .then(() => newId);
1633
- return this._localIdPromises[k];
1647
+ this.kv.updateInPlace((prev) => {
1648
+ if (prev[k]) return;
1649
+ prev[k] = newId;
1650
+ });
1651
+ return await this.kv.waitForKeyToLoad(k);
1634
1652
  }
1635
1653
 
1636
1654
  // ----
@@ -1821,7 +1839,10 @@ export default class Reactor {
1821
1839
  }
1822
1840
 
1823
1841
  async setCurrentUser(user) {
1824
- await this._persister.setItem(currentUserKey, user);
1842
+ this.kv.updateInPlace((prev) => {
1843
+ prev[currentUserKey] = user;
1844
+ });
1845
+ await this.kv.waitForKeyToLoad(currentUserKey);
1825
1846
  }
1826
1847
 
1827
1848
  getCurrentUserCached() {
@@ -1829,7 +1850,7 @@ export default class Reactor {
1829
1850
  }
1830
1851
 
1831
1852
  async _getCurrentUser() {
1832
- const user = await this._persister.getItem(currentUserKey);
1853
+ const user = await this.kv.waitForKeyToLoad(currentUserKey);
1833
1854
  return typeof user === 'string' ? JSON.parse(user) : user;
1834
1855
  }
1835
1856
 
@@ -1840,18 +1861,26 @@ export default class Reactor {
1840
1861
  this._currentUserCached = { isLoading: false, ...errorV };
1841
1862
  return errorV;
1842
1863
  }
1843
- const user = await this._getCurrentUser();
1844
- const userV = { user: user, error: undefined };
1845
- this._currentUserCached = {
1846
- isLoading: false,
1847
- ...userV,
1848
- };
1849
- return userV;
1864
+ try {
1865
+ const user = await this._getCurrentUser();
1866
+ const userV = { user: user, error: undefined };
1867
+ this._currentUserCached = {
1868
+ isLoading: false,
1869
+ ...userV,
1870
+ };
1871
+ return userV;
1872
+ } catch (e) {
1873
+ return {
1874
+ user: undefined,
1875
+ isLoading: false,
1876
+ error: { message: e?.message || 'Error loading user' },
1877
+ };
1878
+ }
1850
1879
  }
1851
1880
 
1852
1881
  async _hasCurrentUser() {
1853
- const user = await this._persister.getItem(currentUserKey);
1854
- return JSON.parse(user) != null;
1882
+ const user = await this.kv.waitForKeyToLoad(currentUserKey);
1883
+ return typeof user === 'string' ? JSON.parse(user) != null : user != null;
1855
1884
  }
1856
1885
 
1857
1886
  async changeCurrentUser(newUser) {
@@ -1877,11 +1906,10 @@ export default class Reactor {
1877
1906
  const newV = { error: undefined, user: newUser };
1878
1907
  this._currentUserCached = { isLoading: false, ...newV };
1879
1908
  this._dataForQueryCache = {};
1880
- this.querySubs.set((prev) => {
1909
+ this.querySubs.updateInPlace((prev) => {
1881
1910
  Object.keys(prev).forEach((k) => {
1882
1911
  delete prev[k].result;
1883
1912
  });
1884
- return prev;
1885
1913
  });
1886
1914
  this._reconnectTimeoutMs = 0;
1887
1915
  this._transport.close();