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