@syncular/client 0.30.15 → 0.30.16

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/README.md CHANGED
@@ -821,10 +821,26 @@ committed import chunks; no write transaction spans that await.
821
821
 
822
822
  ## Preflight authority reads
823
823
 
824
- Pass `authorityReads: defineAuthorityReads([{ table, columns, scopes }])` at
824
+ Import `defineAuthorityReads` from `@syncular/client/authority`. Pass
825
+ `authorityReads: defineAuthorityReads([{ table, columns, scopes }])` at
825
826
  creation to enable the zero-argument `authoritySnapshot()`. It reads approved
826
827
  plain authority columns and accepted bases with revision and persisted scope
827
828
  coverage from one SQLite snapshot, including during security preflight.
828
829
  Unaccepted local intent remains separate. The read changes no keys, transport,
829
830
  subscriptions or lifecycle. See the [authority contract](../../docs/SPEC.md)
830
831
  and [native configuration](../tauri/README.md#authority-evidence-before-activation).
832
+
833
+ Workers must opt in in their entry script:
834
+
835
+ ```ts
836
+ import { defineAuthorityReads } from '@syncular/client/authority';
837
+ import { startSyncWorker } from '@syncular/client/worker';
838
+
839
+ startSyncWorker({ createAuthorityReads: defineAuthorityReads });
840
+ ```
841
+
842
+ Pass the policy to `createSyncClientHandle({ authorityReads, ...config })` on
843
+ the main thread. Declarations without the injected worker factory fail with
844
+ `client.authority_read_forbidden` before opening storage. Ordinary workers
845
+ keep `startSyncWorker()` and bundle no authority reader. No dynamic import
846
+ changes the declared capability.
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * (`worker-host`) and the RPC protocol types are root exports — they
9
9
  * import no SQLite.
10
10
  */
11
- export { defineAuthorityReads, type AuthorityReadPolicy, type AuthorityReadDeclaration, type AuthoritySnapshot, } from './authority.js';
11
+ export type { AuthorityReadPolicy, AuthorityReadDeclaration, AuthoritySnapshot, } from './authority.js';
12
12
  export * from './apply.js';
13
13
  export * from './availability.js';
14
14
  export * from './blob.js';
package/dist/index.js CHANGED
@@ -1,14 +1,3 @@
1
- /**
2
- * @syncular/client is the TypeScript client protocol core.
3
- * SPEC.md is normative.
4
- *
5
- * Browser-safe root: database backends live behind subpath exports
6
- * (`./sqlite` for Node or Bun, `./wasm` for sqlite-wasm + OPFS); the
7
- * worker-side bootstrap lives behind `./worker`. The main-thread handle
8
- * (`worker-host`) and the RPC protocol types are root exports — they
9
- * import no SQLite.
10
- */
11
- export { defineAuthorityReads, } from './authority.js';
12
1
  export * from './apply.js';
13
2
  export * from './availability.js';
14
3
  export * from './blob.js';
@@ -1,7 +1,25 @@
1
+ /**
2
+ * Worker-side bootstrap: constructs the WHOLE
3
+ * client core — SyncClient + transports + realtime + SQLite — inside the
4
+ * worker and serves the `worker-protocol` RPC over postMessage.
5
+ *
6
+ * Browsers get the defaults: opfs-sahpool persistence and the fetch/WS
7
+ * transports from `./http`, wired from `WorkerInitConfig.endpoints`.
8
+ * Tests inject a database factory (bun:sqlite) through `overrides` — the
9
+ * same bootstrap, the same RPC, a different SQLite.
10
+ *
11
+ * The §8.4 host loop lives HERE: wake-ups (hello/`sync` events, delta
12
+ * drops) coalesce into one intent-driven `syncUntilIdle` round in the worker;
13
+ * RPC-driven and auto-driven sync rounds serialize on one queue because
14
+ * the core owns exactly one loop.
15
+ */
16
+ import type { defineAuthorityReads } from './authority.js';
1
17
  import type { ClientDatabase } from './database.js';
2
18
  import type { RealtimeConnector, SegmentDownloader, SyncTransport } from './transport.js';
3
19
  import { type WorkerInitConfig } from './worker-protocol.js';
4
20
  export interface SyncWorkerOverrides {
21
+ /** Explicit authority reader factory. Required when init declares authorityReads. */
22
+ readonly createAuthorityReads?: typeof defineAuthorityReads;
5
23
  /** Database factory indirection: tests inject bun:sqlite here. */
6
24
  readonly openDatabase?: (config: WorkerInitConfig) => Promise<ClientDatabase> | ClientDatabase;
7
25
  /** Retry deadline indirection for deterministic worker-host tests. */
@@ -1,19 +1,3 @@
1
- /**
2
- * Worker-side bootstrap: constructs the WHOLE
3
- * client core — SyncClient + transports + realtime + SQLite — inside the
4
- * worker and serves the `worker-protocol` RPC over postMessage.
5
- *
6
- * Browsers get the defaults: opfs-sahpool persistence and the fetch/WS
7
- * transports from `./http`, wired from `WorkerInitConfig.endpoints`.
8
- * Tests inject a database factory (bun:sqlite) through `overrides` — the
9
- * same bootstrap, the same RPC, a different SQLite.
10
- *
11
- * The §8.4 host loop lives HERE: wake-ups (hello/`sync` events, delta
12
- * drops) coalesce into one intent-driven `syncUntilIdle` round in the worker;
13
- * RPC-driven and auto-driven sync rounds serialize on one queue because
14
- * the core owns exactly one loop.
15
- */
16
- import { defineAuthorityReads } from './authority.js';
17
1
  import { SyncClient } from './client.js';
18
2
  import { withClientDiagnosticsHost, } from './diagnostics.js';
19
3
  import { encryptionConfigFromKeyring } from './encryption.js';
@@ -160,6 +144,8 @@ export function startSyncWorker(overrides = {}) {
160
144
  return client;
161
145
  }
162
146
  async function init(config) {
147
+ if (config.authorityReads !== undefined && !overrides.createAuthorityReads)
148
+ throw new ClientSyncError('client.authority_read_forbidden', 'authorityReads requires startSyncWorker({ createAuthorityReads })');
163
149
  if (client !== undefined) {
164
150
  throw new ClientSyncError(WORKER_FAILED_CODE, 'the worker is already initialized');
165
151
  }
@@ -235,7 +221,9 @@ export function startSyncWorker(overrides = {}) {
235
221
  ? { encryption: encryptionConfigFromKeyring(config.encryption) }
236
222
  : {}),
237
223
  ...(config.authorityReads !== undefined
238
- ? { authorityReads: defineAuthorityReads(config.authorityReads) }
224
+ ? {
225
+ authorityReads: overrides.createAuthorityReads(config.authorityReads),
226
+ }
239
227
  : {}),
240
228
  ...(config.securityPreflight !== undefined
241
229
  ? { securityPreflight: config.securityPreflight }
@@ -300,8 +288,44 @@ export function startSyncWorker(overrides = {}) {
300
288
  consumeSyncIntent({ kind: 'interactive' });
301
289
  return { clientId: started.clientId };
302
290
  }
291
+ // Forward every argument to core methods, including authority's zero-argument check.
292
+ const coreMethods = [
293
+ 'securityLifecycle',
294
+ 'subscribe',
295
+ 'unsubscribe',
296
+ 'windowState',
297
+ 'purgeLocalData',
298
+ 'rebootstrapLocalData',
299
+ 'previousVersionSnapshot',
300
+ 'previousVersionAudit',
301
+ 'previousVersionDiscard',
302
+ 'query',
303
+ 'authoritySnapshot',
304
+ 'querySnapshot',
305
+ 'statusSnapshot',
306
+ 'conflicts',
307
+ 'rejections',
308
+ 'commitOutcome',
309
+ 'commitOutcomes',
310
+ 'resolveCommitOutcome',
311
+ 'pendingCommits',
312
+ 'subscriptions',
313
+ 'subscription',
314
+ 'connectRealtime',
315
+ 'disconnectRealtime',
316
+ 'setPresence',
317
+ 'presence',
318
+ 'uploadBlob',
319
+ 'fetchBlob',
320
+ ];
303
321
  const api = {
304
- securityLifecycle: () => requireClient().securityLifecycle(),
322
+ ...Object.fromEntries(coreMethods.map((method) => [
323
+ method,
324
+ (...args) => {
325
+ const running = requireClient();
326
+ return Reflect.apply(running[method], running, args);
327
+ },
328
+ ])),
305
329
  beginSecurityPreflight: async () => {
306
330
  if (backgroundTimer !== undefined)
307
331
  clearTimeout(backgroundTimer);
@@ -317,12 +341,9 @@ export function startSyncWorker(overrides = {}) {
317
341
  : {}),
318
342
  });
319
343
  },
320
- subscribe: (input) => requireClient().subscribe(input),
321
- unsubscribe: (id) => requireClient().unsubscribe(id),
322
344
  setWindow: async (base, units) => {
323
345
  await requireClient().setWindowCommand(base, units);
324
346
  },
325
- windowState: (base) => requireClient().windowState(base),
326
347
  mutate: (mutations) => {
327
348
  return requireClient().mutateCommand(mutations).value;
328
349
  },
@@ -332,11 +353,6 @@ export function startSyncWorker(overrides = {}) {
332
353
  const result = requireClient().patchCommand(table, rowId, partial, options);
333
354
  return result.value;
334
355
  },
335
- purgeLocalData: (input) => requireClient().purgeLocalData(input),
336
- rebootstrapLocalData: (input) => requireClient().rebootstrapLocalData(input),
337
- previousVersionSnapshot: (spec) => requireClient().previousVersionSnapshot(spec),
338
- previousVersionAudit: () => requireClient().previousVersionAudit(),
339
- previousVersionDiscard: () => requireClient().previousVersionDiscard(),
340
356
  sync: () => {
341
357
  const running = requireClient();
342
358
  return serializedSync(() => running.sync());
@@ -345,11 +361,7 @@ export function startSyncWorker(overrides = {}) {
345
361
  const running = requireClient();
346
362
  return serializedSync(() => running.syncUntilIdle(maxRounds));
347
363
  },
348
- query: (sql, params) => requireClient().query(sql, params),
349
- authoritySnapshot: () => requireClient().authoritySnapshot(),
350
- querySnapshot: (spec) => requireClient().querySnapshot(spec),
351
364
  localRevision: () => requireClient().localRevision,
352
- statusSnapshot: () => requireClient().statusSnapshot(),
353
365
  diagnosticsSnapshot: (request) => {
354
366
  const snapshot = requireClient().diagnosticsSnapshot(request);
355
367
  return withClientDiagnosticsHost(snapshot, {
@@ -359,20 +371,6 @@ export function startSyncWorker(overrides = {}) {
359
371
  realtime: snapshot.host.realtime,
360
372
  });
361
373
  },
362
- conflicts: () => requireClient().conflicts(),
363
- rejections: () => requireClient().rejections(),
364
- commitOutcome: (clientCommitId) => requireClient().commitOutcome(clientCommitId),
365
- commitOutcomes: (query) => requireClient().commitOutcomes(query),
366
- resolveCommitOutcome: (input) => requireClient().resolveCommitOutcome(input),
367
- pendingCommits: () => requireClient().pendingCommits(),
368
- subscriptions: () => requireClient().subscriptions(),
369
- subscription: (id) => requireClient().subscription(id),
370
- connectRealtime: () => requireClient().connectRealtime(),
371
- disconnectRealtime: () => requireClient().disconnectRealtime(),
372
- setPresence: (scopeKey, doc) => requireClient().setPresence(scopeKey, doc),
373
- presence: (scopeKey) => requireClient().presence(scopeKey),
374
- uploadBlob: (bytes, options) => requireClient().uploadBlob(bytes, options),
375
- fetchBlob: (blobIdOrRef) => requireClient().fetchBlob(blobIdOrRef),
376
374
  setHeaders: (next) => {
377
375
  headers = next;
378
376
  },
@@ -425,8 +423,6 @@ export function startSyncWorker(overrides = {}) {
425
423
  'previousVersionDiscard',
426
424
  ]);
427
425
  async function dispatch(message) {
428
- if (message.method === 'authoritySnapshot' && message.args.length)
429
- throw new ClientSyncError('client.authority_read_forbidden', 'authoritySnapshot accepts no arguments');
430
426
  const method = api[message.method];
431
427
  const invoke = () => method.apply(api, message.args);
432
428
  if (syncChainMethods.has(message.method)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.30.15",
3
+ "version": "0.30.16",
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",
@@ -73,6 +73,14 @@
73
73
  "types": "./dist/worker-entry.d.ts",
74
74
  "default": "./dist/worker-entry.js"
75
75
  }
76
+ },
77
+ "./authority": {
78
+ "bun": "./src/authority.ts",
79
+ "browser": "./dist/authority.js",
80
+ "import": {
81
+ "types": "./dist/authority.d.ts",
82
+ "default": "./dist/authority.js"
83
+ }
76
84
  }
77
85
  },
78
86
  "files": [
@@ -89,10 +97,10 @@
89
97
  },
90
98
  "dependencies": {
91
99
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
92
- "@syncular/core": "0.30.15"
100
+ "@syncular/core": "0.30.16"
93
101
  },
94
102
  "devDependencies": {
95
- "@syncular/server": "0.30.15",
103
+ "@syncular/server": "0.30.16",
96
104
  "playwright": "1.63.0",
97
105
  "@electric-sql/pglite": "^0.5.4"
98
106
  }
package/src/index.ts CHANGED
@@ -8,11 +8,10 @@
8
8
  * (`worker-host`) and the RPC protocol types are root exports — they
9
9
  * import no SQLite.
10
10
  */
11
- export {
12
- defineAuthorityReads,
13
- type AuthorityReadPolicy,
14
- type AuthorityReadDeclaration,
15
- type AuthoritySnapshot,
11
+ export type {
12
+ AuthorityReadPolicy,
13
+ AuthorityReadDeclaration,
14
+ AuthoritySnapshot,
16
15
  } from './authority';
17
16
  export * from './apply';
18
17
  export * from './availability';
@@ -13,7 +13,7 @@
13
13
  * RPC-driven and auto-driven sync rounds serialize on one queue because
14
14
  * the core owns exactly one loop.
15
15
  */
16
- import { defineAuthorityReads } from './authority';
16
+ import type { defineAuthorityReads } from './authority';
17
17
  import { SyncClient } from './client';
18
18
  import type { ClientDatabase } from './database';
19
19
  import {
@@ -52,6 +52,8 @@ import {
52
52
  } from './worker-protocol';
53
53
 
54
54
  export interface SyncWorkerOverrides {
55
+ /** Explicit authority reader factory. Required when init declares authorityReads. */
56
+ readonly createAuthorityReads?: typeof defineAuthorityReads;
55
57
  /** Database factory indirection: tests inject bun:sqlite here. */
56
58
  readonly openDatabase?: (
57
59
  config: WorkerInitConfig,
@@ -239,6 +241,11 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
239
241
  }
240
242
 
241
243
  async function init(config: WorkerInitConfig): Promise<WorkerInitResult> {
244
+ if (config.authorityReads !== undefined && !overrides.createAuthorityReads)
245
+ throw new ClientSyncError(
246
+ 'client.authority_read_forbidden',
247
+ 'authorityReads requires startSyncWorker({ createAuthorityReads })',
248
+ );
242
249
  if (client !== undefined) {
243
250
  throw new ClientSyncError(
244
251
  WORKER_FAILED_CODE,
@@ -325,7 +332,11 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
325
332
  ? { encryption: encryptionConfigFromKeyring(config.encryption) }
326
333
  : {}),
327
334
  ...(config.authorityReads !== undefined
328
- ? { authorityReads: defineAuthorityReads(config.authorityReads) }
335
+ ? {
336
+ authorityReads: overrides.createAuthorityReads!(
337
+ config.authorityReads,
338
+ ),
339
+ }
329
340
  : {}),
330
341
  ...(config.securityPreflight !== undefined
331
342
  ? { securityPreflight: config.securityPreflight }
@@ -393,8 +404,46 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
393
404
  return { clientId: started.clientId };
394
405
  }
395
406
 
407
+ // Forward every argument to core methods, including authority's zero-argument check.
408
+ const coreMethods = [
409
+ 'securityLifecycle',
410
+ 'subscribe',
411
+ 'unsubscribe',
412
+ 'windowState',
413
+ 'purgeLocalData',
414
+ 'rebootstrapLocalData',
415
+ 'previousVersionSnapshot',
416
+ 'previousVersionAudit',
417
+ 'previousVersionDiscard',
418
+ 'query',
419
+ 'authoritySnapshot',
420
+ 'querySnapshot',
421
+ 'statusSnapshot',
422
+ 'conflicts',
423
+ 'rejections',
424
+ 'commitOutcome',
425
+ 'commitOutcomes',
426
+ 'resolveCommitOutcome',
427
+ 'pendingCommits',
428
+ 'subscriptions',
429
+ 'subscription',
430
+ 'connectRealtime',
431
+ 'disconnectRealtime',
432
+ 'setPresence',
433
+ 'presence',
434
+ 'uploadBlob',
435
+ 'fetchBlob',
436
+ ] as const satisfies readonly (keyof WorkerApi & keyof SyncClient)[];
396
437
  const api: WorkerApi = {
397
- securityLifecycle: () => requireClient().securityLifecycle(),
438
+ ...(Object.fromEntries(
439
+ coreMethods.map((method) => [
440
+ method,
441
+ (...args: unknown[]) => {
442
+ const running = requireClient();
443
+ return Reflect.apply(running[method], running, args);
444
+ },
445
+ ]),
446
+ ) as Pick<WorkerApi, (typeof coreMethods)[number]>),
398
447
  beginSecurityPreflight: async () => {
399
448
  if (backgroundTimer !== undefined) clearTimeout(backgroundTimer);
400
449
  backgroundTimer = undefined;
@@ -409,12 +458,9 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
409
458
  : {}),
410
459
  });
411
460
  },
412
- subscribe: (input) => requireClient().subscribe(input),
413
- unsubscribe: (id) => requireClient().unsubscribe(id),
414
461
  setWindow: async (base, units) => {
415
462
  await requireClient().setWindowCommand(base, units);
416
463
  },
417
- windowState: (base) => requireClient().windowState(base),
418
464
  mutate: (mutations) => {
419
465
  return requireClient().mutateCommand(mutations).value;
420
466
  },
@@ -429,13 +475,6 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
429
475
  );
430
476
  return result.value;
431
477
  },
432
- purgeLocalData: (input) => requireClient().purgeLocalData(input),
433
- rebootstrapLocalData: (input) =>
434
- requireClient().rebootstrapLocalData(input),
435
- previousVersionSnapshot: (spec) =>
436
- requireClient().previousVersionSnapshot(spec),
437
- previousVersionAudit: () => requireClient().previousVersionAudit(),
438
- previousVersionDiscard: () => requireClient().previousVersionDiscard(),
439
478
  sync: () => {
440
479
  const running = requireClient();
441
480
  return serializedSync(() => running.sync());
@@ -444,11 +483,7 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
444
483
  const running = requireClient();
445
484
  return serializedSync(() => running.syncUntilIdle(maxRounds));
446
485
  },
447
- query: (sql, params) => requireClient().query(sql, params),
448
- authoritySnapshot: () => requireClient().authoritySnapshot(),
449
- querySnapshot: (spec) => requireClient().querySnapshot(spec),
450
486
  localRevision: () => requireClient().localRevision,
451
- statusSnapshot: () => requireClient().statusSnapshot(),
452
487
  diagnosticsSnapshot: (request) => {
453
488
  const snapshot = requireClient().diagnosticsSnapshot(request);
454
489
  return withClientDiagnosticsHost(snapshot, {
@@ -458,22 +493,6 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
458
493
  realtime: snapshot.host.realtime,
459
494
  });
460
495
  },
461
- conflicts: () => requireClient().conflicts(),
462
- rejections: () => requireClient().rejections(),
463
- commitOutcome: (clientCommitId) =>
464
- requireClient().commitOutcome(clientCommitId),
465
- commitOutcomes: (query) => requireClient().commitOutcomes(query),
466
- resolveCommitOutcome: (input) =>
467
- requireClient().resolveCommitOutcome(input),
468
- pendingCommits: () => requireClient().pendingCommits(),
469
- subscriptions: () => requireClient().subscriptions(),
470
- subscription: (id) => requireClient().subscription(id),
471
- connectRealtime: () => requireClient().connectRealtime(),
472
- disconnectRealtime: () => requireClient().disconnectRealtime(),
473
- setPresence: (scopeKey, doc) => requireClient().setPresence(scopeKey, doc),
474
- presence: (scopeKey) => requireClient().presence(scopeKey),
475
- uploadBlob: (bytes, options) => requireClient().uploadBlob(bytes, options),
476
- fetchBlob: (blobIdOrRef) => requireClient().fetchBlob(blobIdOrRef),
477
496
  setHeaders: (next) => {
478
497
  headers = next;
479
498
  },
@@ -525,11 +544,6 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
525
544
  ]);
526
545
 
527
546
  async function dispatch(message: WorkerCallMessage): Promise<unknown> {
528
- if (message.method === 'authoritySnapshot' && message.args.length)
529
- throw new ClientSyncError(
530
- 'client.authority_read_forbidden',
531
- 'authoritySnapshot accepts no arguments',
532
- );
533
547
  const method = api[message.method] as (...args: unknown[]) => unknown;
534
548
  const invoke = () => method.apply(api, message.args as unknown[]);
535
549
  if (syncChainMethods.has(message.method)) {