@syncular/client 0.15.5 → 0.15.7
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.js +16 -4
- package/dist/schema.d.ts +12 -2
- package/dist/schema.js +19 -3
- package/dist/state.d.ts +7 -0
- package/dist/state.js +19 -0
- package/package.json +3 -3
- package/src/client.ts +18 -3
- package/src/schema.ts +24 -3
- package/src/state.ts +23 -0
package/dist/client.js
CHANGED
|
@@ -17,8 +17,8 @@ import { singleOwnerLock, } from './leader-lock.js';
|
|
|
17
17
|
import { appendOutboxCommit, deleteOutboxCommit, dropOutboxCommitsInScope, encodeOutboxCommit, listOutbox, listOutboxBeforeImages, OutboxEncodeError, replaceOutboxBeforeImages, } from './outbox.js';
|
|
18
18
|
import { activeFailureRecords, listCommitOutcomes, persistCommitOutcomeResolution, pruneCommitOutcomes, commitOutcome as readCommitOutcome, recordCommitOutcome, } from './outcomes.js';
|
|
19
19
|
import { assertReadOnlyQuery } from './query-guard.js';
|
|
20
|
-
import { compileClientSchema, dropAndRecreateSyncedTables,
|
|
21
|
-
import { bumpLocalRevision, deleteSubscription, getLocalRevision, getMeta, getSubscription, loadSubscriptions, resetSubscriptionsForBump, saveSubscription, setMeta, } from './state.js';
|
|
20
|
+
import { compileClientSchema, dropAndRecreateSyncedTables, ensureLocalBookkeepingSchema, ensureLocalSyncedSchema, fromSqlValue, jsonToRowValue, LOCAL_SCHEMA_VERSION_KEY, normalizeRecordKeys, OPTIMISTIC_VERSION, quoteIdent, recordToRowValues, rowValueToJson, SYNC_VERSION_COLUMN, stripSyncColumns, } from './schema.js';
|
|
21
|
+
import { bumpLocalRevision, deleteSubscription, getLocalRevision, getMeta, getSubscription, loadSubscriptions, pruneUnknownSubscriptions, resetSubscriptionsForBump, saveSubscription, setMeta, } from './state.js';
|
|
22
22
|
import { deletePendingEviction, deleteWindowUnit, deriveSubId, getWindowUnitBySubId, insertWindowUnit, loadPendingEvictions, loadWindowUnits, savePendingEviction, unitScopes, windowBaseKey, } from './window.js';
|
|
23
23
|
/**
|
|
24
24
|
* True iff `unit` is windowed-in AND its bootstrap completed (§4.8 I3):
|
|
@@ -144,7 +144,10 @@ export class SyncClient {
|
|
|
144
144
|
return;
|
|
145
145
|
const lock = this.#config.leaderLock ?? singleOwnerLock();
|
|
146
146
|
this.#lease = await lock.acquire(this.#config.lockName ?? 'syncular-leader');
|
|
147
|
-
|
|
147
|
+
// Bookkeeping must exist before we inspect the persisted schema marker.
|
|
148
|
+
// Do not materialize new app indexes/FTS projections yet: on a version
|
|
149
|
+
// bump they may reference columns that only exist after the reset.
|
|
150
|
+
ensureLocalBookkeepingSchema(this.#db);
|
|
148
151
|
if (this.#hasBlobs)
|
|
149
152
|
ensureBlobSchema(this.#db);
|
|
150
153
|
this.#db.transaction(() => {
|
|
@@ -175,6 +178,11 @@ export class SyncClient {
|
|
|
175
178
|
// before the first sync round. A fresh install (no marker) is treated as
|
|
176
179
|
// already at the generated version.
|
|
177
180
|
this.#detectAndResetSchema();
|
|
181
|
+
// A registration remains app intent only while its table exists in the
|
|
182
|
+
// running schema. Removed-table registrations would poison every pull.
|
|
183
|
+
this.#db.transaction(() => {
|
|
184
|
+
pruneUnknownSubscriptions(this.#db, new Set(this.#schema.tables.keys()));
|
|
185
|
+
});
|
|
178
186
|
this.#started = true;
|
|
179
187
|
// A persisted active subscription needs one catch-up round on every open:
|
|
180
188
|
// realtime only covers changes after the socket connects, and an
|
|
@@ -214,12 +222,16 @@ export class SyncClient {
|
|
|
214
222
|
const markerJson = getMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY);
|
|
215
223
|
if (markerJson === undefined) {
|
|
216
224
|
// Fresh install: the tables just created match the running code.
|
|
225
|
+
ensureLocalSyncedSchema(this.#db, this.#schema);
|
|
217
226
|
setMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY, String(this.#schema.version));
|
|
218
227
|
return;
|
|
219
228
|
}
|
|
220
229
|
const marker = Number(markerJson);
|
|
221
|
-
if (marker === this.#schema.version)
|
|
230
|
+
if (marker === this.#schema.version) {
|
|
231
|
+
// Same-version opens remain self-healing for absent tables/indexes.
|
|
232
|
+
ensureLocalSyncedSchema(this.#db, this.#schema);
|
|
222
233
|
return;
|
|
234
|
+
}
|
|
223
235
|
this.#runSchemaReset();
|
|
224
236
|
}
|
|
225
237
|
/**
|
package/dist/schema.d.ts
CHANGED
|
@@ -103,9 +103,19 @@ export declare function localColumnType(column: RowColumn): RowColumn['type'];
|
|
|
103
103
|
/** §7.4.1 persisted local schema-version marker (`_syncular_meta` key). */
|
|
104
104
|
export declare const LOCAL_SCHEMA_VERSION_KEY = "localSchemaVersion";
|
|
105
105
|
/**
|
|
106
|
-
* Create the synced tables
|
|
107
|
-
*
|
|
106
|
+
* Create only the application-owned synced tables, indexes, and FTS
|
|
107
|
+
* projections. Callers opening an existing database must compare the
|
|
108
|
+
* persisted schema version before invoking this: a new index may reference a
|
|
109
|
+
* column that does not exist until the schema-bump reset recreates the table.
|
|
108
110
|
*/
|
|
111
|
+
export declare function ensureLocalSyncedSchema(db: ClientDatabase, schema: CompiledClientSchema): void;
|
|
112
|
+
/**
|
|
113
|
+
* Create the protected Syncular bookkeeping tables without touching the
|
|
114
|
+
* application schema. This makes the persisted schema-version marker
|
|
115
|
+
* readable before any new application index or FTS projection is applied.
|
|
116
|
+
*/
|
|
117
|
+
export declare function ensureLocalBookkeepingSchema(db: ClientDatabase): void;
|
|
118
|
+
/** Create both application and protected bookkeeping tables. */
|
|
109
119
|
export declare function ensureLocalSchema(db: ClientDatabase, schema: CompiledClientSchema): void;
|
|
110
120
|
/**
|
|
111
121
|
* §7.4.3 reset: drop every synced local table (whatever the *previous*
|
package/dist/schema.js
CHANGED
|
@@ -241,10 +241,12 @@ function createFtsProjection(db, table, index) {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
/**
|
|
244
|
-
* Create the synced tables
|
|
245
|
-
*
|
|
244
|
+
* Create only the application-owned synced tables, indexes, and FTS
|
|
245
|
+
* projections. Callers opening an existing database must compare the
|
|
246
|
+
* persisted schema version before invoking this: a new index may reference a
|
|
247
|
+
* column that does not exist until the schema-bump reset recreates the table.
|
|
246
248
|
*/
|
|
247
|
-
export function
|
|
249
|
+
export function ensureLocalSyncedSchema(db, schema) {
|
|
248
250
|
db.transaction(() => {
|
|
249
251
|
for (const table of schema.tables.values()) {
|
|
250
252
|
createSyncedTable(db, table);
|
|
@@ -254,6 +256,15 @@ export function ensureLocalSchema(db, schema) {
|
|
|
254
256
|
createFtsProjection(db, table, index);
|
|
255
257
|
}
|
|
256
258
|
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Create the protected Syncular bookkeeping tables without touching the
|
|
263
|
+
* application schema. This makes the persisted schema-version marker
|
|
264
|
+
* readable before any new application index or FTS projection is applied.
|
|
265
|
+
*/
|
|
266
|
+
export function ensureLocalBookkeepingSchema(db) {
|
|
267
|
+
db.transaction(() => {
|
|
257
268
|
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_meta(
|
|
258
269
|
key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
|
|
259
270
|
db.exec(`INSERT OR IGNORE INTO _syncular_meta(key, value) VALUES ('localRevision', '0')`);
|
|
@@ -316,6 +327,11 @@ export function ensureLocalSchema(db, schema) {
|
|
|
316
327
|
effective_scopes TEXT NOT NULL)`);
|
|
317
328
|
});
|
|
318
329
|
}
|
|
330
|
+
/** Create both application and protected bookkeeping tables. */
|
|
331
|
+
export function ensureLocalSchema(db, schema) {
|
|
332
|
+
ensureLocalBookkeepingSchema(db);
|
|
333
|
+
ensureLocalSyncedSchema(db, schema);
|
|
334
|
+
}
|
|
319
335
|
/** Bookkeeping tables the schema-bump reset (§7.4.3) MUST NOT drop. */
|
|
320
336
|
const RESERVED_TABLE_PREFIX = '_syncular_';
|
|
321
337
|
/**
|
package/dist/state.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ export declare function deleteSubscription(db: ClientDatabase, id: string): void
|
|
|
38
38
|
* subscriptions the app still wants. Caller owns the transaction.
|
|
39
39
|
*/
|
|
40
40
|
export declare function resetSubscriptionsForBump(db: ClientDatabase): void;
|
|
41
|
+
/**
|
|
42
|
+
* Remove registrations whose table no longer exists in the running schema.
|
|
43
|
+
* Keeping one would make every subsequent pull fail with
|
|
44
|
+
* `sync.unknown_table`. Window bookkeeping belongs to the registration and
|
|
45
|
+
* is removed with it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function pruneUnknownSubscriptions(db: ClientDatabase, tableNames: ReadonlySet<string>): void;
|
|
41
48
|
export declare function getMeta(db: ClientDatabase, key: string): string | undefined;
|
|
42
49
|
export declare function setMeta(db: ClientDatabase, key: string, value: string): void;
|
|
43
50
|
/** Read the durable client-local observer revision (SPEC §7.5). */
|
package/dist/state.js
CHANGED
|
@@ -65,6 +65,25 @@ export function resetSubscriptionsForBump(db) {
|
|
|
65
65
|
SET cursor = -1, bootstrap_state = NULL, effective_scopes = NULL,
|
|
66
66
|
status = 'active', reason_code = NULL`);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Remove registrations whose table no longer exists in the running schema.
|
|
70
|
+
* Keeping one would make every subsequent pull fail with
|
|
71
|
+
* `sync.unknown_table`. Window bookkeeping belongs to the registration and
|
|
72
|
+
* is removed with it.
|
|
73
|
+
*/
|
|
74
|
+
export function pruneUnknownSubscriptions(db, tableNames) {
|
|
75
|
+
const staleIds = db
|
|
76
|
+
.query('SELECT id, tbl FROM _syncular_subscriptions')
|
|
77
|
+
.filter((row) => !tableNames.has(String(row.tbl)))
|
|
78
|
+
.map((row) => String(row.id));
|
|
79
|
+
for (const id of staleIds) {
|
|
80
|
+
db.exec('DELETE FROM _syncular_windows WHERE sub_id = ?', [id]);
|
|
81
|
+
db.exec('DELETE FROM _syncular_window_pending_evict WHERE sub_id = ?', [
|
|
82
|
+
id,
|
|
83
|
+
]);
|
|
84
|
+
db.exec('DELETE FROM _syncular_subscriptions WHERE id = ?', [id]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
export function getMeta(db, key) {
|
|
69
88
|
const row = db.query('SELECT value FROM _syncular_meta WHERE key = ?', [
|
|
70
89
|
key,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.7",
|
|
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.15.
|
|
84
|
+
"@syncular/core": "0.15.7"
|
|
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.15.
|
|
95
|
+
"@syncular/server": "0.15.7",
|
|
96
96
|
"@types/better-sqlite3": "^7.6.13",
|
|
97
97
|
"better-sqlite3": "^12.11.1"
|
|
98
98
|
}
|
package/src/client.ts
CHANGED
|
@@ -114,7 +114,8 @@ import {
|
|
|
114
114
|
type CompiledClientTable,
|
|
115
115
|
compileClientSchema,
|
|
116
116
|
dropAndRecreateSyncedTables,
|
|
117
|
-
|
|
117
|
+
ensureLocalBookkeepingSchema,
|
|
118
|
+
ensureLocalSyncedSchema,
|
|
118
119
|
fromSqlValue,
|
|
119
120
|
jsonToRowValue,
|
|
120
121
|
LOCAL_SCHEMA_VERSION_KEY,
|
|
@@ -133,6 +134,7 @@ import {
|
|
|
133
134
|
getMeta,
|
|
134
135
|
getSubscription,
|
|
135
136
|
loadSubscriptions,
|
|
137
|
+
pruneUnknownSubscriptions,
|
|
136
138
|
resetSubscriptionsForBump,
|
|
137
139
|
type SubscriptionRecord,
|
|
138
140
|
saveSubscription,
|
|
@@ -544,7 +546,10 @@ export class SyncClient {
|
|
|
544
546
|
this.#lease = await lock.acquire(
|
|
545
547
|
this.#config.lockName ?? 'syncular-leader',
|
|
546
548
|
);
|
|
547
|
-
|
|
549
|
+
// Bookkeeping must exist before we inspect the persisted schema marker.
|
|
550
|
+
// Do not materialize new app indexes/FTS projections yet: on a version
|
|
551
|
+
// bump they may reference columns that only exist after the reset.
|
|
552
|
+
ensureLocalBookkeepingSchema(this.#db);
|
|
548
553
|
if (this.#hasBlobs) ensureBlobSchema(this.#db);
|
|
549
554
|
this.#db.transaction(() => {
|
|
550
555
|
pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
|
|
@@ -581,6 +586,11 @@ export class SyncClient {
|
|
|
581
586
|
// before the first sync round. A fresh install (no marker) is treated as
|
|
582
587
|
// already at the generated version.
|
|
583
588
|
this.#detectAndResetSchema();
|
|
589
|
+
// A registration remains app intent only while its table exists in the
|
|
590
|
+
// running schema. Removed-table registrations would poison every pull.
|
|
591
|
+
this.#db.transaction(() => {
|
|
592
|
+
pruneUnknownSubscriptions(this.#db, new Set(this.#schema.tables.keys()));
|
|
593
|
+
});
|
|
584
594
|
this.#started = true;
|
|
585
595
|
// A persisted active subscription needs one catch-up round on every open:
|
|
586
596
|
// realtime only covers changes after the socket connects, and an
|
|
@@ -622,11 +632,16 @@ export class SyncClient {
|
|
|
622
632
|
const markerJson = getMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY);
|
|
623
633
|
if (markerJson === undefined) {
|
|
624
634
|
// Fresh install: the tables just created match the running code.
|
|
635
|
+
ensureLocalSyncedSchema(this.#db, this.#schema);
|
|
625
636
|
setMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY, String(this.#schema.version));
|
|
626
637
|
return;
|
|
627
638
|
}
|
|
628
639
|
const marker = Number(markerJson);
|
|
629
|
-
if (marker === this.#schema.version)
|
|
640
|
+
if (marker === this.#schema.version) {
|
|
641
|
+
// Same-version opens remain self-healing for absent tables/indexes.
|
|
642
|
+
ensureLocalSyncedSchema(this.#db, this.#schema);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
630
645
|
this.#runSchemaReset();
|
|
631
646
|
}
|
|
632
647
|
|
package/src/schema.ts
CHANGED
|
@@ -395,10 +395,12 @@ function createFtsProjection(
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
/**
|
|
398
|
-
* Create the synced tables
|
|
399
|
-
*
|
|
398
|
+
* Create only the application-owned synced tables, indexes, and FTS
|
|
399
|
+
* projections. Callers opening an existing database must compare the
|
|
400
|
+
* persisted schema version before invoking this: a new index may reference a
|
|
401
|
+
* column that does not exist until the schema-bump reset recreates the table.
|
|
400
402
|
*/
|
|
401
|
-
export function
|
|
403
|
+
export function ensureLocalSyncedSchema(
|
|
402
404
|
db: ClientDatabase,
|
|
403
405
|
schema: CompiledClientSchema,
|
|
404
406
|
): void {
|
|
@@ -411,6 +413,16 @@ export function ensureLocalSchema(
|
|
|
411
413
|
createFtsProjection(db, table, index);
|
|
412
414
|
}
|
|
413
415
|
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create the protected Syncular bookkeeping tables without touching the
|
|
421
|
+
* application schema. This makes the persisted schema-version marker
|
|
422
|
+
* readable before any new application index or FTS projection is applied.
|
|
423
|
+
*/
|
|
424
|
+
export function ensureLocalBookkeepingSchema(db: ClientDatabase): void {
|
|
425
|
+
db.transaction(() => {
|
|
414
426
|
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_meta(
|
|
415
427
|
key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
|
|
416
428
|
db.exec(
|
|
@@ -477,6 +489,15 @@ export function ensureLocalSchema(
|
|
|
477
489
|
});
|
|
478
490
|
}
|
|
479
491
|
|
|
492
|
+
/** Create both application and protected bookkeeping tables. */
|
|
493
|
+
export function ensureLocalSchema(
|
|
494
|
+
db: ClientDatabase,
|
|
495
|
+
schema: CompiledClientSchema,
|
|
496
|
+
): void {
|
|
497
|
+
ensureLocalBookkeepingSchema(db);
|
|
498
|
+
ensureLocalSyncedSchema(db, schema);
|
|
499
|
+
}
|
|
500
|
+
|
|
480
501
|
/** Bookkeeping tables the schema-bump reset (§7.4.3) MUST NOT drop. */
|
|
481
502
|
const RESERVED_TABLE_PREFIX = '_syncular_';
|
|
482
503
|
|
package/src/state.ts
CHANGED
|
@@ -115,6 +115,29 @@ export function resetSubscriptionsForBump(db: ClientDatabase): void {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Remove registrations whose table no longer exists in the running schema.
|
|
120
|
+
* Keeping one would make every subsequent pull fail with
|
|
121
|
+
* `sync.unknown_table`. Window bookkeeping belongs to the registration and
|
|
122
|
+
* is removed with it.
|
|
123
|
+
*/
|
|
124
|
+
export function pruneUnknownSubscriptions(
|
|
125
|
+
db: ClientDatabase,
|
|
126
|
+
tableNames: ReadonlySet<string>,
|
|
127
|
+
): void {
|
|
128
|
+
const staleIds = db
|
|
129
|
+
.query('SELECT id, tbl FROM _syncular_subscriptions')
|
|
130
|
+
.filter((row) => !tableNames.has(String(row.tbl)))
|
|
131
|
+
.map((row) => String(row.id));
|
|
132
|
+
for (const id of staleIds) {
|
|
133
|
+
db.exec('DELETE FROM _syncular_windows WHERE sub_id = ?', [id]);
|
|
134
|
+
db.exec('DELETE FROM _syncular_window_pending_evict WHERE sub_id = ?', [
|
|
135
|
+
id,
|
|
136
|
+
]);
|
|
137
|
+
db.exec('DELETE FROM _syncular_subscriptions WHERE id = ?', [id]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
export function getMeta(db: ClientDatabase, key: string): string | undefined {
|
|
119
142
|
const row = db.query('SELECT value FROM _syncular_meta WHERE key = ?', [
|
|
120
143
|
key,
|