powersync-bridge 0.0.1-alpha.0 → 0.0.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -20
- package/dist/{chunk-TANEBEGU.js → chunk-WJDQWVO5.js} +64 -67
- package/dist/chunk-WJDQWVO5.js.map +1 -0
- package/dist/{clock-BAt_Za0Q.d.cts → clock-DANrBsZH.d.cts} +2 -2
- package/dist/{clock-BAt_Za0Q.d.ts → clock-DANrBsZH.d.ts} +2 -2
- package/dist/index.cjs +895 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +327 -175
- package/dist/index.d.ts +327 -175
- package/dist/index.js +833 -55
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-TANEBEGU.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,68 @@ async function withTimeout(clock, p, ms, signal) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// src/keyset.ts
|
|
28
|
+
function keyColumns(orderKey) {
|
|
29
|
+
if (typeof orderKey === "string") return [orderKey, "id"];
|
|
30
|
+
return [orderKey[0], orderKey[1]];
|
|
31
|
+
}
|
|
32
|
+
function idOf(row) {
|
|
33
|
+
const r = row;
|
|
34
|
+
if (r == null || r.id === void 0 || r.id === null) {
|
|
35
|
+
throw new Error("powersync-bridge: every row must have an `id`");
|
|
36
|
+
}
|
|
37
|
+
return String(r.id);
|
|
38
|
+
}
|
|
39
|
+
function compareValues(a, b) {
|
|
40
|
+
if (a === b) return 0;
|
|
41
|
+
if (a === null || a === void 0) return 1;
|
|
42
|
+
if (b === null || b === void 0) return -1;
|
|
43
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
44
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
45
|
+
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
46
|
+
const sa = String(a);
|
|
47
|
+
const sb = String(b);
|
|
48
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
49
|
+
}
|
|
50
|
+
function rowComparator(orderKey, direction = "asc") {
|
|
51
|
+
const [k1, k2] = keyColumns(orderKey);
|
|
52
|
+
const sign = direction === "desc" ? -1 : 1;
|
|
53
|
+
return (a, b) => {
|
|
54
|
+
const c1 = compareValues(a[k1], b[k1]);
|
|
55
|
+
if (c1 !== 0) return c1 * sign;
|
|
56
|
+
return compareValues(a[k2], b[k2]) * sign;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function cursorOfRow(row, orderKey) {
|
|
60
|
+
const r = row;
|
|
61
|
+
if (typeof orderKey === "string") return r[orderKey];
|
|
62
|
+
return [r[orderKey[0]], r[orderKey[1]]];
|
|
63
|
+
}
|
|
64
|
+
function isAfterCursor(row, cursor, orderKey, direction = "asc") {
|
|
65
|
+
const [k1, k2] = keyColumns(orderKey);
|
|
66
|
+
const r = row;
|
|
67
|
+
const sign = direction === "desc" ? -1 : 1;
|
|
68
|
+
const [c1, c2] = typeof orderKey === "string" ? [cursor, void 0] : cursor;
|
|
69
|
+
const d1 = compareValues(r[k1], c1) * sign;
|
|
70
|
+
if (d1 !== 0) return d1 > 0;
|
|
71
|
+
if (c2 === void 0) return false;
|
|
72
|
+
return compareValues(r[k2], c2) * sign > 0;
|
|
73
|
+
}
|
|
74
|
+
function toBase64(s) {
|
|
75
|
+
if (typeof btoa === "function") return btoa(unescape(encodeURIComponent(s)));
|
|
76
|
+
return Buffer.from(s, "utf8").toString("base64");
|
|
77
|
+
}
|
|
78
|
+
function fromBase64(s) {
|
|
79
|
+
if (typeof atob === "function") return decodeURIComponent(escape(atob(s)));
|
|
80
|
+
return Buffer.from(s, "base64").toString("utf8");
|
|
81
|
+
}
|
|
82
|
+
function encodeCursor(cursor) {
|
|
83
|
+
return toBase64(JSON.stringify(cursor));
|
|
84
|
+
}
|
|
85
|
+
function decodeCursor(opaque) {
|
|
86
|
+
return JSON.parse(fromBase64(opaque));
|
|
87
|
+
}
|
|
88
|
+
|
|
27
89
|
// src/removal.ts
|
|
28
90
|
var PROBE_CHUNK = 500;
|
|
29
91
|
async function probePresence(driver, table, ids) {
|
|
@@ -68,6 +130,89 @@ async function classifyRemovals(ctx, removed) {
|
|
|
68
130
|
return out;
|
|
69
131
|
}
|
|
70
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
|
+
|
|
71
216
|
// src/store.ts
|
|
72
217
|
var SnapshotStore = class {
|
|
73
218
|
snapshot;
|
|
@@ -75,13 +220,14 @@ var SnapshotStore = class {
|
|
|
75
220
|
constructor(initial) {
|
|
76
221
|
this.snapshot = initial;
|
|
77
222
|
}
|
|
78
|
-
|
|
223
|
+
// React invokes external-store callbacks as standalone functions.
|
|
224
|
+
getSnapshot = () => {
|
|
79
225
|
return this.snapshot;
|
|
80
|
-
}
|
|
81
|
-
subscribe(listener) {
|
|
226
|
+
};
|
|
227
|
+
subscribe = (listener) => {
|
|
82
228
|
this.listeners.add(listener);
|
|
83
229
|
return () => this.listeners.delete(listener);
|
|
84
|
-
}
|
|
230
|
+
};
|
|
85
231
|
replace(next) {
|
|
86
232
|
if (Object.is(next, this.snapshot)) return;
|
|
87
233
|
this.snapshot = next;
|
|
@@ -112,6 +258,538 @@ function shallowEqualResult(a, b) {
|
|
|
112
258
|
return true;
|
|
113
259
|
}
|
|
114
260
|
|
|
261
|
+
// src/query.ts
|
|
262
|
+
var infiniteQueryKey = (options) => JSON.stringify([
|
|
263
|
+
options.queryKey,
|
|
264
|
+
options.orderBy,
|
|
265
|
+
options.direction ?? "desc",
|
|
266
|
+
options.pageSize ?? 25,
|
|
267
|
+
options.maxRows ?? 500,
|
|
268
|
+
options.enabled !== false
|
|
269
|
+
]);
|
|
270
|
+
var OfflineError = class extends Error {
|
|
271
|
+
name = "OfflineError";
|
|
272
|
+
constructor() {
|
|
273
|
+
super("Additional records require a connection.");
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
var InfiniteQueryStore = class {
|
|
277
|
+
constructor(deps, options) {
|
|
278
|
+
this.deps = deps;
|
|
279
|
+
this.options = options;
|
|
280
|
+
this.pageSize = options.pageSize ?? 25;
|
|
281
|
+
this.maxRows = options.maxRows ?? 500;
|
|
282
|
+
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || !Number.isInteger(this.maxRows) || this.maxRows < this.pageSize)
|
|
283
|
+
throw new Error(
|
|
284
|
+
"powersync-bridge: pageSize must be positive and maxRows must be at least pageSize"
|
|
285
|
+
);
|
|
286
|
+
this.target = this.pageSize;
|
|
287
|
+
this.compare = rowComparator(options.orderBy, options.direction ?? "desc");
|
|
288
|
+
this.table = options.table;
|
|
289
|
+
this.offline = this.readOffline();
|
|
290
|
+
this.store = new SnapshotStore(this.result());
|
|
291
|
+
this.checkpointRefresh = new CoalescedRefresh(
|
|
292
|
+
(isCurrent) => this.checkpoint(isCurrent),
|
|
293
|
+
() => this.render()
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
deps;
|
|
297
|
+
options;
|
|
298
|
+
store;
|
|
299
|
+
pageSize;
|
|
300
|
+
maxRows;
|
|
301
|
+
compare;
|
|
302
|
+
table;
|
|
303
|
+
target;
|
|
304
|
+
anchor;
|
|
305
|
+
revision = 0;
|
|
306
|
+
local = /* @__PURE__ */ new Map();
|
|
307
|
+
retained = /* @__PURE__ */ new Map();
|
|
308
|
+
tombstones = /* @__PURE__ */ new Set();
|
|
309
|
+
filtered = /* @__PURE__ */ new Set();
|
|
310
|
+
inSession = /* @__PURE__ */ new Set();
|
|
311
|
+
pages = [];
|
|
312
|
+
localPending = true;
|
|
313
|
+
localStamp = Number.NEGATIVE_INFINITY;
|
|
314
|
+
localError;
|
|
315
|
+
apiError;
|
|
316
|
+
apiPending = false;
|
|
317
|
+
apiFailed = false;
|
|
318
|
+
offline;
|
|
319
|
+
generation = 0;
|
|
320
|
+
watchGeneration = 0;
|
|
321
|
+
lastLocalWatch = 0;
|
|
322
|
+
refs = 0;
|
|
323
|
+
activeRefs = 0;
|
|
324
|
+
checkpointRefresh;
|
|
325
|
+
localAbort = new AbortController();
|
|
326
|
+
ready = false;
|
|
327
|
+
disposed = false;
|
|
328
|
+
unwatch;
|
|
329
|
+
stops = [];
|
|
330
|
+
request;
|
|
331
|
+
requestKey;
|
|
332
|
+
get isDisposed() {
|
|
333
|
+
return this.disposed || this.refs <= 0 && this.unwatch !== void 0;
|
|
334
|
+
}
|
|
335
|
+
retain(active = true) {
|
|
336
|
+
this.refs += 1;
|
|
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;
|
|
343
|
+
this.stops = [
|
|
344
|
+
this.deps.crud.onChange(() => this.render()),
|
|
345
|
+
this.deps.freshness.onCompletion((completion) => {
|
|
346
|
+
if (completion.stream === null) this.checkpointRefresh.request();
|
|
347
|
+
}),
|
|
348
|
+
this.deps.driver.onStatusChanged(() => this.networkChanged()),
|
|
349
|
+
this.deps.connectivity.subscribe(() => this.networkChanged())
|
|
350
|
+
];
|
|
351
|
+
this.watch();
|
|
352
|
+
void this.deps.crud.ready().then(() => {
|
|
353
|
+
this.ready = true;
|
|
354
|
+
this.pump();
|
|
355
|
+
}).catch((error) => {
|
|
356
|
+
if (this.disposed) return;
|
|
357
|
+
this.localPending = false;
|
|
358
|
+
this.localError = error instanceof Error ? error : new Error(String(error));
|
|
359
|
+
this.render();
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
release(active = true) {
|
|
363
|
+
this.refs -= 1;
|
|
364
|
+
if (active) this.activeRefs -= 1;
|
|
365
|
+
queueMicrotask(() => {
|
|
366
|
+
if (this.refs <= 0) this.dispose();
|
|
367
|
+
else if (this.activeRefs <= 0) this.pause();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
swapRoot() {
|
|
371
|
+
this.disposed = true;
|
|
372
|
+
this.local.clear();
|
|
373
|
+
this.retained.clear();
|
|
374
|
+
this.clearPages();
|
|
375
|
+
this.render(true);
|
|
376
|
+
}
|
|
377
|
+
dispose() {
|
|
378
|
+
this.disposed = true;
|
|
379
|
+
this.pause();
|
|
380
|
+
this.clearPages();
|
|
381
|
+
this.local.clear();
|
|
382
|
+
this.retained.clear();
|
|
383
|
+
this.tombstones.clear();
|
|
384
|
+
this.filtered.clear();
|
|
385
|
+
this.inSession.clear();
|
|
386
|
+
this.render(true);
|
|
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
|
+
}
|
|
401
|
+
readOffline() {
|
|
402
|
+
return (this.deps.connectivity.getSnapshot() ?? this.deps.driver.onlineHint()) === false;
|
|
403
|
+
}
|
|
404
|
+
cancelRequest() {
|
|
405
|
+
this.request?.abort();
|
|
406
|
+
if (this.requestKey) this.deps.snapshots.invalidate(this.requestKey);
|
|
407
|
+
this.requestKey = void 0;
|
|
408
|
+
}
|
|
409
|
+
networkChanged() {
|
|
410
|
+
if (this.disposed) return;
|
|
411
|
+
const offline = this.readOffline();
|
|
412
|
+
const reconnect = this.offline && !offline;
|
|
413
|
+
this.offline = offline;
|
|
414
|
+
if (offline && this.request) {
|
|
415
|
+
this.cancelRequest();
|
|
416
|
+
this.generation += 1;
|
|
417
|
+
this.apiPending = false;
|
|
418
|
+
this.request = void 0;
|
|
419
|
+
this.watch();
|
|
420
|
+
}
|
|
421
|
+
if (reconnect) {
|
|
422
|
+
this.apiFailed = false;
|
|
423
|
+
this.apiError = void 0;
|
|
424
|
+
this.pump();
|
|
425
|
+
}
|
|
426
|
+
this.render();
|
|
427
|
+
}
|
|
428
|
+
identity(row) {
|
|
429
|
+
if (this.options.getRowIdentity) return this.options.getRowIdentity(row);
|
|
430
|
+
if (!this.table)
|
|
431
|
+
throw new Error(
|
|
432
|
+
"powersync-bridge: provide table or getRowIdentity when the adapter cannot infer the business table"
|
|
433
|
+
);
|
|
434
|
+
return { table: this.table, id: idOf(row) };
|
|
435
|
+
}
|
|
436
|
+
pending(row) {
|
|
437
|
+
const { table, id } = this.identity(row);
|
|
438
|
+
return Boolean(
|
|
439
|
+
this.deps.crud.pendingOp(table, id) || this.pages.some((page) => this.deps.crud.pendingOp(table, id, page.window))
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
localBuilder() {
|
|
443
|
+
return this.options.query({ cursor: this.anchor, limit: this.target + 1 });
|
|
444
|
+
}
|
|
445
|
+
watch() {
|
|
446
|
+
if (this.activeRefs <= 0 || this.disposed) return;
|
|
447
|
+
this.unwatch?.();
|
|
448
|
+
this.localAbort.abort();
|
|
449
|
+
this.localAbort = new AbortController();
|
|
450
|
+
this.checkpointRefresh.cancel();
|
|
451
|
+
const token = ++this.watchGeneration;
|
|
452
|
+
const generation = this.generation;
|
|
453
|
+
this.localPending = true;
|
|
454
|
+
let prepared;
|
|
455
|
+
try {
|
|
456
|
+
prepared = this.deps.adapter.prepare(this.localBuilder());
|
|
457
|
+
if (!this.table && prepared.tables.length === 1) this.table = prepared.tables[0];
|
|
458
|
+
if (!this.table && !this.options.getRowIdentity)
|
|
459
|
+
throw new Error(
|
|
460
|
+
"powersync-bridge: provide table or getRowIdentity when the adapter cannot infer the business table"
|
|
461
|
+
);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
this.localPending = false;
|
|
464
|
+
this.localError = error instanceof Error ? error : new Error(String(error));
|
|
465
|
+
this.render();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.unwatch = this.deps.driver.diffs(prepared).subscribe(
|
|
469
|
+
(diff) => {
|
|
470
|
+
void this.acceptLocal(diff.all, token, generation).catch((error) => {
|
|
471
|
+
if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
|
|
472
|
+
return;
|
|
473
|
+
this.localPending = false;
|
|
474
|
+
this.localError = error instanceof Error ? error : new Error(String(error));
|
|
475
|
+
this.render();
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
(error) => {
|
|
479
|
+
if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
|
|
480
|
+
return;
|
|
481
|
+
this.localPending = false;
|
|
482
|
+
this.localError = error;
|
|
483
|
+
this.render();
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
this.render();
|
|
487
|
+
}
|
|
488
|
+
async acceptLocal(rows, token, generation, stamp = this.deps.freshness.freshAtFor(void 0), isCurrent = () => true) {
|
|
489
|
+
await this.deps.crud.resync();
|
|
490
|
+
if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
|
|
491
|
+
return;
|
|
492
|
+
const previous = this.local;
|
|
493
|
+
const firstEmission = this.lastLocalWatch !== token;
|
|
494
|
+
this.lastLocalWatch = token;
|
|
495
|
+
const next = new Map(rows.map((row) => [idOf(row), row]));
|
|
496
|
+
const removed = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const [id, row] of previous)
|
|
498
|
+
if (!next.has(id)) {
|
|
499
|
+
removed.set(id, row);
|
|
500
|
+
this.tombstones.add(id);
|
|
501
|
+
}
|
|
502
|
+
for (const [id, row] of next) {
|
|
503
|
+
if (previous.size && (!firstEmission || previous.has(id)) && JSON.stringify(previous.get(id)) !== JSON.stringify(row) && !this.pending(row))
|
|
504
|
+
this.inSession.add(id);
|
|
505
|
+
this.tombstones.delete(id);
|
|
506
|
+
this.filtered.delete(id);
|
|
507
|
+
this.retained.delete(id);
|
|
508
|
+
}
|
|
509
|
+
this.local = next;
|
|
510
|
+
this.localPending = false;
|
|
511
|
+
this.localError = void 0;
|
|
512
|
+
this.localStamp = stamp;
|
|
513
|
+
this.render();
|
|
514
|
+
const groups = /* @__PURE__ */ new Map();
|
|
515
|
+
for (const row of removed.values()) {
|
|
516
|
+
const identity = this.identity(row);
|
|
517
|
+
const group = groups.get(identity.table) ?? /* @__PURE__ */ new Map();
|
|
518
|
+
group.set(identity.id, row);
|
|
519
|
+
groups.set(identity.table, group);
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
for (const [table, group] of groups) {
|
|
523
|
+
const classes = await classifyRemovals(
|
|
524
|
+
{
|
|
525
|
+
driver: this.deps.driver,
|
|
526
|
+
clock: this.deps.clock,
|
|
527
|
+
table,
|
|
528
|
+
provenance: () => void 0,
|
|
529
|
+
isInWindow: this.options.isInWindow
|
|
530
|
+
},
|
|
531
|
+
group
|
|
532
|
+
);
|
|
533
|
+
if (token !== this.watchGeneration || generation !== this.generation || this.disposed)
|
|
534
|
+
return;
|
|
535
|
+
for (const row of removed.values()) {
|
|
536
|
+
const identity = this.identity(row);
|
|
537
|
+
if (identity.table !== table || this.local.has(row.id)) continue;
|
|
538
|
+
const classification = classes.get(identity.id);
|
|
539
|
+
if (!classification) continue;
|
|
540
|
+
if (classification === "delete") this.tombstones.add(row.id);
|
|
541
|
+
else if (classification === "filtered") {
|
|
542
|
+
this.tombstones.delete(row.id);
|
|
543
|
+
this.filtered.add(row.id);
|
|
544
|
+
} else {
|
|
545
|
+
this.tombstones.delete(row.id);
|
|
546
|
+
this.retained.set(row.id, row);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (token !== this.watchGeneration || generation !== this.generation || this.disposed) return;
|
|
552
|
+
this.localError = error instanceof Error ? error : new Error(String(error));
|
|
553
|
+
}
|
|
554
|
+
const retained = [...this.retained.values()].sort(this.compare).slice(0, this.maxRows);
|
|
555
|
+
this.retained.clear();
|
|
556
|
+
for (const row of retained) this.retained.set(row.id, row);
|
|
557
|
+
const known = /* @__PURE__ */ new Set([
|
|
558
|
+
...this.local.keys(),
|
|
559
|
+
...this.retained.keys(),
|
|
560
|
+
...this.pages.flatMap((page) => page.rows.map((row) => row.id))
|
|
561
|
+
]);
|
|
562
|
+
for (const set of [this.inSession, this.tombstones, this.filtered]) {
|
|
563
|
+
if (set.size > this.maxRows * 4) {
|
|
564
|
+
for (const id of set) if (!known.has(id)) set.delete(id);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
this.render();
|
|
568
|
+
}
|
|
569
|
+
async checkpoint(isCurrent) {
|
|
570
|
+
if (this.disposed || !this.unwatch || this.activeRefs <= 0) return;
|
|
571
|
+
const token = this.watchGeneration;
|
|
572
|
+
const generation = this.generation;
|
|
573
|
+
const stamp = this.deps.freshness.freshAtFor(void 0);
|
|
574
|
+
try {
|
|
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);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (token !== this.watchGeneration || generation !== this.generation || this.disposed || !isCurrent())
|
|
583
|
+
return;
|
|
584
|
+
this.localError = error instanceof Error ? error : new Error(String(error));
|
|
585
|
+
this.render();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
clearPages() {
|
|
589
|
+
for (const page of this.pages) this.deps.crud.closeWindow(page.window);
|
|
590
|
+
this.pages = [];
|
|
591
|
+
}
|
|
592
|
+
apiEnd() {
|
|
593
|
+
return this.pages.at(-1)?.end === true;
|
|
594
|
+
}
|
|
595
|
+
apiCount() {
|
|
596
|
+
return this.pages.reduce((count, page) => count + page.rows.length, 0);
|
|
597
|
+
}
|
|
598
|
+
apiCursor() {
|
|
599
|
+
const last = this.pages.at(-1)?.rows.at(-1);
|
|
600
|
+
return last ? last : this.anchor;
|
|
601
|
+
}
|
|
602
|
+
cursorKey(row) {
|
|
603
|
+
if (!row) return void 0;
|
|
604
|
+
const [first, second] = keyColumns(this.options.orderBy);
|
|
605
|
+
return [row[first], row[second]];
|
|
606
|
+
}
|
|
607
|
+
pump() {
|
|
608
|
+
if (!this.ready || this.activeRefs <= 0 || this.disposed || this.options.enabled === false || this.offline || this.apiPending || this.apiFailed || this.apiEnd())
|
|
609
|
+
return;
|
|
610
|
+
if (!this.table && !this.options.getRowIdentity) return;
|
|
611
|
+
if (this.pages.length >= Math.ceil(this.maxRows / this.pageSize) + 1) return;
|
|
612
|
+
if (this.apiCount() >= this.target && this.entries().length >= this.target) return;
|
|
613
|
+
const cursor = this.apiCursor();
|
|
614
|
+
const generation = this.generation;
|
|
615
|
+
const request = new AbortController();
|
|
616
|
+
this.request = request;
|
|
617
|
+
const window = this.deps.crud.openWindow();
|
|
618
|
+
this.apiPending = true;
|
|
619
|
+
this.render();
|
|
620
|
+
const key = [
|
|
621
|
+
"powersync-bridge.query",
|
|
622
|
+
this.deps.identityKey,
|
|
623
|
+
this.deps.epoch,
|
|
624
|
+
infiniteQueryKey(this.options),
|
|
625
|
+
this.cursorKey(this.anchor),
|
|
626
|
+
this.revision,
|
|
627
|
+
generation,
|
|
628
|
+
this.cursorKey(cursor)
|
|
629
|
+
];
|
|
630
|
+
this.requestKey = key;
|
|
631
|
+
void this.deps.snapshots.fetch(key, this.options.staleTime ?? 6e4, async (signal) => {
|
|
632
|
+
const abort = () => request.abort();
|
|
633
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
634
|
+
try {
|
|
635
|
+
return await withTimeout(
|
|
636
|
+
this.deps.clock,
|
|
637
|
+
this.options.fetchPage({ cursor, limit: this.pageSize + 1, signal: request.signal }).then((rows) => ({ rows })),
|
|
638
|
+
3e4,
|
|
639
|
+
request.signal
|
|
640
|
+
);
|
|
641
|
+
} finally {
|
|
642
|
+
signal.removeEventListener("abort", abort);
|
|
643
|
+
}
|
|
644
|
+
}).then((record) => {
|
|
645
|
+
if (this.disposed || generation !== this.generation || request.signal.aborted) {
|
|
646
|
+
this.deps.crud.closeWindow(window);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const rows = [...record.rows];
|
|
650
|
+
for (let index = 1; index < rows.length; index++) {
|
|
651
|
+
const before = rows[index - 1];
|
|
652
|
+
const row = rows[index];
|
|
653
|
+
if (before && row && this.compare(before, row) >= 0)
|
|
654
|
+
throw new Error(
|
|
655
|
+
"powersync-bridge: API rows must use a strict, stable order with an id tiebreaker"
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
if (cursor !== void 0 && rows.some((row) => this.compare(row, cursor) <= 0))
|
|
659
|
+
throw new Error("powersync-bridge: API page must advance beyond the supplied cursor");
|
|
660
|
+
this.pages.push({
|
|
661
|
+
cursor,
|
|
662
|
+
rows: rows.slice(0, this.pageSize),
|
|
663
|
+
end: rows.length <= this.pageSize,
|
|
664
|
+
fetchedAt: record.fetchedAt,
|
|
665
|
+
window
|
|
666
|
+
});
|
|
667
|
+
this.apiError = void 0;
|
|
668
|
+
}).catch((error) => {
|
|
669
|
+
this.deps.crud.closeWindow(window);
|
|
670
|
+
if (this.disposed || generation !== this.generation || request.signal.aborted) return;
|
|
671
|
+
this.apiFailed = true;
|
|
672
|
+
this.apiError = error instanceof Error ? error : new Error(String(error));
|
|
673
|
+
request.abort();
|
|
674
|
+
}).finally(() => {
|
|
675
|
+
if (generation !== this.generation || this.disposed) return;
|
|
676
|
+
this.apiPending = false;
|
|
677
|
+
this.request = void 0;
|
|
678
|
+
this.requestKey = void 0;
|
|
679
|
+
this.render();
|
|
680
|
+
this.pump();
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
entries() {
|
|
684
|
+
const api = /* @__PURE__ */ new Map();
|
|
685
|
+
for (const page of this.pages) for (const row of page.rows) api.set(row.id, { row, page });
|
|
686
|
+
const ids = /* @__PURE__ */ new Set([...this.local.keys(), ...this.retained.keys(), ...api.keys()]);
|
|
687
|
+
const entries = [];
|
|
688
|
+
for (const id of ids) {
|
|
689
|
+
if (this.tombstones.has(id) || this.filtered.has(id)) continue;
|
|
690
|
+
const local = this.local.get(id);
|
|
691
|
+
const remote = api.get(id);
|
|
692
|
+
const row = local ?? remote?.row ?? this.retained.get(id);
|
|
693
|
+
if (!row) continue;
|
|
694
|
+
if (this.pending(row)) {
|
|
695
|
+
if (local) entries.push({ row: local, origin: "local", presentLocally: true });
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (remote) {
|
|
699
|
+
entries.push(
|
|
700
|
+
local && this.localStamp > remote.page.fetchedAt ? { row: local, origin: "local", presentLocally: true } : { row: remote.row, origin: "api", presentLocally: Boolean(local) }
|
|
701
|
+
);
|
|
702
|
+
} else if (local) {
|
|
703
|
+
const covering = this.pages.find((page) => {
|
|
704
|
+
if (page.cursor !== void 0 && this.compare(local, page.cursor) <= 0) return false;
|
|
705
|
+
const last = page.rows.at(-1);
|
|
706
|
+
return page.end || Boolean(last && this.compare(local, last) <= 0);
|
|
707
|
+
});
|
|
708
|
+
if (!covering || this.inSession.has(id) || this.localStamp > covering.fetchedAt)
|
|
709
|
+
entries.push({ row: local, origin: "local", presentLocally: true });
|
|
710
|
+
} else entries.push({ row, origin: "local", presentLocally: false });
|
|
711
|
+
}
|
|
712
|
+
return entries.sort((a, b) => this.compare(a.row, b.row));
|
|
713
|
+
}
|
|
714
|
+
result(empty = false) {
|
|
715
|
+
const enabled = !empty && this.options.enabled !== false;
|
|
716
|
+
const entries = enabled ? this.entries() : [];
|
|
717
|
+
const items = entries.slice(0, this.target);
|
|
718
|
+
const more = entries.length > this.target || !this.apiEnd();
|
|
719
|
+
const missing = items.length < this.target;
|
|
720
|
+
const errors = {};
|
|
721
|
+
if (this.localError) errors.local = this.localError;
|
|
722
|
+
if (this.apiError) errors.api = this.apiError;
|
|
723
|
+
const offlineError = this.offline && missing && !this.localPending ? new OfflineError() : void 0;
|
|
724
|
+
const error = enabled && missing && !this.localPending && !this.apiPending ? this.localError ?? this.apiError ?? offlineError : void 0;
|
|
725
|
+
return {
|
|
726
|
+
data: items.map((item) => item.row),
|
|
727
|
+
items,
|
|
728
|
+
isLoading: enabled && items.length === 0 && !error && (this.localPending || this.apiPending),
|
|
729
|
+
isFetching: enabled && (this.localPending || this.apiPending),
|
|
730
|
+
isFetchingNextPage: enabled && this.target > this.pageSize && (this.localPending || this.apiPending),
|
|
731
|
+
hasNextPage: enabled && this.target < this.maxRows && more,
|
|
732
|
+
needsOlderWindow: enabled && items.length >= this.maxRows && more,
|
|
733
|
+
error,
|
|
734
|
+
errors,
|
|
735
|
+
isError: Boolean(error),
|
|
736
|
+
windowRevision: this.revision
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
render(empty = false) {
|
|
740
|
+
if (this.disposed && !empty) return;
|
|
741
|
+
const result = this.result(empty);
|
|
742
|
+
if (!shallowEqualResult(
|
|
743
|
+
this.store.getSnapshot(),
|
|
744
|
+
result
|
|
745
|
+
))
|
|
746
|
+
this.store.replace(result);
|
|
747
|
+
}
|
|
748
|
+
fetchNextPage = () => {
|
|
749
|
+
if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false || this.target >= this.maxRows)
|
|
750
|
+
return;
|
|
751
|
+
if (!this.store.getSnapshot().hasNextPage) return;
|
|
752
|
+
this.target = Math.min(this.target + this.pageSize, this.maxRows);
|
|
753
|
+
this.watch();
|
|
754
|
+
this.pump();
|
|
755
|
+
};
|
|
756
|
+
retry = () => {
|
|
757
|
+
if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
|
|
758
|
+
this.apiFailed = false;
|
|
759
|
+
this.apiError = void 0;
|
|
760
|
+
this.watch();
|
|
761
|
+
this.pump();
|
|
762
|
+
};
|
|
763
|
+
refresh = () => {
|
|
764
|
+
this.reset(this.anchor);
|
|
765
|
+
};
|
|
766
|
+
continueOlder = () => {
|
|
767
|
+
const last = this.store.getSnapshot().data.at(-1);
|
|
768
|
+
if (last) this.reset(last);
|
|
769
|
+
};
|
|
770
|
+
reset(anchor) {
|
|
771
|
+
if (this.disposed || this.activeRefs <= 0 || this.options.enabled === false) return;
|
|
772
|
+
this.generation += 1;
|
|
773
|
+
this.cancelRequest();
|
|
774
|
+
this.request = void 0;
|
|
775
|
+
this.apiPending = false;
|
|
776
|
+
this.apiFailed = false;
|
|
777
|
+
this.apiError = void 0;
|
|
778
|
+
this.localError = void 0;
|
|
779
|
+
this.clearPages();
|
|
780
|
+
this.local.clear();
|
|
781
|
+
this.retained.clear();
|
|
782
|
+
this.inSession.clear();
|
|
783
|
+
this.tombstones.clear();
|
|
784
|
+
this.filtered.clear();
|
|
785
|
+
this.target = this.pageSize;
|
|
786
|
+
this.anchor = anchor;
|
|
787
|
+
this.revision += 1;
|
|
788
|
+
this.watch();
|
|
789
|
+
this.pump();
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
115
793
|
// src/flagger.ts
|
|
116
794
|
var AutoSyncFlagStore = class {
|
|
117
795
|
constructor(driver, clock, config, ids) {
|
|
@@ -201,7 +879,6 @@ var FreshnessTracker = class {
|
|
|
201
879
|
lastHasSynced = void 0;
|
|
202
880
|
streamFreshAt = /* @__PURE__ */ new Map();
|
|
203
881
|
streamSeenSyncedAt = /* @__PURE__ */ new Map();
|
|
204
|
-
completions = [];
|
|
205
882
|
completionListeners = /* @__PURE__ */ new Set();
|
|
206
883
|
clearListeners = /* @__PURE__ */ new Set();
|
|
207
884
|
unsubscribe = null;
|
|
@@ -235,7 +912,6 @@ var FreshnessTracker = class {
|
|
|
235
912
|
this.lastSeenSyncedAt = null;
|
|
236
913
|
this.streamFreshAt.clear();
|
|
237
914
|
this.streamSeenSyncedAt.clear();
|
|
238
|
-
this.completions.length = 0;
|
|
239
915
|
}
|
|
240
916
|
get sessionConnectAtMs() {
|
|
241
917
|
return this.sessionConnectAt;
|
|
@@ -308,7 +984,6 @@ var FreshnessTracker = class {
|
|
|
308
984
|
this.prevCompletionAt = now;
|
|
309
985
|
this.lastDownloadStart = null;
|
|
310
986
|
const c = { stamp: this.localFreshAt, completedAt: now, stream };
|
|
311
|
-
this.completions.push(c);
|
|
312
987
|
for (const l of this.completionListeners) l(c);
|
|
313
988
|
}
|
|
314
989
|
};
|
|
@@ -391,68 +1066,6 @@ var IdentityTracker = class {
|
|
|
391
1066
|
}
|
|
392
1067
|
};
|
|
393
1068
|
|
|
394
|
-
// src/keyset.ts
|
|
395
|
-
function keyColumns(orderKey) {
|
|
396
|
-
if (typeof orderKey === "string") return [orderKey, "id"];
|
|
397
|
-
return [orderKey[0], orderKey[1]];
|
|
398
|
-
}
|
|
399
|
-
function idOf(row) {
|
|
400
|
-
const r = row;
|
|
401
|
-
if (r == null || r.id === void 0 || r.id === null) {
|
|
402
|
-
throw new Error("powersync-bridge: every row must have an `id`");
|
|
403
|
-
}
|
|
404
|
-
return String(r.id);
|
|
405
|
-
}
|
|
406
|
-
function compareValues(a, b) {
|
|
407
|
-
if (a === b) return 0;
|
|
408
|
-
if (a === null || a === void 0) return 1;
|
|
409
|
-
if (b === null || b === void 0) return -1;
|
|
410
|
-
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
411
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
412
|
-
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
413
|
-
const sa = String(a);
|
|
414
|
-
const sb = String(b);
|
|
415
|
-
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
416
|
-
}
|
|
417
|
-
function rowComparator(orderKey, direction = "asc") {
|
|
418
|
-
const [k1, k2] = keyColumns(orderKey);
|
|
419
|
-
const sign = direction === "desc" ? -1 : 1;
|
|
420
|
-
return (a, b) => {
|
|
421
|
-
const c1 = compareValues(a[k1], b[k1]);
|
|
422
|
-
if (c1 !== 0) return c1 * sign;
|
|
423
|
-
return compareValues(a[k2], b[k2]) * sign;
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
function cursorOfRow(row, orderKey) {
|
|
427
|
-
const r = row;
|
|
428
|
-
if (typeof orderKey === "string") return r[orderKey];
|
|
429
|
-
return [r[orderKey[0]], r[orderKey[1]]];
|
|
430
|
-
}
|
|
431
|
-
function isAfterCursor(row, cursor, orderKey, direction = "asc") {
|
|
432
|
-
const [k1, k2] = keyColumns(orderKey);
|
|
433
|
-
const r = row;
|
|
434
|
-
const sign = direction === "desc" ? -1 : 1;
|
|
435
|
-
const [c1, c2] = typeof orderKey === "string" ? [cursor, void 0] : cursor;
|
|
436
|
-
const d1 = compareValues(r[k1], c1) * sign;
|
|
437
|
-
if (d1 !== 0) return d1 > 0;
|
|
438
|
-
if (c2 === void 0) return false;
|
|
439
|
-
return compareValues(r[k2], c2) * sign > 0;
|
|
440
|
-
}
|
|
441
|
-
function toBase64(s) {
|
|
442
|
-
if (typeof btoa === "function") return btoa(unescape(encodeURIComponent(s)));
|
|
443
|
-
return Buffer.from(s, "utf8").toString("base64");
|
|
444
|
-
}
|
|
445
|
-
function fromBase64(s) {
|
|
446
|
-
if (typeof atob === "function") return decodeURIComponent(escape(atob(s)));
|
|
447
|
-
return Buffer.from(s, "base64").toString("utf8");
|
|
448
|
-
}
|
|
449
|
-
function encodeCursor(cursor) {
|
|
450
|
-
return toBase64(JSON.stringify(cursor));
|
|
451
|
-
}
|
|
452
|
-
function decodeCursor(opaque) {
|
|
453
|
-
return JSON.parse(fromBase64(opaque));
|
|
454
|
-
}
|
|
455
|
-
|
|
456
1069
|
// src/infinite.ts
|
|
457
1070
|
var EMPTY = Object.freeze({
|
|
458
1071
|
items: Object.freeze([]),
|
|
@@ -503,6 +1116,7 @@ var InfiniteListStore = class {
|
|
|
503
1116
|
reason;
|
|
504
1117
|
awaitingSync = false;
|
|
505
1118
|
loading = false;
|
|
1119
|
+
localPagePending = false;
|
|
506
1120
|
errors = {};
|
|
507
1121
|
warnings = [];
|
|
508
1122
|
errorCount = 0;
|
|
@@ -511,14 +1125,16 @@ var InfiniteListStore = class {
|
|
|
511
1125
|
refs = 0;
|
|
512
1126
|
retain() {
|
|
513
1127
|
this.refs += 1;
|
|
514
|
-
if (this.refs === 1) this.subscribeLocal();
|
|
1128
|
+
if (this.refs === 1 && this.unsubDiffs === null) this.subscribeLocal();
|
|
515
1129
|
}
|
|
516
1130
|
release() {
|
|
517
1131
|
this.refs -= 1;
|
|
518
|
-
if (this.refs <= 0)
|
|
1132
|
+
if (this.refs <= 0) queueMicrotask(() => {
|
|
1133
|
+
if (this.refs <= 0) this.dispose();
|
|
1134
|
+
});
|
|
519
1135
|
}
|
|
520
1136
|
get isDisposed() {
|
|
521
|
-
return this.disposed;
|
|
1137
|
+
return this.disposed || this.refs <= 0 && this.unsubDiffs !== null;
|
|
522
1138
|
}
|
|
523
1139
|
swapRoot() {
|
|
524
1140
|
this.store.replace(emptyInfiniteResult());
|
|
@@ -532,6 +1148,11 @@ var InfiniteListStore = class {
|
|
|
532
1148
|
for (const h of this.subs.values()) h.unsubscribe();
|
|
533
1149
|
this.subs.clear();
|
|
534
1150
|
this.lru.length = 0;
|
|
1151
|
+
this.local.clear();
|
|
1152
|
+
this.apiRows.clear();
|
|
1153
|
+
this.retained.clear();
|
|
1154
|
+
this.tombstones.clear();
|
|
1155
|
+
this.store.replace(emptyInfiniteResult());
|
|
535
1156
|
}
|
|
536
1157
|
// ---------------------------------------------------------------------------
|
|
537
1158
|
subscribeLocal() {
|
|
@@ -543,6 +1164,10 @@ var InfiniteListStore = class {
|
|
|
543
1164
|
(diff) => void this.onDiff(diff),
|
|
544
1165
|
(err) => {
|
|
545
1166
|
this.localError = err;
|
|
1167
|
+
if (this.localPagePending) {
|
|
1168
|
+
this.localPagePending = false;
|
|
1169
|
+
this.loading = false;
|
|
1170
|
+
}
|
|
546
1171
|
this.render();
|
|
547
1172
|
}
|
|
548
1173
|
);
|
|
@@ -562,6 +1187,11 @@ var InfiniteListStore = class {
|
|
|
562
1187
|
this.apiRows.delete(id);
|
|
563
1188
|
}
|
|
564
1189
|
this.local = next;
|
|
1190
|
+
this.localError = void 0;
|
|
1191
|
+
if (this.localPagePending) {
|
|
1192
|
+
this.localPagePending = false;
|
|
1193
|
+
this.loading = false;
|
|
1194
|
+
}
|
|
565
1195
|
this.emissionSeen = true;
|
|
566
1196
|
const removed = /* @__PURE__ */ new Map();
|
|
567
1197
|
for (const [id, row] of prev) if (!next.has(id)) removed.set(id, row);
|
|
@@ -616,6 +1246,8 @@ var InfiniteListStore = class {
|
|
|
616
1246
|
async loadMore() {
|
|
617
1247
|
if (this.disposed || this.loading || this.boundary === "end") return;
|
|
618
1248
|
if (this.lookahead) {
|
|
1249
|
+
this.localPagePending = true;
|
|
1250
|
+
this.loading = true;
|
|
619
1251
|
this.pages += 1;
|
|
620
1252
|
this.boundary = "more";
|
|
621
1253
|
this.reason = void 0;
|
|
@@ -770,6 +1402,8 @@ var InfiniteListStore = class {
|
|
|
770
1402
|
}
|
|
771
1403
|
}
|
|
772
1404
|
absorbApi(rows) {
|
|
1405
|
+
const { api: _previousApiError, ...remainingErrors } = this.errors;
|
|
1406
|
+
this.errors = remainingErrors;
|
|
773
1407
|
this.errorCount = 0;
|
|
774
1408
|
this.nextRetryAt = 0;
|
|
775
1409
|
for (const row of rows) {
|
|
@@ -777,6 +1411,11 @@ var InfiniteListStore = class {
|
|
|
777
1411
|
if (this.local.has(id) || this.apiRows.has(id) || this.tombstones.has(id)) continue;
|
|
778
1412
|
this.apiRows.set(id, row);
|
|
779
1413
|
}
|
|
1414
|
+
const loadedPages = Math.max(this.pages, Math.ceil(this.mergedEntries().length / this.def.pageSize));
|
|
1415
|
+
if (loadedPages > this.pages) {
|
|
1416
|
+
this.pages = loadedPages;
|
|
1417
|
+
this.subscribeLocal();
|
|
1418
|
+
}
|
|
780
1419
|
if (rows.length === 0) {
|
|
781
1420
|
this.boundary = "end";
|
|
782
1421
|
this.reason = void 0;
|
|
@@ -944,7 +1583,7 @@ var CrudMirror = class {
|
|
|
944
1583
|
for (const r of refs) next.set(refKey(r.table, r.id), r);
|
|
945
1584
|
const added = [];
|
|
946
1585
|
const completed = [];
|
|
947
|
-
for (const [k, r] of next) if (
|
|
1586
|
+
for (const [k, r] of next) if (this.live.get(k)?.op !== r.op) added.push(r);
|
|
948
1587
|
for (const [k, r] of this.live) if (!next.has(k)) completed.push(r);
|
|
949
1588
|
if (added.length || completed.length) this.apply({ added, completed });
|
|
950
1589
|
}
|
|
@@ -1013,6 +1652,10 @@ var RacedListStore = class {
|
|
|
1013
1652
|
this.compare = rowComparator(def.orderKey, def.direction ?? "asc");
|
|
1014
1653
|
this.table = def.table ?? def.id;
|
|
1015
1654
|
this.startedAt = deps.clock.now();
|
|
1655
|
+
this.checkpointRefresh = new CoalescedRefresh(
|
|
1656
|
+
(isCurrent) => this.onCompletion(isCurrent),
|
|
1657
|
+
() => this.reconcile()
|
|
1658
|
+
);
|
|
1016
1659
|
}
|
|
1017
1660
|
deps;
|
|
1018
1661
|
def;
|
|
@@ -1022,6 +1665,9 @@ var RacedListStore = class {
|
|
|
1022
1665
|
compare;
|
|
1023
1666
|
table;
|
|
1024
1667
|
abort = new AbortController();
|
|
1668
|
+
observation = 0;
|
|
1669
|
+
checkpointRefresh;
|
|
1670
|
+
localFreshAt = Number.NEGATIVE_INFINITY;
|
|
1025
1671
|
startedAt;
|
|
1026
1672
|
snap = null;
|
|
1027
1673
|
snapPending = false;
|
|
@@ -1040,19 +1686,25 @@ var RacedListStore = class {
|
|
|
1040
1686
|
tombstones = /* @__PURE__ */ new Set();
|
|
1041
1687
|
disposed = false;
|
|
1042
1688
|
refs = 0;
|
|
1689
|
+
activeRefs = 0;
|
|
1043
1690
|
measured = false;
|
|
1044
1691
|
localMs = null;
|
|
1045
1692
|
apiMs = null;
|
|
1046
|
-
retain() {
|
|
1693
|
+
retain(active = true) {
|
|
1047
1694
|
this.refs += 1;
|
|
1048
|
-
if (this.
|
|
1695
|
+
if (active) this.activeRefs += 1;
|
|
1696
|
+
if (this.activeRefs > 0 && this.unsubDiffs === null) this.start();
|
|
1049
1697
|
}
|
|
1050
|
-
release() {
|
|
1698
|
+
release(active = true) {
|
|
1051
1699
|
this.refs -= 1;
|
|
1052
|
-
if (this.
|
|
1700
|
+
if (active) this.activeRefs -= 1;
|
|
1701
|
+
queueMicrotask(() => {
|
|
1702
|
+
if (this.refs <= 0) this.dispose();
|
|
1703
|
+
else if (this.activeRefs <= 0) this.pause();
|
|
1704
|
+
});
|
|
1053
1705
|
}
|
|
1054
1706
|
get isDisposed() {
|
|
1055
|
-
return this.disposed;
|
|
1707
|
+
return this.disposed || this.refs <= 0 && this.unsubDiffs !== null;
|
|
1056
1708
|
}
|
|
1057
1709
|
/** Synchronous part of an identity swap: the snapshot becomes empty 'pending' at once (R15). */
|
|
1058
1710
|
swapRoot() {
|
|
@@ -1061,46 +1713,83 @@ var RacedListStore = class {
|
|
|
1061
1713
|
}
|
|
1062
1714
|
dispose() {
|
|
1063
1715
|
this.disposed = true;
|
|
1064
|
-
this.
|
|
1716
|
+
this.pause();
|
|
1717
|
+
if (this.snapWindow) this.deps.crud.closeWindow(this.snapWindow);
|
|
1718
|
+
this.snapWindow = null;
|
|
1719
|
+
this.local?.clear();
|
|
1720
|
+
this.local = null;
|
|
1721
|
+
this.snap = null;
|
|
1722
|
+
this.prepared = null;
|
|
1723
|
+
this.overlay.clear();
|
|
1724
|
+
this.inSession.clear();
|
|
1725
|
+
this.tombstones.clear();
|
|
1726
|
+
this.store.replace(emptyRacedResult());
|
|
1727
|
+
}
|
|
1728
|
+
pause() {
|
|
1729
|
+
this.observation += 1;
|
|
1730
|
+
this.checkpointRefresh.cancel();
|
|
1731
|
+
this.abort.abort(new Error("observer stopped"));
|
|
1065
1732
|
this.unsubDiffs?.();
|
|
1066
1733
|
this.unsubCrud?.();
|
|
1067
1734
|
this.unsubFresh?.();
|
|
1068
|
-
|
|
1069
|
-
this.
|
|
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
|
+
}
|
|
1070
1744
|
}
|
|
1071
1745
|
// ---------------------------------------------------------------------------
|
|
1072
1746
|
start() {
|
|
1747
|
+
if (this.disposed) return;
|
|
1748
|
+
this.abort = new AbortController();
|
|
1749
|
+
const observation = ++this.observation;
|
|
1073
1750
|
const { driver, adapter, crud, freshness } = this.deps;
|
|
1074
1751
|
const prepared = adapter.prepare(this.def.query(this.scope));
|
|
1075
1752
|
this.prepared = prepared;
|
|
1076
1753
|
this.unsubDiffs = driver.diffs(prepared).subscribe(
|
|
1077
|
-
(diff) =>
|
|
1754
|
+
(diff) => {
|
|
1755
|
+
if (observation === this.observation) void this.onDiff(diff);
|
|
1756
|
+
},
|
|
1078
1757
|
(err) => {
|
|
1758
|
+
if (observation !== this.observation) return;
|
|
1079
1759
|
this.localError = err;
|
|
1080
1760
|
this.reconcile();
|
|
1081
1761
|
}
|
|
1082
1762
|
);
|
|
1083
1763
|
this.unsubCrud = crud.onChange(() => this.reconcile());
|
|
1084
|
-
this.unsubFresh = freshness.onCompletion(() =>
|
|
1085
|
-
|
|
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();
|
|
1086
1770
|
this.reconcile();
|
|
1087
1771
|
}
|
|
1088
1772
|
startApiLeg() {
|
|
1773
|
+
const observation = this.observation;
|
|
1774
|
+
const abort = this.abort;
|
|
1089
1775
|
const { snapshots, crud } = this.deps;
|
|
1090
1776
|
const staleTime = this.def.staleTime ?? 10 * 6e4;
|
|
1091
1777
|
this.snapPending = true;
|
|
1092
1778
|
const window = crud.openWindow();
|
|
1093
1779
|
this.snapWindow = window;
|
|
1094
|
-
const key =
|
|
1780
|
+
const key = this.snapshotKey();
|
|
1095
1781
|
snapshots.fetch(key, staleTime, (signal) => {
|
|
1096
1782
|
const combined = new AbortController();
|
|
1097
1783
|
const onAbort = () => combined.abort();
|
|
1098
1784
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1099
|
-
|
|
1100
|
-
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
|
+
});
|
|
1101
1790
|
}).then(
|
|
1102
1791
|
(record) => {
|
|
1103
|
-
if (this.disposed) return;
|
|
1792
|
+
if (this.disposed || observation !== this.observation) return;
|
|
1104
1793
|
this.snapPending = false;
|
|
1105
1794
|
this.apiMs = this.deps.clock.now() - this.startedAt;
|
|
1106
1795
|
const rows = /* @__PURE__ */ new Map();
|
|
@@ -1108,32 +1797,38 @@ var RacedListStore = class {
|
|
|
1108
1797
|
const item = this.def.mapApi(raw);
|
|
1109
1798
|
rows.set(idOf(item), item);
|
|
1110
1799
|
}
|
|
1800
|
+
if (this.snap) crud.closeWindow(this.snap.window);
|
|
1111
1801
|
this.snap = { rows, record, window };
|
|
1112
1802
|
this.reconcile();
|
|
1113
1803
|
},
|
|
1114
1804
|
(err) => {
|
|
1115
|
-
if (this.disposed) return;
|
|
1805
|
+
if (this.disposed || observation !== this.observation) return;
|
|
1116
1806
|
this.snapPending = false;
|
|
1117
1807
|
this.snapError = err instanceof Error ? err : new Error(String(err));
|
|
1118
1808
|
crud.closeWindow(window);
|
|
1119
|
-
this.snapWindow = null;
|
|
1809
|
+
this.snapWindow = this.snap?.window ?? null;
|
|
1120
1810
|
this.reconcile();
|
|
1121
1811
|
}
|
|
1122
1812
|
);
|
|
1123
1813
|
}
|
|
1814
|
+
snapshotKey() {
|
|
1815
|
+
return ["powersync-bridge", this.deps.identityKey, this.deps.epoch, this.def.id, this.scopeKey];
|
|
1816
|
+
}
|
|
1124
1817
|
/**
|
|
1125
1818
|
* A checkpoint completed. Status events and diff emissions are separate SDK events with no
|
|
1126
1819
|
* ordering guarantee; taking ownership on the stale local map would render the pre-checkpoint
|
|
1127
1820
|
* value for one frame (fresh→old→fresh). Pre-ownership we re-query once, then reconcile.
|
|
1128
1821
|
*/
|
|
1129
|
-
async onCompletion() {
|
|
1130
|
-
if (this.disposed || this.owned || !this.prepared ||
|
|
1131
|
-
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
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);
|
|
1134
1825
|
try {
|
|
1135
|
-
const
|
|
1136
|
-
|
|
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;
|
|
1137
1832
|
const prev = this.local ?? /* @__PURE__ */ new Map();
|
|
1138
1833
|
const next = /* @__PURE__ */ new Map();
|
|
1139
1834
|
for (const row of rows) {
|
|
@@ -1146,12 +1841,16 @@ var RacedListStore = class {
|
|
|
1146
1841
|
}
|
|
1147
1842
|
}
|
|
1148
1843
|
this.local = next;
|
|
1844
|
+
this.localFreshAt = Math.max(this.localFreshAt, freshAt);
|
|
1845
|
+
this.emissionSeen = true;
|
|
1846
|
+
this.localError = void 0;
|
|
1149
1847
|
} catch {
|
|
1150
1848
|
}
|
|
1151
1849
|
this.reconcile();
|
|
1152
1850
|
}
|
|
1153
1851
|
async onDiff(diff) {
|
|
1154
|
-
if (this.disposed) return;
|
|
1852
|
+
if (this.disposed || this.activeRefs <= 0) return;
|
|
1853
|
+
const observation = this.observation;
|
|
1155
1854
|
const { crud } = this.deps;
|
|
1156
1855
|
const prev = this.local;
|
|
1157
1856
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -1164,6 +1863,8 @@ var RacedListStore = class {
|
|
|
1164
1863
|
}
|
|
1165
1864
|
}
|
|
1166
1865
|
this.local = next;
|
|
1866
|
+
if (!this.checkpointRefresh.pending)
|
|
1867
|
+
this.localFreshAt = this.deps.freshness.freshAtFor(this.def.stream);
|
|
1167
1868
|
if (!this.emissionSeen) {
|
|
1168
1869
|
this.emissionSeen = true;
|
|
1169
1870
|
this.localMs = this.deps.clock.now() - this.startedAt;
|
|
@@ -1171,15 +1872,13 @@ var RacedListStore = class {
|
|
|
1171
1872
|
void crud.resync();
|
|
1172
1873
|
if (this.owned && prev) {
|
|
1173
1874
|
const removed = /* @__PURE__ */ new Map();
|
|
1174
|
-
for (const id of
|
|
1175
|
-
|
|
1176
|
-
if (row) removed.set(id, row);
|
|
1177
|
-
}
|
|
1178
|
-
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);
|
|
1179
1877
|
}
|
|
1878
|
+
if (observation !== this.observation) return;
|
|
1180
1879
|
this.reconcile();
|
|
1181
1880
|
}
|
|
1182
|
-
async applyRemovals(removed) {
|
|
1881
|
+
async applyRemovals(removed, observation) {
|
|
1183
1882
|
const classes = await classifyRemovals(
|
|
1184
1883
|
{
|
|
1185
1884
|
driver: this.deps.driver,
|
|
@@ -1190,7 +1889,7 @@ var RacedListStore = class {
|
|
|
1190
1889
|
},
|
|
1191
1890
|
removed
|
|
1192
1891
|
);
|
|
1193
|
-
if (this.disposed) return;
|
|
1892
|
+
if (this.disposed || observation !== this.observation) return;
|
|
1194
1893
|
for (const [id, cls] of classes) {
|
|
1195
1894
|
const entry = this.overlay.get(id);
|
|
1196
1895
|
switch (cls) {
|
|
@@ -1213,18 +1912,24 @@ var RacedListStore = class {
|
|
|
1213
1912
|
if (this.disposed) return;
|
|
1214
1913
|
const result = this.owned ? this.renderOwned() : this.renderRace();
|
|
1215
1914
|
const prev = this.store.getSnapshot();
|
|
1216
|
-
if (shallowEqualResult(
|
|
1915
|
+
if (shallowEqualResult(
|
|
1916
|
+
prev,
|
|
1917
|
+
result
|
|
1918
|
+
))
|
|
1919
|
+
return;
|
|
1217
1920
|
this.store.replace(result);
|
|
1218
1921
|
this.report(result);
|
|
1219
1922
|
}
|
|
1220
1923
|
freshAt() {
|
|
1221
|
-
return this.
|
|
1924
|
+
return this.localFreshAt;
|
|
1222
1925
|
}
|
|
1223
1926
|
localFresh() {
|
|
1224
|
-
|
|
1927
|
+
const connectedAt = this.deps.freshness.sessionConnectAtMs;
|
|
1928
|
+
return this.emissionSeen && connectedAt !== null && this.localFreshAt >= connectedAt;
|
|
1225
1929
|
}
|
|
1226
1930
|
tryTakeOwnership() {
|
|
1227
1931
|
if (this.owned) return true;
|
|
1932
|
+
if (this.checkpointRefresh.pending) return false;
|
|
1228
1933
|
const apiFreshAt = this.snap?.record.fetchedAt ?? Number.NEGATIVE_INFINITY;
|
|
1229
1934
|
if (!(this.localFresh() && this.freshAt() > apiFreshAt)) return false;
|
|
1230
1935
|
this.owned = true;
|
|
@@ -1234,7 +1939,8 @@ var RacedListStore = class {
|
|
|
1234
1939
|
if (this.snap) {
|
|
1235
1940
|
if (this.def.apiScopeSupersetOfLocal) {
|
|
1236
1941
|
for (const [id, row] of this.snap.rows) {
|
|
1237
|
-
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 });
|
|
1238
1944
|
}
|
|
1239
1945
|
}
|
|
1240
1946
|
this.deps.crud.closeWindow(this.snap.window);
|
|
@@ -1281,11 +1987,14 @@ var RacedListStore = class {
|
|
|
1281
1987
|
const inS = S?.rows.get(id);
|
|
1282
1988
|
const pending = crud.pendingOp(this.table, id, S?.window);
|
|
1283
1989
|
if (pending) {
|
|
1284
|
-
if (inLocal !== void 0)
|
|
1990
|
+
if (inLocal !== void 0)
|
|
1991
|
+
entries.push({ row: inLocal, origin: "local", presentLocally: true });
|
|
1285
1992
|
continue;
|
|
1286
1993
|
}
|
|
1287
1994
|
if (inLocal !== void 0 && inS !== void 0) {
|
|
1288
|
-
entries.push(
|
|
1995
|
+
entries.push(
|
|
1996
|
+
apiFresher ? { row: inS, origin: "api", presentLocally: true } : { row: inLocal, origin: "local", presentLocally: true }
|
|
1997
|
+
);
|
|
1289
1998
|
continue;
|
|
1290
1999
|
}
|
|
1291
2000
|
if (inLocal !== void 0) {
|
|
@@ -1309,7 +2018,9 @@ var RacedListStore = class {
|
|
|
1309
2018
|
finalize(entries, stale = false) {
|
|
1310
2019
|
entries.sort((a, b) => this.compare(a.row, b.row));
|
|
1311
2020
|
const limited = entries.slice(0, this.def.limit);
|
|
1312
|
-
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
|
+
);
|
|
1313
2024
|
}
|
|
1314
2025
|
build(items, state, awaiting) {
|
|
1315
2026
|
const errors = {};
|
|
@@ -1360,12 +2071,27 @@ var SnapshotCache = class {
|
|
|
1360
2071
|
if (existing && (existing.result === null || now - existing.result.fetchedAt < staleTime)) {
|
|
1361
2072
|
return existing.promise;
|
|
1362
2073
|
}
|
|
2074
|
+
for (const [entryKey, entry2] of this.entries) {
|
|
2075
|
+
if (entry2.result && now - entry2.result.fetchedAt >= staleTime) this.entries.delete(entryKey);
|
|
2076
|
+
}
|
|
2077
|
+
if (this.entries.size >= 64) {
|
|
2078
|
+
for (const [entryKey, entry2] of this.entries) {
|
|
2079
|
+
if (entry2.result) {
|
|
2080
|
+
this.entries.delete(entryKey);
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
1363
2085
|
const controller = new AbortController();
|
|
1364
2086
|
const run = async (signal) => {
|
|
1365
2087
|
const res = await fetcher(signal);
|
|
1366
2088
|
return { rows: res.rows, asOf: res.asOf, requestedAt: now, fetchedAt: this.clock.now() };
|
|
1367
2089
|
};
|
|
1368
|
-
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);
|
|
1369
2095
|
const entry = { requestedAt: now, promise, result: null, controller };
|
|
1370
2096
|
this.entries.set(k, entry);
|
|
1371
2097
|
promise.then(
|
|
@@ -1379,6 +2105,14 @@ var SnapshotCache = class {
|
|
|
1379
2105
|
return promise;
|
|
1380
2106
|
}
|
|
1381
2107
|
/** Cancel every in-flight fetch under a key prefix (identity swap). */
|
|
2108
|
+
invalidate(key) {
|
|
2109
|
+
const encoded = JSON.stringify(key);
|
|
2110
|
+
this.entries.get(encoded)?.controller.abort(new Error("invalidated"));
|
|
2111
|
+
void this.queryClient?.cancelQueries?.({ queryKey: key, exact: true }).catch(() => {
|
|
2112
|
+
});
|
|
2113
|
+
this.entries.delete(encoded);
|
|
2114
|
+
}
|
|
2115
|
+
/** Cancel every in-flight fetch under a key prefix (identity swap). */
|
|
1382
2116
|
abortAll(prefix) {
|
|
1383
2117
|
const p = JSON.stringify(prefix).slice(0, -1);
|
|
1384
2118
|
for (const [k, e] of this.entries) {
|
|
@@ -1409,10 +2143,12 @@ var Bridge = class {
|
|
|
1409
2143
|
clock;
|
|
1410
2144
|
/** Increments on every identity swap; hooks subscribe to re-acquire their stores. */
|
|
1411
2145
|
epochStore;
|
|
2146
|
+
connectivity = new SnapshotStore(void 0);
|
|
1412
2147
|
identity = new IdentityTracker();
|
|
1413
2148
|
freshness;
|
|
1414
2149
|
crud;
|
|
1415
2150
|
snapshots;
|
|
2151
|
+
backgroundReads = new BackgroundReads();
|
|
1416
2152
|
epochSnapshot;
|
|
1417
2153
|
stores = /* @__PURE__ */ new Map();
|
|
1418
2154
|
gates;
|
|
@@ -1420,12 +2156,16 @@ var Bridge = class {
|
|
|
1420
2156
|
identityOpts;
|
|
1421
2157
|
onEvicted;
|
|
1422
2158
|
disposed = false;
|
|
2159
|
+
onNetwork = () => this.setOnline(globalThis.navigator?.onLine);
|
|
1423
2160
|
constructor(opts) {
|
|
1424
2161
|
this.driver = opts.driver;
|
|
1425
2162
|
this.adapter = opts.adapter;
|
|
1426
2163
|
this.clock = opts.clock ?? realClock;
|
|
1427
2164
|
this.platform = opts.platform ?? detectPlatform();
|
|
1428
|
-
this.gates = {
|
|
2165
|
+
this.gates = {
|
|
2166
|
+
native: opts.platformGates?.native ?? "local-first",
|
|
2167
|
+
web: opts.platformGates?.web ?? "race"
|
|
2168
|
+
};
|
|
1429
2169
|
this.measure = opts.measure?.sink;
|
|
1430
2170
|
this.identityOpts = opts.identity ?? {};
|
|
1431
2171
|
this.onEvicted = opts.onEvicted;
|
|
@@ -1438,6 +2178,8 @@ var Bridge = class {
|
|
|
1438
2178
|
this.crud.start();
|
|
1439
2179
|
this.freshness.onClear(() => this.identity.signalClear());
|
|
1440
2180
|
this.identity.onChange(() => this.swapEpoch());
|
|
2181
|
+
globalThis.addEventListener?.("online", this.onNetwork);
|
|
2182
|
+
globalThis.addEventListener?.("offline", this.onNetwork);
|
|
1441
2183
|
}
|
|
1442
2184
|
get epoch() {
|
|
1443
2185
|
return this.identity.epoch;
|
|
@@ -1445,6 +2187,32 @@ var Bridge = class {
|
|
|
1445
2187
|
get identityKey() {
|
|
1446
2188
|
return this.identity.currentKey;
|
|
1447
2189
|
}
|
|
2190
|
+
setOnline(online) {
|
|
2191
|
+
this.connectivity.replace(online);
|
|
2192
|
+
}
|
|
2193
|
+
query(options) {
|
|
2194
|
+
const key = `query:${this.epoch}:${infiniteQueryKey(options)}`;
|
|
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
|
+
);
|
|
2215
|
+
}
|
|
1448
2216
|
/** Resolves once the initial upload-queue read has landed (tests). */
|
|
1449
2217
|
ready() {
|
|
1450
2218
|
return this.crud.ready();
|
|
@@ -1458,7 +2226,8 @@ var Bridge = class {
|
|
|
1458
2226
|
Object.defineProperty(wrapped, "fetchCredentials", {
|
|
1459
2227
|
value: async () => {
|
|
1460
2228
|
const creds = await connector.fetchCredentials();
|
|
1461
|
-
if (creds?.token)
|
|
2229
|
+
if (creds?.token)
|
|
2230
|
+
this.identity.observe(identityKeyFromToken(creds.token, this.identityOpts));
|
|
1462
2231
|
return creds;
|
|
1463
2232
|
},
|
|
1464
2233
|
configurable: true,
|
|
@@ -1520,7 +2289,8 @@ var Bridge = class {
|
|
|
1520
2289
|
epoch: this.epoch,
|
|
1521
2290
|
identityKey: this.identityKey,
|
|
1522
2291
|
apiLegEnabled: this.apiLegEnabled(),
|
|
1523
|
-
measure: this.measure
|
|
2292
|
+
measure: this.measure,
|
|
2293
|
+
backgroundReads: this.backgroundReads
|
|
1524
2294
|
},
|
|
1525
2295
|
def,
|
|
1526
2296
|
scope,
|
|
@@ -1546,7 +2316,10 @@ var Bridge = class {
|
|
|
1546
2316
|
}
|
|
1547
2317
|
onDemand(name, params, opts = {}) {
|
|
1548
2318
|
const key = `ondemand:${this.epoch}:${name}:${JSON.stringify(params)}`;
|
|
1549
|
-
return this.acquire(
|
|
2319
|
+
return this.acquire(
|
|
2320
|
+
key,
|
|
2321
|
+
() => new OnDemandStreamStore(this.driver, this.clock, name, params, opts)
|
|
2322
|
+
);
|
|
1550
2323
|
}
|
|
1551
2324
|
autoSync(config, ids) {
|
|
1552
2325
|
const key = `flag:${this.epoch}:${config.stream}:${JSON.stringify([...ids].sort())}`;
|
|
@@ -1555,6 +2328,8 @@ var Bridge = class {
|
|
|
1555
2328
|
dispose() {
|
|
1556
2329
|
if (this.disposed) return;
|
|
1557
2330
|
this.disposed = true;
|
|
2331
|
+
globalThis.removeEventListener?.("online", this.onNetwork);
|
|
2332
|
+
globalThis.removeEventListener?.("offline", this.onNetwork);
|
|
1558
2333
|
for (const s of this.stores.values()) s.dispose();
|
|
1559
2334
|
this.stores.clear();
|
|
1560
2335
|
this.snapshots.clear();
|
|
@@ -1578,6 +2353,8 @@ exports.AutoSyncFlagStore = AutoSyncFlagStore;
|
|
|
1578
2353
|
exports.Bridge = Bridge;
|
|
1579
2354
|
exports.DEFAULT_VOLATILE_CLAIMS = DEFAULT_VOLATILE_CLAIMS;
|
|
1580
2355
|
exports.InfiniteListStore = InfiniteListStore;
|
|
2356
|
+
exports.InfiniteQueryStore = InfiniteQueryStore;
|
|
2357
|
+
exports.OfflineError = OfflineError;
|
|
1581
2358
|
exports.OnDemandStreamStore = OnDemandStreamStore;
|
|
1582
2359
|
exports.RacedListStore = RacedListStore;
|
|
1583
2360
|
exports.SnapshotStore = SnapshotStore;
|
|
@@ -1593,6 +2370,7 @@ exports.emptyRacedResult = emptyRacedResult;
|
|
|
1593
2370
|
exports.encodeCursor = encodeCursor;
|
|
1594
2371
|
exports.identityKeyFromClaims = identityKeyFromClaims;
|
|
1595
2372
|
exports.identityKeyFromToken = identityKeyFromToken;
|
|
2373
|
+
exports.infiniteQueryKey = infiniteQueryKey;
|
|
1596
2374
|
exports.keyColumns = keyColumns;
|
|
1597
2375
|
exports.probePresence = probePresence;
|
|
1598
2376
|
exports.realClock = realClock;
|