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