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

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-KAR4QIIU.js';
2
+ export { cursorOfRow, decodeCursor, encodeCursor, keyColumns, rowComparator } from './chunk-KAR4QIIU.js';
3
3
 
4
4
  // src/clock.ts
5
5
  var realClock = {
@@ -76,13 +76,14 @@ var SnapshotStore = class {
76
76
  constructor(initial) {
77
77
  this.snapshot = initial;
78
78
  }
79
- getSnapshot() {
79
+ // React invokes external-store callbacks as standalone functions.
80
+ getSnapshot = () => {
80
81
  return this.snapshot;
81
- }
82
- subscribe(listener) {
82
+ };
83
+ subscribe = (listener) => {
83
84
  this.listeners.add(listener);
84
85
  return () => this.listeners.delete(listener);
85
- }
86
+ };
86
87
  replace(next) {
87
88
  if (Object.is(next, this.snapshot)) return;
88
89
  this.snapshot = next;
@@ -113,6 +114,496 @@ function shallowEqualResult(a, b) {
113
114
  return true;
114
115
  }
115
116
 
117
+ // src/query.ts
118
+ var infiniteQueryKey = (options) => JSON.stringify([
119
+ options.queryKey,
120
+ options.orderBy,
121
+ options.direction ?? "desc",
122
+ options.pageSize ?? 25,
123
+ options.maxRows ?? 500,
124
+ options.enabled !== false
125
+ ]);
126
+ var OfflineError = class extends Error {
127
+ name = "OfflineError";
128
+ constructor() {
129
+ super("Additional records require a connection.");
130
+ }
131
+ };
132
+ var InfiniteQueryStore = class {
133
+ constructor(deps, options) {
134
+ this.deps = deps;
135
+ this.options = options;
136
+ this.pageSize = options.pageSize ?? 25;
137
+ this.maxRows = options.maxRows ?? 500;
138
+ 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");
140
+ this.target = this.pageSize;
141
+ this.compare = rowComparator(options.orderBy, options.direction ?? "desc");
142
+ this.table = options.table;
143
+ this.offline = this.readOffline();
144
+ this.store = new SnapshotStore(this.result());
145
+ }
146
+ deps;
147
+ options;
148
+ store;
149
+ pageSize;
150
+ maxRows;
151
+ compare;
152
+ table;
153
+ target;
154
+ anchor;
155
+ revision = 0;
156
+ local = /* @__PURE__ */ new Map();
157
+ retained = /* @__PURE__ */ new Map();
158
+ tombstones = /* @__PURE__ */ new Set();
159
+ filtered = /* @__PURE__ */ new Set();
160
+ inSession = /* @__PURE__ */ new Set();
161
+ pages = [];
162
+ localPending = true;
163
+ localStamp = Number.NEGATIVE_INFINITY;
164
+ localError;
165
+ apiError;
166
+ apiPending = false;
167
+ apiFailed = false;
168
+ offline;
169
+ generation = 0;
170
+ watchGeneration = 0;
171
+ lastLocalWatch = 0;
172
+ refs = 0;
173
+ ready = false;
174
+ disposed = false;
175
+ unwatch;
176
+ stops = [];
177
+ request;
178
+ requestKey;
179
+ get isDisposed() {
180
+ return this.disposed || this.refs <= 0 && this.unwatch !== void 0;
181
+ }
182
+ retain() {
183
+ this.refs += 1;
184
+ if (this.refs !== 1 || this.unwatch || this.options.enabled === false) return;
185
+ this.stops = [
186
+ this.deps.crud.onChange(() => this.render()),
187
+ this.deps.freshness.onCompletion(() => void this.checkpoint()),
188
+ this.deps.driver.onStatusChanged(() => this.networkChanged()),
189
+ this.deps.connectivity.subscribe(() => this.networkChanged())
190
+ ];
191
+ this.watch();
192
+ void this.deps.crud.ready().then(() => {
193
+ this.ready = true;
194
+ this.pump();
195
+ }).catch((error) => {
196
+ if (this.disposed) return;
197
+ this.localPending = false;
198
+ this.localError = error instanceof Error ? error : new Error(String(error));
199
+ this.render();
200
+ });
201
+ }
202
+ release() {
203
+ this.refs -= 1;
204
+ if (this.refs <= 0)
205
+ queueMicrotask(() => {
206
+ if (this.refs <= 0) this.dispose();
207
+ });
208
+ }
209
+ swapRoot() {
210
+ this.disposed = true;
211
+ this.local.clear();
212
+ this.retained.clear();
213
+ this.clearPages();
214
+ this.render(true);
215
+ }
216
+ dispose() {
217
+ 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 = [];
225
+ this.clearPages();
226
+ this.local.clear();
227
+ this.retained.clear();
228
+ this.tombstones.clear();
229
+ this.filtered.clear();
230
+ this.inSession.clear();
231
+ this.render(true);
232
+ }
233
+ readOffline() {
234
+ return (this.deps.connectivity.getSnapshot() ?? this.deps.driver.onlineHint()) === false;
235
+ }
236
+ cancelRequest() {
237
+ this.request?.abort();
238
+ if (this.requestKey) this.deps.snapshots.invalidate(this.requestKey);
239
+ this.requestKey = void 0;
240
+ }
241
+ networkChanged() {
242
+ if (this.disposed) return;
243
+ const offline = this.readOffline();
244
+ const reconnect = this.offline && !offline;
245
+ this.offline = offline;
246
+ if (offline && this.request) {
247
+ this.cancelRequest();
248
+ this.generation += 1;
249
+ this.apiPending = false;
250
+ this.request = void 0;
251
+ }
252
+ if (reconnect) {
253
+ this.apiFailed = false;
254
+ this.apiError = void 0;
255
+ this.pump();
256
+ }
257
+ this.render();
258
+ }
259
+ identity(row) {
260
+ if (this.options.getRowIdentity) return this.options.getRowIdentity(row);
261
+ if (!this.table)
262
+ throw new Error(
263
+ "powersync-bridge: provide table or getRowIdentity when the adapter cannot infer the business table"
264
+ );
265
+ return { table: this.table, id: idOf(row) };
266
+ }
267
+ pending(row) {
268
+ const { table, id } = this.identity(row);
269
+ return Boolean(
270
+ this.deps.crud.pendingOp(table, id) || this.pages.some((page) => this.deps.crud.pendingOp(table, id, page.window))
271
+ );
272
+ }
273
+ localBuilder() {
274
+ return this.options.query({ cursor: this.anchor, limit: this.target + 1 });
275
+ }
276
+ watch() {
277
+ this.unwatch?.();
278
+ const token = ++this.watchGeneration;
279
+ const generation = this.generation;
280
+ this.localPending = true;
281
+ let prepared;
282
+ try {
283
+ prepared = this.deps.adapter.prepare(this.localBuilder());
284
+ if (!this.table && prepared.tables.length === 1) this.table = prepared.tables[0];
285
+ if (!this.table && !this.options.getRowIdentity)
286
+ throw new Error(
287
+ "powersync-bridge: provide table or getRowIdentity when the adapter cannot infer the business table"
288
+ );
289
+ } catch (error) {
290
+ this.localPending = false;
291
+ this.localError = error instanceof Error ? error : new Error(String(error));
292
+ this.render();
293
+ return;
294
+ }
295
+ this.unwatch = this.deps.driver.diffs(prepared).subscribe(
296
+ (diff) => {
297
+ void this.acceptLocal(diff.all, token, generation).catch((error) => {
298
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
299
+ this.localPending = false;
300
+ this.localError = error instanceof Error ? error : new Error(String(error));
301
+ this.render();
302
+ });
303
+ },
304
+ (error) => {
305
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
306
+ this.localPending = false;
307
+ this.localError = error;
308
+ this.render();
309
+ }
310
+ );
311
+ this.render();
312
+ }
313
+ async acceptLocal(rows, token, generation) {
314
+ await this.deps.crud.resync();
315
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
316
+ const previous = this.local;
317
+ const firstEmission = this.lastLocalWatch !== token;
318
+ this.lastLocalWatch = token;
319
+ const next = new Map(rows.map((row) => [idOf(row), row]));
320
+ const removed = /* @__PURE__ */ new Map();
321
+ for (const [id, row] of previous)
322
+ if (!next.has(id)) {
323
+ removed.set(id, row);
324
+ this.tombstones.add(id);
325
+ }
326
+ for (const [id, row] of next) {
327
+ if (previous.size && (!firstEmission || previous.has(id)) && JSON.stringify(previous.get(id)) !== JSON.stringify(row) && !this.pending(row))
328
+ this.inSession.add(id);
329
+ this.tombstones.delete(id);
330
+ this.filtered.delete(id);
331
+ this.retained.delete(id);
332
+ }
333
+ this.local = next;
334
+ this.localPending = false;
335
+ this.localError = void 0;
336
+ this.localStamp = this.deps.freshness.freshAtFor(void 0);
337
+ this.render();
338
+ const groups = /* @__PURE__ */ new Map();
339
+ for (const row of removed.values()) {
340
+ const identity = this.identity(row);
341
+ const group = groups.get(identity.table) ?? /* @__PURE__ */ new Map();
342
+ group.set(identity.id, row);
343
+ groups.set(identity.table, group);
344
+ }
345
+ try {
346
+ for (const [table, group] of groups) {
347
+ const classes = await classifyRemovals(
348
+ {
349
+ driver: this.deps.driver,
350
+ clock: this.deps.clock,
351
+ table,
352
+ provenance: () => void 0,
353
+ isInWindow: this.options.isInWindow
354
+ },
355
+ group
356
+ );
357
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
358
+ for (const row of removed.values()) {
359
+ const identity = this.identity(row);
360
+ if (identity.table !== table || this.local.has(row.id)) continue;
361
+ const classification = classes.get(identity.id);
362
+ if (!classification) continue;
363
+ if (classification === "delete") this.tombstones.add(row.id);
364
+ else if (classification === "filtered") {
365
+ this.tombstones.delete(row.id);
366
+ this.filtered.add(row.id);
367
+ } else {
368
+ this.tombstones.delete(row.id);
369
+ this.retained.set(row.id, row);
370
+ }
371
+ }
372
+ }
373
+ } catch (error) {
374
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
375
+ this.localError = error instanceof Error ? error : new Error(String(error));
376
+ }
377
+ const retained = [...this.retained.values()].sort(this.compare).slice(0, this.maxRows);
378
+ this.retained.clear();
379
+ for (const row of retained) this.retained.set(row.id, row);
380
+ const known = /* @__PURE__ */ new Set([
381
+ ...this.local.keys(),
382
+ ...this.retained.keys(),
383
+ ...this.pages.flatMap((page) => page.rows.map((row) => row.id))
384
+ ]);
385
+ for (const set of [this.inSession, this.tombstones, this.filtered]) {
386
+ if (set.size > this.maxRows * 4) {
387
+ for (const id of set) if (!known.has(id)) set.delete(id);
388
+ }
389
+ }
390
+ this.render();
391
+ }
392
+ async checkpoint() {
393
+ if (this.disposed || !this.unwatch) return;
394
+ const token = this.watchGeneration;
395
+ const generation = this.generation;
396
+ try {
397
+ const rows = await this.deps.driver.query(this.deps.adapter.prepare(this.localBuilder()));
398
+ await this.acceptLocal(rows, token, generation);
399
+ } catch (error) {
400
+ if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
401
+ this.localError = error instanceof Error ? error : new Error(String(error));
402
+ this.render();
403
+ }
404
+ }
405
+ clearPages() {
406
+ for (const page of this.pages) this.deps.crud.closeWindow(page.window);
407
+ this.pages = [];
408
+ }
409
+ apiEnd() {
410
+ return this.pages.at(-1)?.end === true;
411
+ }
412
+ apiCount() {
413
+ return this.pages.reduce((count, page) => count + page.rows.length, 0);
414
+ }
415
+ apiCursor() {
416
+ const last = this.pages.at(-1)?.rows.at(-1);
417
+ return last ? last : this.anchor;
418
+ }
419
+ cursorKey(row) {
420
+ if (!row) return void 0;
421
+ const [first, second] = keyColumns(this.options.orderBy);
422
+ return [row[first], row[second]];
423
+ }
424
+ pump() {
425
+ if (!this.ready || this.disposed || this.options.enabled === false || this.offline || this.apiPending || this.apiFailed || this.apiEnd())
426
+ return;
427
+ if (!this.table && !this.options.getRowIdentity) return;
428
+ if (this.pages.length >= Math.ceil(this.maxRows / this.pageSize) + 1) return;
429
+ if (this.apiCount() >= this.target && this.entries().length >= this.target) return;
430
+ const cursor = this.apiCursor();
431
+ const generation = this.generation;
432
+ const request = new AbortController();
433
+ this.request = request;
434
+ const window = this.deps.crud.openWindow();
435
+ this.apiPending = true;
436
+ this.render();
437
+ const key = [
438
+ "powersync-bridge.query",
439
+ this.deps.identityKey,
440
+ this.deps.epoch,
441
+ infiniteQueryKey(this.options),
442
+ this.cursorKey(this.anchor),
443
+ this.revision,
444
+ generation,
445
+ this.cursorKey(cursor)
446
+ ];
447
+ this.requestKey = key;
448
+ void this.deps.snapshots.fetch(key, this.options.staleTime ?? 6e4, async (signal) => {
449
+ const abort = () => request.abort();
450
+ signal.addEventListener("abort", abort, { once: true });
451
+ try {
452
+ return await withTimeout(
453
+ this.deps.clock,
454
+ this.options.fetchPage({ cursor, limit: this.pageSize + 1, signal: request.signal }).then((rows) => ({ rows })),
455
+ 3e4,
456
+ request.signal
457
+ );
458
+ } finally {
459
+ signal.removeEventListener("abort", abort);
460
+ }
461
+ }).then((record) => {
462
+ if (this.disposed || generation !== this.generation || request.signal.aborted) {
463
+ this.deps.crud.closeWindow(window);
464
+ return;
465
+ }
466
+ const rows = [...record.rows];
467
+ for (let index = 1; index < rows.length; index++) {
468
+ const before = rows[index - 1];
469
+ const row = rows[index];
470
+ 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");
472
+ }
473
+ if (cursor !== void 0 && rows.some((row) => this.compare(row, cursor) <= 0))
474
+ throw new Error("powersync-bridge: API page must advance beyond the supplied cursor");
475
+ this.pages.push({
476
+ cursor,
477
+ rows: rows.slice(0, this.pageSize),
478
+ end: rows.length <= this.pageSize,
479
+ fetchedAt: record.fetchedAt,
480
+ window
481
+ });
482
+ this.apiError = void 0;
483
+ }).catch((error) => {
484
+ this.deps.crud.closeWindow(window);
485
+ if (this.disposed || generation !== this.generation || request.signal.aborted) return;
486
+ this.apiFailed = true;
487
+ this.apiError = error instanceof Error ? error : new Error(String(error));
488
+ request.abort();
489
+ }).finally(() => {
490
+ if (generation !== this.generation || this.disposed) return;
491
+ this.apiPending = false;
492
+ this.request = void 0;
493
+ this.requestKey = void 0;
494
+ this.render();
495
+ this.pump();
496
+ });
497
+ }
498
+ entries() {
499
+ const api = /* @__PURE__ */ new Map();
500
+ for (const page of this.pages) for (const row of page.rows) api.set(row.id, { row, page });
501
+ const ids = /* @__PURE__ */ new Set([...this.local.keys(), ...this.retained.keys(), ...api.keys()]);
502
+ const entries = [];
503
+ for (const id of ids) {
504
+ if (this.tombstones.has(id) || this.filtered.has(id)) continue;
505
+ const local = this.local.get(id);
506
+ const remote = api.get(id);
507
+ const row = local ?? remote?.row ?? this.retained.get(id);
508
+ if (!row) continue;
509
+ if (this.pending(row)) {
510
+ if (local) entries.push({ row: local, origin: "local", presentLocally: true });
511
+ continue;
512
+ }
513
+ if (remote) {
514
+ entries.push(
515
+ local && this.localStamp > remote.page.fetchedAt ? { row: local, origin: "local", presentLocally: true } : { row: remote.row, origin: "api", presentLocally: Boolean(local) }
516
+ );
517
+ } else if (local) {
518
+ const covering = this.pages.find((page) => {
519
+ if (page.cursor !== void 0 && this.compare(local, page.cursor) <= 0) return false;
520
+ const last = page.rows.at(-1);
521
+ return page.end || Boolean(last && this.compare(local, last) <= 0);
522
+ });
523
+ if (!covering || this.inSession.has(id) || this.localStamp > covering.fetchedAt)
524
+ entries.push({ row: local, origin: "local", presentLocally: true });
525
+ } else entries.push({ row, origin: "local", presentLocally: false });
526
+ }
527
+ return entries.sort((a, b) => this.compare(a.row, b.row));
528
+ }
529
+ result(empty = false) {
530
+ const enabled = !empty && this.options.enabled !== false;
531
+ const entries = enabled ? this.entries() : [];
532
+ const items = entries.slice(0, this.target);
533
+ const more = entries.length > this.target || !this.apiEnd();
534
+ const missing = items.length < this.target;
535
+ const errors = {};
536
+ if (this.localError) errors.local = this.localError;
537
+ if (this.apiError) errors.api = this.apiError;
538
+ const offlineError = this.offline && missing && !this.localPending ? new OfflineError() : void 0;
539
+ const error = enabled && missing && !this.localPending && !this.apiPending ? this.localError ?? this.apiError ?? offlineError : void 0;
540
+ return {
541
+ data: items.map((item) => item.row),
542
+ items,
543
+ isLoading: enabled && items.length === 0 && !error && (this.localPending || this.apiPending),
544
+ isFetching: enabled && (this.localPending || this.apiPending),
545
+ isFetchingNextPage: enabled && this.target > this.pageSize && (this.localPending || this.apiPending),
546
+ hasNextPage: enabled && this.target < this.maxRows && more,
547
+ needsOlderWindow: enabled && items.length >= this.maxRows && more,
548
+ error,
549
+ errors,
550
+ isError: Boolean(error),
551
+ windowRevision: this.revision
552
+ };
553
+ }
554
+ render(empty = false) {
555
+ if (this.disposed && !empty) return;
556
+ const result = this.result(empty);
557
+ if (!shallowEqualResult(
558
+ this.store.getSnapshot(),
559
+ result
560
+ ))
561
+ this.store.replace(result);
562
+ }
563
+ fetchNextPage = () => {
564
+ if (this.disposed || this.options.enabled === false || this.target >= this.maxRows) return;
565
+ if (!this.store.getSnapshot().hasNextPage) return;
566
+ this.target = Math.min(this.target + this.pageSize, this.maxRows);
567
+ this.watch();
568
+ this.pump();
569
+ };
570
+ retry = () => {
571
+ if (this.disposed || this.options.enabled === false) return;
572
+ this.apiFailed = false;
573
+ this.apiError = void 0;
574
+ this.watch();
575
+ this.pump();
576
+ };
577
+ refresh = () => {
578
+ this.reset(this.anchor);
579
+ };
580
+ continueOlder = () => {
581
+ const last = this.store.getSnapshot().data.at(-1);
582
+ if (last) this.reset(last);
583
+ };
584
+ reset(anchor) {
585
+ if (this.disposed || this.options.enabled === false) return;
586
+ this.generation += 1;
587
+ this.cancelRequest();
588
+ this.request = void 0;
589
+ this.apiPending = false;
590
+ this.apiFailed = false;
591
+ this.apiError = void 0;
592
+ this.localError = void 0;
593
+ this.clearPages();
594
+ this.local.clear();
595
+ this.retained.clear();
596
+ this.inSession.clear();
597
+ this.tombstones.clear();
598
+ this.filtered.clear();
599
+ this.target = this.pageSize;
600
+ this.anchor = anchor;
601
+ this.revision += 1;
602
+ this.watch();
603
+ this.pump();
604
+ }
605
+ };
606
+
116
607
  // src/flagger.ts
117
608
  var AutoSyncFlagStore = class {
118
609
  constructor(driver, clock, config, ids) {
@@ -309,6 +800,7 @@ var InfiniteListStore = class {
309
800
  reason;
310
801
  awaitingSync = false;
311
802
  loading = false;
803
+ localPagePending = false;
312
804
  errors = {};
313
805
  warnings = [];
314
806
  errorCount = 0;
@@ -317,14 +809,16 @@ var InfiniteListStore = class {
317
809
  refs = 0;
318
810
  retain() {
319
811
  this.refs += 1;
320
- if (this.refs === 1) this.subscribeLocal();
812
+ if (this.refs === 1 && this.unsubDiffs === null) this.subscribeLocal();
321
813
  }
322
814
  release() {
323
815
  this.refs -= 1;
324
- if (this.refs <= 0) this.dispose();
816
+ if (this.refs <= 0) queueMicrotask(() => {
817
+ if (this.refs <= 0) this.dispose();
818
+ });
325
819
  }
326
820
  get isDisposed() {
327
- return this.disposed;
821
+ return this.disposed || this.refs <= 0 && this.unsubDiffs !== null;
328
822
  }
329
823
  swapRoot() {
330
824
  this.store.replace(emptyInfiniteResult());
@@ -338,6 +832,11 @@ var InfiniteListStore = class {
338
832
  for (const h of this.subs.values()) h.unsubscribe();
339
833
  this.subs.clear();
340
834
  this.lru.length = 0;
835
+ this.local.clear();
836
+ this.apiRows.clear();
837
+ this.retained.clear();
838
+ this.tombstones.clear();
839
+ this.store.replace(emptyInfiniteResult());
341
840
  }
342
841
  // ---------------------------------------------------------------------------
343
842
  subscribeLocal() {
@@ -349,6 +848,10 @@ var InfiniteListStore = class {
349
848
  (diff) => void this.onDiff(diff),
350
849
  (err) => {
351
850
  this.localError = err;
851
+ if (this.localPagePending) {
852
+ this.localPagePending = false;
853
+ this.loading = false;
854
+ }
352
855
  this.render();
353
856
  }
354
857
  );
@@ -368,6 +871,11 @@ var InfiniteListStore = class {
368
871
  this.apiRows.delete(id);
369
872
  }
370
873
  this.local = next;
874
+ this.localError = void 0;
875
+ if (this.localPagePending) {
876
+ this.localPagePending = false;
877
+ this.loading = false;
878
+ }
371
879
  this.emissionSeen = true;
372
880
  const removed = /* @__PURE__ */ new Map();
373
881
  for (const [id, row] of prev) if (!next.has(id)) removed.set(id, row);
@@ -422,6 +930,8 @@ var InfiniteListStore = class {
422
930
  async loadMore() {
423
931
  if (this.disposed || this.loading || this.boundary === "end") return;
424
932
  if (this.lookahead) {
933
+ this.localPagePending = true;
934
+ this.loading = true;
425
935
  this.pages += 1;
426
936
  this.boundary = "more";
427
937
  this.reason = void 0;
@@ -576,6 +1086,8 @@ var InfiniteListStore = class {
576
1086
  }
577
1087
  }
578
1088
  absorbApi(rows) {
1089
+ const { api: _previousApiError, ...remainingErrors } = this.errors;
1090
+ this.errors = remainingErrors;
579
1091
  this.errorCount = 0;
580
1092
  this.nextRetryAt = 0;
581
1093
  for (const row of rows) {
@@ -583,6 +1095,11 @@ var InfiniteListStore = class {
583
1095
  if (this.local.has(id) || this.apiRows.has(id) || this.tombstones.has(id)) continue;
584
1096
  this.apiRows.set(id, row);
585
1097
  }
1098
+ const loadedPages = Math.max(this.pages, Math.ceil(this.mergedEntries().length / this.def.pageSize));
1099
+ if (loadedPages > this.pages) {
1100
+ this.pages = loadedPages;
1101
+ this.subscribeLocal();
1102
+ }
586
1103
  if (rows.length === 0) {
587
1104
  this.boundary = "end";
588
1105
  this.reason = void 0;
@@ -750,7 +1267,7 @@ var CrudMirror = class {
750
1267
  for (const r of refs) next.set(refKey(r.table, r.id), r);
751
1268
  const added = [];
752
1269
  const completed = [];
753
- for (const [k, r] of next) if (!this.live.has(k)) added.push(r);
1270
+ for (const [k, r] of next) if (this.live.get(k)?.op !== r.op) added.push(r);
754
1271
  for (const [k, r] of this.live) if (!next.has(k)) completed.push(r);
755
1272
  if (added.length || completed.length) this.apply({ added, completed });
756
1273
  }
@@ -851,14 +1368,16 @@ var RacedListStore = class {
851
1368
  apiMs = null;
852
1369
  retain() {
853
1370
  this.refs += 1;
854
- if (this.refs === 1) this.start();
1371
+ if (this.refs === 1 && this.unsubDiffs === null) this.start();
855
1372
  }
856
1373
  release() {
857
1374
  this.refs -= 1;
858
- if (this.refs <= 0) this.dispose();
1375
+ if (this.refs <= 0) queueMicrotask(() => {
1376
+ if (this.refs <= 0) this.dispose();
1377
+ });
859
1378
  }
860
1379
  get isDisposed() {
861
- return this.disposed;
1380
+ return this.disposed || this.refs <= 0 && this.unsubDiffs !== null;
862
1381
  }
863
1382
  /** Synchronous part of an identity swap: the snapshot becomes empty 'pending' at once (R15). */
864
1383
  swapRoot() {
@@ -873,6 +1392,14 @@ var RacedListStore = class {
873
1392
  this.unsubFresh?.();
874
1393
  if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
875
1394
  this.snapWindow = null;
1395
+ this.local?.clear();
1396
+ this.local = null;
1397
+ this.snap = null;
1398
+ this.prepared = null;
1399
+ this.overlay.clear();
1400
+ this.inSession.clear();
1401
+ this.tombstones.clear();
1402
+ this.store.replace(emptyRacedResult());
876
1403
  }
877
1404
  // ---------------------------------------------------------------------------
878
1405
  start() {
@@ -1166,6 +1693,17 @@ var SnapshotCache = class {
1166
1693
  if (existing && (existing.result === null || now - existing.result.fetchedAt < staleTime)) {
1167
1694
  return existing.promise;
1168
1695
  }
1696
+ for (const [entryKey, entry2] of this.entries) {
1697
+ if (entry2.result && now - entry2.result.fetchedAt >= staleTime) this.entries.delete(entryKey);
1698
+ }
1699
+ if (this.entries.size >= 64) {
1700
+ for (const [entryKey, entry2] of this.entries) {
1701
+ if (entry2.result) {
1702
+ this.entries.delete(entryKey);
1703
+ break;
1704
+ }
1705
+ }
1706
+ }
1169
1707
  const controller = new AbortController();
1170
1708
  const run = async (signal) => {
1171
1709
  const res = await fetcher(signal);
@@ -1185,6 +1723,12 @@ var SnapshotCache = class {
1185
1723
  return promise;
1186
1724
  }
1187
1725
  /** Cancel every in-flight fetch under a key prefix (identity swap). */
1726
+ invalidate(key) {
1727
+ const encoded = JSON.stringify(key);
1728
+ this.entries.get(encoded)?.controller.abort(new Error("invalidated"));
1729
+ this.entries.delete(encoded);
1730
+ }
1731
+ /** Cancel every in-flight fetch under a key prefix (identity swap). */
1188
1732
  abortAll(prefix) {
1189
1733
  const p = JSON.stringify(prefix).slice(0, -1);
1190
1734
  for (const [k, e] of this.entries) {
@@ -1215,6 +1759,7 @@ var Bridge = class {
1215
1759
  clock;
1216
1760
  /** Increments on every identity swap; hooks subscribe to re-acquire their stores. */
1217
1761
  epochStore;
1762
+ connectivity = new SnapshotStore(void 0);
1218
1763
  identity = new IdentityTracker();
1219
1764
  freshness;
1220
1765
  crud;
@@ -1226,6 +1771,7 @@ var Bridge = class {
1226
1771
  identityOpts;
1227
1772
  onEvicted;
1228
1773
  disposed = false;
1774
+ onNetwork = () => this.setOnline(globalThis.navigator?.onLine);
1229
1775
  constructor(opts) {
1230
1776
  this.driver = opts.driver;
1231
1777
  this.adapter = opts.adapter;
@@ -1244,6 +1790,8 @@ var Bridge = class {
1244
1790
  this.crud.start();
1245
1791
  this.freshness.onClear(() => this.identity.signalClear());
1246
1792
  this.identity.onChange(() => this.swapEpoch());
1793
+ globalThis.addEventListener?.("online", this.onNetwork);
1794
+ globalThis.addEventListener?.("offline", this.onNetwork);
1247
1795
  }
1248
1796
  get epoch() {
1249
1797
  return this.identity.epoch;
@@ -1251,6 +1799,25 @@ var Bridge = class {
1251
1799
  get identityKey() {
1252
1800
  return this.identity.currentKey;
1253
1801
  }
1802
+ setOnline(online) {
1803
+ this.connectivity.replace(online);
1804
+ }
1805
+ query(options) {
1806
+ 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));
1820
+ }
1254
1821
  /** Resolves once the initial upload-queue read has landed (tests). */
1255
1822
  ready() {
1256
1823
  return this.crud.ready();
@@ -1361,6 +1928,8 @@ var Bridge = class {
1361
1928
  dispose() {
1362
1929
  if (this.disposed) return;
1363
1930
  this.disposed = true;
1931
+ globalThis.removeEventListener?.("online", this.onNetwork);
1932
+ globalThis.removeEventListener?.("offline", this.onNetwork);
1364
1933
  for (const s of this.stores.values()) s.dispose();
1365
1934
  this.stores.clear();
1366
1935
  this.snapshots.clear();
@@ -1380,6 +1949,6 @@ function defineInfiniteList(input) {
1380
1949
  return { kind: "infinite", ...input };
1381
1950
  }
1382
1951
 
1383
- export { AutoSyncFlagStore, Bridge, DEFAULT_VOLATILE_CLAIMS, InfiniteListStore, OnDemandStreamStore, RacedListStore, SnapshotStore, classifyRemovals, createBridge, decodeJwtPayload, defineInfiniteList, defineRacedList, emptyInfiniteResult, emptyRacedResult, identityKeyFromClaims, identityKeyFromToken, probePresence, realClock };
1952
+ 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
1953
  //# sourceMappingURL=index.js.map
1385
1954
  //# sourceMappingURL=index.js.map