powersync-bridge 0.0.1-alpha.0 → 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, cursorOfRow, streamKey, isAfterCursor, FreshnessTracker } from './chunk-TANEBEGU.js';
2
- export { cursorOfRow, decodeCursor, encodeCursor, keyColumns, rowComparator } from './chunk-TANEBEGU.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;
@@ -76,13 +159,14 @@ var SnapshotStore = class {
76
159
  constructor(initial) {
77
160
  this.snapshot = initial;
78
161
  }
79
- getSnapshot() {
162
+ // React invokes external-store callbacks as standalone functions.
163
+ getSnapshot = () => {
80
164
  return this.snapshot;
81
- }
82
- subscribe(listener) {
165
+ };
166
+ subscribe = (listener) => {
83
167
  this.listeners.add(listener);
84
168
  return () => this.listeners.delete(listener);
85
- }
169
+ };
86
170
  replace(next) {
87
171
  if (Object.is(next, this.snapshot)) return;
88
172
  this.snapshot = next;
@@ -113,6 +197,538 @@ function shallowEqualResult(a, b) {
113
197
  return true;
114
198
  }
115
199
 
200
+ // src/query.ts
201
+ var infiniteQueryKey = (options) => JSON.stringify([
202
+ options.queryKey,
203
+ options.orderBy,
204
+ options.direction ?? "desc",
205
+ options.pageSize ?? 25,
206
+ options.maxRows ?? 500,
207
+ options.enabled !== false
208
+ ]);
209
+ var OfflineError = class extends Error {
210
+ name = "OfflineError";
211
+ constructor() {
212
+ super("Additional records require a connection.");
213
+ }
214
+ };
215
+ var InfiniteQueryStore = class {
216
+ constructor(deps, options) {
217
+ this.deps = deps;
218
+ this.options = options;
219
+ this.pageSize = options.pageSize ?? 25;
220
+ this.maxRows = options.maxRows ?? 500;
221
+ if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || !Number.isInteger(this.maxRows) || this.maxRows < this.pageSize)
222
+ throw new Error(
223
+ "powersync-bridge: pageSize must be positive and maxRows must be at least pageSize"
224
+ );
225
+ this.target = this.pageSize;
226
+ this.compare = rowComparator(options.orderBy, options.direction ?? "desc");
227
+ this.table = options.table;
228
+ this.offline = this.readOffline();
229
+ this.store = new SnapshotStore(this.result());
230
+ this.checkpointRefresh = new CoalescedRefresh(
231
+ (isCurrent) => this.checkpoint(isCurrent),
232
+ () => this.render()
233
+ );
234
+ }
235
+ deps;
236
+ options;
237
+ store;
238
+ pageSize;
239
+ maxRows;
240
+ compare;
241
+ table;
242
+ target;
243
+ anchor;
244
+ revision = 0;
245
+ local = /* @__PURE__ */ new Map();
246
+ retained = /* @__PURE__ */ new Map();
247
+ tombstones = /* @__PURE__ */ new Set();
248
+ filtered = /* @__PURE__ */ new Set();
249
+ inSession = /* @__PURE__ */ new Set();
250
+ pages = [];
251
+ localPending = true;
252
+ localStamp = Number.NEGATIVE_INFINITY;
253
+ localError;
254
+ apiError;
255
+ apiPending = false;
256
+ apiFailed = false;
257
+ offline;
258
+ generation = 0;
259
+ watchGeneration = 0;
260
+ lastLocalWatch = 0;
261
+ refs = 0;
262
+ activeRefs = 0;
263
+ checkpointRefresh;
264
+ localAbort = new AbortController();
265
+ ready = false;
266
+ disposed = false;
267
+ unwatch;
268
+ stops = [];
269
+ request;
270
+ requestKey;
271
+ get isDisposed() {
272
+ return this.disposed || this.refs <= 0 && this.unwatch !== void 0;
273
+ }
274
+ retain(active = true) {
275
+ this.refs += 1;
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;
282
+ this.stops = [
283
+ this.deps.crud.onChange(() => this.render()),
284
+ this.deps.freshness.onCompletion((completion) => {
285
+ if (completion.stream === null) this.checkpointRefresh.request();
286
+ }),
287
+ this.deps.driver.onStatusChanged(() => this.networkChanged()),
288
+ this.deps.connectivity.subscribe(() => this.networkChanged())
289
+ ];
290
+ this.watch();
291
+ void this.deps.crud.ready().then(() => {
292
+ this.ready = true;
293
+ this.pump();
294
+ }).catch((error) => {
295
+ if (this.disposed) return;
296
+ this.localPending = false;
297
+ this.localError = error instanceof Error ? error : new Error(String(error));
298
+ this.render();
299
+ });
300
+ }
301
+ release(active = true) {
302
+ this.refs -= 1;
303
+ if (active) this.activeRefs -= 1;
304
+ queueMicrotask(() => {
305
+ if (this.refs <= 0) this.dispose();
306
+ else if (this.activeRefs <= 0) this.pause();
307
+ });
308
+ }
309
+ swapRoot() {
310
+ this.disposed = true;
311
+ this.local.clear();
312
+ this.retained.clear();
313
+ this.clearPages();
314
+ this.render(true);
315
+ }
316
+ dispose() {
317
+ this.disposed = true;
318
+ this.pause();
319
+ this.clearPages();
320
+ this.local.clear();
321
+ this.retained.clear();
322
+ this.tombstones.clear();
323
+ this.filtered.clear();
324
+ this.inSession.clear();
325
+ this.render(true);
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
+ }
340
+ readOffline() {
341
+ return (this.deps.connectivity.getSnapshot() ?? this.deps.driver.onlineHint()) === false;
342
+ }
343
+ cancelRequest() {
344
+ this.request?.abort();
345
+ if (this.requestKey) this.deps.snapshots.invalidate(this.requestKey);
346
+ this.requestKey = void 0;
347
+ }
348
+ networkChanged() {
349
+ if (this.disposed) return;
350
+ const offline = this.readOffline();
351
+ const reconnect = this.offline && !offline;
352
+ this.offline = offline;
353
+ if (offline && this.request) {
354
+ this.cancelRequest();
355
+ this.generation += 1;
356
+ this.apiPending = false;
357
+ this.request = void 0;
358
+ this.watch();
359
+ }
360
+ if (reconnect) {
361
+ this.apiFailed = false;
362
+ this.apiError = void 0;
363
+ this.pump();
364
+ }
365
+ this.render();
366
+ }
367
+ identity(row) {
368
+ if (this.options.getRowIdentity) return this.options.getRowIdentity(row);
369
+ if (!this.table)
370
+ throw new Error(
371
+ "powersync-bridge: provide table or getRowIdentity when the adapter cannot infer the business table"
372
+ );
373
+ return { table: this.table, id: idOf(row) };
374
+ }
375
+ pending(row) {
376
+ const { table, id } = this.identity(row);
377
+ return Boolean(
378
+ this.deps.crud.pendingOp(table, id) || this.pages.some((page) => this.deps.crud.pendingOp(table, id, page.window))
379
+ );
380
+ }
381
+ localBuilder() {
382
+ return this.options.query({ cursor: this.anchor, limit: this.target + 1 });
383
+ }
384
+ watch() {
385
+ if (this.activeRefs <= 0 || this.disposed) return;
386
+ this.unwatch?.();
387
+ this.localAbort.abort();
388
+ this.localAbort = new AbortController();
389
+ this.checkpointRefresh.cancel();
390
+ const token = ++this.watchGeneration;
391
+ const generation = this.generation;
392
+ this.localPending = true;
393
+ let prepared;
394
+ try {
395
+ prepared = this.deps.adapter.prepare(this.localBuilder());
396
+ if (!this.table && prepared.tables.length === 1) this.table = prepared.tables[0];
397
+ if (!this.table && !this.options.getRowIdentity)
398
+ throw new Error(
399
+ "powersync-bridge: provide table or getRowIdentity when the adapter cannot infer the business table"
400
+ );
401
+ } catch (error) {
402
+ this.localPending = false;
403
+ this.localError = error instanceof Error ? error : new Error(String(error));
404
+ this.render();
405
+ return;
406
+ }
407
+ this.unwatch = this.deps.driver.diffs(prepared).subscribe(
408
+ (diff) => {
409
+ void this.acceptLocal(diff.all, token, generation).catch((error) => {
410
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
411
+ return;
412
+ this.localPending = false;
413
+ this.localError = error instanceof Error ? error : new Error(String(error));
414
+ this.render();
415
+ });
416
+ },
417
+ (error) => {
418
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
419
+ return;
420
+ this.localPending = false;
421
+ this.localError = error;
422
+ this.render();
423
+ }
424
+ );
425
+ this.render();
426
+ }
427
+ async acceptLocal(rows, token, generation, stamp = this.deps.freshness.freshAtFor(void 0), isCurrent = () => true) {
428
+ await this.deps.crud.resync();
429
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
430
+ return;
431
+ const previous = this.local;
432
+ const firstEmission = this.lastLocalWatch !== token;
433
+ this.lastLocalWatch = token;
434
+ const next = new Map(rows.map((row) => [idOf(row), row]));
435
+ const removed = /* @__PURE__ */ new Map();
436
+ for (const [id, row] of previous)
437
+ if (!next.has(id)) {
438
+ removed.set(id, row);
439
+ this.tombstones.add(id);
440
+ }
441
+ for (const [id, row] of next) {
442
+ if (previous.size && (!firstEmission || previous.has(id)) && JSON.stringify(previous.get(id)) !== JSON.stringify(row) && !this.pending(row))
443
+ this.inSession.add(id);
444
+ this.tombstones.delete(id);
445
+ this.filtered.delete(id);
446
+ this.retained.delete(id);
447
+ }
448
+ this.local = next;
449
+ this.localPending = false;
450
+ this.localError = void 0;
451
+ this.localStamp = stamp;
452
+ this.render();
453
+ const groups = /* @__PURE__ */ new Map();
454
+ for (const row of removed.values()) {
455
+ const identity = this.identity(row);
456
+ const group = groups.get(identity.table) ?? /* @__PURE__ */ new Map();
457
+ group.set(identity.id, row);
458
+ groups.set(identity.table, group);
459
+ }
460
+ try {
461
+ for (const [table, group] of groups) {
462
+ const classes = await classifyRemovals(
463
+ {
464
+ driver: this.deps.driver,
465
+ clock: this.deps.clock,
466
+ table,
467
+ provenance: () => void 0,
468
+ isInWindow: this.options.isInWindow
469
+ },
470
+ group
471
+ );
472
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
473
+ return;
474
+ for (const row of removed.values()) {
475
+ const identity = this.identity(row);
476
+ if (identity.table !== table || this.local.has(row.id)) continue;
477
+ const classification = classes.get(identity.id);
478
+ if (!classification) continue;
479
+ if (classification === "delete") this.tombstones.add(row.id);
480
+ else if (classification === "filtered") {
481
+ this.tombstones.delete(row.id);
482
+ this.filtered.add(row.id);
483
+ } else {
484
+ this.tombstones.delete(row.id);
485
+ this.retained.set(row.id, row);
486
+ }
487
+ }
488
+ }
489
+ } catch (error) {
490
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
491
+ this.localError = error instanceof Error ? error : new Error(String(error));
492
+ }
493
+ const retained = [...this.retained.values()].sort(this.compare).slice(0, this.maxRows);
494
+ this.retained.clear();
495
+ for (const row of retained) this.retained.set(row.id, row);
496
+ const known = /* @__PURE__ */ new Set([
497
+ ...this.local.keys(),
498
+ ...this.retained.keys(),
499
+ ...this.pages.flatMap((page) => page.rows.map((row) => row.id))
500
+ ]);
501
+ for (const set of [this.inSession, this.tombstones, this.filtered]) {
502
+ if (set.size > this.maxRows * 4) {
503
+ for (const id of set) if (!known.has(id)) set.delete(id);
504
+ }
505
+ }
506
+ this.render();
507
+ }
508
+ async checkpoint(isCurrent) {
509
+ if (this.disposed || !this.unwatch || this.activeRefs <= 0) return;
510
+ const token = this.watchGeneration;
511
+ const generation = this.generation;
512
+ const stamp = this.deps.freshness.freshAtFor(void 0);
513
+ try {
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);
520
+ } catch (error) {
521
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
522
+ return;
523
+ this.localError = error instanceof Error ? error : new Error(String(error));
524
+ this.render();
525
+ }
526
+ }
527
+ clearPages() {
528
+ for (const page of this.pages) this.deps.crud.closeWindow(page.window);
529
+ this.pages = [];
530
+ }
531
+ apiEnd() {
532
+ return this.pages.at(-1)?.end === true;
533
+ }
534
+ apiCount() {
535
+ return this.pages.reduce((count, page) => count + page.rows.length, 0);
536
+ }
537
+ apiCursor() {
538
+ const last = this.pages.at(-1)?.rows.at(-1);
539
+ return last ? last : this.anchor;
540
+ }
541
+ cursorKey(row) {
542
+ if (!row) return void 0;
543
+ const [first, second] = keyColumns(this.options.orderBy);
544
+ return [row[first], row[second]];
545
+ }
546
+ pump() {
547
+ if (!this.ready || this.activeRefs <= 0 || this.disposed || this.options.enabled === false || this.offline || this.apiPending || this.apiFailed || this.apiEnd())
548
+ return;
549
+ if (!this.table && !this.options.getRowIdentity) return;
550
+ if (this.pages.length >= Math.ceil(this.maxRows / this.pageSize) + 1) return;
551
+ if (this.apiCount() >= this.target && this.entries().length >= this.target) return;
552
+ const cursor = this.apiCursor();
553
+ const generation = this.generation;
554
+ const request = new AbortController();
555
+ this.request = request;
556
+ const window = this.deps.crud.openWindow();
557
+ this.apiPending = true;
558
+ this.render();
559
+ const key = [
560
+ "powersync-bridge.query",
561
+ this.deps.identityKey,
562
+ this.deps.epoch,
563
+ infiniteQueryKey(this.options),
564
+ this.cursorKey(this.anchor),
565
+ this.revision,
566
+ generation,
567
+ this.cursorKey(cursor)
568
+ ];
569
+ this.requestKey = key;
570
+ void this.deps.snapshots.fetch(key, this.options.staleTime ?? 6e4, async (signal) => {
571
+ const abort = () => request.abort();
572
+ signal.addEventListener("abort", abort, { once: true });
573
+ try {
574
+ return await withTimeout(
575
+ this.deps.clock,
576
+ this.options.fetchPage({ cursor, limit: this.pageSize + 1, signal: request.signal }).then((rows) => ({ rows })),
577
+ 3e4,
578
+ request.signal
579
+ );
580
+ } finally {
581
+ signal.removeEventListener("abort", abort);
582
+ }
583
+ }).then((record) => {
584
+ if (this.disposed || generation !== this.generation || request.signal.aborted) {
585
+ this.deps.crud.closeWindow(window);
586
+ return;
587
+ }
588
+ const rows = [...record.rows];
589
+ for (let index = 1; index < rows.length; index++) {
590
+ const before = rows[index - 1];
591
+ const row = rows[index];
592
+ if (before && row && this.compare(before, row) >= 0)
593
+ throw new Error(
594
+ "powersync-bridge: API rows must use a strict, stable order with an id tiebreaker"
595
+ );
596
+ }
597
+ if (cursor !== void 0 && rows.some((row) => this.compare(row, cursor) <= 0))
598
+ throw new Error("powersync-bridge: API page must advance beyond the supplied cursor");
599
+ this.pages.push({
600
+ cursor,
601
+ rows: rows.slice(0, this.pageSize),
602
+ end: rows.length <= this.pageSize,
603
+ fetchedAt: record.fetchedAt,
604
+ window
605
+ });
606
+ this.apiError = void 0;
607
+ }).catch((error) => {
608
+ this.deps.crud.closeWindow(window);
609
+ if (this.disposed || generation !== this.generation || request.signal.aborted) return;
610
+ this.apiFailed = true;
611
+ this.apiError = error instanceof Error ? error : new Error(String(error));
612
+ request.abort();
613
+ }).finally(() => {
614
+ if (generation !== this.generation || this.disposed) return;
615
+ this.apiPending = false;
616
+ this.request = void 0;
617
+ this.requestKey = void 0;
618
+ this.render();
619
+ this.pump();
620
+ });
621
+ }
622
+ entries() {
623
+ const api = /* @__PURE__ */ new Map();
624
+ for (const page of this.pages) for (const row of page.rows) api.set(row.id, { row, page });
625
+ const ids = /* @__PURE__ */ new Set([...this.local.keys(), ...this.retained.keys(), ...api.keys()]);
626
+ const entries = [];
627
+ for (const id of ids) {
628
+ if (this.tombstones.has(id) || this.filtered.has(id)) continue;
629
+ const local = this.local.get(id);
630
+ const remote = api.get(id);
631
+ const row = local ?? remote?.row ?? this.retained.get(id);
632
+ if (!row) continue;
633
+ if (this.pending(row)) {
634
+ if (local) entries.push({ row: local, origin: "local", presentLocally: true });
635
+ continue;
636
+ }
637
+ if (remote) {
638
+ entries.push(
639
+ local && this.localStamp > remote.page.fetchedAt ? { row: local, origin: "local", presentLocally: true } : { row: remote.row, origin: "api", presentLocally: Boolean(local) }
640
+ );
641
+ } else if (local) {
642
+ const covering = this.pages.find((page) => {
643
+ if (page.cursor !== void 0 && this.compare(local, page.cursor) <= 0) return false;
644
+ const last = page.rows.at(-1);
645
+ return page.end || Boolean(last && this.compare(local, last) <= 0);
646
+ });
647
+ if (!covering || this.inSession.has(id) || this.localStamp > covering.fetchedAt)
648
+ entries.push({ row: local, origin: "local", presentLocally: true });
649
+ } else entries.push({ row, origin: "local", presentLocally: false });
650
+ }
651
+ return entries.sort((a, b) => this.compare(a.row, b.row));
652
+ }
653
+ result(empty = false) {
654
+ const enabled = !empty && this.options.enabled !== false;
655
+ const entries = enabled ? this.entries() : [];
656
+ const items = entries.slice(0, this.target);
657
+ const more = entries.length > this.target || !this.apiEnd();
658
+ const missing = items.length < this.target;
659
+ const errors = {};
660
+ if (this.localError) errors.local = this.localError;
661
+ if (this.apiError) errors.api = this.apiError;
662
+ const offlineError = this.offline && missing && !this.localPending ? new OfflineError() : void 0;
663
+ const error = enabled && missing && !this.localPending && !this.apiPending ? this.localError ?? this.apiError ?? offlineError : void 0;
664
+ return {
665
+ data: items.map((item) => item.row),
666
+ items,
667
+ isLoading: enabled && items.length === 0 && !error && (this.localPending || this.apiPending),
668
+ isFetching: enabled && (this.localPending || this.apiPending),
669
+ isFetchingNextPage: enabled && this.target > this.pageSize && (this.localPending || this.apiPending),
670
+ hasNextPage: enabled && this.target < this.maxRows && more,
671
+ needsOlderWindow: enabled && items.length >= this.maxRows && more,
672
+ error,
673
+ errors,
674
+ isError: Boolean(error),
675
+ windowRevision: this.revision
676
+ };
677
+ }
678
+ render(empty = false) {
679
+ if (this.disposed && !empty) return;
680
+ const result = this.result(empty);
681
+ if (!shallowEqualResult(
682
+ this.store.getSnapshot(),
683
+ result
684
+ ))
685
+ this.store.replace(result);
686
+ }
687
+ fetchNextPage = () => {
688
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false || this.target >= this.maxRows)
689
+ return;
690
+ if (!this.store.getSnapshot().hasNextPage) return;
691
+ this.target = Math.min(this.target + this.pageSize, this.maxRows);
692
+ this.watch();
693
+ this.pump();
694
+ };
695
+ retry = () => {
696
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
697
+ this.apiFailed = false;
698
+ this.apiError = void 0;
699
+ this.watch();
700
+ this.pump();
701
+ };
702
+ refresh = () => {
703
+ this.reset(this.anchor);
704
+ };
705
+ continueOlder = () => {
706
+ const last = this.store.getSnapshot().data.at(-1);
707
+ if (last) this.reset(last);
708
+ };
709
+ reset(anchor) {
710
+ if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
711
+ this.generation += 1;
712
+ this.cancelRequest();
713
+ this.request = void 0;
714
+ this.apiPending = false;
715
+ this.apiFailed = false;
716
+ this.apiError = void 0;
717
+ this.localError = void 0;
718
+ this.clearPages();
719
+ this.local.clear();
720
+ this.retained.clear();
721
+ this.inSession.clear();
722
+ this.tombstones.clear();
723
+ this.filtered.clear();
724
+ this.target = this.pageSize;
725
+ this.anchor = anchor;
726
+ this.revision += 1;
727
+ this.watch();
728
+ this.pump();
729
+ }
730
+ };
731
+
116
732
  // src/flagger.ts
117
733
  var AutoSyncFlagStore = class {
118
734
  constructor(driver, clock, config, ids) {
@@ -309,6 +925,7 @@ var InfiniteListStore = class {
309
925
  reason;
310
926
  awaitingSync = false;
311
927
  loading = false;
928
+ localPagePending = false;
312
929
  errors = {};
313
930
  warnings = [];
314
931
  errorCount = 0;
@@ -317,14 +934,16 @@ var InfiniteListStore = class {
317
934
  refs = 0;
318
935
  retain() {
319
936
  this.refs += 1;
320
- if (this.refs === 1) this.subscribeLocal();
937
+ if (this.refs === 1 && this.unsubDiffs === null) this.subscribeLocal();
321
938
  }
322
939
  release() {
323
940
  this.refs -= 1;
324
- if (this.refs <= 0) this.dispose();
941
+ if (this.refs <= 0) queueMicrotask(() => {
942
+ if (this.refs <= 0) this.dispose();
943
+ });
325
944
  }
326
945
  get isDisposed() {
327
- return this.disposed;
946
+ return this.disposed || this.refs <= 0 && this.unsubDiffs !== null;
328
947
  }
329
948
  swapRoot() {
330
949
  this.store.replace(emptyInfiniteResult());
@@ -338,6 +957,11 @@ var InfiniteListStore = class {
338
957
  for (const h of this.subs.values()) h.unsubscribe();
339
958
  this.subs.clear();
340
959
  this.lru.length = 0;
960
+ this.local.clear();
961
+ this.apiRows.clear();
962
+ this.retained.clear();
963
+ this.tombstones.clear();
964
+ this.store.replace(emptyInfiniteResult());
341
965
  }
342
966
  // ---------------------------------------------------------------------------
343
967
  subscribeLocal() {
@@ -349,6 +973,10 @@ var InfiniteListStore = class {
349
973
  (diff) => void this.onDiff(diff),
350
974
  (err) => {
351
975
  this.localError = err;
976
+ if (this.localPagePending) {
977
+ this.localPagePending = false;
978
+ this.loading = false;
979
+ }
352
980
  this.render();
353
981
  }
354
982
  );
@@ -368,6 +996,11 @@ var InfiniteListStore = class {
368
996
  this.apiRows.delete(id);
369
997
  }
370
998
  this.local = next;
999
+ this.localError = void 0;
1000
+ if (this.localPagePending) {
1001
+ this.localPagePending = false;
1002
+ this.loading = false;
1003
+ }
371
1004
  this.emissionSeen = true;
372
1005
  const removed = /* @__PURE__ */ new Map();
373
1006
  for (const [id, row] of prev) if (!next.has(id)) removed.set(id, row);
@@ -422,6 +1055,8 @@ var InfiniteListStore = class {
422
1055
  async loadMore() {
423
1056
  if (this.disposed || this.loading || this.boundary === "end") return;
424
1057
  if (this.lookahead) {
1058
+ this.localPagePending = true;
1059
+ this.loading = true;
425
1060
  this.pages += 1;
426
1061
  this.boundary = "more";
427
1062
  this.reason = void 0;
@@ -576,6 +1211,8 @@ var InfiniteListStore = class {
576
1211
  }
577
1212
  }
578
1213
  absorbApi(rows) {
1214
+ const { api: _previousApiError, ...remainingErrors } = this.errors;
1215
+ this.errors = remainingErrors;
579
1216
  this.errorCount = 0;
580
1217
  this.nextRetryAt = 0;
581
1218
  for (const row of rows) {
@@ -583,6 +1220,11 @@ var InfiniteListStore = class {
583
1220
  if (this.local.has(id) || this.apiRows.has(id) || this.tombstones.has(id)) continue;
584
1221
  this.apiRows.set(id, row);
585
1222
  }
1223
+ const loadedPages = Math.max(this.pages, Math.ceil(this.mergedEntries().length / this.def.pageSize));
1224
+ if (loadedPages > this.pages) {
1225
+ this.pages = loadedPages;
1226
+ this.subscribeLocal();
1227
+ }
586
1228
  if (rows.length === 0) {
587
1229
  this.boundary = "end";
588
1230
  this.reason = void 0;
@@ -750,7 +1392,7 @@ var CrudMirror = class {
750
1392
  for (const r of refs) next.set(refKey(r.table, r.id), r);
751
1393
  const added = [];
752
1394
  const completed = [];
753
- for (const [k, r] of next) if (!this.live.has(k)) added.push(r);
1395
+ for (const [k, r] of next) if (this.live.get(k)?.op !== r.op) added.push(r);
754
1396
  for (const [k, r] of this.live) if (!next.has(k)) completed.push(r);
755
1397
  if (added.length || completed.length) this.apply({ added, completed });
756
1398
  }
@@ -819,6 +1461,10 @@ var RacedListStore = class {
819
1461
  this.compare = rowComparator(def.orderKey, def.direction ?? "asc");
820
1462
  this.table = def.table ?? def.id;
821
1463
  this.startedAt = deps.clock.now();
1464
+ this.checkpointRefresh = new CoalescedRefresh(
1465
+ (isCurrent) => this.onCompletion(isCurrent),
1466
+ () => this.reconcile()
1467
+ );
822
1468
  }
823
1469
  deps;
824
1470
  def;
@@ -828,6 +1474,9 @@ var RacedListStore = class {
828
1474
  compare;
829
1475
  table;
830
1476
  abort = new AbortController();
1477
+ observation = 0;
1478
+ checkpointRefresh;
1479
+ localFreshAt = Number.NEGATIVE_INFINITY;
831
1480
  startedAt;
832
1481
  snap = null;
833
1482
  snapPending = false;
@@ -846,19 +1495,25 @@ var RacedListStore = class {
846
1495
  tombstones = /* @__PURE__ */ new Set();
847
1496
  disposed = false;
848
1497
  refs = 0;
1498
+ activeRefs = 0;
849
1499
  measured = false;
850
1500
  localMs = null;
851
1501
  apiMs = null;
852
- retain() {
1502
+ retain(active = true) {
853
1503
  this.refs += 1;
854
- if (this.refs === 1) this.start();
1504
+ if (active) this.activeRefs += 1;
1505
+ if (this.activeRefs > 0 && this.unsubDiffs === null) this.start();
855
1506
  }
856
- release() {
1507
+ release(active = true) {
857
1508
  this.refs -= 1;
858
- if (this.refs <= 0) this.dispose();
1509
+ if (active) this.activeRefs -= 1;
1510
+ queueMicrotask(() => {
1511
+ if (this.refs <= 0) this.dispose();
1512
+ else if (this.activeRefs <= 0) this.pause();
1513
+ });
859
1514
  }
860
1515
  get isDisposed() {
861
- return this.disposed;
1516
+ return this.disposed || this.refs <= 0 && this.unsubDiffs !== null;
862
1517
  }
863
1518
  /** Synchronous part of an identity swap: the snapshot becomes empty 'pending' at once (R15). */
864
1519
  swapRoot() {
@@ -867,46 +1522,83 @@ var RacedListStore = class {
867
1522
  }
868
1523
  dispose() {
869
1524
  this.disposed = true;
870
- this.abort.abort(new Error("disposed"));
1525
+ this.pause();
1526
+ if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
1527
+ this.snapWindow = null;
1528
+ this.local?.clear();
1529
+ this.local = null;
1530
+ this.snap = null;
1531
+ this.prepared = null;
1532
+ this.overlay.clear();
1533
+ this.inSession.clear();
1534
+ this.tombstones.clear();
1535
+ this.store.replace(emptyRacedResult());
1536
+ }
1537
+ pause() {
1538
+ this.observation += 1;
1539
+ this.checkpointRefresh.cancel();
1540
+ this.abort.abort(new Error("observer stopped"));
871
1541
  this.unsubDiffs?.();
872
1542
  this.unsubCrud?.();
873
1543
  this.unsubFresh?.();
874
- if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
875
- this.snapWindow = null;
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
+ }
876
1553
  }
877
1554
  // ---------------------------------------------------------------------------
878
1555
  start() {
1556
+ if (this.disposed) return;
1557
+ this.abort = new AbortController();
1558
+ const observation = ++this.observation;
879
1559
  const { driver, adapter, crud, freshness } = this.deps;
880
1560
  const prepared = adapter.prepare(this.def.query(this.scope));
881
1561
  this.prepared = prepared;
882
1562
  this.unsubDiffs = driver.diffs(prepared).subscribe(
883
- (diff) => void this.onDiff(diff),
1563
+ (diff) => {
1564
+ if (observation === this.observation) void this.onDiff(diff);
1565
+ },
884
1566
  (err) => {
1567
+ if (observation !== this.observation) return;
885
1568
  this.localError = err;
886
1569
  this.reconcile();
887
1570
  }
888
1571
  );
889
1572
  this.unsubCrud = crud.onChange(() => this.reconcile());
890
- this.unsubFresh = freshness.onCompletion(() => void this.onCompletion());
891
- 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();
892
1579
  this.reconcile();
893
1580
  }
894
1581
  startApiLeg() {
1582
+ const observation = this.observation;
1583
+ const abort = this.abort;
895
1584
  const { snapshots, crud } = this.deps;
896
1585
  const staleTime = this.def.staleTime ?? 10 * 6e4;
897
1586
  this.snapPending = true;
898
1587
  const window = crud.openWindow();
899
1588
  this.snapWindow = window;
900
- const key = ["powersync-bridge", this.deps.identityKey, this.deps.epoch, this.def.id, this.scopeKey];
1589
+ const key = this.snapshotKey();
901
1590
  snapshots.fetch(key, staleTime, (signal) => {
902
1591
  const combined = new AbortController();
903
1592
  const onAbort = () => combined.abort();
904
1593
  signal.addEventListener("abort", onAbort, { once: true });
905
- this.abort.signal.addEventListener("abort", onAbort, { once: true });
906
- 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
+ });
907
1599
  }).then(
908
1600
  (record) => {
909
- if (this.disposed) return;
1601
+ if (this.disposed || observation !== this.observation) return;
910
1602
  this.snapPending = false;
911
1603
  this.apiMs = this.deps.clock.now() - this.startedAt;
912
1604
  const rows = /* @__PURE__ */ new Map();
@@ -914,32 +1606,38 @@ var RacedListStore = class {
914
1606
  const item = this.def.mapApi(raw);
915
1607
  rows.set(idOf(item), item);
916
1608
  }
1609
+ if (this.snap) crud.closeWindow(this.snap.window);
917
1610
  this.snap = { rows, record, window };
918
1611
  this.reconcile();
919
1612
  },
920
1613
  (err) => {
921
- if (this.disposed) return;
1614
+ if (this.disposed || observation !== this.observation) return;
922
1615
  this.snapPending = false;
923
1616
  this.snapError = err instanceof Error ? err : new Error(String(err));
924
1617
  crud.closeWindow(window);
925
- this.snapWindow = null;
1618
+ this.snapWindow = this.snap?.window ?? null;
926
1619
  this.reconcile();
927
1620
  }
928
1621
  );
929
1622
  }
1623
+ snapshotKey() {
1624
+ return ["powersync-bridge", this.deps.identityKey, this.deps.epoch, this.def.id, this.scopeKey];
1625
+ }
930
1626
  /**
931
1627
  * A checkpoint completed. Status events and diff emissions are separate SDK events with no
932
1628
  * ordering guarantee; taking ownership on the stale local map would render the pre-checkpoint
933
1629
  * value for one frame (fresh→old→fresh). Pre-ownership we re-query once, then reconcile.
934
1630
  */
935
- async onCompletion() {
936
- if (this.disposed || this.owned || !this.prepared || !this.emissionSeen) {
937
- this.reconcile();
938
- return;
939
- }
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);
940
1634
  try {
941
- const rows = await this.deps.driver.query(this.prepared, this.abort.signal);
942
- 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;
943
1641
  const prev = this.local ?? /* @__PURE__ */ new Map();
944
1642
  const next = /* @__PURE__ */ new Map();
945
1643
  for (const row of rows) {
@@ -952,12 +1650,16 @@ var RacedListStore = class {
952
1650
  }
953
1651
  }
954
1652
  this.local = next;
1653
+ this.localFreshAt = Math.max(this.localFreshAt, freshAt);
1654
+ this.emissionSeen = true;
1655
+ this.localError = void 0;
955
1656
  } catch {
956
1657
  }
957
1658
  this.reconcile();
958
1659
  }
959
1660
  async onDiff(diff) {
960
- if (this.disposed) return;
1661
+ if (this.disposed || this.activeRefs <= 0) return;
1662
+ const observation = this.observation;
961
1663
  const { crud } = this.deps;
962
1664
  const prev = this.local;
963
1665
  const next = /* @__PURE__ */ new Map();
@@ -970,6 +1672,8 @@ var RacedListStore = class {
970
1672
  }
971
1673
  }
972
1674
  this.local = next;
1675
+ if (!this.checkpointRefresh.pending)
1676
+ this.localFreshAt = this.deps.freshness.freshAtFor(this.def.stream);
973
1677
  if (!this.emissionSeen) {
974
1678
  this.emissionSeen = true;
975
1679
  this.localMs = this.deps.clock.now() - this.startedAt;
@@ -977,15 +1681,13 @@ var RacedListStore = class {
977
1681
  void crud.resync();
978
1682
  if (this.owned && prev) {
979
1683
  const removed = /* @__PURE__ */ new Map();
980
- for (const id of diff.removed) {
981
- const row = prev.get(id);
982
- if (row) removed.set(id, row);
983
- }
984
- 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);
985
1686
  }
1687
+ if (observation !== this.observation) return;
986
1688
  this.reconcile();
987
1689
  }
988
- async applyRemovals(removed) {
1690
+ async applyRemovals(removed, observation) {
989
1691
  const classes = await classifyRemovals(
990
1692
  {
991
1693
  driver: this.deps.driver,
@@ -996,7 +1698,7 @@ var RacedListStore = class {
996
1698
  },
997
1699
  removed
998
1700
  );
999
- if (this.disposed) return;
1701
+ if (this.disposed || observation !== this.observation) return;
1000
1702
  for (const [id, cls] of classes) {
1001
1703
  const entry = this.overlay.get(id);
1002
1704
  switch (cls) {
@@ -1019,18 +1721,24 @@ var RacedListStore = class {
1019
1721
  if (this.disposed) return;
1020
1722
  const result = this.owned ? this.renderOwned() : this.renderRace();
1021
1723
  const prev = this.store.getSnapshot();
1022
- if (shallowEqualResult(prev, result)) return;
1724
+ if (shallowEqualResult(
1725
+ prev,
1726
+ result
1727
+ ))
1728
+ return;
1023
1729
  this.store.replace(result);
1024
1730
  this.report(result);
1025
1731
  }
1026
1732
  freshAt() {
1027
- return this.deps.freshness.freshAtFor(this.def.stream);
1733
+ return this.localFreshAt;
1028
1734
  }
1029
1735
  localFresh() {
1030
- 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;
1031
1738
  }
1032
1739
  tryTakeOwnership() {
1033
1740
  if (this.owned) return true;
1741
+ if (this.checkpointRefresh.pending) return false;
1034
1742
  const apiFreshAt = this.snap?.record.fetchedAt ?? Number.NEGATIVE_INFINITY;
1035
1743
  if (!(this.localFresh() && this.freshAt() > apiFreshAt)) return false;
1036
1744
  this.owned = true;
@@ -1040,7 +1748,8 @@ var RacedListStore = class {
1040
1748
  if (this.snap) {
1041
1749
  if (this.def.apiScopeSupersetOfLocal) {
1042
1750
  for (const [id, row] of this.snap.rows) {
1043
- 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 });
1044
1753
  }
1045
1754
  }
1046
1755
  this.deps.crud.closeWindow(this.snap.window);
@@ -1087,11 +1796,14 @@ var RacedListStore = class {
1087
1796
  const inS = S?.rows.get(id);
1088
1797
  const pending = crud.pendingOp(this.table, id, S?.window);
1089
1798
  if (pending) {
1090
- 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 });
1091
1801
  continue;
1092
1802
  }
1093
1803
  if (inLocal !== void 0 && inS !== void 0) {
1094
- 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
+ );
1095
1807
  continue;
1096
1808
  }
1097
1809
  if (inLocal !== void 0) {
@@ -1115,7 +1827,9 @@ var RacedListStore = class {
1115
1827
  finalize(entries, stale = false) {
1116
1828
  entries.sort((a, b) => this.compare(a.row, b.row));
1117
1829
  const limited = entries.slice(0, this.def.limit);
1118
- 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
+ );
1119
1833
  }
1120
1834
  build(items, state, awaiting) {
1121
1835
  const errors = {};
@@ -1166,12 +1880,27 @@ var SnapshotCache = class {
1166
1880
  if (existing && (existing.result === null || now - existing.result.fetchedAt < staleTime)) {
1167
1881
  return existing.promise;
1168
1882
  }
1883
+ for (const [entryKey, entry2] of this.entries) {
1884
+ if (entry2.result && now - entry2.result.fetchedAt >= staleTime) this.entries.delete(entryKey);
1885
+ }
1886
+ if (this.entries.size >= 64) {
1887
+ for (const [entryKey, entry2] of this.entries) {
1888
+ if (entry2.result) {
1889
+ this.entries.delete(entryKey);
1890
+ break;
1891
+ }
1892
+ }
1893
+ }
1169
1894
  const controller = new AbortController();
1170
1895
  const run = async (signal) => {
1171
1896
  const res = await fetcher(signal);
1172
1897
  return { rows: res.rows, asOf: res.asOf, requestedAt: now, fetchedAt: this.clock.now() };
1173
1898
  };
1174
- 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);
1175
1904
  const entry = { requestedAt: now, promise, result: null, controller };
1176
1905
  this.entries.set(k, entry);
1177
1906
  promise.then(
@@ -1185,6 +1914,14 @@ var SnapshotCache = class {
1185
1914
  return promise;
1186
1915
  }
1187
1916
  /** Cancel every in-flight fetch under a key prefix (identity swap). */
1917
+ invalidate(key) {
1918
+ const encoded = JSON.stringify(key);
1919
+ this.entries.get(encoded)?.controller.abort(new Error("invalidated"));
1920
+ void this.queryClient?.cancelQueries?.({ queryKey: key, exact: true }).catch(() => {
1921
+ });
1922
+ this.entries.delete(encoded);
1923
+ }
1924
+ /** Cancel every in-flight fetch under a key prefix (identity swap). */
1188
1925
  abortAll(prefix) {
1189
1926
  const p = JSON.stringify(prefix).slice(0, -1);
1190
1927
  for (const [k, e] of this.entries) {
@@ -1215,10 +1952,12 @@ var Bridge = class {
1215
1952
  clock;
1216
1953
  /** Increments on every identity swap; hooks subscribe to re-acquire their stores. */
1217
1954
  epochStore;
1955
+ connectivity = new SnapshotStore(void 0);
1218
1956
  identity = new IdentityTracker();
1219
1957
  freshness;
1220
1958
  crud;
1221
1959
  snapshots;
1960
+ backgroundReads = new BackgroundReads();
1222
1961
  epochSnapshot;
1223
1962
  stores = /* @__PURE__ */ new Map();
1224
1963
  gates;
@@ -1226,12 +1965,16 @@ var Bridge = class {
1226
1965
  identityOpts;
1227
1966
  onEvicted;
1228
1967
  disposed = false;
1968
+ onNetwork = () => this.setOnline(globalThis.navigator?.onLine);
1229
1969
  constructor(opts) {
1230
1970
  this.driver = opts.driver;
1231
1971
  this.adapter = opts.adapter;
1232
1972
  this.clock = opts.clock ?? realClock;
1233
1973
  this.platform = opts.platform ?? detectPlatform();
1234
- 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
+ };
1235
1978
  this.measure = opts.measure?.sink;
1236
1979
  this.identityOpts = opts.identity ?? {};
1237
1980
  this.onEvicted = opts.onEvicted;
@@ -1244,6 +1987,8 @@ var Bridge = class {
1244
1987
  this.crud.start();
1245
1988
  this.freshness.onClear(() => this.identity.signalClear());
1246
1989
  this.identity.onChange(() => this.swapEpoch());
1990
+ globalThis.addEventListener?.("online", this.onNetwork);
1991
+ globalThis.addEventListener?.("offline", this.onNetwork);
1247
1992
  }
1248
1993
  get epoch() {
1249
1994
  return this.identity.epoch;
@@ -1251,6 +1996,32 @@ var Bridge = class {
1251
1996
  get identityKey() {
1252
1997
  return this.identity.currentKey;
1253
1998
  }
1999
+ setOnline(online) {
2000
+ this.connectivity.replace(online);
2001
+ }
2002
+ query(options) {
2003
+ const key = `query:${this.epoch}:${infiniteQueryKey(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
+ );
2024
+ }
1254
2025
  /** Resolves once the initial upload-queue read has landed (tests). */
1255
2026
  ready() {
1256
2027
  return this.crud.ready();
@@ -1264,7 +2035,8 @@ var Bridge = class {
1264
2035
  Object.defineProperty(wrapped, "fetchCredentials", {
1265
2036
  value: async () => {
1266
2037
  const creds = await connector.fetchCredentials();
1267
- if (creds?.token) this.identity.observe(identityKeyFromToken(creds.token, this.identityOpts));
2038
+ if (creds?.token)
2039
+ this.identity.observe(identityKeyFromToken(creds.token, this.identityOpts));
1268
2040
  return creds;
1269
2041
  },
1270
2042
  configurable: true,
@@ -1326,7 +2098,8 @@ var Bridge = class {
1326
2098
  epoch: this.epoch,
1327
2099
  identityKey: this.identityKey,
1328
2100
  apiLegEnabled: this.apiLegEnabled(),
1329
- measure: this.measure
2101
+ measure: this.measure,
2102
+ backgroundReads: this.backgroundReads
1330
2103
  },
1331
2104
  def,
1332
2105
  scope,
@@ -1352,7 +2125,10 @@ var Bridge = class {
1352
2125
  }
1353
2126
  onDemand(name, params, opts = {}) {
1354
2127
  const key = `ondemand:${this.epoch}:${name}:${JSON.stringify(params)}`;
1355
- 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
+ );
1356
2132
  }
1357
2133
  autoSync(config, ids) {
1358
2134
  const key = `flag:${this.epoch}:${config.stream}:${JSON.stringify([...ids].sort())}`;
@@ -1361,6 +2137,8 @@ var Bridge = class {
1361
2137
  dispose() {
1362
2138
  if (this.disposed) return;
1363
2139
  this.disposed = true;
2140
+ globalThis.removeEventListener?.("online", this.onNetwork);
2141
+ globalThis.removeEventListener?.("offline", this.onNetwork);
1364
2142
  for (const s of this.stores.values()) s.dispose();
1365
2143
  this.stores.clear();
1366
2144
  this.snapshots.clear();
@@ -1380,6 +2158,6 @@ function defineInfiniteList(input) {
1380
2158
  return { kind: "infinite", ...input };
1381
2159
  }
1382
2160
 
1383
- export { AutoSyncFlagStore, Bridge, DEFAULT_VOLATILE_CLAIMS, InfiniteListStore, OnDemandStreamStore, RacedListStore, SnapshotStore, classifyRemovals, createBridge, decodeJwtPayload, defineInfiniteList, defineRacedList, emptyInfiniteResult, emptyRacedResult, identityKeyFromClaims, identityKeyFromToken, probePresence, realClock };
2161
+ export { AutoSyncFlagStore, Bridge, DEFAULT_VOLATILE_CLAIMS, InfiniteListStore, InfiniteQueryStore, OfflineError, OnDemandStreamStore, RacedListStore, SnapshotStore, classifyRemovals, createBridge, decodeJwtPayload, defineInfiniteList, defineRacedList, emptyInfiniteResult, emptyRacedResult, identityKeyFromClaims, identityKeyFromToken, infiniteQueryKey, probePresence, realClock };
1384
2162
  //# sourceMappingURL=index.js.map
1385
2163
  //# sourceMappingURL=index.js.map