@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
@@ -101,30 +101,46 @@ const ignoreLogging = {
101
101
  'refresh-presence': true,
102
102
  'patch-presence': true,
103
103
  };
104
- function querySubsFromStorage(x, useDateObjects) {
104
+ function querySubFromStorage(x, useDateObjects) {
105
105
  var _a;
106
- const parsed = typeof x === 'string' ? JSON.parse(x) : x;
107
- for (const key in parsed) {
108
- const v = parsed[key];
109
- if ((_a = v === null || v === void 0 ? void 0 : v.result) === null || _a === void 0 ? void 0 : _a.store) {
110
- const storeJSON = v.result.store;
111
- v.result.store = s.fromJSON(Object.assign(Object.assign({}, storeJSON), { useDateObjects: useDateObjects }));
112
- }
106
+ const v = typeof x === 'string' ? JSON.parse(x) : x;
107
+ if ((_a = v === null || v === void 0 ? void 0 : v.result) === null || _a === void 0 ? void 0 : _a.store) {
108
+ const storeJSON = v.result.store;
109
+ v.result.store = s.fromJSON(Object.assign(Object.assign({}, storeJSON), { useDateObjects: useDateObjects }));
113
110
  }
114
- return parsed;
111
+ return v;
115
112
  }
116
- function querySubsToStorage(querySubs) {
113
+ function querySubToStorage(_key, sub) {
117
114
  var _a;
118
- const jsonSubs = {};
119
- for (const key in querySubs) {
120
- const sub = querySubs[key];
121
- const jsonSub = Object.assign({}, sub);
122
- if ((_a = sub.result) === null || _a === void 0 ? void 0 : _a.store) {
123
- jsonSub.result = Object.assign(Object.assign({}, sub.result), { store: s.toJSON(sub.result.store) });
124
- }
125
- jsonSubs[key] = jsonSub;
115
+ const jsonSub = Object.assign({}, sub);
116
+ if ((_a = sub.result) === null || _a === void 0 ? void 0 : _a.store) {
117
+ jsonSub.result = Object.assign(Object.assign({}, sub.result), { store: s.toJSON(sub.result.store) });
118
+ }
119
+ return jsonSub;
120
+ }
121
+ function kvFromStorage(key, x) {
122
+ switch (key) {
123
+ case 'pendingMutations':
124
+ return new Map(typeof x === 'string' ? JSON.parse(x) : x);
125
+ default:
126
+ return x;
126
127
  }
127
- return jsonSubs;
128
+ }
129
+ function kvToStorage(key, x) {
130
+ switch (key) {
131
+ case 'pendingMutations':
132
+ return [...x.entries()];
133
+ default:
134
+ return x;
135
+ }
136
+ }
137
+ function onMergeQuerySub(_k, storageSub, inMemorySub) {
138
+ const storageResult = storageSub === null || storageSub === void 0 ? void 0 : storageSub.result;
139
+ const memoryResult = inMemorySub === null || inMemorySub === void 0 ? void 0 : inMemorySub.result;
140
+ if (storageResult && !memoryResult && inMemorySub) {
141
+ inMemorySub.result = storageResult;
142
+ }
143
+ return inMemorySub || storageSub;
128
144
  }
129
145
  function sortedMutationEntries(entries) {
130
146
  return [...entries].sort((a, b) => {
@@ -179,65 +195,27 @@ export default class Reactor {
179
195
  this._beforeUnloadCbs = [];
180
196
  this._dataForQueryCache = {};
181
197
  this._inFlightMutationEventIds = new Set();
182
- /**
183
- * merge querySubs from storage and in memory. Has the following side
184
- * effects:
185
- * - We notify all queryCbs because results may been added during merge
186
- */
187
- this._onMergeQuerySubs = (_storageSubs, inMemorySubs) => {
188
- const storageSubs = _storageSubs || {};
189
- const ret = Object.assign({}, inMemorySubs);
190
- // Consider an inMemorySub with no result;
191
- // If we have a result from storageSubs, let's add it
192
- Object.entries(inMemorySubs).forEach(([hash, querySub]) => {
193
- var _a;
194
- const storageResult = (_a = storageSubs === null || storageSubs === void 0 ? void 0 : storageSubs[hash]) === null || _a === void 0 ? void 0 : _a.result;
195
- const memoryResult = querySub.result;
196
- if (storageResult && !memoryResult) {
197
- ret[hash].result = storageResult;
198
- }
199
- });
200
- // Consider a storageSub with no corresponding inMemorySub
201
- // This means that at least at this point,
202
- // the user has not asked to subscribe to the query.
203
- // We may _still_ want to add it, because in just a
204
- // few milliseconds, the user will ask to subscribe to the
205
- // query.
206
- // For now, we can't really tell if the user will ask to subscribe
207
- // or not. So for now let's just add the first 10 queries from storage.
208
- // Eventually, we could be smarter about this. For example,
209
- // we can keep usage information about which queries are popular.
210
- const storageKsToAdd = Object.keys(storageSubs)
211
- .filter((k) => !inMemorySubs[k])
212
- .sort((a, b) => {
213
- var _a, _b;
214
- // Sort by lastAccessed, newest first
215
- const aTime = ((_a = storageSubs[a]) === null || _a === void 0 ? void 0 : _a.lastAccessed) || 0;
216
- const bTime = ((_b = storageSubs[b]) === null || _b === void 0 ? void 0 : _b.lastAccessed) || 0;
217
- return bTime - aTime;
218
- })
219
- .slice(0, this.queryCacheLimit);
220
- storageKsToAdd.forEach((k) => {
221
- ret[k] = storageSubs[k];
222
- });
223
- // Okay, now we have merged our querySubs
224
- this.querySubs.set((_) => ret);
225
- this.loadedNotifyAll();
226
- };
227
- /**
228
- * merge pendingMutations from storage and in memory. Has a side effect of
229
- * sending mutations that were stored but not acked
230
- */
231
- this._onMergePendingMutations = (storageMuts, inMemoryMuts) => {
232
- const ret = new Map([...storageMuts.entries(), ...inMemoryMuts.entries()]);
233
- this.pendingMutations.set((_) => ret);
234
- this.loadedNotifyAll();
235
- const rewrittenStorageMuts = this._rewriteMutationsSorted(this.attrs, storageMuts);
236
- rewrittenStorageMuts.forEach(([k, mut]) => {
237
- if (!inMemoryMuts.has(k) && !mut['tx-id']) {
238
- this._sendMutation(k, mut);
198
+ this._onMergeKv = (key, storageV, inMemoryV) => {
199
+ var _a, _b;
200
+ switch (key) {
201
+ case 'pendingMutations': {
202
+ const storageEntries = (_a = storageV === null || storageV === void 0 ? void 0 : storageV.entries()) !== null && _a !== void 0 ? _a : [];
203
+ const inMemoryEntries = (_b = inMemoryV === null || inMemoryV === void 0 ? void 0 : inMemoryV.entries()) !== null && _b !== void 0 ? _b : [];
204
+ const muts = new Map([...storageEntries, ...inMemoryEntries]);
205
+ const rewrittenStorageMuts = storageV
206
+ ? this._rewriteMutationsSorted(this.attrs, storageV)
207
+ : [];
208
+ rewrittenStorageMuts.forEach(([k, mut]) => {
209
+ var _a;
210
+ if (!((_a = inMemoryV === null || inMemoryV === void 0 ? void 0 : inMemoryV.pendingMutations) === null || _a === void 0 ? void 0 : _a.has(k)) && !mut['tx-id']) {
211
+ this._sendMutation(k, mut);
212
+ }
213
+ });
214
+ return muts;
239
215
  }
240
- });
216
+ default:
217
+ return inMemoryV || storageV;
218
+ }
241
219
  };
242
220
  // ---------------------------
243
221
  // Queries
@@ -296,7 +274,7 @@ export default class Reactor {
296
274
  */
297
275
  this.pushOps = (txSteps, error) => {
298
276
  const eventId = uuid();
299
- const mutations = [...this.pendingMutations.currentValue.values()];
277
+ const mutations = [...this._pendingMutations().values()];
300
278
  const order = Math.max(0, ...mutations.map((mut) => mut.order || 0)) + 1;
301
279
  const mutation = {
302
280
  op: 'transact',
@@ -305,9 +283,8 @@ export default class Reactor {
305
283
  error,
306
284
  order,
307
285
  };
308
- this.pendingMutations.set((prev) => {
286
+ this._updatePendingMutations((prev) => {
309
287
  prev.set(eventId, mutation);
310
- return prev;
311
288
  });
312
289
  const dfd = new Deferred();
313
290
  this.mutationDeferredStore.set(eventId, dfd);
@@ -436,7 +413,7 @@ export default class Reactor {
436
413
  }));
437
414
  }
438
415
  this._initStorage(Storage);
439
- this._syncTable = new SyncTable(this._trySendAuthed.bind(this), this._persister, {
416
+ this._syncTable = new SyncTable(this._trySendAuthed.bind(this), new Storage(this.config.appId, 'syncSubs'), {
440
417
  useDateObjects: this.config.useDateObjects,
441
418
  }, this._log, (triples) => s.createStore(this.attrs, triples, this.config.enableCardinalityInference, this._linkIndex, this.config.useDateObjects));
442
419
  this._oauthCallbackResponse = this._oauthLoginInit();
@@ -475,21 +452,55 @@ export default class Reactor {
475
452
  _reactorStats() {
476
453
  return {
477
454
  inFlightMutationCount: this._inFlightMutationEventIds.size,
478
- pendingMutationCount: this.pendingMutations.currentValue.size,
455
+ pendingMutationCount: this._pendingMutations().size,
479
456
  transportType: this._transportType,
480
457
  };
481
458
  }
459
+ _onQuerySubLoaded(hash) {
460
+ this.kv
461
+ .waitForKeyToLoad('pendingMutations')
462
+ .then(() => this.notifyOne(hash));
463
+ }
482
464
  _initStorage(Storage) {
483
- this._persister = new Storage(`instant_${this.config.appId}_5`);
484
- this.querySubs = new PersistedObject(this._persister, 'querySubs', {}, this._onMergeQuerySubs, querySubsToStorage, (x) => querySubsFromStorage(x, this.config.useDateObjects));
485
- this.pendingMutations = new PersistedObject(this._persister, 'pendingMutations', new Map(), this._onMergePendingMutations, (x) => {
486
- return [...x.entries()];
487
- }, (x) => {
488
- return new Map(typeof x === 'string' ? JSON.parse(x) : x);
489
- }, 100, // throttle
490
- 100);
465
+ this.querySubs = new PersistedObject({
466
+ persister: new Storage(this.config.appId, 'querySubs'),
467
+ merge: onMergeQuerySub,
468
+ serialize: querySubToStorage,
469
+ parse: (_key, x) => querySubFromStorage(x, this.config.useDateObjects),
470
+ // objectSize
471
+ objectSize: (x) => { var _a, _b, _c, _d; return (_d = (_c = (_b = (_a = x.result) === null || _a === void 0 ? void 0 : _a.store) === null || _b === void 0 ? void 0 : _b.triples) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0; },
472
+ logger: this._log,
473
+ preloadEntryCount: 10,
474
+ gc: {
475
+ maxAgeMs: 1000 * 60 * 60 * 24 * 7 * 52, // 1 year
476
+ maxEntries: 1000,
477
+ // Size of each query is the number of triples
478
+ maxSize: 1000000, // 1 million triples
479
+ },
480
+ });
481
+ this.querySubs.onKeyLoaded = (k) => this._onQuerySubLoaded(k);
482
+ this.kv = new PersistedObject({
483
+ persister: new Storage(this.config.appId, 'kv'),
484
+ merge: this._onMergeKv,
485
+ serialize: kvToStorage,
486
+ parse: kvFromStorage,
487
+ objectSize: () => 0,
488
+ logger: this._log,
489
+ saveThrottleMs: 100,
490
+ idleCallbackMaxWaitMs: 100,
491
+ // Don't GC the kv store
492
+ gc: null,
493
+ });
494
+ this.kv.onKeyLoaded = (k) => {
495
+ if (k === 'pendingMutations') {
496
+ this.notifyAll();
497
+ }
498
+ };
499
+ // Trigger immediate load for pendingMutations and currentUser
500
+ this.kv.waitForKeyToLoad('pendingMutations');
501
+ this.kv.waitForKeyToLoad(currentUserKey);
491
502
  this._beforeUnloadCbs.push(() => {
492
- this.pendingMutations.flush();
503
+ this.kv.flush();
493
504
  this.querySubs.flush();
494
505
  });
495
506
  }
@@ -585,18 +596,24 @@ export default class Reactor {
585
596
  case 'add-query-ok': {
586
597
  const { q, result } = msg;
587
598
  const hash = weakHash(q);
599
+ if (!this._hasQueryListeners() && !this.querySubs.currentValue[hash]) {
600
+ break;
601
+ }
588
602
  const pageInfo = (_d = (_c = result === null || result === void 0 ? void 0 : result[0]) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d['page-info'];
589
603
  const aggregate = (_f = (_e = result === null || result === void 0 ? void 0 : result[0]) === null || _e === void 0 ? void 0 : _e.data) === null || _f === void 0 ? void 0 : _f['aggregate'];
590
604
  const triples = extractTriples(result);
591
605
  const store = s.createStore(this.attrs, triples, enableCardinalityInference, this._linkIndex, this.config.useDateObjects);
592
- this.querySubs.set((prev) => {
606
+ this.querySubs.updateInPlace((prev) => {
607
+ if (!prev[hash]) {
608
+ this._log.info('Missing value in querySubs', { hash, q });
609
+ return;
610
+ }
593
611
  prev[hash].result = {
594
612
  store,
595
613
  pageInfo,
596
614
  aggregate,
597
615
  processedTxId: msg['processed-tx-id'],
598
616
  };
599
- return prev;
600
617
  });
601
618
  this._cleanupPendingMutationsQueries();
602
619
  this.notifyOne(hash);
@@ -627,12 +644,14 @@ export default class Reactor {
627
644
  this._setAttrs(attrs);
628
645
  }
629
646
  this._cleanupPendingMutationsTimeout();
630
- const rewrittenMutations = this._rewriteMutations(this.attrs, this.pendingMutations.currentValue, processedTxId);
631
- if (rewrittenMutations !== this.pendingMutations.currentValue) {
647
+ const rewrittenMutations = this._rewriteMutations(this.attrs, this._pendingMutations(), processedTxId);
648
+ if (rewrittenMutations !== this._pendingMutations()) {
632
649
  // We know we've changed the mutations to fix the attr ids and removed
633
650
  // processed attrs, so we'll persist those changes to prevent optimisticAttrs
634
651
  // from using old attr definitions
635
- this.pendingMutations.set(() => rewrittenMutations);
652
+ this.kv.updateInPlace((prev) => {
653
+ prev.pendingMutations = rewrittenMutations;
654
+ });
636
655
  }
637
656
  const mutations = sortedMutationEntries(rewrittenMutations.entries());
638
657
  const updates = computations.map((x) => {
@@ -645,12 +664,15 @@ export default class Reactor {
645
664
  const newStore = this._applyOptimisticUpdates(store, mutations, processedTxId);
646
665
  const pageInfo = (_b = (_a = result === null || result === void 0 ? void 0 : result[0]) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b['page-info'];
647
666
  const aggregate = (_d = (_c = result === null || result === void 0 ? void 0 : result[0]) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d['aggregate'];
648
- return { hash, store: newStore, pageInfo, aggregate };
667
+ return { q, hash, store: newStore, pageInfo, aggregate };
649
668
  });
650
- updates.forEach(({ hash, store, pageInfo, aggregate }) => {
651
- this.querySubs.set((prev) => {
669
+ updates.forEach(({ hash, q, store, pageInfo, aggregate }) => {
670
+ this.querySubs.updateInPlace((prev) => {
671
+ if (!prev[hash]) {
672
+ this._log.error('Missing value in querySubs', { hash, q });
673
+ return;
674
+ }
652
675
  prev[hash].result = { store, pageInfo, aggregate, processedTxId };
653
- return prev;
654
676
  });
655
677
  });
656
678
  this._cleanupPendingMutationsQueries();
@@ -662,15 +684,14 @@ export default class Reactor {
662
684
  case 'transact-ok': {
663
685
  const { 'client-event-id': eventId, 'tx-id': txId } = msg;
664
686
  this._inFlightMutationEventIds.delete(eventId);
665
- const muts = this._rewriteMutations(this.attrs, this.pendingMutations.currentValue);
687
+ const muts = this._rewriteMutations(this.attrs, this._pendingMutations());
666
688
  const prevMutation = muts.get(eventId);
667
689
  if (!prevMutation) {
668
690
  break;
669
691
  }
670
692
  // update pendingMutation with server-side tx-id
671
- this.pendingMutations.set((prev) => {
693
+ this._updatePendingMutations((prev) => {
672
694
  prev.set(eventId, Object.assign(Object.assign({}, prev.get(eventId)), { 'tx-id': txId, confirmed: Date.now() }));
673
- return prev;
674
695
  });
675
696
  const newAttrs = prevMutation['tx-steps']
676
697
  .filter(([action, ..._args]) => action === 'add-attr')
@@ -737,15 +758,27 @@ export default class Reactor {
737
758
  break;
738
759
  }
739
760
  }
761
+ _pendingMutations() {
762
+ var _a;
763
+ return (_a = this.kv.currentValue.pendingMutations) !== null && _a !== void 0 ? _a : new Map();
764
+ }
765
+ _updatePendingMutations(f) {
766
+ this.kv.updateInPlace((prev) => {
767
+ var _a;
768
+ const muts = (_a = prev.pendingMutations) !== null && _a !== void 0 ? _a : new Map();
769
+ prev.pendingMutations = muts;
770
+ f(muts);
771
+ });
772
+ }
740
773
  /**
741
774
  * @param {'timeout' | 'error'} status
742
775
  * @param {string} eventId
743
776
  * @param {{message: string, type?: string, status?: number, hint?: unknown}} errorMsg
744
777
  */
745
778
  _handleMutationError(status, eventId, errorMsg) {
746
- const mut = this.pendingMutations.currentValue.get(eventId);
779
+ const mut = this._pendingMutations().get(eventId);
747
780
  if (mut && (status !== 'timeout' || !mut['tx-id'])) {
748
- this.pendingMutations.set((prev) => {
781
+ this._updatePendingMutations((prev) => {
749
782
  prev.delete(eventId);
750
783
  return prev;
751
784
  });
@@ -766,7 +799,7 @@ export default class Reactor {
766
799
  const eventId = msg['client-event-id'];
767
800
  // This might not be a mutation, but it can't hurt to delete it
768
801
  this._inFlightMutationEventIds.delete(eventId);
769
- const prevMutation = this.pendingMutations.currentValue.get(eventId);
802
+ const prevMutation = this._pendingMutations().get(eventId);
770
803
  const errorMessage = {
771
804
  message: msg.message || 'Uh-oh, something went wrong. Ping Joe & Stopa.',
772
805
  };
@@ -833,10 +866,9 @@ export default class Reactor {
833
866
  }
834
867
  _startQuerySub(q, hash) {
835
868
  const eventId = uuid();
836
- this.querySubs.set((prev) => {
869
+ this.querySubs.updateInPlace((prev) => {
837
870
  prev[hash] = prev[hash] || { q, result: null, eventId };
838
871
  prev[hash].lastAccessed = Date.now();
839
- return prev;
840
872
  });
841
873
  this._trySendAuthed(eventId, { op: 'add-query', q });
842
874
  return eventId;
@@ -910,13 +942,18 @@ export default class Reactor {
910
942
  this.queryCbs[hash] = this.queryCbs[hash].filter((r) => r.cb !== cb);
911
943
  this._cleanupQuery(q, hash);
912
944
  }
913
- _cleanupQuery(q, hash) {
945
+ _hasQueryListeners(hash) {
914
946
  var _a, _b;
915
- const hasListeners = ((_a = this.queryCbs[hash]) === null || _a === void 0 ? void 0 : _a.length) || ((_b = this.queryOnceDfds[hash]) === null || _b === void 0 ? void 0 : _b.length);
947
+ return !!(((_a = this.queryCbs[hash]) === null || _a === void 0 ? void 0 : _a.length) || ((_b = this.queryOnceDfds[hash]) === null || _b === void 0 ? void 0 : _b.length));
948
+ }
949
+ _cleanupQuery(q, hash) {
950
+ const hasListeners = this._hasQueryListeners(hash);
916
951
  if (hasListeners)
917
952
  return;
918
953
  delete this.queryCbs[hash];
919
954
  delete this.queryOnceDfds[hash];
955
+ delete this._dataForQueryCache[hash];
956
+ this.querySubs.unloadKey(hash);
920
957
  this._trySendAuthed(uuid(), { op: 'remove-query', q });
921
958
  }
922
959
  // When we `pushTx`, it's possible that we don't yet have `this.attrs`
@@ -933,6 +970,8 @@ export default class Reactor {
933
970
  _rewriteMutations(attrs, muts, processedTxId) {
934
971
  if (!attrs)
935
972
  return muts;
973
+ if (!muts)
974
+ return new Map();
936
975
  const findExistingAttr = (attr) => {
937
976
  const [_, etype, label] = attr['forward-identity'];
938
977
  const existing = instaml.getAttrByFwdIdentName(attrs, etype, label);
@@ -1005,9 +1044,7 @@ export default class Reactor {
1005
1044
  // Transact
1006
1045
  optimisticAttrs() {
1007
1046
  var _a;
1008
- const pendingMutationSteps = [
1009
- ...this.pendingMutations.currentValue.values(),
1010
- ] // hack due to Map()
1047
+ const pendingMutationSteps = [...this._pendingMutations().values()] // hack due to Map()
1011
1048
  .flatMap((x) => x['tx-steps']);
1012
1049
  const deletedAttrIds = new Set(pendingMutationSteps
1013
1050
  .filter(([action, _attr]) => action === 'delete-attr')
@@ -1039,12 +1076,12 @@ export default class Reactor {
1039
1076
  }
1040
1077
  if (!this.querySubs)
1041
1078
  return;
1042
- if (!this.pendingMutations)
1079
+ if (!this.kv.currentValue.pendingMutations)
1043
1080
  return;
1044
1081
  const querySubVersion = this.querySubs.version();
1045
1082
  const querySubs = this.querySubs.currentValue;
1046
- const pendingMutationsVersion = this.pendingMutations.version();
1047
- const pendingMutations = this.pendingMutations.currentValue;
1083
+ const pendingMutationsVersion = this.kv.version();
1084
+ const pendingMutations = this._pendingMutations();
1048
1085
  const { q, result } = querySubs[hash] || {};
1049
1086
  if (!result)
1050
1087
  return;
@@ -1076,13 +1113,17 @@ export default class Reactor {
1076
1113
  /** Re-compute all subscriptions */
1077
1114
  notifyAll() {
1078
1115
  Object.keys(this.queryCbs).forEach((hash) => {
1079
- this.notifyOne(hash);
1116
+ this.querySubs
1117
+ .waitForKeyToLoad(hash)
1118
+ .then(() => this.notifyOne(hash))
1119
+ .catch(() => this.notifyOne(hash));
1080
1120
  });
1081
1121
  }
1082
1122
  loadedNotifyAll() {
1083
- if (this.pendingMutations.isLoading() || this.querySubs.isLoading())
1084
- return;
1085
- this.notifyAll();
1123
+ this.kv
1124
+ .waitForKeyToLoad('pendingMutations')
1125
+ .then(() => this.notifyAll())
1126
+ .catch(() => this.notifyAll());
1086
1127
  }
1087
1128
  shutdown() {
1088
1129
  var _a;
@@ -1110,7 +1151,7 @@ export default class Reactor {
1110
1151
  }
1111
1152
  const timeoutMs = Math.max(6000, Math.min(this._inFlightMutationEventIds.size + 1,
1112
1153
  // Defensive code in case we don't clean up in flight mutation event ids
1113
- this.pendingMutations.currentValue.size + 1) * 6000);
1154
+ this._pendingMutations().size + 1) * 6000);
1114
1155
  if (!this._isOnline) {
1115
1156
  this._finishTransaction('enqueued', eventId);
1116
1157
  }
@@ -1147,7 +1188,7 @@ export default class Reactor {
1147
1188
  .forEach(({ eventId, q }) => {
1148
1189
  this._trySendAuthed(eventId, { op: 'add-query', q });
1149
1190
  });
1150
- const muts = this._rewriteMutationsSorted(this.attrs, this.pendingMutations.currentValue);
1191
+ const muts = this._rewriteMutationsSorted(this.attrs, this._pendingMutations());
1151
1192
  muts.forEach(([eventId, mut]) => {
1152
1193
  if (!mut['tx-id']) {
1153
1194
  this._sendMutation(eventId, mut);
@@ -1165,13 +1206,12 @@ export default class Reactor {
1165
1206
  minProcessedTxId = Math.min(minProcessedTxId, result === null || result === void 0 ? void 0 : result.processedTxId);
1166
1207
  }
1167
1208
  }
1168
- this.pendingMutations.set((prev) => {
1209
+ this._updatePendingMutations((prev) => {
1169
1210
  for (const [eventId, mut] of Array.from(prev.entries())) {
1170
1211
  if (mut['tx-id'] && mut['tx-id'] <= minProcessedTxId) {
1171
1212
  prev.delete(eventId);
1172
1213
  }
1173
1214
  }
1174
- return prev;
1175
1215
  });
1176
1216
  }
1177
1217
  /**
@@ -1180,19 +1220,17 @@ export default class Reactor {
1180
1220
  * unaffected by this mutation and it’s safe to delete it from local queue
1181
1221
  */
1182
1222
  _cleanupPendingMutationsTimeout() {
1183
- if (this.pendingMutations.currentValue.size <
1184
- this._pendingMutationCleanupThreshold) {
1223
+ if (this._pendingMutations().size < this._pendingMutationCleanupThreshold) {
1185
1224
  return;
1186
1225
  }
1187
1226
  const now = Date.now();
1188
- this.pendingMutations.set((prev) => {
1227
+ this._updatePendingMutations((prev) => {
1189
1228
  for (const [eventId, mut] of Array.from(prev.entries())) {
1190
1229
  if (mut.confirmed &&
1191
1230
  mut.confirmed + this._pendingTxCleanupTimeout < now) {
1192
1231
  prev.delete(eventId);
1193
1232
  }
1194
1233
  }
1195
- return prev;
1196
1234
  });
1197
1235
  }
1198
1236
  _trySendAuthed(...args) {
@@ -1268,23 +1306,24 @@ export default class Reactor {
1268
1306
  *
1269
1307
  * Note: If the user deletes their local storage, this id will change.
1270
1308
  *
1271
- * We use this._localIdPromises to ensure that we only generate a local
1272
- * id once, even if multiple callers call this function concurrently.
1273
1309
  */
1274
1310
  getLocalId(name) {
1275
1311
  return __awaiter(this, void 0, void 0, function* () {
1276
1312
  const k = `localToken_${name}`;
1277
- const id = yield this._persister.getItem(k);
1278
- if (id)
1279
- return id;
1280
- if (this._localIdPromises[k]) {
1281
- return this._localIdPromises[k];
1313
+ if (this.kv.currentValue[k]) {
1314
+ return this.kv.currentValue[k];
1315
+ }
1316
+ const current = yield this.kv.waitForKeyToLoad(k);
1317
+ if (current) {
1318
+ return current;
1282
1319
  }
1283
1320
  const newId = uuid();
1284
- this._localIdPromises[k] = this._persister
1285
- .setItem(k, newId)
1286
- .then(() => newId);
1287
- return this._localIdPromises[k];
1321
+ this.kv.updateInPlace((prev) => {
1322
+ if (prev[k])
1323
+ return;
1324
+ prev[k] = newId;
1325
+ });
1326
+ return yield this.kv.waitForKeyToLoad(k);
1288
1327
  });
1289
1328
  }
1290
1329
  // ----
@@ -1459,7 +1498,10 @@ export default class Reactor {
1459
1498
  }
1460
1499
  setCurrentUser(user) {
1461
1500
  return __awaiter(this, void 0, void 0, function* () {
1462
- yield this._persister.setItem(currentUserKey, user);
1501
+ this.kv.updateInPlace((prev) => {
1502
+ prev[currentUserKey] = user;
1503
+ });
1504
+ yield this.kv.waitForKeyToLoad(currentUserKey);
1463
1505
  });
1464
1506
  }
1465
1507
  getCurrentUserCached() {
@@ -1467,7 +1509,7 @@ export default class Reactor {
1467
1509
  }
1468
1510
  _getCurrentUser() {
1469
1511
  return __awaiter(this, void 0, void 0, function* () {
1470
- const user = yield this._persister.getItem(currentUserKey);
1512
+ const user = yield this.kv.waitForKeyToLoad(currentUserKey);
1471
1513
  return typeof user === 'string' ? JSON.parse(user) : user;
1472
1514
  });
1473
1515
  }
@@ -1479,16 +1521,25 @@ export default class Reactor {
1479
1521
  this._currentUserCached = Object.assign({ isLoading: false }, errorV);
1480
1522
  return errorV;
1481
1523
  }
1482
- const user = yield this._getCurrentUser();
1483
- const userV = { user: user, error: undefined };
1484
- this._currentUserCached = Object.assign({ isLoading: false }, userV);
1485
- return userV;
1524
+ try {
1525
+ const user = yield this._getCurrentUser();
1526
+ const userV = { user: user, error: undefined };
1527
+ this._currentUserCached = Object.assign({ isLoading: false }, userV);
1528
+ return userV;
1529
+ }
1530
+ catch (e) {
1531
+ return {
1532
+ user: undefined,
1533
+ isLoading: false,
1534
+ error: { message: (e === null || e === void 0 ? void 0 : e.message) || 'Error loading user' },
1535
+ };
1536
+ }
1486
1537
  });
1487
1538
  }
1488
1539
  _hasCurrentUser() {
1489
1540
  return __awaiter(this, void 0, void 0, function* () {
1490
- const user = yield this._persister.getItem(currentUserKey);
1491
- return JSON.parse(user) != null;
1541
+ const user = yield this.kv.waitForKeyToLoad(currentUserKey);
1542
+ return typeof user === 'string' ? JSON.parse(user) != null : user != null;
1492
1543
  });
1493
1544
  }
1494
1545
  changeCurrentUser(newUser) {
@@ -1516,11 +1567,10 @@ export default class Reactor {
1516
1567
  const newV = { error: undefined, user: newUser };
1517
1568
  this._currentUserCached = Object.assign({ isLoading: false }, newV);
1518
1569
  this._dataForQueryCache = {};
1519
- this.querySubs.set((prev) => {
1570
+ this.querySubs.updateInPlace((prev) => {
1520
1571
  Object.keys(prev).forEach((k) => {
1521
1572
  delete prev[k].result;
1522
1573
  });
1523
- return prev;
1524
1574
  });
1525
1575
  this._reconnectTimeoutMs = 0;
1526
1576
  this._transport.close();