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.js CHANGED
@@ -1,5 +1,5 @@
1
- import { rowComparator, idOf, keyColumns, cursorOfRow, streamKey, isAfterCursor, FreshnessTracker } from './chunk-KAR4QIIU.js';
2
- export { cursorOfRow, decodeCursor, encodeCursor, keyColumns, rowComparator } from './chunk-KAR4QIIU.js';
1
+ import { rowComparator, idOf, keyColumns, cursorOfRow, streamKey, isAfterCursor, FreshnessTracker } from './chunk-WJDQWVO5.js';
2
+ export { cursorOfRow, decodeCursor, encodeCursor, keyColumns, rowComparator } from './chunk-WJDQWVO5.js';
3
3
 
4
4
  // src/clock.ts
5
5
  var realClock = {
@@ -69,6 +69,89 @@ async function classifyRemovals(ctx, removed) {
69
69
  return out;
70
70
  }
71
71
 
72
+ // src/refresh.ts
73
+ var CoalescedRefresh = class {
74
+ constructor(read, settled) {
75
+ this.read = read;
76
+ this.settled = settled;
77
+ }
78
+ read;
79
+ settled;
80
+ revision = 0;
81
+ lifecycle = 0;
82
+ scheduled = false;
83
+ running = false;
84
+ get pending() {
85
+ return this.scheduled || this.running;
86
+ }
87
+ request() {
88
+ this.revision += 1;
89
+ if (this.pending) return;
90
+ this.scheduled = true;
91
+ const lifecycle = this.lifecycle;
92
+ queueMicrotask(() => {
93
+ if (lifecycle !== this.lifecycle) return;
94
+ this.scheduled = false;
95
+ void this.drain(lifecycle);
96
+ });
97
+ }
98
+ /** Fence queued/late work without retaining results or preventing a later resume. */
99
+ cancel() {
100
+ this.lifecycle += 1;
101
+ this.scheduled = false;
102
+ this.running = false;
103
+ }
104
+ async drain(lifecycle) {
105
+ this.running = true;
106
+ let revision;
107
+ try {
108
+ do {
109
+ revision = this.revision;
110
+ await this.read(() => lifecycle === this.lifecycle && revision === this.revision);
111
+ } while (lifecycle === this.lifecycle && revision !== this.revision);
112
+ } finally {
113
+ if (lifecycle === this.lifecycle) {
114
+ this.running = false;
115
+ this.settled();
116
+ }
117
+ }
118
+ }
119
+ };
120
+ var BackgroundReads = class {
121
+ running = false;
122
+ queue = /* @__PURE__ */ new Set();
123
+ run(read, signal) {
124
+ if (signal.aborted) return Promise.reject(signal.reason);
125
+ return new Promise((resolve, reject) => {
126
+ const entry = {
127
+ cancel: () => {
128
+ if (!this.queue.delete(entry)) return;
129
+ signal.removeEventListener("abort", entry.cancel);
130
+ reject(signal.reason);
131
+ },
132
+ run: () => {
133
+ signal.removeEventListener("abort", entry.cancel);
134
+ this.running = true;
135
+ void Promise.resolve().then(read).then(resolve, reject).finally(() => {
136
+ this.running = false;
137
+ this.drain();
138
+ });
139
+ }
140
+ };
141
+ signal.addEventListener("abort", entry.cancel, { once: true });
142
+ this.queue.add(entry);
143
+ this.drain();
144
+ });
145
+ }
146
+ drain() {
147
+ if (this.running) return;
148
+ const next = this.queue.values().next().value;
149
+ if (!next) return;
150
+ this.queue.delete(next);
151
+ next.run();
152
+ }
153
+ };
154
+
72
155
  // src/store.ts
73
156
  var SnapshotStore = class {
74
157
  snapshot;
@@ -136,12 +219,18 @@ var InfiniteQueryStore = class {
136
219
  this.pageSize = options.pageSize ?? 25;
137
220
  this.maxRows = options.maxRows ?? 500;
138
221
  if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || !Number.isInteger(this.maxRows) || this.maxRows < this.pageSize)
139
- throw new Error("powersync-bridge: pageSize must be positive and maxRows must be at least pageSize");
222
+ throw new Error(
223
+ "powersync-bridge: pageSize must be positive and maxRows must be at least pageSize"
224
+ );
140
225
  this.target = this.pageSize;
141
226
  this.compare = rowComparator(options.orderBy, options.direction ?? "desc");
142
227
  this.table = options.table;
143
228
  this.offline = this.readOffline();
144
229
  this.store = new SnapshotStore(this.result());
230
+ this.checkpointRefresh = new CoalescedRefresh(
231
+ (isCurrent) => this.checkpoint(isCurrent),
232
+ () => this.render()
233
+ );
145
234
  }
146
235
  deps;
147
236
  options;
@@ -170,6 +259,9 @@ var InfiniteQueryStore = class {
170
259
  watchGeneration = 0;
171
260
  lastLocalWatch = 0;
172
261
  refs = 0;
262
+ activeRefs = 0;
263
+ checkpointRefresh;
264
+ localAbort = new AbortController();
173
265
  ready = false;
174
266
  disposed = false;
175
267
  unwatch;
@@ -179,12 +271,19 @@ var InfiniteQueryStore = class {
179
271
  get isDisposed() {
180
272
  return this.disposed || this.refs <= 0 && this.unwatch !== void 0;
181
273
  }
182
- retain() {
274
+ retain(active = true) {
183
275
  this.refs += 1;
184
- if (this.refs !== 1 || this.unwatch || this.options.enabled === false) return;
276
+ if (active) this.activeRefs += 1;
277
+ if (this.activeRefs <= 0 || this.unwatch || this.options.enabled === false || this.disposed)
278
+ return;
279
+ this.offline = this.readOffline();
280
+ this.apiFailed = false;
281
+ this.apiError = void 0;
185
282
  this.stops = [
186
283
  this.deps.crud.onChange(() => this.render()),
187
- this.deps.freshness.onCompletion(() => void this.checkpoint()),
284
+ this.deps.freshness.onCompletion((completion) => {
285
+ if (completion.stream === null) this.checkpointRefresh.request();
286
+ }),
188
287
  this.deps.driver.onStatusChanged(() => this.networkChanged()),
189
288
  this.deps.connectivity.subscribe(() => this.networkChanged())
190
289
  ];
@@ -199,12 +298,13 @@ var InfiniteQueryStore = class {
199
298
  this.render();
200
299
  });
201
300
  }
202
- release() {
301
+ release(active = true) {
203
302
  this.refs -= 1;
204
- if (this.refs <= 0)
205
- queueMicrotask(() => {
206
- if (this.refs <= 0) this.dispose();
207
- });
303
+ if (active) this.activeRefs -= 1;
304
+ queueMicrotask(() => {
305
+ if (this.refs <= 0) this.dispose();
306
+ else if (this.activeRefs <= 0) this.pause();
307
+ });
208
308
  }
209
309
  swapRoot() {
210
310
  this.disposed = true;
@@ -215,13 +315,7 @@ var InfiniteQueryStore = class {
215
315
  }
216
316
  dispose() {
217
317
  this.disposed = true;
218
- this.generation += 1;
219
- this.watchGeneration += 1;
220
- this.cancelRequest();
221
- this.unwatch?.();
222
- this.unwatch = void 0;
223
- for (const stop of this.stops) stop();
224
- this.stops = [];
318
+ this.pause();
225
319
  this.clearPages();
226
320
  this.local.clear();
227
321
  this.retained.clear();
@@ -230,6 +324,19 @@ var InfiniteQueryStore = class {
230
324
  this.inSession.clear();
231
325
  this.render(true);
232
326
  }
327
+ pause() {
328
+ this.generation += 1;
329
+ this.watchGeneration += 1;
330
+ this.checkpointRefresh.cancel();
331
+ this.localAbort.abort();
332
+ this.cancelRequest();
333
+ this.request = void 0;
334
+ this.apiPending = false;
335
+ this.unwatch?.();
336
+ this.unwatch = void 0;
337
+ for (const stop of this.stops) stop();
338
+ this.stops = [];
339
+ }
233
340
  readOffline() {
234
341
  return (this.deps.connectivity.getSnapshot() ?? this.deps.driver.onlineHint()) === false;
235
342
  }
@@ -248,6 +355,7 @@ var InfiniteQueryStore = class {
248
355
  this.generation += 1;
249
356
  this.apiPending = false;
250
357
  this.request = void 0;
358
+ this.watch();
251
359
  }
252
360
  if (reconnect) {
253
361
  this.apiFailed = false;
@@ -274,7 +382,11 @@ var InfiniteQueryStore = class {
274
382
  return this.options.query({ cursor: this.anchor, limit: this.target + 1 });
275
383
  }
276
384
  watch() {
385
+ if (this.activeRefs <= 0 || this.disposed) return;
277
386
  this.unwatch?.();
387
+ this.localAbort.abort();
388
+ this.localAbort = new AbortController();
389
+ this.checkpointRefresh.cancel();
278
390
  const token = ++this.watchGeneration;
279
391
  const generation = this.generation;
280
392
  this.localPending = true;
@@ -295,14 +407,16 @@ var InfiniteQueryStore = class {
295
407
  this.unwatch = this.deps.driver.diffs(prepared).subscribe(
296
408
  (diff) => {
297
409
  void this.acceptLocal(diff.all, token, generation).catch((error) => {
298
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
410
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
411
+ return;
299
412
  this.localPending = false;
300
413
  this.localError = error instanceof Error ? error : new Error(String(error));
301
414
  this.render();
302
415
  });
303
416
  },
304
417
  (error) => {
305
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
418
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
419
+ return;
306
420
  this.localPending = false;
307
421
  this.localError = error;
308
422
  this.render();
@@ -310,9 +424,10 @@ var InfiniteQueryStore = class {
310
424
  );
311
425
  this.render();
312
426
  }
313
- async acceptLocal(rows, token, generation) {
427
+ async acceptLocal(rows, token, generation, stamp = this.deps.freshness.freshAtFor(void 0), isCurrent = () => true) {
314
428
  await this.deps.crud.resync();
315
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
429
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
430
+ return;
316
431
  const previous = this.local;
317
432
  const firstEmission = this.lastLocalWatch !== token;
318
433
  this.lastLocalWatch = token;
@@ -333,7 +448,7 @@ var InfiniteQueryStore = class {
333
448
  this.local = next;
334
449
  this.localPending = false;
335
450
  this.localError = void 0;
336
- this.localStamp = this.deps.freshness.freshAtFor(void 0);
451
+ this.localStamp = stamp;
337
452
  this.render();
338
453
  const groups = /* @__PURE__ */ new Map();
339
454
  for (const row of removed.values()) {
@@ -354,7 +469,8 @@ var InfiniteQueryStore = class {
354
469
  },
355
470
  group
356
471
  );
357
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
472
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
473
+ return;
358
474
  for (const row of removed.values()) {
359
475
  const identity = this.identity(row);
360
476
  if (identity.table !== table || this.local.has(row.id)) continue;
@@ -389,15 +505,21 @@ var InfiniteQueryStore = class {
389
505
  }
390
506
  this.render();
391
507
  }
392
- async checkpoint() {
393
- if (this.disposed || !this.unwatch) return;
508
+ async checkpoint(isCurrent) {
509
+ if (this.disposed || !this.unwatch || this.activeRefs <= 0) return;
394
510
  const token = this.watchGeneration;
395
511
  const generation = this.generation;
512
+ const stamp = this.deps.freshness.freshAtFor(void 0);
396
513
  try {
397
- const rows = await this.deps.driver.query(this.deps.adapter.prepare(this.localBuilder()));
398
- await this.acceptLocal(rows, token, generation);
514
+ const signal = this.localAbort.signal;
515
+ const read = () => isCurrent() ? this.deps.driver.query(this.deps.adapter.prepare(this.localBuilder()), signal) : Promise.resolve(void 0);
516
+ const rows = await (this.deps.backgroundReads ? this.deps.backgroundReads.run(read, signal) : read());
517
+ if (!rows) return;
518
+ if (!isCurrent()) return;
519
+ await this.acceptLocal(rows, token, generation, stamp, isCurrent);
399
520
  } catch (error) {
400
- if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
521
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
522
+ return;
401
523
  this.localError = error instanceof Error ? error : new Error(String(error));
402
524
  this.render();
403
525
  }
@@ -422,7 +544,7 @@ var InfiniteQueryStore = class {
422
544
  return [row[first], row[second]];
423
545
  }
424
546
  pump() {
425
- if (!this.ready || this.disposed || this.options.enabled === false || this.offline || this.apiPending || this.apiFailed || this.apiEnd())
547
+ if (!this.ready || this.activeRefs <= 0 || this.disposed || this.options.enabled === false || this.offline || this.apiPending || this.apiFailed || this.apiEnd())
426
548
  return;
427
549
  if (!this.table && !this.options.getRowIdentity) return;
428
550
  if (this.pages.length >= Math.ceil(this.maxRows / this.pageSize) + 1) return;
@@ -468,7 +590,9 @@ var InfiniteQueryStore = class {
468
590
  const before = rows[index - 1];
469
591
  const row = rows[index];
470
592
  if (before && row && this.compare(before, row) >= 0)
471
- throw new Error("powersync-bridge: API rows must use a strict, stable order with an id tiebreaker");
593
+ throw new Error(
594
+ "powersync-bridge: API rows must use a strict, stable order with an id tiebreaker"
595
+ );
472
596
  }
473
597
  if (cursor !== void 0 && rows.some((row) => this.compare(row, cursor) <= 0))
474
598
  throw new Error("powersync-bridge: API page must advance beyond the supplied cursor");
@@ -561,14 +685,15 @@ var InfiniteQueryStore = class {
561
685
  this.store.replace(result);
562
686
  }
563
687
  fetchNextPage = () => {
564
- if (this.disposed || this.options.enabled === false || this.target >= this.maxRows) return;
688
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false || this.target >= this.maxRows)
689
+ return;
565
690
  if (!this.store.getSnapshot().hasNextPage) return;
566
691
  this.target = Math.min(this.target + this.pageSize, this.maxRows);
567
692
  this.watch();
568
693
  this.pump();
569
694
  };
570
695
  retry = () => {
571
- if (this.disposed || this.options.enabled === false) return;
696
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
572
697
  this.apiFailed = false;
573
698
  this.apiError = void 0;
574
699
  this.watch();
@@ -582,7 +707,7 @@ var InfiniteQueryStore = class {
582
707
  if (last) this.reset(last);
583
708
  };
584
709
  reset(anchor) {
585
- if (this.disposed || this.options.enabled === false) return;
710
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
586
711
  this.generation += 1;
587
712
  this.cancelRequest();
588
713
  this.request = void 0;
@@ -1336,6 +1461,10 @@ var RacedListStore = class {
1336
1461
  this.compare = rowComparator(def.orderKey, def.direction ?? "asc");
1337
1462
  this.table = def.table ?? def.id;
1338
1463
  this.startedAt = deps.clock.now();
1464
+ this.checkpointRefresh = new CoalescedRefresh(
1465
+ (isCurrent) => this.onCompletion(isCurrent),
1466
+ () => this.reconcile()
1467
+ );
1339
1468
  }
1340
1469
  deps;
1341
1470
  def;
@@ -1345,6 +1474,9 @@ var RacedListStore = class {
1345
1474
  compare;
1346
1475
  table;
1347
1476
  abort = new AbortController();
1477
+ observation = 0;
1478
+ checkpointRefresh;
1479
+ localFreshAt = Number.NEGATIVE_INFINITY;
1348
1480
  startedAt;
1349
1481
  snap = null;
1350
1482
  snapPending = false;
@@ -1363,17 +1495,21 @@ var RacedListStore = class {
1363
1495
  tombstones = /* @__PURE__ */ new Set();
1364
1496
  disposed = false;
1365
1497
  refs = 0;
1498
+ activeRefs = 0;
1366
1499
  measured = false;
1367
1500
  localMs = null;
1368
1501
  apiMs = null;
1369
- retain() {
1502
+ retain(active = true) {
1370
1503
  this.refs += 1;
1371
- if (this.refs === 1 && this.unsubDiffs === null) this.start();
1504
+ if (active) this.activeRefs += 1;
1505
+ if (this.activeRefs > 0 && this.unsubDiffs === null) this.start();
1372
1506
  }
1373
- release() {
1507
+ release(active = true) {
1374
1508
  this.refs -= 1;
1375
- if (this.refs <= 0) queueMicrotask(() => {
1509
+ if (active) this.activeRefs -= 1;
1510
+ queueMicrotask(() => {
1376
1511
  if (this.refs <= 0) this.dispose();
1512
+ else if (this.activeRefs <= 0) this.pause();
1377
1513
  });
1378
1514
  }
1379
1515
  get isDisposed() {
@@ -1386,10 +1522,7 @@ var RacedListStore = class {
1386
1522
  }
1387
1523
  dispose() {
1388
1524
  this.disposed = true;
1389
- this.abort.abort(new Error("disposed"));
1390
- this.unsubDiffs?.();
1391
- this.unsubCrud?.();
1392
- this.unsubFresh?.();
1525
+ this.pause();
1393
1526
  if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
1394
1527
  this.snapWindow = null;
1395
1528
  this.local?.clear();
@@ -1401,39 +1534,71 @@ var RacedListStore = class {
1401
1534
  this.tombstones.clear();
1402
1535
  this.store.replace(emptyRacedResult());
1403
1536
  }
1537
+ pause() {
1538
+ this.observation += 1;
1539
+ this.checkpointRefresh.cancel();
1540
+ this.abort.abort(new Error("observer stopped"));
1541
+ this.unsubDiffs?.();
1542
+ this.unsubCrud?.();
1543
+ this.unsubFresh?.();
1544
+ this.unsubDiffs = null;
1545
+ this.unsubCrud = null;
1546
+ this.unsubFresh = null;
1547
+ if (this.snapPending) {
1548
+ this.deps.snapshots.invalidate(this.snapshotKey());
1549
+ if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
1550
+ this.snapWindow = this.snap?.window ?? null;
1551
+ this.snapPending = false;
1552
+ }
1553
+ }
1404
1554
  // ---------------------------------------------------------------------------
1405
1555
  start() {
1556
+ if (this.disposed) return;
1557
+ this.abort = new AbortController();
1558
+ const observation = ++this.observation;
1406
1559
  const { driver, adapter, crud, freshness } = this.deps;
1407
1560
  const prepared = adapter.prepare(this.def.query(this.scope));
1408
1561
  this.prepared = prepared;
1409
1562
  this.unsubDiffs = driver.diffs(prepared).subscribe(
1410
- (diff) => void this.onDiff(diff),
1563
+ (diff) => {
1564
+ if (observation === this.observation) void this.onDiff(diff);
1565
+ },
1411
1566
  (err) => {
1567
+ if (observation !== this.observation) return;
1412
1568
  this.localError = err;
1413
1569
  this.reconcile();
1414
1570
  }
1415
1571
  );
1416
1572
  this.unsubCrud = crud.onChange(() => this.reconcile());
1417
- this.unsubFresh = freshness.onCompletion(() => void this.onCompletion());
1418
- if (this.deps.apiLegEnabled) this.startApiLeg();
1573
+ this.unsubFresh = freshness.onCompletion((completion) => {
1574
+ if (completion.stream === null || this.def.stream && completion.stream === streamKey(this.def.stream)) {
1575
+ if (!this.owned) this.checkpointRefresh.request();
1576
+ }
1577
+ });
1578
+ if (this.deps.apiLegEnabled && !this.owned) this.startApiLeg();
1419
1579
  this.reconcile();
1420
1580
  }
1421
1581
  startApiLeg() {
1582
+ const observation = this.observation;
1583
+ const abort = this.abort;
1422
1584
  const { snapshots, crud } = this.deps;
1423
1585
  const staleTime = this.def.staleTime ?? 10 * 6e4;
1424
1586
  this.snapPending = true;
1425
1587
  const window = crud.openWindow();
1426
1588
  this.snapWindow = window;
1427
- const key = ["powersync-bridge", this.deps.identityKey, this.deps.epoch, this.def.id, this.scopeKey];
1589
+ const key = this.snapshotKey();
1428
1590
  snapshots.fetch(key, staleTime, (signal) => {
1429
1591
  const combined = new AbortController();
1430
1592
  const onAbort = () => combined.abort();
1431
1593
  signal.addEventListener("abort", onAbort, { once: true });
1432
- this.abort.signal.addEventListener("abort", onAbort, { once: true });
1433
- return this.def.fetchSnapshot(this.scope, combined.signal);
1594
+ abort.signal.addEventListener("abort", onAbort, { once: true });
1595
+ return this.def.fetchSnapshot(this.scope, combined.signal).finally(() => {
1596
+ signal.removeEventListener("abort", onAbort);
1597
+ abort.signal.removeEventListener("abort", onAbort);
1598
+ });
1434
1599
  }).then(
1435
1600
  (record) => {
1436
- if (this.disposed) return;
1601
+ if (this.disposed || observation !== this.observation) return;
1437
1602
  this.snapPending = false;
1438
1603
  this.apiMs = this.deps.clock.now() - this.startedAt;
1439
1604
  const rows = /* @__PURE__ */ new Map();
@@ -1441,32 +1606,38 @@ var RacedListStore = class {
1441
1606
  const item = this.def.mapApi(raw);
1442
1607
  rows.set(idOf(item), item);
1443
1608
  }
1609
+ if (this.snap) crud.closeWindow(this.snap.window);
1444
1610
  this.snap = { rows, record, window };
1445
1611
  this.reconcile();
1446
1612
  },
1447
1613
  (err) => {
1448
- if (this.disposed) return;
1614
+ if (this.disposed || observation !== this.observation) return;
1449
1615
  this.snapPending = false;
1450
1616
  this.snapError = err instanceof Error ? err : new Error(String(err));
1451
1617
  crud.closeWindow(window);
1452
- this.snapWindow = null;
1618
+ this.snapWindow = this.snap?.window ?? null;
1453
1619
  this.reconcile();
1454
1620
  }
1455
1621
  );
1456
1622
  }
1623
+ snapshotKey() {
1624
+ return ["powersync-bridge", this.deps.identityKey, this.deps.epoch, this.def.id, this.scopeKey];
1625
+ }
1457
1626
  /**
1458
1627
  * A checkpoint completed. Status events and diff emissions are separate SDK events with no
1459
1628
  * ordering guarantee; taking ownership on the stale local map would render the pre-checkpoint
1460
1629
  * value for one frame (fresh→old→fresh). Pre-ownership we re-query once, then reconcile.
1461
1630
  */
1462
- async onCompletion() {
1463
- if (this.disposed || this.owned || !this.prepared || !this.emissionSeen) {
1464
- this.reconcile();
1465
- return;
1466
- }
1631
+ async onCompletion(isCurrent) {
1632
+ if (this.disposed || this.owned || !this.prepared || this.activeRefs <= 0) return;
1633
+ const freshAt = this.deps.freshness.freshAtFor(this.def.stream);
1467
1634
  try {
1468
- const rows = await this.deps.driver.query(this.prepared, this.abort.signal);
1469
- if (this.disposed) return;
1635
+ const prepared = this.prepared;
1636
+ const signal = this.abort.signal;
1637
+ const read = () => isCurrent() ? this.deps.driver.query(prepared, signal) : Promise.resolve(void 0);
1638
+ const rows = await (this.deps.backgroundReads ? this.deps.backgroundReads.run(read, signal) : read());
1639
+ if (!rows) return;
1640
+ if (this.disposed || !isCurrent()) return;
1470
1641
  const prev = this.local ?? /* @__PURE__ */ new Map();
1471
1642
  const next = /* @__PURE__ */ new Map();
1472
1643
  for (const row of rows) {
@@ -1479,12 +1650,16 @@ var RacedListStore = class {
1479
1650
  }
1480
1651
  }
1481
1652
  this.local = next;
1653
+ this.localFreshAt = Math.max(this.localFreshAt, freshAt);
1654
+ this.emissionSeen = true;
1655
+ this.localError = void 0;
1482
1656
  } catch {
1483
1657
  }
1484
1658
  this.reconcile();
1485
1659
  }
1486
1660
  async onDiff(diff) {
1487
- if (this.disposed) return;
1661
+ if (this.disposed || this.activeRefs <= 0) return;
1662
+ const observation = this.observation;
1488
1663
  const { crud } = this.deps;
1489
1664
  const prev = this.local;
1490
1665
  const next = /* @__PURE__ */ new Map();
@@ -1497,6 +1672,8 @@ var RacedListStore = class {
1497
1672
  }
1498
1673
  }
1499
1674
  this.local = next;
1675
+ if (!this.checkpointRefresh.pending)
1676
+ this.localFreshAt = this.deps.freshness.freshAtFor(this.def.stream);
1500
1677
  if (!this.emissionSeen) {
1501
1678
  this.emissionSeen = true;
1502
1679
  this.localMs = this.deps.clock.now() - this.startedAt;
@@ -1504,15 +1681,13 @@ var RacedListStore = class {
1504
1681
  void crud.resync();
1505
1682
  if (this.owned && prev) {
1506
1683
  const removed = /* @__PURE__ */ new Map();
1507
- for (const id of diff.removed) {
1508
- const row = prev.get(id);
1509
- if (row) removed.set(id, row);
1510
- }
1511
- if (removed.size > 0) await this.applyRemovals(removed);
1684
+ for (const [id, row] of prev) if (!next.has(id)) removed.set(id, row);
1685
+ if (removed.size > 0) await this.applyRemovals(removed, observation);
1512
1686
  }
1687
+ if (observation !== this.observation) return;
1513
1688
  this.reconcile();
1514
1689
  }
1515
- async applyRemovals(removed) {
1690
+ async applyRemovals(removed, observation) {
1516
1691
  const classes = await classifyRemovals(
1517
1692
  {
1518
1693
  driver: this.deps.driver,
@@ -1523,7 +1698,7 @@ var RacedListStore = class {
1523
1698
  },
1524
1699
  removed
1525
1700
  );
1526
- if (this.disposed) return;
1701
+ if (this.disposed || observation !== this.observation) return;
1527
1702
  for (const [id, cls] of classes) {
1528
1703
  const entry = this.overlay.get(id);
1529
1704
  switch (cls) {
@@ -1546,18 +1721,24 @@ var RacedListStore = class {
1546
1721
  if (this.disposed) return;
1547
1722
  const result = this.owned ? this.renderOwned() : this.renderRace();
1548
1723
  const prev = this.store.getSnapshot();
1549
- if (shallowEqualResult(prev, result)) return;
1724
+ if (shallowEqualResult(
1725
+ prev,
1726
+ result
1727
+ ))
1728
+ return;
1550
1729
  this.store.replace(result);
1551
1730
  this.report(result);
1552
1731
  }
1553
1732
  freshAt() {
1554
- return this.deps.freshness.freshAtFor(this.def.stream);
1733
+ return this.localFreshAt;
1555
1734
  }
1556
1735
  localFresh() {
1557
- return this.emissionSeen && this.deps.freshness.isFresh(this.def.stream);
1736
+ const connectedAt = this.deps.freshness.sessionConnectAtMs;
1737
+ return this.emissionSeen && connectedAt !== null && this.localFreshAt >= connectedAt;
1558
1738
  }
1559
1739
  tryTakeOwnership() {
1560
1740
  if (this.owned) return true;
1741
+ if (this.checkpointRefresh.pending) return false;
1561
1742
  const apiFreshAt = this.snap?.record.fetchedAt ?? Number.NEGATIVE_INFINITY;
1562
1743
  if (!(this.localFresh() && this.freshAt() > apiFreshAt)) return false;
1563
1744
  this.owned = true;
@@ -1567,7 +1748,8 @@ var RacedListStore = class {
1567
1748
  if (this.snap) {
1568
1749
  if (this.def.apiScopeSupersetOfLocal) {
1569
1750
  for (const [id, row] of this.snap.rows) {
1570
- if (!overlay.has(id) && !this.tombstones.has(id)) overlay.set(id, { row, origin: "api", presentLocally: false });
1751
+ if (!overlay.has(id) && !this.tombstones.has(id))
1752
+ overlay.set(id, { row, origin: "api", presentLocally: false });
1571
1753
  }
1572
1754
  }
1573
1755
  this.deps.crud.closeWindow(this.snap.window);
@@ -1614,11 +1796,14 @@ var RacedListStore = class {
1614
1796
  const inS = S?.rows.get(id);
1615
1797
  const pending = crud.pendingOp(this.table, id, S?.window);
1616
1798
  if (pending) {
1617
- if (inLocal !== void 0) entries.push({ row: inLocal, origin: "local", presentLocally: true });
1799
+ if (inLocal !== void 0)
1800
+ entries.push({ row: inLocal, origin: "local", presentLocally: true });
1618
1801
  continue;
1619
1802
  }
1620
1803
  if (inLocal !== void 0 && inS !== void 0) {
1621
- entries.push(apiFresher ? { row: inS, origin: "api", presentLocally: true } : { row: inLocal, origin: "local", presentLocally: true });
1804
+ entries.push(
1805
+ apiFresher ? { row: inS, origin: "api", presentLocally: true } : { row: inLocal, origin: "local", presentLocally: true }
1806
+ );
1622
1807
  continue;
1623
1808
  }
1624
1809
  if (inLocal !== void 0) {
@@ -1642,7 +1827,9 @@ var RacedListStore = class {
1642
1827
  finalize(entries, stale = false) {
1643
1828
  entries.sort((a, b) => this.compare(a.row, b.row));
1644
1829
  const limited = entries.slice(0, this.def.limit);
1645
- 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 });
1830
+ return limited.map(
1831
+ (e) => stale ? { row: e.row, origin: e.origin, presentLocally: e.presentLocally, stale: true } : { row: e.row, origin: e.origin, presentLocally: e.presentLocally }
1832
+ );
1646
1833
  }
1647
1834
  build(items, state, awaiting) {
1648
1835
  const errors = {};
@@ -1709,7 +1896,11 @@ var SnapshotCache = class {
1709
1896
  const res = await fetcher(signal);
1710
1897
  return { rows: res.rows, asOf: res.asOf, requestedAt: now, fetchedAt: this.clock.now() };
1711
1898
  };
1712
- const promise = this.queryClient ? this.queryClient.fetchQuery({ queryKey: key, queryFn: ({ signal }) => run(signal), staleTime }) : run(controller.signal);
1899
+ const promise = this.queryClient ? this.queryClient.fetchQuery({
1900
+ queryKey: key,
1901
+ queryFn: ({ signal }) => run(signal),
1902
+ staleTime
1903
+ }) : run(controller.signal);
1713
1904
  const entry = { requestedAt: now, promise, result: null, controller };
1714
1905
  this.entries.set(k, entry);
1715
1906
  promise.then(
@@ -1726,6 +1917,8 @@ var SnapshotCache = class {
1726
1917
  invalidate(key) {
1727
1918
  const encoded = JSON.stringify(key);
1728
1919
  this.entries.get(encoded)?.controller.abort(new Error("invalidated"));
1920
+ void this.queryClient?.cancelQueries?.({ queryKey: key, exact: true }).catch(() => {
1921
+ });
1729
1922
  this.entries.delete(encoded);
1730
1923
  }
1731
1924
  /** Cancel every in-flight fetch under a key prefix (identity swap). */
@@ -1764,6 +1957,7 @@ var Bridge = class {
1764
1957
  freshness;
1765
1958
  crud;
1766
1959
  snapshots;
1960
+ backgroundReads = new BackgroundReads();
1767
1961
  epochSnapshot;
1768
1962
  stores = /* @__PURE__ */ new Map();
1769
1963
  gates;
@@ -1777,7 +1971,10 @@ var Bridge = class {
1777
1971
  this.adapter = opts.adapter;
1778
1972
  this.clock = opts.clock ?? realClock;
1779
1973
  this.platform = opts.platform ?? detectPlatform();
1780
- this.gates = { native: opts.platformGates?.native ?? "local-first", web: opts.platformGates?.web ?? "race" };
1974
+ this.gates = {
1975
+ native: opts.platformGates?.native ?? "local-first",
1976
+ web: opts.platformGates?.web ?? "race"
1977
+ };
1781
1978
  this.measure = opts.measure?.sink;
1782
1979
  this.identityOpts = opts.identity ?? {};
1783
1980
  this.onEvicted = opts.onEvicted;
@@ -1804,19 +2001,26 @@ var Bridge = class {
1804
2001
  }
1805
2002
  query(options) {
1806
2003
  const key = `query:${this.epoch}:${infiniteQueryKey(options)}`;
1807
- return this.acquire(key, () => new InfiniteQueryStore({
1808
- driver: this.driver,
1809
- adapter: this.adapter,
1810
- crud: this.crud,
1811
- freshness: this.freshness,
1812
- snapshots: this.snapshots,
1813
- clock: this.clock,
1814
- epoch: this.epoch,
1815
- identityKey: this.identityKey,
1816
- apiLegEnabled: true,
1817
- measure: this.measure,
1818
- connectivity: this.connectivity
1819
- }, options));
2004
+ return this.acquire(
2005
+ key,
2006
+ () => new InfiniteQueryStore(
2007
+ {
2008
+ driver: this.driver,
2009
+ adapter: this.adapter,
2010
+ crud: this.crud,
2011
+ freshness: this.freshness,
2012
+ snapshots: this.snapshots,
2013
+ clock: this.clock,
2014
+ epoch: this.epoch,
2015
+ identityKey: this.identityKey,
2016
+ apiLegEnabled: true,
2017
+ measure: this.measure,
2018
+ connectivity: this.connectivity,
2019
+ backgroundReads: this.backgroundReads
2020
+ },
2021
+ options
2022
+ )
2023
+ );
1820
2024
  }
1821
2025
  /** Resolves once the initial upload-queue read has landed (tests). */
1822
2026
  ready() {
@@ -1831,7 +2035,8 @@ var Bridge = class {
1831
2035
  Object.defineProperty(wrapped, "fetchCredentials", {
1832
2036
  value: async () => {
1833
2037
  const creds = await connector.fetchCredentials();
1834
- if (creds?.token) this.identity.observe(identityKeyFromToken(creds.token, this.identityOpts));
2038
+ if (creds?.token)
2039
+ this.identity.observe(identityKeyFromToken(creds.token, this.identityOpts));
1835
2040
  return creds;
1836
2041
  },
1837
2042
  configurable: true,
@@ -1893,7 +2098,8 @@ var Bridge = class {
1893
2098
  epoch: this.epoch,
1894
2099
  identityKey: this.identityKey,
1895
2100
  apiLegEnabled: this.apiLegEnabled(),
1896
- measure: this.measure
2101
+ measure: this.measure,
2102
+ backgroundReads: this.backgroundReads
1897
2103
  },
1898
2104
  def,
1899
2105
  scope,
@@ -1919,7 +2125,10 @@ var Bridge = class {
1919
2125
  }
1920
2126
  onDemand(name, params, opts = {}) {
1921
2127
  const key = `ondemand:${this.epoch}:${name}:${JSON.stringify(params)}`;
1922
- return this.acquire(key, () => new OnDemandStreamStore(this.driver, this.clock, name, params, opts));
2128
+ return this.acquire(
2129
+ key,
2130
+ () => new OnDemandStreamStore(this.driver, this.clock, name, params, opts)
2131
+ );
1923
2132
  }
1924
2133
  autoSync(config, ids) {
1925
2134
  const key = `flag:${this.epoch}:${config.stream}:${JSON.stringify([...ids].sort())}`;