@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/SyncTable.ts CHANGED
@@ -6,6 +6,7 @@ import { Logger } from './Reactor.js';
6
6
  import instaql, { compareOrder } from './instaql.js';
7
7
  import { InstaQLResponse, ValidQuery } from './queryTypes.ts';
8
8
  import { EntitiesDef, IContainEntitiesAndLinks } from './schemaTypes.ts';
9
+ import { StorageInterface } from './index.ts';
9
10
 
10
11
  type SubState = {
11
12
  txId?: number;
@@ -21,15 +22,16 @@ type Sub = {
21
22
  orderDirection: 'asc' | 'desc';
22
23
  orderFieldType: 'string' | 'number' | 'date' | 'boolean';
23
24
  state?: SubState;
24
- entities?: Array<{ entity: any; store: any; serverCreatedAt: number }>;
25
+ values?: {
26
+ attrs: Record<string, any>;
27
+ entities: Array<{ entity: any; store: any; serverCreatedAt: number }>;
28
+ };
25
29
  createdAt: number;
26
30
  updatedAt: number;
27
31
  };
28
32
 
29
33
  // We could make a better type for this if we had a return type for s.toJSON
30
- type SubsInStorage = Subs;
31
-
32
- type Subs = { [hash: string]: Sub };
34
+ type SubInStorage = Sub;
33
35
 
34
36
  type StartMsg = {
35
37
  op: 'start-sync';
@@ -39,6 +41,7 @@ type StartMsg = {
39
41
  type EndMsg = {
40
42
  op: 'remove-sync';
41
43
  'subscription-id': string;
44
+ 'keep-subscription': boolean;
42
45
  };
43
46
 
44
47
  type ResyncMsg = {
@@ -82,48 +85,52 @@ type TrySend = (eventId: string, msg: SendMsg) => void;
82
85
  type Config = { useDateObjects: boolean };
83
86
 
84
87
  // Modifies the data in place because it comes directly from storage
85
- function syncSubsFromStorage(
86
- parsed: SubsInStorage,
87
- useDateObjects: boolean,
88
- ): Subs {
89
- for (const key in parsed) {
90
- const sub = parsed[key];
91
- for (const e of sub.entities || []) {
88
+ function syncSubFromStorage(sub: SubInStorage, useDateObjects: boolean): Sub {
89
+ const values = sub.values;
90
+ if (values) {
91
+ for (const e of values.entities || []) {
92
92
  e.store.useDateObjects = useDateObjects;
93
+ e.store.attrs = values.attrs;
93
94
  e.store = s.fromJSON(e.store);
94
95
  }
95
96
  }
96
- return parsed;
97
+
98
+ return sub;
97
99
  }
98
100
 
99
- function syncSubsToStorage(subs: Subs): SubsInStorage {
100
- const jsonSubs = {};
101
- for (const key in subs) {
102
- const sub = subs[key];
103
- if (sub.entities) {
104
- const entities = [];
105
- for (const e of sub.entities) {
106
- entities.push({ ...e, store: s.toJSON(e.store) });
107
- }
108
- jsonSubs[key] = { ...sub, entities };
109
- } else {
110
- jsonSubs[key] = sub;
101
+ function syncSubToStorage(_k: string, sub: Sub): SubInStorage {
102
+ if (sub.values?.entities) {
103
+ const entities = [];
104
+ for (const e of sub.values?.entities) {
105
+ const store = s.toJSON(e.store);
106
+ // We'll store the attrs once on values, and put the
107
+ // attrs back into the store on hydration
108
+ delete store['attrs'];
109
+ entities.push({ ...e, store });
111
110
  }
111
+ return { ...sub, values: { ...sub.values, entities } };
112
+ } else {
113
+ return sub;
112
114
  }
113
- return jsonSubs;
114
115
  }
115
116
 
116
- function onMergeSubs(storageSubs: Subs | null, inMemorySubs: Subs): Subs {
117
- const subs = {};
118
- for (const [k, v] of Object.entries(inMemorySubs)) {
119
- subs[k] = v;
117
+ function onMergeSub(
118
+ _key: string,
119
+ storageSub: SubInStorage | null,
120
+ inMemorySub: Sub | null,
121
+ ): Sub {
122
+ const storageTxId = storageSub?.state.txId;
123
+ const memoryTxId = inMemorySub?.state.txId;
124
+
125
+ if (storageTxId && (!memoryTxId || storageTxId > memoryTxId)) {
126
+ return storageSub;
120
127
  }
121
- if (storageSubs) {
122
- for (const [k, v] of Object.entries(storageSubs)) {
123
- subs[k] = v;
124
- }
128
+
129
+ if (memoryTxId && (!storageTxId || memoryTxId > storageTxId)) {
130
+ return inMemorySub;
125
131
  }
126
- return subs;
132
+
133
+ return storageSub || inMemorySub;
127
134
  }
128
135
 
129
136
  function queryEntity(sub: Sub, store: any) {
@@ -198,11 +205,14 @@ function changedFieldsOfChanges(
198
205
  return changedFields;
199
206
  }
200
207
 
201
- function subData(sub: Sub, entities: NonNullable<Sub['entities']>) {
208
+ function subData(sub: Sub, entities: NonNullable<Sub['values']['entities']>) {
202
209
  return { [sub.table]: entities.map((e) => e.entity) };
203
210
  }
204
211
 
205
- function sortEntitiesInPlace(sub: Sub, entities: NonNullable<Sub['entities']>) {
212
+ function sortEntitiesInPlace(
213
+ sub: Sub,
214
+ entities: NonNullable<Sub['values']['entities']>,
215
+ ) {
206
216
  const dataType = sub.orderFieldType;
207
217
  if (sub.orderField === 'serverCreatedAt') {
208
218
  entities.sort(
@@ -362,7 +372,7 @@ export type SyncTableCallback<
362
372
 
363
373
  export class SyncTable {
364
374
  private trySend: TrySend;
365
- private subs: PersistedObject<Subs>;
375
+ private subs: PersistedObject<string, Sub, SubInStorage>;
366
376
  // Using any for the SyncCallback because we'd need Reactor to be typed
367
377
  private callbacks: { [hash: string]: SyncTableCallback<any, any, any>[] } =
368
378
  {};
@@ -373,7 +383,7 @@ export class SyncTable {
373
383
 
374
384
  constructor(
375
385
  trySend: TrySend,
376
- persister: Storage,
386
+ storage: StorageInterface,
377
387
  config: Config,
378
388
  log: Logger,
379
389
  createStore: (triples: Triple[]) => any,
@@ -383,14 +393,20 @@ export class SyncTable {
383
393
  this.log = log;
384
394
  this.createStore = createStore;
385
395
 
386
- this.subs = new PersistedObject<Subs>(
387
- persister,
388
- 'subs',
389
- {},
390
- onMergeSubs,
391
- syncSubsToStorage,
392
- (x) => syncSubsFromStorage(x, this.config.useDateObjects),
393
- );
396
+ this.subs = new PersistedObject<string, Sub, SubInStorage>({
397
+ persister: storage,
398
+ merge: onMergeSub,
399
+ serialize: syncSubToStorage,
400
+ parse: (_key, x) => syncSubFromStorage(x, this.config.useDateObjects),
401
+ objectSize: (sub) => sub.values?.entities.length || 0,
402
+ logger: log,
403
+ gc: {
404
+ maxAgeMs: 1000 * 60 * 60 * 24 * 7 * 52, // 1 year
405
+ maxEntries: 1000,
406
+ // Size of each sub is the number of entity
407
+ maxSize: 1_000_000, // 1 million entities
408
+ },
409
+ });
394
410
  }
395
411
 
396
412
  public beforeUnload() {
@@ -424,17 +440,15 @@ export class SyncTable {
424
440
 
425
441
  if (!cbs.length) {
426
442
  delete this.callbacks[hash];
427
- if (keepSubscription) {
428
- return;
429
- }
430
443
  const sub = this.subs.currentValue[hash];
431
444
  if (sub?.state) {
432
- this.clearSubscriptionData(sub.state.subscriptionId);
445
+ this.clearSubscriptionData(sub.state.subscriptionId, keepSubscription);
446
+ }
447
+ if (!keepSubscription) {
448
+ this.subs.updateInPlace((prev) => {
449
+ delete prev[hash];
450
+ });
433
451
  }
434
- this.subs.set((prev) => {
435
- delete prev[hash];
436
- return prev;
437
- });
438
452
  }
439
453
  }
440
454
 
@@ -456,10 +470,11 @@ export class SyncTable {
456
470
  });
457
471
  }
458
472
 
459
- private sendRemove(state: SubState) {
473
+ private sendRemove(state: SubState, keepSubscription: boolean) {
460
474
  this.trySend(uuid(), {
461
475
  op: 'remove-sync',
462
476
  'subscription-id': state.subscriptionId,
477
+ 'keep-subscription': keepSubscription,
463
478
  });
464
479
  }
465
480
 
@@ -469,17 +484,17 @@ export class SyncTable {
469
484
  cb?: SyncTableCallback<any, any, any>,
470
485
  ) {
471
486
  // Wait for storage to load so that we know if we already have an existing subscription
472
- await this.subs.waitForLoaded();
487
+ await this.subs.waitForKeyToLoad(hash);
473
488
  const existingSub = this.subs.currentValue[hash];
474
489
 
475
490
  if (existingSub && existingSub.state && existingSub.state.txId) {
476
491
  this.sendResync(existingSub, existingSub.state);
477
492
 
478
- if (existingSub.entities && cb) {
493
+ if (existingSub.values?.entities && cb) {
479
494
  const k = Object.keys(query)[0];
480
495
  cb({
481
496
  type: CallbackEventType.LoadFromStorage,
482
- data: subData(existingSub, existingSub.entities),
497
+ data: subData(existingSub, existingSub.values?.entities),
483
498
  });
484
499
  }
485
500
 
@@ -500,7 +515,7 @@ export class SyncTable {
500
515
  'checked-data-type'
501
516
  ];
502
517
 
503
- this.subs.set((prev) => {
518
+ this.subs.updateInPlace((prev) => {
504
519
  prev[hash] = {
505
520
  query,
506
521
  hash: hash,
@@ -511,15 +526,14 @@ export class SyncTable {
511
526
  createdAt: Date.now(),
512
527
  updatedAt: Date.now(),
513
528
  };
514
- return prev;
515
529
  });
516
530
 
517
531
  this.sendStart(query);
518
532
  }
519
533
 
520
534
  public async flushPending() {
521
- await this.subs.waitForLoaded();
522
535
  for (const hash of Object.keys(this.callbacks)) {
536
+ await this.subs.waitForKeyToLoad(hash);
523
537
  const sub = this.subs.currentValue[hash];
524
538
  if (sub) {
525
539
  await this.initSubscription(sub.query, sub.hash);
@@ -536,7 +550,7 @@ export class SyncTable {
536
550
 
537
551
  this.idToHash[subscriptionId] = hash;
538
552
 
539
- this.subs.set((prev) => {
553
+ this.subs.updateInPlace((prev) => {
540
554
  const sub = prev[hash];
541
555
  if (!sub) {
542
556
  this.log.error(
@@ -554,7 +568,6 @@ export class SyncTable {
554
568
  subscriptionId: subscriptionId,
555
569
  token: msg.token,
556
570
  };
557
- return prev;
558
571
  });
559
572
  }
560
573
 
@@ -574,37 +587,42 @@ export class SyncTable {
574
587
  }
575
588
 
576
589
  const batch = [];
577
- const currentValue = this.subs.set((prev) => {
578
- const sub = prev[hash];
579
- if (!sub) {
580
- this.log.error('Missing sub for hash', hash, msg);
581
- return prev;
582
- }
583
-
584
- const entities = sub.entities ?? [];
585
- sub.entities = entities;
586
-
587
- for (const entRows of joinRows) {
588
- const store = this.createStore(entRows);
589
- const entity = queryEntity(sub, store);
590
- entities.push({
591
- store,
592
- entity,
593
- serverCreatedAt: getServerCreatedAt(sub, store, entity.id),
594
- });
595
- batch.push(entity);
596
- }
590
+ const sub = this.subs.currentValue[hash];
591
+ if (!sub) {
592
+ this.log.error('Missing sub for hash', hash, msg);
593
+ return;
594
+ }
597
595
 
598
- sub.updatedAt = Date.now();
596
+ const values = sub.values ?? {
597
+ entities: [],
598
+ attrs: this.createStore([]).attrs,
599
+ };
600
+ sub.values = values;
601
+ const entities = values.entities;
602
+
603
+ for (const entRows of joinRows) {
604
+ const store = this.createStore(entRows);
605
+ values.attrs = store.attrs;
606
+ const entity = queryEntity(sub, store);
607
+ entities.push({
608
+ store,
609
+ entity,
610
+ serverCreatedAt: getServerCreatedAt(sub, store, entity.id),
611
+ });
612
+ batch.push(entity);
613
+ }
599
614
 
600
- return prev;
615
+ this.subs.updateInPlace((prev) => {
616
+ prev[hash] = sub;
617
+ // Make sure we write a field or mutative won't
618
+ // see the change because sub === prev[hash]
619
+ prev[hash].updatedAt = Date.now();
601
620
  });
602
621
 
603
- const sub = currentValue[hash];
604
- if (sub.entities) {
622
+ if (sub.values) {
605
623
  this.notifyCbs(hash, {
606
624
  type: CallbackEventType.InitialSyncBatch,
607
- data: subData(sub, sub.entities),
625
+ data: subData(sub, sub.values.entities),
608
626
  batch,
609
627
  });
610
628
  }
@@ -617,11 +635,11 @@ export class SyncTable {
617
635
  this.log.error('Missing hash for subscription', msg);
618
636
  return;
619
637
  }
620
- this.subs.set((prev) => {
638
+ this.subs.updateInPlace((prev) => {
621
639
  const sub = prev[hash];
622
640
  if (!sub) {
623
641
  this.log.error('Missing sub for hash', hash, msg);
624
- return prev;
642
+ return;
625
643
  }
626
644
  const state = sub.state;
627
645
  if (!state) {
@@ -630,7 +648,6 @@ export class SyncTable {
630
648
  }
631
649
  state.txId = msg['tx-id'];
632
650
  sub.updatedAt = Date.now();
633
- return prev;
634
651
  });
635
652
 
636
653
  const sub = this.subs.currentValue[hash];
@@ -638,7 +655,7 @@ export class SyncTable {
638
655
  if (sub) {
639
656
  this.notifyCbs(hash, {
640
657
  type: CallbackEventType.InitialSyncComplete,
641
- data: subData(sub, sub.entities || []),
658
+ data: subData(sub, sub.values?.entities || []),
642
659
  });
643
660
  }
644
661
  }
@@ -650,128 +667,140 @@ export class SyncTable {
650
667
  this.log.error('Missing hash for subscription', msg);
651
668
  return;
652
669
  }
653
- this.subs.set((prev) => {
654
- const sub = prev[hash];
655
- if (!sub) {
656
- this.log.error('Missing sub for hash', hash, msg);
657
- return prev;
658
- }
659
670
 
660
- const state = sub.state;
661
- if (!state) {
662
- this.log.error('Missing state for sub', sub, msg);
663
- return prev;
664
- }
671
+ const sub = this.subs.currentValue[hash];
672
+ if (!sub) {
673
+ this.log.error('Missing sub for hash', hash, msg);
674
+ return;
675
+ }
665
676
 
666
- for (const tx of msg.txes) {
667
- if (state.txId >= tx['tx-id']) {
668
- continue;
669
- }
670
- state.txId = tx['tx-id'];
671
- const idxesToDelete = [];
672
- // Note: this won't work as well when links are involved
673
- const byEid: {
674
- [eid: string]: SyncUpdateTriplesMsg['txes'][number]['changes'];
675
- } = {};
676
- for (const change of tx.changes) {
677
- const eidChanges = byEid[change.triple[0]] ?? [];
678
- byEid[change.triple[0]] = eidChanges;
679
- eidChanges.push(change);
680
- }
677
+ const state = sub.state;
678
+ if (!state) {
679
+ this.log.error('Missing state for sub', sub, msg);
680
+ return;
681
+ }
681
682
 
682
- const entities = sub.entities || [];
683
- sub.entities = entities;
684
-
685
- const updated: SyncTransaction<any, any, any>['updated'] = [];
686
- // Update the existing stores, if we already know about this entity
687
- eidLoop: for (const [eid, changes] of Object.entries(byEid)) {
688
- for (const [entIdx, ent] of Object.entries(entities)) {
689
- if (s.hasEntity(ent.store, eid)) {
690
- applyChangesToStore(ent.store, changes);
691
- const entity = queryEntity(sub, ent.store);
692
- const changedFields = changedFieldsOfChanges(ent.store, changes)[
693
- eid
694
- ];
695
- for (const [k, { oldValue, newValue }] of Object.entries(
696
- changedFields || {},
697
- )) {
698
- if (oldValue === newValue) {
699
- delete changedFields[k];
700
- }
701
- }
702
- if (entity) {
703
- updated.push({
704
- oldEntity: ent.entity,
705
- newEntity: entity,
706
- changedFields: changedFields || {},
707
- });
708
- ent.entity = entity;
709
- } else {
710
- idxesToDelete.push(entIdx);
683
+ for (const tx of msg.txes) {
684
+ if (state.txId >= tx['tx-id']) {
685
+ continue;
686
+ }
687
+ state.txId = tx['tx-id'];
688
+ const idxesToDelete = [];
689
+ // Note: this won't work as well when links are involved
690
+ const byEid: {
691
+ [eid: string]: SyncUpdateTriplesMsg['txes'][number]['changes'];
692
+ } = {};
693
+ for (const change of tx.changes) {
694
+ const eidChanges = byEid[change.triple[0]] ?? [];
695
+ byEid[change.triple[0]] = eidChanges;
696
+ eidChanges.push(change);
697
+ }
698
+
699
+ const values = sub.values ?? {
700
+ entities: [],
701
+ attrs: this.createStore([]).attrs,
702
+ };
703
+ const entities = values.entities;
704
+ sub.values = values;
705
+
706
+ const updated: SyncTransaction<any, any, any>['updated'] = [];
707
+ // Update the existing stores, if we already know about this entity
708
+ eidLoop: for (const [eid, changes] of Object.entries(byEid)) {
709
+ for (const [entIdx, ent] of Object.entries(entities)) {
710
+ if (s.hasEntity(ent.store, eid)) {
711
+ applyChangesToStore(ent.store, changes);
712
+ const entity = queryEntity(sub, ent.store);
713
+ const changedFields = changedFieldsOfChanges(ent.store, changes)[
714
+ eid
715
+ ];
716
+ for (const [k, { oldValue, newValue }] of Object.entries(
717
+ changedFields || {},
718
+ )) {
719
+ if (oldValue === newValue) {
720
+ delete changedFields[k];
711
721
  }
712
- delete byEid[eid];
713
- continue eidLoop;
714
722
  }
723
+ if (entity) {
724
+ updated.push({
725
+ oldEntity: ent.entity,
726
+ newEntity: entity,
727
+ changedFields: changedFields || {},
728
+ });
729
+ ent.entity = entity;
730
+ } else {
731
+ idxesToDelete.push(entIdx);
732
+ }
733
+ delete byEid[eid];
734
+ continue eidLoop;
715
735
  }
716
736
  }
737
+ }
717
738
 
718
- const added = [];
719
- // If we have anything left in byEid, then this must be a new entity we don't know about
720
- for (const [_eid, changes] of Object.entries(byEid)) {
721
- const store = this.createStore([]);
722
- applyChangesToStore(store, changes);
723
- const entity = queryEntity(sub, store);
724
- if (!entity) {
725
- this.log.error('No entity found after applying change', {
726
- sub,
727
- changes,
728
- store,
729
- });
730
- 1;
731
- continue;
732
- }
733
- entities.push({
739
+ const added = [];
740
+ // If we have anything left in byEid, then this must be a new entity we don't know about
741
+ for (const [_eid, changes] of Object.entries(byEid)) {
742
+ const store = this.createStore([]);
743
+ values.attrs = store.attrs;
744
+ applyChangesToStore(store, changes);
745
+ const entity = queryEntity(sub, store);
746
+ if (!entity) {
747
+ this.log.error('No entity found after applying change', {
748
+ sub,
749
+ changes,
734
750
  store,
735
- entity,
736
- serverCreatedAt: getServerCreatedAt(sub, store, entity.id),
737
751
  });
738
- added.push(entity);
752
+ continue;
739
753
  }
754
+ entities.push({
755
+ store,
756
+ entity,
757
+ serverCreatedAt: getServerCreatedAt(sub, store, entity.id),
758
+ });
759
+ added.push(entity);
760
+ }
740
761
 
741
- const removed = [];
742
-
743
- for (const idx of idxesToDelete.sort().reverse()) {
744
- removed.push(entities[idx].entity);
745
- entities.splice(idx, 1);
746
- }
762
+ const removed = [];
747
763
 
748
- sortEntitiesInPlace(sub, entities);
749
- this.notifyCbs(hash, {
750
- type: CallbackEventType.SyncTransaction,
751
- data: subData(sub, sub.entities),
752
- added,
753
- removed,
754
- updated,
755
- });
756
- sub.updatedAt = Date.now();
764
+ for (const idx of idxesToDelete.sort().reverse()) {
765
+ removed.push(entities[idx].entity);
766
+ entities.splice(idx, 1);
757
767
  }
758
768
 
759
- return prev;
769
+ sortEntitiesInPlace(sub, entities);
770
+ this.notifyCbs(hash, {
771
+ type: CallbackEventType.SyncTransaction,
772
+ data: subData(sub, sub.values?.entities),
773
+ added,
774
+ removed,
775
+ updated,
776
+ });
777
+ }
778
+ this.subs.updateInPlace((prev) => {
779
+ prev[hash] = sub;
780
+ // Make sure we write a field or mutative won't
781
+ // see the change because sub === prev[hash]
782
+ prev[hash].updatedAt = Date.now();
760
783
  });
761
784
  }
762
785
 
763
- private clearSubscriptionData(subscriptionId: string) {
786
+ private clearSubscriptionData(
787
+ subscriptionId: string,
788
+ keepSubscription: boolean,
789
+ ) {
764
790
  const hash = this.idToHash[subscriptionId];
765
791
  if (hash) {
766
792
  delete this.idToHash[subscriptionId];
767
793
  const sub = this.subs.currentValue[hash];
768
794
  if (sub.state) {
769
- this.sendRemove(sub.state);
795
+ this.sendRemove(sub.state, keepSubscription);
796
+ }
797
+ if (keepSubscription) {
798
+ this.subs.unloadKey(hash);
799
+ } else {
800
+ this.subs.updateInPlace((prev) => {
801
+ delete prev[hash];
802
+ });
770
803
  }
771
- this.subs.set((prev) => {
772
- delete prev[hash];
773
- return prev;
774
- });
775
804
  if (sub) {
776
805
  return sub;
777
806
  }
@@ -813,7 +842,7 @@ export class SyncTable {
813
842
  // This can happen if the auth changed and we need to refetch with the
814
843
  // new auth or if the subscription is too far behind.
815
844
  const subscriptionId = msg['original-event']['subscription-id'];
816
- const removedSub = this.clearSubscriptionData(subscriptionId);
845
+ const removedSub = this.clearSubscriptionData(subscriptionId, false);
817
846
  if (removedSub) {
818
847
  this.initSubscription(removedSub.query, removedSub.hash);
819
848
  }
package/src/index.ts CHANGED
@@ -20,6 +20,10 @@ import {
20
20
  validateTransactions,
21
21
  TransactionValidationError,
22
22
  } from './transactionValidation.ts';
23
+ import {
24
+ StorageInterface,
25
+ type StorageInterfaceStoreName,
26
+ } from './utils/PersistedObject.ts';
23
27
 
24
28
  import type {
25
29
  PresenceOpts,
@@ -1013,4 +1017,8 @@ export {
1013
1017
 
1014
1018
  // error types
1015
1019
  type InstantIssue,
1020
+
1021
+ // storage (e.g. indexeddb) interface
1022
+ StorageInterface,
1023
+ type StorageInterfaceStoreName,
1016
1024
  };