@syncular/client 0.3.1 → 0.4.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/dist/client.d.ts +19 -4
- package/dist/client.js +30 -6
- package/package.json +3 -3
- package/src/client.ts +40 -6
package/dist/client.d.ts
CHANGED
|
@@ -182,8 +182,20 @@ export interface SubscribeInput {
|
|
|
182
182
|
export interface WindowState {
|
|
183
183
|
/** Windowed-in units for this base, ordered by value. */
|
|
184
184
|
readonly units: readonly string[];
|
|
185
|
+
/**
|
|
186
|
+
* Registered units whose bootstrap has not yet completed (§4.8): the
|
|
187
|
+
* unit's subscription still has `cursor: -1` (never synced) or holds a
|
|
188
|
+
* resume token (mid-bootstrap). Between `setWindow` and the bootstrap
|
|
189
|
+
* landing, the local replica for a pending unit may be empty or partial —
|
|
190
|
+
* it MUST NOT be rendered as complete.
|
|
191
|
+
*/
|
|
192
|
+
readonly pending: readonly string[];
|
|
185
193
|
}
|
|
186
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* True iff `unit` is windowed-in AND its bootstrap completed (§4.8 I3):
|
|
196
|
+
* registered and not pending. A unit with zero server rows still becomes
|
|
197
|
+
* complete once its bootstrap round finishes — emptiness ≠ pendency.
|
|
198
|
+
*/
|
|
187
199
|
export declare function windowComplete(state: WindowState, unit: string): boolean;
|
|
188
200
|
export declare class SyncClient {
|
|
189
201
|
#private;
|
|
@@ -305,10 +317,13 @@ export declare class SyncClient {
|
|
|
305
317
|
setWindow(base: WindowBase, units: readonly string[]): Promise<void>;
|
|
306
318
|
/**
|
|
307
319
|
* The completeness oracle (§4.8 I3): which units of a base are windowed-in
|
|
308
|
-
* locally,
|
|
309
|
-
*
|
|
320
|
+
* locally, which of those are still bootstrap-pending, and thereby the
|
|
321
|
+
* per-unit verdict a live query renders "may be partial" from. A query
|
|
322
|
+
* whose scope footprint includes an un-windowed OR still-pending unit is
|
|
310
323
|
* NOT answerable in full — the host widens or shows partial, never
|
|
311
|
-
* silently-complete.
|
|
324
|
+
* silently-complete. Registration alone is not completeness: a unit is
|
|
325
|
+
* pending until its subscription completes a bootstrap round (cursor
|
|
326
|
+
* advances past -1 with no resume token held).
|
|
312
327
|
*/
|
|
313
328
|
windowState(base: WindowBase): WindowState;
|
|
314
329
|
/**
|
package/dist/client.js
CHANGED
|
@@ -19,9 +19,13 @@ import { assertReadOnlyQuery } from './query-guard.js';
|
|
|
19
19
|
import { compileClientSchema, dropAndRecreateSyncedTables, ensureLocalSchema, fromSqlValue, jsonToRowValue, LOCAL_SCHEMA_VERSION_KEY, normalizeRecordKeys, OPTIMISTIC_VERSION, quoteIdent, recordToRowValues, rowValueToJson, SYNC_VERSION_COLUMN, stripSyncColumns, } from './schema.js';
|
|
20
20
|
import { deleteSubscription, getMeta, getSubscription, loadSubscriptions, resetSubscriptionsForBump, saveSubscription, setMeta, } from './state.js';
|
|
21
21
|
import { deletePendingEviction, deleteWindowUnit, deriveSubId, insertWindowUnit, loadPendingEvictions, loadWindowUnits, savePendingEviction, unitScopes, windowBaseKey, } from './window.js';
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* True iff `unit` is windowed-in AND its bootstrap completed (§4.8 I3):
|
|
24
|
+
* registered and not pending. A unit with zero server rows still becomes
|
|
25
|
+
* complete once its bootstrap round finishes — emptiness ≠ pendency.
|
|
26
|
+
*/
|
|
23
27
|
export function windowComplete(state, unit) {
|
|
24
|
-
return state.units.includes(unit);
|
|
28
|
+
return state.units.includes(unit) && !state.pending.includes(unit);
|
|
25
29
|
}
|
|
26
30
|
// ---------------------------------------------------------------------------
|
|
27
31
|
// Internals
|
|
@@ -645,15 +649,28 @@ export class SyncClient {
|
|
|
645
649
|
}
|
|
646
650
|
/**
|
|
647
651
|
* The completeness oracle (§4.8 I3): which units of a base are windowed-in
|
|
648
|
-
* locally,
|
|
649
|
-
*
|
|
652
|
+
* locally, which of those are still bootstrap-pending, and thereby the
|
|
653
|
+
* per-unit verdict a live query renders "may be partial" from. A query
|
|
654
|
+
* whose scope footprint includes an un-windowed OR still-pending unit is
|
|
650
655
|
* NOT answerable in full — the host widens or shows partial, never
|
|
651
|
-
* silently-complete.
|
|
656
|
+
* silently-complete. Registration alone is not completeness: a unit is
|
|
657
|
+
* pending until its subscription completes a bootstrap round (cursor
|
|
658
|
+
* advances past -1 with no resume token held).
|
|
652
659
|
*/
|
|
653
660
|
windowState(base) {
|
|
654
661
|
this.#requireStarted();
|
|
655
662
|
const baseKey = windowBaseKey(base);
|
|
656
|
-
|
|
663
|
+
const live = loadWindowUnits(this.#db, baseKey);
|
|
664
|
+
const pending = [];
|
|
665
|
+
for (const { unit, subId } of live) {
|
|
666
|
+
const sub = getSubscription(this.#db, subId);
|
|
667
|
+
if (sub === undefined ||
|
|
668
|
+
sub.cursor < 0 ||
|
|
669
|
+
sub.bootstrapState !== undefined) {
|
|
670
|
+
pending.push(unit);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return { units: live.map((u) => u.unit), pending };
|
|
657
674
|
}
|
|
658
675
|
/**
|
|
659
676
|
* §4.8 E1–E4: evict one departing unit, fused with its unsubscription in
|
|
@@ -1600,6 +1617,7 @@ export class SyncClient {
|
|
|
1600
1617
|
// §3.3: the effective-scope echo is persisted for the purge contract.
|
|
1601
1618
|
// An absent bootstrapState clears any previous resume token (§4.4:
|
|
1602
1619
|
// absent = bootstrap complete, or not bootstrapping).
|
|
1620
|
+
const wasPending = sub.cursor < 0 || sub.bootstrapState !== undefined;
|
|
1603
1621
|
saveSubscription(this.#db, {
|
|
1604
1622
|
id: sub.id,
|
|
1605
1623
|
table: sub.table,
|
|
@@ -1610,6 +1628,12 @@ export class SyncClient {
|
|
|
1610
1628
|
effectiveScopes: start.effectiveScopes,
|
|
1611
1629
|
status: 'active',
|
|
1612
1630
|
});
|
|
1631
|
+
if (wasPending && nextCursor >= 0 && bootstrapState === undefined) {
|
|
1632
|
+
// §4.8: the completeness verdict flipped pending → complete. A
|
|
1633
|
+
// zero-row bootstrap applies nothing, so the flip itself must reach
|
|
1634
|
+
// live oracles through the choke point (shares the pull's batch).
|
|
1635
|
+
this.#applyBatch((batch) => batch.table(sub.table));
|
|
1636
|
+
}
|
|
1613
1637
|
return true;
|
|
1614
1638
|
}
|
|
1615
1639
|
if (start.status === 'reset') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
84
|
-
"@syncular/core": "0.
|
|
84
|
+
"@syncular/core": "0.4.1"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"better-sqlite3": ">=11"
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.
|
|
95
|
+
"@syncular/server": "0.4.1",
|
|
96
96
|
"@types/better-sqlite3": "^7.6.13",
|
|
97
97
|
"better-sqlite3": "^12.11.1"
|
|
98
98
|
}
|
package/src/client.ts
CHANGED
|
@@ -309,11 +309,23 @@ export interface SubscribeInput {
|
|
|
309
309
|
export interface WindowState {
|
|
310
310
|
/** Windowed-in units for this base, ordered by value. */
|
|
311
311
|
readonly units: readonly string[];
|
|
312
|
+
/**
|
|
313
|
+
* Registered units whose bootstrap has not yet completed (§4.8): the
|
|
314
|
+
* unit's subscription still has `cursor: -1` (never synced) or holds a
|
|
315
|
+
* resume token (mid-bootstrap). Between `setWindow` and the bootstrap
|
|
316
|
+
* landing, the local replica for a pending unit may be empty or partial —
|
|
317
|
+
* it MUST NOT be rendered as complete.
|
|
318
|
+
*/
|
|
319
|
+
readonly pending: readonly string[];
|
|
312
320
|
}
|
|
313
321
|
|
|
314
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* True iff `unit` is windowed-in AND its bootstrap completed (§4.8 I3):
|
|
324
|
+
* registered and not pending. A unit with zero server rows still becomes
|
|
325
|
+
* complete once its bootstrap round finishes — emptiness ≠ pendency.
|
|
326
|
+
*/
|
|
315
327
|
export function windowComplete(state: WindowState, unit: string): boolean {
|
|
316
|
-
return state.units.includes(unit);
|
|
328
|
+
return state.units.includes(unit) && !state.pending.includes(unit);
|
|
317
329
|
}
|
|
318
330
|
|
|
319
331
|
// ---------------------------------------------------------------------------
|
|
@@ -1069,15 +1081,30 @@ export class SyncClient {
|
|
|
1069
1081
|
|
|
1070
1082
|
/**
|
|
1071
1083
|
* The completeness oracle (§4.8 I3): which units of a base are windowed-in
|
|
1072
|
-
* locally,
|
|
1073
|
-
*
|
|
1084
|
+
* locally, which of those are still bootstrap-pending, and thereby the
|
|
1085
|
+
* per-unit verdict a live query renders "may be partial" from. A query
|
|
1086
|
+
* whose scope footprint includes an un-windowed OR still-pending unit is
|
|
1074
1087
|
* NOT answerable in full — the host widens or shows partial, never
|
|
1075
|
-
* silently-complete.
|
|
1088
|
+
* silently-complete. Registration alone is not completeness: a unit is
|
|
1089
|
+
* pending until its subscription completes a bootstrap round (cursor
|
|
1090
|
+
* advances past -1 with no resume token held).
|
|
1076
1091
|
*/
|
|
1077
1092
|
windowState(base: WindowBase): WindowState {
|
|
1078
1093
|
this.#requireStarted();
|
|
1079
1094
|
const baseKey = windowBaseKey(base);
|
|
1080
|
-
|
|
1095
|
+
const live = loadWindowUnits(this.#db, baseKey);
|
|
1096
|
+
const pending: string[] = [];
|
|
1097
|
+
for (const { unit, subId } of live) {
|
|
1098
|
+
const sub = getSubscription(this.#db, subId);
|
|
1099
|
+
if (
|
|
1100
|
+
sub === undefined ||
|
|
1101
|
+
sub.cursor < 0 ||
|
|
1102
|
+
sub.bootstrapState !== undefined
|
|
1103
|
+
) {
|
|
1104
|
+
pending.push(unit);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return { units: live.map((u) => u.unit), pending };
|
|
1081
1108
|
}
|
|
1082
1109
|
|
|
1083
1110
|
/**
|
|
@@ -2236,6 +2263,7 @@ export class SyncClient {
|
|
|
2236
2263
|
// §3.3: the effective-scope echo is persisted for the purge contract.
|
|
2237
2264
|
// An absent bootstrapState clears any previous resume token (§4.4:
|
|
2238
2265
|
// absent = bootstrap complete, or not bootstrapping).
|
|
2266
|
+
const wasPending = sub.cursor < 0 || sub.bootstrapState !== undefined;
|
|
2239
2267
|
saveSubscription(this.#db, {
|
|
2240
2268
|
id: sub.id,
|
|
2241
2269
|
table: sub.table,
|
|
@@ -2246,6 +2274,12 @@ export class SyncClient {
|
|
|
2246
2274
|
effectiveScopes: start.effectiveScopes,
|
|
2247
2275
|
status: 'active',
|
|
2248
2276
|
});
|
|
2277
|
+
if (wasPending && nextCursor >= 0 && bootstrapState === undefined) {
|
|
2278
|
+
// §4.8: the completeness verdict flipped pending → complete. A
|
|
2279
|
+
// zero-row bootstrap applies nothing, so the flip itself must reach
|
|
2280
|
+
// live oracles through the choke point (shares the pull's batch).
|
|
2281
|
+
this.#applyBatch((batch) => batch.table(sub.table));
|
|
2282
|
+
}
|
|
2249
2283
|
return true;
|
|
2250
2284
|
}
|
|
2251
2285
|
|