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