@syncular/client 0.15.6 → 0.15.8

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
@@ -18,7 +18,7 @@ import { appendOutboxCommit, deleteOutboxCommit, dropOutboxCommitsInScope, encod
18
18
  import { activeFailureRecords, listCommitOutcomes, persistCommitOutcomeResolution, pruneCommitOutcomes, commitOutcome as readCommitOutcome, recordCommitOutcome, } from './outcomes.js';
19
19
  import { assertReadOnlyQuery } from './query-guard.js';
20
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, resetSubscriptionsForBump, saveSubscription, setMeta, } from './state.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):
@@ -178,6 +178,11 @@ export class SyncClient {
178
178
  // before the first sync round. A fresh install (no marker) is treated as
179
179
  // already at the generated version.
180
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
+ });
181
186
  this.#started = true;
182
187
  // A persisted active subscription needs one catch-up round on every open:
183
188
  // realtime only covers changes after the socket connects, and an
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.6",
3
+ "version": "0.15.8",
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.6"
84
+ "@syncular/core": "0.15.8"
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.6",
95
+ "@syncular/server": "0.15.8",
96
96
  "@types/better-sqlite3": "^7.6.13",
97
97
  "better-sqlite3": "^12.11.1"
98
98
  }
package/src/client.ts CHANGED
@@ -134,6 +134,7 @@ import {
134
134
  getMeta,
135
135
  getSubscription,
136
136
  loadSubscriptions,
137
+ pruneUnknownSubscriptions,
137
138
  resetSubscriptionsForBump,
138
139
  type SubscriptionRecord,
139
140
  saveSubscription,
@@ -585,6 +586,11 @@ export class SyncClient {
585
586
  // before the first sync round. A fresh install (no marker) is treated as
586
587
  // already at the generated version.
587
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
+ });
588
594
  this.#started = true;
589
595
  // A persisted active subscription needs one catch-up round on every open:
590
596
  // realtime only covers changes after the socket connects, and an
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,