@syncular/client 0.15.12 → 0.15.13

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
@@ -181,9 +181,7 @@ export class SyncClient {
181
181
  this.#detectAndResetSchema();
182
182
  // A registration remains app intent only while its table exists in the
183
183
  // running schema. Removed-table registrations would poison every pull.
184
- this.#db.transaction(() => {
185
- pruneUnknownSubscriptions(this.#db, new Set(this.#schema.tables.keys()));
186
- });
184
+ const subscriptions = pruneUnknownSubscriptions(this.#db, loadSubscriptions(this.#db), new Set(this.#schema.tables.keys()));
187
185
  this.#started = true;
188
186
  // A persisted active subscription needs one catch-up round on every open:
189
187
  // realtime only covers changes after the socket connects, and an
@@ -193,7 +191,7 @@ export class SyncClient {
193
191
  // an application-issued sync() call.
194
192
  const startupWork = this.#schemaFloor === undefined &&
195
193
  (listOutbox(this.#db).length > 0 ||
196
- loadSubscriptions(this.#db).some((sub) => sub.status === 'active'));
194
+ subscriptions.some((sub) => sub.status === 'active'));
197
195
  if (startupWork) {
198
196
  this.#needsPull = true;
199
197
  this.#config.onSyncNeeded?.('startup');
package/dist/state.d.ts CHANGED
@@ -42,9 +42,10 @@ export declare function resetSubscriptionsForBump(db: ClientDatabase): void;
42
42
  * Remove registrations whose table no longer exists in the running schema.
43
43
  * Keeping one would make every subsequent pull fail with
44
44
  * `sync.unknown_table`. Window bookkeeping belongs to the registration and
45
- * is removed with it.
45
+ * is removed with it. The caller supplies its startup subscription snapshot
46
+ * so pruning and startup-work detection stay a single read.
46
47
  */
47
- export declare function pruneUnknownSubscriptions(db: ClientDatabase, tableNames: ReadonlySet<string>): void;
48
+ export declare function pruneUnknownSubscriptions(db: ClientDatabase, subscriptions: readonly SubscriptionRecord[], tableNames: ReadonlySet<string>): SubscriptionRecord[];
48
49
  export declare function getMeta(db: ClientDatabase, key: string): string | undefined;
49
50
  export declare function setMeta(db: ClientDatabase, key: string, value: string): void;
50
51
  /** Read the durable client-local observer revision (SPEC §7.5). */
package/dist/state.js CHANGED
@@ -69,20 +69,30 @@ export function resetSubscriptionsForBump(db) {
69
69
  * Remove registrations whose table no longer exists in the running schema.
70
70
  * Keeping one would make every subsequent pull fail with
71
71
  * `sync.unknown_table`. Window bookkeeping belongs to the registration and
72
- * is removed with it.
72
+ * is removed with it. The caller supplies its startup subscription snapshot
73
+ * so pruning and startup-work detection stay a single read.
73
74
  */
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
- }
75
+ export function pruneUnknownSubscriptions(db, subscriptions, tableNames) {
76
+ const retained = subscriptions.filter((record) => tableNames.has(record.table));
77
+ const staleIds = subscriptions
78
+ .filter((record) => !tableNames.has(record.table))
79
+ .map((record) => record.id);
80
+ // A redundant subscription scan plus an empty write transaction cut the
81
+ // fresh-client bootstrap lane by more than half on bun:sqlite. The supplied
82
+ // snapshot and this branch are synchronous, so no application work can
83
+ // interleave before a real pruning transaction begins.
84
+ if (staleIds.length === 0)
85
+ return retained;
86
+ db.transaction(() => {
87
+ for (const id of staleIds) {
88
+ db.exec('DELETE FROM _syncular_windows WHERE sub_id = ?', [id]);
89
+ db.exec('DELETE FROM _syncular_window_pending_evict WHERE sub_id = ?', [
90
+ id,
91
+ ]);
92
+ db.exec('DELETE FROM _syncular_subscriptions WHERE id = ?', [id]);
93
+ }
94
+ });
95
+ return retained;
86
96
  }
87
97
  export function getMeta(db, key) {
88
98
  const row = db.query('SELECT value FROM _syncular_meta WHERE key = ?', [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.15.12",
3
+ "version": "0.15.13",
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.12"
84
+ "@syncular/core": "0.15.13"
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.12",
95
+ "@syncular/server": "0.15.13",
96
96
  "@types/better-sqlite3": "^7.6.13",
97
97
  "better-sqlite3": "^12.11.1"
98
98
  }
package/src/client.ts CHANGED
@@ -597,9 +597,11 @@ export class SyncClient {
597
597
  this.#detectAndResetSchema();
598
598
  // A registration remains app intent only while its table exists in the
599
599
  // running schema. Removed-table registrations would poison every pull.
600
- this.#db.transaction(() => {
601
- pruneUnknownSubscriptions(this.#db, new Set(this.#schema.tables.keys()));
602
- });
600
+ const subscriptions = pruneUnknownSubscriptions(
601
+ this.#db,
602
+ loadSubscriptions(this.#db),
603
+ new Set(this.#schema.tables.keys()),
604
+ );
603
605
  this.#started = true;
604
606
  // A persisted active subscription needs one catch-up round on every open:
605
607
  // realtime only covers changes after the socket connects, and an
@@ -610,7 +612,7 @@ export class SyncClient {
610
612
  const startupWork =
611
613
  this.#schemaFloor === undefined &&
612
614
  (listOutbox(this.#db).length > 0 ||
613
- loadSubscriptions(this.#db).some((sub) => sub.status === 'active'));
615
+ subscriptions.some((sub) => sub.status === 'active'));
614
616
  if (startupWork) {
615
617
  this.#needsPull = true;
616
618
  this.#config.onSyncNeeded?.('startup');
package/src/state.ts CHANGED
@@ -119,23 +119,35 @@ export function resetSubscriptionsForBump(db: ClientDatabase): void {
119
119
  * Remove registrations whose table no longer exists in the running schema.
120
120
  * Keeping one would make every subsequent pull fail with
121
121
  * `sync.unknown_table`. Window bookkeeping belongs to the registration and
122
- * is removed with it.
122
+ * is removed with it. The caller supplies its startup subscription snapshot
123
+ * so pruning and startup-work detection stay a single read.
123
124
  */
124
125
  export function pruneUnknownSubscriptions(
125
126
  db: ClientDatabase,
127
+ subscriptions: readonly SubscriptionRecord[],
126
128
  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
- }
129
+ ): SubscriptionRecord[] {
130
+ const retained = subscriptions.filter((record) =>
131
+ tableNames.has(record.table),
132
+ );
133
+ const staleIds = subscriptions
134
+ .filter((record) => !tableNames.has(record.table))
135
+ .map((record) => record.id);
136
+ // A redundant subscription scan plus an empty write transaction cut the
137
+ // fresh-client bootstrap lane by more than half on bun:sqlite. The supplied
138
+ // snapshot and this branch are synchronous, so no application work can
139
+ // interleave before a real pruning transaction begins.
140
+ if (staleIds.length === 0) return retained;
141
+ db.transaction(() => {
142
+ for (const id of staleIds) {
143
+ db.exec('DELETE FROM _syncular_windows WHERE sub_id = ?', [id]);
144
+ db.exec('DELETE FROM _syncular_window_pending_evict WHERE sub_id = ?', [
145
+ id,
146
+ ]);
147
+ db.exec('DELETE FROM _syncular_subscriptions WHERE id = ?', [id]);
148
+ }
149
+ });
150
+ return retained;
139
151
  }
140
152
 
141
153
  export function getMeta(db: ClientDatabase, key: string): string | undefined {