@syncular/client 0.15.5 → 0.15.6

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 CHANGED
@@ -17,7 +17,7 @@ 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, ensureLocalSchema, fromSqlValue, jsonToRowValue, LOCAL_SCHEMA_VERSION_KEY, normalizeRecordKeys, OPTIMISTIC_VERSION, quoteIdent, recordToRowValues, rowValueToJson, SYNC_VERSION_COLUMN, stripSyncColumns, } from './schema.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
21
  import { bumpLocalRevision, deleteSubscription, getLocalRevision, getMeta, getSubscription, loadSubscriptions, resetSubscriptionsForBump, saveSubscription, setMeta, } from './state.js';
22
22
  import { deletePendingEviction, deleteWindowUnit, deriveSubId, getWindowUnitBySubId, insertWindowUnit, loadPendingEvictions, loadWindowUnits, savePendingEviction, unitScopes, windowBaseKey, } from './window.js';
23
23
  /**
@@ -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
- ensureLocalSchema(this.#db, this.#schema);
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(() => {
@@ -214,12 +217,16 @@ export class SyncClient {
214
217
  const markerJson = getMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY);
215
218
  if (markerJson === undefined) {
216
219
  // Fresh install: the tables just created match the running code.
220
+ ensureLocalSyncedSchema(this.#db, this.#schema);
217
221
  setMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY, String(this.#schema.version));
218
222
  return;
219
223
  }
220
224
  const marker = Number(markerJson);
221
- if (marker === this.#schema.version)
225
+ if (marker === this.#schema.version) {
226
+ // Same-version opens remain self-healing for absent tables/indexes.
227
+ ensureLocalSyncedSchema(this.#db, this.#schema);
222
228
  return;
229
+ }
223
230
  this.#runSchemaReset();
224
231
  }
225
232
  /**
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 plus client bookkeeping tables (outbox,
107
- * subscription state, meta). Idempotent.
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 plus client bookkeeping tables (outbox,
245
- * subscription state, meta). Idempotent.
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 ensureLocalSchema(db, schema) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.15.5",
3
+ "version": "0.15.6",
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.5"
84
+ "@syncular/core": "0.15.6"
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.5",
95
+ "@syncular/server": "0.15.6",
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
- ensureLocalSchema,
117
+ ensureLocalBookkeepingSchema,
118
+ ensureLocalSyncedSchema,
118
119
  fromSqlValue,
119
120
  jsonToRowValue,
120
121
  LOCAL_SCHEMA_VERSION_KEY,
@@ -544,7 +545,10 @@ export class SyncClient {
544
545
  this.#lease = await lock.acquire(
545
546
  this.#config.lockName ?? 'syncular-leader',
546
547
  );
547
- ensureLocalSchema(this.#db, this.#schema);
548
+ // Bookkeeping must exist before we inspect the persisted schema marker.
549
+ // Do not materialize new app indexes/FTS projections yet: on a version
550
+ // bump they may reference columns that only exist after the reset.
551
+ ensureLocalBookkeepingSchema(this.#db);
548
552
  if (this.#hasBlobs) ensureBlobSchema(this.#db);
549
553
  this.#db.transaction(() => {
550
554
  pruneCommitOutcomes(this.#db, this.#outcomeRetentionMaxEntries);
@@ -622,11 +626,16 @@ export class SyncClient {
622
626
  const markerJson = getMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY);
623
627
  if (markerJson === undefined) {
624
628
  // Fresh install: the tables just created match the running code.
629
+ ensureLocalSyncedSchema(this.#db, this.#schema);
625
630
  setMeta(this.#db, LOCAL_SCHEMA_VERSION_KEY, String(this.#schema.version));
626
631
  return;
627
632
  }
628
633
  const marker = Number(markerJson);
629
- if (marker === this.#schema.version) return;
634
+ if (marker === this.#schema.version) {
635
+ // Same-version opens remain self-healing for absent tables/indexes.
636
+ ensureLocalSyncedSchema(this.#db, this.#schema);
637
+ return;
638
+ }
630
639
  this.#runSchemaReset();
631
640
  }
632
641
 
package/src/schema.ts CHANGED
@@ -395,10 +395,12 @@ function createFtsProjection(
395
395
  }
396
396
 
397
397
  /**
398
- * Create the synced tables plus client bookkeeping tables (outbox,
399
- * subscription state, meta). Idempotent.
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 ensureLocalSchema(
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