powersync-bridge 0.0.1-alpha.1 → 0.0.1-alpha.2

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.
package/dist/index.cjs CHANGED
@@ -130,6 +130,89 @@ async function classifyRemovals(ctx, removed) {
130
130
  return out;
131
131
  }
132
132
 
133
+ // src/refresh.ts
134
+ var CoalescedRefresh = class {
135
+ constructor(read, settled) {
136
+ this.read = read;
137
+ this.settled = settled;
138
+ }
139
+ read;
140
+ settled;
141
+ revision = 0;
142
+ lifecycle = 0;
143
+ scheduled = false;
144
+ running = false;
145
+ get pending() {
146
+ return this.scheduled || this.running;
147
+ }
148
+ request() {
149
+ this.revision += 1;
150
+ if (this.pending) return;
151
+ this.scheduled = true;
152
+ const lifecycle = this.lifecycle;
153
+ queueMicrotask(() => {
154
+ if (lifecycle !== this.lifecycle) return;
155
+ this.scheduled = false;
156
+ void this.drain(lifecycle);
157
+ });
158
+ }
159
+ /** Fence queued/late work without retaining results or preventing a later resume. */
160
+ cancel() {
161
+ this.lifecycle += 1;
162
+ this.scheduled = false;
163
+ this.running = false;
164
+ }
165
+ async drain(lifecycle) {
166
+ this.running = true;
167
+ let revision;
168
+ try {
169
+ do {
170
+ revision = this.revision;
171
+ await this.read(() => lifecycle === this.lifecycle && revision === this.revision);
172
+ } while (lifecycle === this.lifecycle && revision !== this.revision);
173
+ } finally {
174
+ if (lifecycle === this.lifecycle) {
175
+ this.running = false;
176
+ this.settled();
177
+ }
178
+ }
179
+ }
180
+ };
181
+ var BackgroundReads = class {
182
+ running = false;
183
+ queue = /* @__PURE__ */ new Set();
184
+ run(read, signal) {
185
+ if (signal.aborted) return Promise.reject(signal.reason);
186
+ return new Promise((resolve, reject) => {
187
+ const entry = {
188
+ cancel: () => {
189
+ if (!this.queue.delete(entry)) return;
190
+ signal.removeEventListener("abort", entry.cancel);
191
+ reject(signal.reason);
192
+ },
193
+ run: () => {
194
+ signal.removeEventListener("abort", entry.cancel);
195
+ this.running = true;
196
+ void Promise.resolve().then(read).then(resolve, reject).finally(() => {
197
+ this.running = false;
198
+ this.drain();
199
+ });
200
+ }
201
+ };
202
+ signal.addEventListener("abort", entry.cancel, { once: true });
203
+ this.queue.add(entry);
204
+ this.drain();
205
+ });
206
+ }
207
+ drain() {
208
+ if (this.running) return;
209
+ const next = this.queue.values().next().value;
210
+ if (!next) return;
211
+ this.queue.delete(next);
212
+ next.run();
213
+ }
214
+ };
215
+
133
216
  // src/store.ts
134
217
  var SnapshotStore = class {
135
218
  snapshot;
@@ -197,12 +280,18 @@ var InfiniteQueryStore = class {
197
280
  this.pageSize = options.pageSize ?? 25;
198
281
  this.maxRows = options.maxRows ?? 500;
199
282
  if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || !Number.isInteger(this.maxRows) || this.maxRows < this.pageSize)
200
- throw new Error("powersync-bridge: pageSize must be positive and maxRows must be at least pageSize");
283
+ throw new Error(
284
+ "powersync-bridge: pageSize must be positive and maxRows must be at least pageSize"
285
+ );
201
286
  this.target = this.pageSize;
202
287
  this.compare = rowComparator(options.orderBy, options.direction ?? "desc");
203
288
  this.table = options.table;
204
289
  this.offline = this.readOffline();
205
290
  this.store = new SnapshotStore(this.result());
291
+ this.checkpointRefresh = new CoalescedRefresh(
292
+ (isCurrent) => this.checkpoint(isCurrent),
293
+ () => this.render()
294
+ );
206
295
  }
207
296
  deps;
208
297
  options;
@@ -231,6 +320,9 @@ var InfiniteQueryStore = class {
231
320
  watchGeneration = 0;
232
321
  lastLocalWatch = 0;
233
322
  refs = 0;
323
+ activeRefs = 0;
324
+ checkpointRefresh;
325
+ localAbort = new AbortController();
234
326
  ready = false;
235
327
  disposed = false;
236
328
  unwatch;
@@ -240,12 +332,19 @@ var InfiniteQueryStore = class {
240
332
  get isDisposed() {
241
333
  return this.disposed || this.refs <= 0 && this.unwatch !== void 0;
242
334
  }
243
- retain() {
335
+ retain(active = true) {
244
336
  this.refs += 1;
245
- if (this.refs !== 1 || this.unwatch || this.options.enabled === false) return;
337
+ if (active) this.activeRefs += 1;
338
+ if (this.activeRefs <= 0 || this.unwatch || this.options.enabled === false || this.disposed)
339
+ return;
340
+ this.offline = this.readOffline();
341
+ this.apiFailed = false;
342
+ this.apiError = void 0;
246
343
  this.stops = [
247
344
  this.deps.crud.onChange(() => this.render()),
248
- this.deps.freshness.onCompletion(() => void this.checkpoint()),
345
+ this.deps.freshness.onCompletion((completion) => {
346
+ if (completion.stream === null) this.checkpointRefresh.request();
347
+ }),
249
348
  this.deps.driver.onStatusChanged(() => this.networkChanged()),
250
349
  this.deps.connectivity.subscribe(() => this.networkChanged())
251
350
  ];
@@ -260,12 +359,13 @@ var InfiniteQueryStore = class {
260
359
  this.render();
261
360
  });
262
361
  }
263
- release() {
362
+ release(active = true) {
264
363
  this.refs -= 1;
265
- if (this.refs <= 0)
266
- queueMicrotask(() => {
267
- if (this.refs <= 0) this.dispose();
268
- });
364
+ if (active) this.activeRefs -= 1;
365
+ queueMicrotask(() => {
366
+ if (this.refs <= 0) this.dispose();
367
+ else if (this.activeRefs <= 0) this.pause();
368
+ });
269
369
  }
270
370
  swapRoot() {
271
371
  this.disposed = true;
@@ -276,13 +376,7 @@ var InfiniteQueryStore = class {
276
376
  }
277
377
  dispose() {
278
378
  this.disposed = true;
279
- this.generation += 1;
280
- this.watchGeneration += 1;
281
- this.cancelRequest();
282
- this.unwatch?.();
283
- this.unwatch = void 0;
284
- for (const stop of this.stops) stop();
285
- this.stops = [];
379
+ this.pause();
286
380
  this.clearPages();
287
381
  this.local.clear();
288
382
  this.retained.clear();
@@ -291,6 +385,19 @@ var InfiniteQueryStore = class {
291
385
  this.inSession.clear();
292
386
  this.render(true);
293
387
  }
388
+ pause() {
389
+ this.generation += 1;
390
+ this.watchGeneration += 1;
391
+ this.checkpointRefresh.cancel();
392
+ this.localAbort.abort();
393
+ this.cancelRequest();
394
+ this.request = void 0;
395
+ this.apiPending = false;
396
+ this.unwatch?.();
397
+ this.unwatch = void 0;
398
+ for (const stop of this.stops) stop();
399
+ this.stops = [];
400
+ }
294
401
  readOffline() {
295
402
  return (this.deps.connectivity.getSnapshot() ?? this.deps.driver.onlineHint()) === false;
296
403
  }
@@ -309,6 +416,7 @@ var InfiniteQueryStore = class {
309
416
  this.generation += 1;
310
417
  this.apiPending = false;
311
418
  this.request = void 0;
419
+ this.watch();
312
420
  }
313
421
  if (reconnect) {
314
422
  this.apiFailed = false;
@@ -335,7 +443,11 @@ var InfiniteQueryStore = class {
335
443
  return this.options.query({ cursor: this.anchor, limit: this.target + 1 });
336
444
  }
337
445
  watch() {
446
+ if (this.activeRefs <= 0 || this.disposed) return;
338
447
  this.unwatch?.();
448
+ this.localAbort.abort();
449
+ this.localAbort = new AbortController();
450
+ this.checkpointRefresh.cancel();
339
451
  const token = ++this.watchGeneration;
340
452
  const generation = this.generation;
341
453
  this.localPending = true;
@@ -356,14 +468,16 @@ var InfiniteQueryStore = class {
356
468
  this.unwatch = this.deps.driver.diffs(prepared).subscribe(
357
469
  (diff) => {
358
470
  void this.acceptLocal(diff.all, token, generation).catch((error) => {
359
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
471
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
472
+ return;
360
473
  this.localPending = false;
361
474
  this.localError = error instanceof Error ? error : new Error(String(error));
362
475
  this.render();
363
476
  });
364
477
  },
365
478
  (error) => {
366
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
479
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
480
+ return;
367
481
  this.localPending = false;
368
482
  this.localError = error;
369
483
  this.render();
@@ -371,9 +485,10 @@ var InfiniteQueryStore = class {
371
485
  );
372
486
  this.render();
373
487
  }
374
- async acceptLocal(rows, token, generation) {
488
+ async acceptLocal(rows, token, generation, stamp = this.deps.freshness.freshAtFor(void 0), isCurrent = () => true) {
375
489
  await this.deps.crud.resync();
376
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
490
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
491
+ return;
377
492
  const previous = this.local;
378
493
  const firstEmission = this.lastLocalWatch !== token;
379
494
  this.lastLocalWatch = token;
@@ -394,7 +509,7 @@ var InfiniteQueryStore = class {
394
509
  this.local = next;
395
510
  this.localPending = false;
396
511
  this.localError = void 0;
397
- this.localStamp = this.deps.freshness.freshAtFor(void 0);
512
+ this.localStamp = stamp;
398
513
  this.render();
399
514
  const groups = /* @__PURE__ */ new Map();
400
515
  for (const row of removed.values()) {
@@ -415,7 +530,8 @@ var InfiniteQueryStore = class {
415
530
  },
416
531
  group
417
532
  );
418
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
533
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
534
+ return;
419
535
  for (const row of removed.values()) {
420
536
  const identity = this.identity(row);
421
537
  if (identity.table !== table || this.local.has(row.id)) continue;
@@ -450,15 +566,21 @@ var InfiniteQueryStore = class {
450
566
  }
451
567
  this.render();
452
568
  }
453
- async checkpoint() {
454
- if (this.disposed || !this.unwatch) return;
569
+ async checkpoint(isCurrent) {
570
+ if (this.disposed || !this.unwatch || this.activeRefs <= 0) return;
455
571
  const token = this.watchGeneration;
456
572
  const generation = this.generation;
573
+ const stamp = this.deps.freshness.freshAtFor(void 0);
457
574
  try {
458
- const rows = await this.deps.driver.query(this.deps.adapter.prepare(this.localBuilder()));
459
- await this.acceptLocal(rows, token, generation);
575
+ const signal = this.localAbort.signal;
576
+ const read = () => isCurrent() ? this.deps.driver.query(this.deps.adapter.prepare(this.localBuilder()), signal) : Promise.resolve(void 0);
577
+ const rows = await (this.deps.backgroundReads ? this.deps.backgroundReads.run(read, signal) : read());
578
+ if (!rows) return;
579
+ if (!isCurrent()) return;
580
+ await this.acceptLocal(rows, token, generation, stamp, isCurrent);
460
581
  } catch (error) {
461
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
582
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
583
+ return;
462
584
  this.localError = error instanceof Error ? error : new Error(String(error));
463
585
  this.render();
464
586
  }
@@ -483,7 +605,7 @@ var InfiniteQueryStore = class {
483
605
  return [row[first], row[second]];
484
606
  }
485
607
  pump() {
486
- if (!this.ready || this.disposed || this.options.enabled === false || this.offline || this.apiPending || this.apiFailed || this.apiEnd())
608
+ if (!this.ready || this.activeRefs <= 0 || this.disposed || this.options.enabled === false || this.offline || this.apiPending || this.apiFailed || this.apiEnd())
487
609
  return;
488
610
  if (!this.table && !this.options.getRowIdentity) return;
489
611
  if (this.pages.length >= Math.ceil(this.maxRows / this.pageSize) + 1) return;
@@ -529,7 +651,9 @@ var InfiniteQueryStore = class {
529
651
  const before = rows[index - 1];
530
652
  const row = rows[index];
531
653
  if (before && row && this.compare(before, row) >= 0)
532
- throw new Error("powersync-bridge: API rows must use a strict, stable order with an id tiebreaker");
654
+ throw new Error(
655
+ "powersync-bridge: API rows must use a strict, stable order with an id tiebreaker"
656
+ );
533
657
  }
534
658
  if (cursor !== void 0 && rows.some((row) => this.compare(row, cursor) <= 0))
535
659
  throw new Error("powersync-bridge: API page must advance beyond the supplied cursor");
@@ -622,14 +746,15 @@ var InfiniteQueryStore = class {
622
746
  this.store.replace(result);
623
747
  }
624
748
  fetchNextPage = () => {
625
- if (this.disposed || this.options.enabled === false || this.target >= this.maxRows) return;
749
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false || this.target >= this.maxRows)
750
+ return;
626
751
  if (!this.store.getSnapshot().hasNextPage) return;
627
752
  this.target = Math.min(this.target + this.pageSize, this.maxRows);
628
753
  this.watch();
629
754
  this.pump();
630
755
  };
631
756
  retry = () => {
632
- if (this.disposed || this.options.enabled === false) return;
757
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
633
758
  this.apiFailed = false;
634
759
  this.apiError = void 0;
635
760
  this.watch();
@@ -643,7 +768,7 @@ var InfiniteQueryStore = class {
643
768
  if (last) this.reset(last);
644
769
  };
645
770
  reset(anchor) {
646
- if (this.disposed || this.options.enabled === false) return;
771
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
647
772
  this.generation += 1;
648
773
  this.cancelRequest();
649
774
  this.request = void 0;
@@ -754,7 +879,6 @@ var FreshnessTracker = class {
754
879
  lastHasSynced = void 0;
755
880
  streamFreshAt = /* @__PURE__ */ new Map();
756
881
  streamSeenSyncedAt = /* @__PURE__ */ new Map();
757
- completions = [];
758
882
  completionListeners = /* @__PURE__ */ new Set();
759
883
  clearListeners = /* @__PURE__ */ new Set();
760
884
  unsubscribe = null;
@@ -788,7 +912,6 @@ var FreshnessTracker = class {
788
912
  this.lastSeenSyncedAt = null;
789
913
  this.streamFreshAt.clear();
790
914
  this.streamSeenSyncedAt.clear();
791
- this.completions.length = 0;
792
915
  }
793
916
  get sessionConnectAtMs() {
794
917
  return this.sessionConnectAt;
@@ -861,7 +984,6 @@ var FreshnessTracker = class {
861
984
  this.prevCompletionAt = now;
862
985
  this.lastDownloadStart = null;
863
986
  const c = { stamp: this.localFreshAt, completedAt: now, stream };
864
- this.completions.push(c);
865
987
  for (const l of this.completionListeners) l(c);
866
988
  }
867
989
  };
@@ -1530,6 +1652,10 @@ var RacedListStore = class {
1530
1652
  this.compare = rowComparator(def.orderKey, def.direction ?? "asc");
1531
1653
  this.table = def.table ?? def.id;
1532
1654
  this.startedAt = deps.clock.now();
1655
+ this.checkpointRefresh = new CoalescedRefresh(
1656
+ (isCurrent) => this.onCompletion(isCurrent),
1657
+ () => this.reconcile()
1658
+ );
1533
1659
  }
1534
1660
  deps;
1535
1661
  def;
@@ -1539,6 +1665,9 @@ var RacedListStore = class {
1539
1665
  compare;
1540
1666
  table;
1541
1667
  abort = new AbortController();
1668
+ observation = 0;
1669
+ checkpointRefresh;
1670
+ localFreshAt = Number.NEGATIVE_INFINITY;
1542
1671
  startedAt;
1543
1672
  snap = null;
1544
1673
  snapPending = false;
@@ -1557,17 +1686,21 @@ var RacedListStore = class {
1557
1686
  tombstones = /* @__PURE__ */ new Set();
1558
1687
  disposed = false;
1559
1688
  refs = 0;
1689
+ activeRefs = 0;
1560
1690
  measured = false;
1561
1691
  localMs = null;
1562
1692
  apiMs = null;
1563
- retain() {
1693
+ retain(active = true) {
1564
1694
  this.refs += 1;
1565
- if (this.refs === 1 && this.unsubDiffs === null) this.start();
1695
+ if (active) this.activeRefs += 1;
1696
+ if (this.activeRefs > 0 && this.unsubDiffs === null) this.start();
1566
1697
  }
1567
- release() {
1698
+ release(active = true) {
1568
1699
  this.refs -= 1;
1569
- if (this.refs <= 0) queueMicrotask(() => {
1700
+ if (active) this.activeRefs -= 1;
1701
+ queueMicrotask(() => {
1570
1702
  if (this.refs <= 0) this.dispose();
1703
+ else if (this.activeRefs <= 0) this.pause();
1571
1704
  });
1572
1705
  }
1573
1706
  get isDisposed() {
@@ -1580,10 +1713,7 @@ var RacedListStore = class {
1580
1713
  }
1581
1714
  dispose() {
1582
1715
  this.disposed = true;
1583
- this.abort.abort(new Error("disposed"));
1584
- this.unsubDiffs?.();
1585
- this.unsubCrud?.();
1586
- this.unsubFresh?.();
1716
+ this.pause();
1587
1717
  if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
1588
1718
  this.snapWindow = null;
1589
1719
  this.local?.clear();
@@ -1595,39 +1725,71 @@ var RacedListStore = class {
1595
1725
  this.tombstones.clear();
1596
1726
  this.store.replace(emptyRacedResult());
1597
1727
  }
1728
+ pause() {
1729
+ this.observation += 1;
1730
+ this.checkpointRefresh.cancel();
1731
+ this.abort.abort(new Error("observer stopped"));
1732
+ this.unsubDiffs?.();
1733
+ this.unsubCrud?.();
1734
+ this.unsubFresh?.();
1735
+ this.unsubDiffs = null;
1736
+ this.unsubCrud = null;
1737
+ this.unsubFresh = null;
1738
+ if (this.snapPending) {
1739
+ this.deps.snapshots.invalidate(this.snapshotKey());
1740
+ if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
1741
+ this.snapWindow = this.snap?.window ?? null;
1742
+ this.snapPending = false;
1743
+ }
1744
+ }
1598
1745
  // ---------------------------------------------------------------------------
1599
1746
  start() {
1747
+ if (this.disposed) return;
1748
+ this.abort = new AbortController();
1749
+ const observation = ++this.observation;
1600
1750
  const { driver, adapter, crud, freshness } = this.deps;
1601
1751
  const prepared = adapter.prepare(this.def.query(this.scope));
1602
1752
  this.prepared = prepared;
1603
1753
  this.unsubDiffs = driver.diffs(prepared).subscribe(
1604
- (diff) => void this.onDiff(diff),
1754
+ (diff) => {
1755
+ if (observation === this.observation) void this.onDiff(diff);
1756
+ },
1605
1757
  (err) => {
1758
+ if (observation !== this.observation) return;
1606
1759
  this.localError = err;
1607
1760
  this.reconcile();
1608
1761
  }
1609
1762
  );
1610
1763
  this.unsubCrud = crud.onChange(() => this.reconcile());
1611
- this.unsubFresh = freshness.onCompletion(() => void this.onCompletion());
1612
- if (this.deps.apiLegEnabled) this.startApiLeg();
1764
+ this.unsubFresh = freshness.onCompletion((completion) => {
1765
+ if (completion.stream === null || this.def.stream && completion.stream === streamKey(this.def.stream)) {
1766
+ if (!this.owned) this.checkpointRefresh.request();
1767
+ }
1768
+ });
1769
+ if (this.deps.apiLegEnabled && !this.owned) this.startApiLeg();
1613
1770
  this.reconcile();
1614
1771
  }
1615
1772
  startApiLeg() {
1773
+ const observation = this.observation;
1774
+ const abort = this.abort;
1616
1775
  const { snapshots, crud } = this.deps;
1617
1776
  const staleTime = this.def.staleTime ?? 10 * 6e4;
1618
1777
  this.snapPending = true;
1619
1778
  const window = crud.openWindow();
1620
1779
  this.snapWindow = window;
1621
- const key = ["powersync-bridge", this.deps.identityKey, this.deps.epoch, this.def.id, this.scopeKey];
1780
+ const key = this.snapshotKey();
1622
1781
  snapshots.fetch(key, staleTime, (signal) => {
1623
1782
  const combined = new AbortController();
1624
1783
  const onAbort = () => combined.abort();
1625
1784
  signal.addEventListener("abort", onAbort, { once: true });
1626
- this.abort.signal.addEventListener("abort", onAbort, { once: true });
1627
- return this.def.fetchSnapshot(this.scope, combined.signal);
1785
+ abort.signal.addEventListener("abort", onAbort, { once: true });
1786
+ return this.def.fetchSnapshot(this.scope, combined.signal).finally(() => {
1787
+ signal.removeEventListener("abort", onAbort);
1788
+ abort.signal.removeEventListener("abort", onAbort);
1789
+ });
1628
1790
  }).then(
1629
1791
  (record) => {
1630
- if (this.disposed) return;
1792
+ if (this.disposed || observation !== this.observation) return;
1631
1793
  this.snapPending = false;
1632
1794
  this.apiMs = this.deps.clock.now() - this.startedAt;
1633
1795
  const rows = /* @__PURE__ */ new Map();
@@ -1635,32 +1797,38 @@ var RacedListStore = class {
1635
1797
  const item = this.def.mapApi(raw);
1636
1798
  rows.set(idOf(item), item);
1637
1799
  }
1800
+ if (this.snap) crud.closeWindow(this.snap.window);
1638
1801
  this.snap = { rows, record, window };
1639
1802
  this.reconcile();
1640
1803
  },
1641
1804
  (err) => {
1642
- if (this.disposed) return;
1805
+ if (this.disposed || observation !== this.observation) return;
1643
1806
  this.snapPending = false;
1644
1807
  this.snapError = err instanceof Error ? err : new Error(String(err));
1645
1808
  crud.closeWindow(window);
1646
- this.snapWindow = null;
1809
+ this.snapWindow = this.snap?.window ?? null;
1647
1810
  this.reconcile();
1648
1811
  }
1649
1812
  );
1650
1813
  }
1814
+ snapshotKey() {
1815
+ return ["powersync-bridge", this.deps.identityKey, this.deps.epoch, this.def.id, this.scopeKey];
1816
+ }
1651
1817
  /**
1652
1818
  * A checkpoint completed. Status events and diff emissions are separate SDK events with no
1653
1819
  * ordering guarantee; taking ownership on the stale local map would render the pre-checkpoint
1654
1820
  * value for one frame (fresh→old→fresh). Pre-ownership we re-query once, then reconcile.
1655
1821
  */
1656
- async onCompletion() {
1657
- if (this.disposed || this.owned || !this.prepared || !this.emissionSeen) {
1658
- this.reconcile();
1659
- return;
1660
- }
1822
+ async onCompletion(isCurrent) {
1823
+ if (this.disposed || this.owned || !this.prepared || this.activeRefs <= 0) return;
1824
+ const freshAt = this.deps.freshness.freshAtFor(this.def.stream);
1661
1825
  try {
1662
- const rows = await this.deps.driver.query(this.prepared, this.abort.signal);
1663
- if (this.disposed) return;
1826
+ const prepared = this.prepared;
1827
+ const signal = this.abort.signal;
1828
+ const read = () => isCurrent() ? this.deps.driver.query(prepared, signal) : Promise.resolve(void 0);
1829
+ const rows = await (this.deps.backgroundReads ? this.deps.backgroundReads.run(read, signal) : read());
1830
+ if (!rows) return;
1831
+ if (this.disposed || !isCurrent()) return;
1664
1832
  const prev = this.local ?? /* @__PURE__ */ new Map();
1665
1833
  const next = /* @__PURE__ */ new Map();
1666
1834
  for (const row of rows) {
@@ -1673,12 +1841,16 @@ var RacedListStore = class {
1673
1841
  }
1674
1842
  }
1675
1843
  this.local = next;
1844
+ this.localFreshAt = Math.max(this.localFreshAt, freshAt);
1845
+ this.emissionSeen = true;
1846
+ this.localError = void 0;
1676
1847
  } catch {
1677
1848
  }
1678
1849
  this.reconcile();
1679
1850
  }
1680
1851
  async onDiff(diff) {
1681
- if (this.disposed) return;
1852
+ if (this.disposed || this.activeRefs <= 0) return;
1853
+ const observation = this.observation;
1682
1854
  const { crud } = this.deps;
1683
1855
  const prev = this.local;
1684
1856
  const next = /* @__PURE__ */ new Map();
@@ -1691,6 +1863,8 @@ var RacedListStore = class {
1691
1863
  }
1692
1864
  }
1693
1865
  this.local = next;
1866
+ if (!this.checkpointRefresh.pending)
1867
+ this.localFreshAt = this.deps.freshness.freshAtFor(this.def.stream);
1694
1868
  if (!this.emissionSeen) {
1695
1869
  this.emissionSeen = true;
1696
1870
  this.localMs = this.deps.clock.now() - this.startedAt;
@@ -1698,15 +1872,13 @@ var RacedListStore = class {
1698
1872
  void crud.resync();
1699
1873
  if (this.owned && prev) {
1700
1874
  const removed = /* @__PURE__ */ new Map();
1701
- for (const id of diff.removed) {
1702
- const row = prev.get(id);
1703
- if (row) removed.set(id, row);
1704
- }
1705
- if (removed.size > 0) await this.applyRemovals(removed);
1875
+ for (const [id, row] of prev) if (!next.has(id)) removed.set(id, row);
1876
+ if (removed.size > 0) await this.applyRemovals(removed, observation);
1706
1877
  }
1878
+ if (observation !== this.observation) return;
1707
1879
  this.reconcile();
1708
1880
  }
1709
- async applyRemovals(removed) {
1881
+ async applyRemovals(removed, observation) {
1710
1882
  const classes = await classifyRemovals(
1711
1883
  {
1712
1884
  driver: this.deps.driver,
@@ -1717,7 +1889,7 @@ var RacedListStore = class {
1717
1889
  },
1718
1890
  removed
1719
1891
  );
1720
- if (this.disposed) return;
1892
+ if (this.disposed || observation !== this.observation) return;
1721
1893
  for (const [id, cls] of classes) {
1722
1894
  const entry = this.overlay.get(id);
1723
1895
  switch (cls) {
@@ -1740,18 +1912,24 @@ var RacedListStore = class {
1740
1912
  if (this.disposed) return;
1741
1913
  const result = this.owned ? this.renderOwned() : this.renderRace();
1742
1914
  const prev = this.store.getSnapshot();
1743
- if (shallowEqualResult(prev, result)) return;
1915
+ if (shallowEqualResult(
1916
+ prev,
1917
+ result
1918
+ ))
1919
+ return;
1744
1920
  this.store.replace(result);
1745
1921
  this.report(result);
1746
1922
  }
1747
1923
  freshAt() {
1748
- return this.deps.freshness.freshAtFor(this.def.stream);
1924
+ return this.localFreshAt;
1749
1925
  }
1750
1926
  localFresh() {
1751
- return this.emissionSeen && this.deps.freshness.isFresh(this.def.stream);
1927
+ const connectedAt = this.deps.freshness.sessionConnectAtMs;
1928
+ return this.emissionSeen && connectedAt !== null && this.localFreshAt >= connectedAt;
1752
1929
  }
1753
1930
  tryTakeOwnership() {
1754
1931
  if (this.owned) return true;
1932
+ if (this.checkpointRefresh.pending) return false;
1755
1933
  const apiFreshAt = this.snap?.record.fetchedAt ?? Number.NEGATIVE_INFINITY;
1756
1934
  if (!(this.localFresh() && this.freshAt() > apiFreshAt)) return false;
1757
1935
  this.owned = true;
@@ -1761,7 +1939,8 @@ var RacedListStore = class {
1761
1939
  if (this.snap) {
1762
1940
  if (this.def.apiScopeSupersetOfLocal) {
1763
1941
  for (const [id, row] of this.snap.rows) {
1764
- if (!overlay.has(id) && !this.tombstones.has(id)) overlay.set(id, { row, origin: "api", presentLocally: false });
1942
+ if (!overlay.has(id) && !this.tombstones.has(id))
1943
+ overlay.set(id, { row, origin: "api", presentLocally: false });
1765
1944
  }
1766
1945
  }
1767
1946
  this.deps.crud.closeWindow(this.snap.window);
@@ -1808,11 +1987,14 @@ var RacedListStore = class {
1808
1987
  const inS = S?.rows.get(id);
1809
1988
  const pending = crud.pendingOp(this.table, id, S?.window);
1810
1989
  if (pending) {
1811
- if (inLocal !== void 0) entries.push({ row: inLocal, origin: "local", presentLocally: true });
1990
+ if (inLocal !== void 0)
1991
+ entries.push({ row: inLocal, origin: "local", presentLocally: true });
1812
1992
  continue;
1813
1993
  }
1814
1994
  if (inLocal !== void 0 && inS !== void 0) {
1815
- entries.push(apiFresher ? { row: inS, origin: "api", presentLocally: true } : { row: inLocal, origin: "local", presentLocally: true });
1995
+ entries.push(
1996
+ apiFresher ? { row: inS, origin: "api", presentLocally: true } : { row: inLocal, origin: "local", presentLocally: true }
1997
+ );
1816
1998
  continue;
1817
1999
  }
1818
2000
  if (inLocal !== void 0) {
@@ -1836,7 +2018,9 @@ var RacedListStore = class {
1836
2018
  finalize(entries, stale = false) {
1837
2019
  entries.sort((a, b) => this.compare(a.row, b.row));
1838
2020
  const limited = entries.slice(0, this.def.limit);
1839
- return limited.map((e) => stale ? { row: e.row, origin: e.origin, presentLocally: e.presentLocally, stale: true } : { row: e.row, origin: e.origin, presentLocally: e.presentLocally });
2021
+ return limited.map(
2022
+ (e) => stale ? { row: e.row, origin: e.origin, presentLocally: e.presentLocally, stale: true } : { row: e.row, origin: e.origin, presentLocally: e.presentLocally }
2023
+ );
1840
2024
  }
1841
2025
  build(items, state, awaiting) {
1842
2026
  const errors = {};
@@ -1903,7 +2087,11 @@ var SnapshotCache = class {
1903
2087
  const res = await fetcher(signal);
1904
2088
  return { rows: res.rows, asOf: res.asOf, requestedAt: now, fetchedAt: this.clock.now() };
1905
2089
  };
1906
- const promise = this.queryClient ? this.queryClient.fetchQuery({ queryKey: key, queryFn: ({ signal }) => run(signal), staleTime }) : run(controller.signal);
2090
+ const promise = this.queryClient ? this.queryClient.fetchQuery({
2091
+ queryKey: key,
2092
+ queryFn: ({ signal }) => run(signal),
2093
+ staleTime
2094
+ }) : run(controller.signal);
1907
2095
  const entry = { requestedAt: now, promise, result: null, controller };
1908
2096
  this.entries.set(k, entry);
1909
2097
  promise.then(
@@ -1920,6 +2108,8 @@ var SnapshotCache = class {
1920
2108
  invalidate(key) {
1921
2109
  const encoded = JSON.stringify(key);
1922
2110
  this.entries.get(encoded)?.controller.abort(new Error("invalidated"));
2111
+ void this.queryClient?.cancelQueries?.({ queryKey: key, exact: true }).catch(() => {
2112
+ });
1923
2113
  this.entries.delete(encoded);
1924
2114
  }
1925
2115
  /** Cancel every in-flight fetch under a key prefix (identity swap). */
@@ -1958,6 +2148,7 @@ var Bridge = class {
1958
2148
  freshness;
1959
2149
  crud;
1960
2150
  snapshots;
2151
+ backgroundReads = new BackgroundReads();
1961
2152
  epochSnapshot;
1962
2153
  stores = /* @__PURE__ */ new Map();
1963
2154
  gates;
@@ -1971,7 +2162,10 @@ var Bridge = class {
1971
2162
  this.adapter = opts.adapter;
1972
2163
  this.clock = opts.clock ?? realClock;
1973
2164
  this.platform = opts.platform ?? detectPlatform();
1974
- this.gates = { native: opts.platformGates?.native ?? "local-first", web: opts.platformGates?.web ?? "race" };
2165
+ this.gates = {
2166
+ native: opts.platformGates?.native ?? "local-first",
2167
+ web: opts.platformGates?.web ?? "race"
2168
+ };
1975
2169
  this.measure = opts.measure?.sink;
1976
2170
  this.identityOpts = opts.identity ?? {};
1977
2171
  this.onEvicted = opts.onEvicted;
@@ -1998,19 +2192,26 @@ var Bridge = class {
1998
2192
  }
1999
2193
  query(options) {
2000
2194
  const key = `query:${this.epoch}:${infiniteQueryKey(options)}`;
2001
- return this.acquire(key, () => new InfiniteQueryStore({
2002
- driver: this.driver,
2003
- adapter: this.adapter,
2004
- crud: this.crud,
2005
- freshness: this.freshness,
2006
- snapshots: this.snapshots,
2007
- clock: this.clock,
2008
- epoch: this.epoch,
2009
- identityKey: this.identityKey,
2010
- apiLegEnabled: true,
2011
- measure: this.measure,
2012
- connectivity: this.connectivity
2013
- }, options));
2195
+ return this.acquire(
2196
+ key,
2197
+ () => new InfiniteQueryStore(
2198
+ {
2199
+ driver: this.driver,
2200
+ adapter: this.adapter,
2201
+ crud: this.crud,
2202
+ freshness: this.freshness,
2203
+ snapshots: this.snapshots,
2204
+ clock: this.clock,
2205
+ epoch: this.epoch,
2206
+ identityKey: this.identityKey,
2207
+ apiLegEnabled: true,
2208
+ measure: this.measure,
2209
+ connectivity: this.connectivity,
2210
+ backgroundReads: this.backgroundReads
2211
+ },
2212
+ options
2213
+ )
2214
+ );
2014
2215
  }
2015
2216
  /** Resolves once the initial upload-queue read has landed (tests). */
2016
2217
  ready() {
@@ -2025,7 +2226,8 @@ var Bridge = class {
2025
2226
  Object.defineProperty(wrapped, "fetchCredentials", {
2026
2227
  value: async () => {
2027
2228
  const creds = await connector.fetchCredentials();
2028
- if (creds?.token) this.identity.observe(identityKeyFromToken(creds.token, this.identityOpts));
2229
+ if (creds?.token)
2230
+ this.identity.observe(identityKeyFromToken(creds.token, this.identityOpts));
2029
2231
  return creds;
2030
2232
  },
2031
2233
  configurable: true,
@@ -2087,7 +2289,8 @@ var Bridge = class {
2087
2289
  epoch: this.epoch,
2088
2290
  identityKey: this.identityKey,
2089
2291
  apiLegEnabled: this.apiLegEnabled(),
2090
- measure: this.measure
2292
+ measure: this.measure,
2293
+ backgroundReads: this.backgroundReads
2091
2294
  },
2092
2295
  def,
2093
2296
  scope,
@@ -2113,7 +2316,10 @@ var Bridge = class {
2113
2316
  }
2114
2317
  onDemand(name, params, opts = {}) {
2115
2318
  const key = `ondemand:${this.epoch}:${name}:${JSON.stringify(params)}`;
2116
- return this.acquire(key, () => new OnDemandStreamStore(this.driver, this.clock, name, params, opts));
2319
+ return this.acquire(
2320
+ key,
2321
+ () => new OnDemandStreamStore(this.driver, this.clock, name, params, opts)
2322
+ );
2117
2323
  }
2118
2324
  autoSync(config, ids) {
2119
2325
  const key = `flag:${this.epoch}:${config.stream}:${JSON.stringify([...ids].sort())}`;