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