@syncular/tauri 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
@@ -154,7 +154,8 @@ available. The native database opens and migrates, but query/snapshot, mutation,
154
154
  subscription, sync, realtime, presence, blob, and automatic retry work fails
155
155
  with `client.security_preflight_required`. Status, local revision, lifecycle,
156
156
  and `purgeLocalData` remain available. A creation-time `defineAuthorityReads`
157
- policy also enables the read-only `authoritySnapshot()` described below.
157
+ policy on `createTauriAuthoritySyncClient` enables the read-only
158
+ `authoritySnapshot()` described below.
158
159
 
159
160
  ```ts
160
161
  const client = await createTauriSyncClient({
@@ -184,16 +185,17 @@ registration and local progress reads throw that code. `close()` is idempotent.
184
185
  ## Authority evidence before activation
185
186
 
186
187
  Declare authority reads at client creation with `defineAuthorityReads` from
187
- `@syncular/client`. Each table declares plain columns, including its primary
188
- key and scope columns, and concrete scope selectors. These selectors remain
188
+ `@syncular/client/authority`. Each table declares plain columns, including
189
+ its primary key and scope columns, and concrete scope selectors. These selectors remain
189
190
  fixed for that client. A declaration cannot include encrypted, bytes, blob,
190
191
  CRDT or internal columns. Choose only authority fields; clinical fields and
191
192
  credentials do not belong in this policy.
192
193
 
193
194
  ```ts
194
- import { defineAuthorityReads } from '@syncular/client';
195
+ import { defineAuthorityReads } from '@syncular/client/authority';
196
+ import { createTauriAuthoritySyncClient } from '@syncular/tauri/authority';
195
197
 
196
- const client = await createTauriSyncClient({
198
+ const client = await createTauriAuthoritySyncClient({
197
199
  schema,
198
200
  securityPreflight: true,
199
201
  transportEnabled: false,
@@ -206,6 +208,11 @@ const client = await createTauriSyncClient({
206
208
  const evidence = await client.authoritySnapshot();
207
209
  ```
208
210
 
211
+ Use `createTauriAuthoritySyncClient` for the bridge that exposes
212
+ `authoritySnapshot()`. The ordinary `createTauriSyncClient` class does not
213
+ contain that method or its reply decoder. Upgrade 0.30.15 callers to these
214
+ explicit subpaths; the snapshot and native security checks keep their semantics.
215
+
209
216
  The native application must also set an independent ceiling at plugin creation:
210
217
 
211
218
  ```rust
@@ -0,0 +1,10 @@
1
+ import type { AuthorityReadPolicy, AuthoritySnapshot } from '@syncular/client/authority';
2
+ import { TauriSyncClient, type TauriSyncClientConfig } from './index.js';
3
+ /** Explicit native bridge with the zero-argument preflight authority read. */
4
+ export declare class TauriAuthoritySyncClient extends TauriSyncClient {
5
+ authoritySnapshot(): Promise<AuthoritySnapshot>;
6
+ }
7
+ /** Construct an authority-enabled bridge; Rust still enforces its independent ceiling. */
8
+ export declare function createTauriAuthoritySyncClient(config: TauriSyncClientConfig & {
9
+ readonly authorityReads: AuthorityReadPolicy;
10
+ }): Promise<TauriAuthoritySyncClient>;
@@ -0,0 +1,26 @@
1
+ import { createTauriSyncClient, decodeRow, TauriSyncClient, TauriSyncError, } from './index.js';
2
+ /** Explicit native bridge with the zero-argument preflight authority read. */
3
+ export class TauriAuthoritySyncClient extends TauriSyncClient {
4
+ async authoritySnapshot() {
5
+ this.requireOpen();
6
+ if (arguments.length)
7
+ throw new TauriSyncError('client.authority_read_forbidden', 'authoritySnapshot accepts no arguments');
8
+ const result = (await this.command('authoritySnapshot', {}));
9
+ this.requireOpen();
10
+ return {
11
+ ...result,
12
+ revision: BigInt(result.revision),
13
+ tables: result.tables.map((table) => ({
14
+ ...table,
15
+ rows: table.rows.map((row) => ({
16
+ ...row,
17
+ values: decodeRow(row.values),
18
+ })),
19
+ })),
20
+ };
21
+ }
22
+ }
23
+ /** Construct an authority-enabled bridge; Rust still enforces its independent ceiling. */
24
+ export function createTauriAuthoritySyncClient(config) {
25
+ return createTauriSyncClient(config, (...args) => new TauriAuthoritySyncClient(...args));
26
+ }
package/dist/index.d.ts CHANGED
@@ -28,7 +28,7 @@ import type { ClientSnapshotMethods, PromiseMethods } from '@syncular/client';
28
28
  * `invoke`/`listen` either from its ESM entry points, from the ambient
29
29
  * `window.__TAURI__`, or via injected doubles (tests).
30
30
  */
31
- import type { AuthorityReadPolicy, AuthoritySnapshot, ClientChangeListener, ClientDiagnosticsListener, ClientDiagnosticsRequest, ClientDiagnosticsSnapshot, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LocalDataPurgeInput, LocalDataPurgeResult, LocalDataRebootstrapInput, LocalDataRebootstrapResult, MutationInput, PresencePeer, PreviousVersionAudit, PreviousVersionReadSpec, PreviousVersionSnapshot, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SecurityLifecycle, SqlRow, SqlValue, SyncClientConfig, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
31
+ import type { AuthorityReadPolicy, ClientChangeListener, ClientDiagnosticsListener, ClientDiagnosticsRequest, ClientDiagnosticsSnapshot, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LocalDataPurgeInput, LocalDataPurgeResult, LocalDataRebootstrapInput, LocalDataRebootstrapResult, MutationInput, PresencePeer, PreviousVersionAudit, PreviousVersionReadSpec, PreviousVersionSnapshot, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SecurityLifecycle, SqlRow, SqlValue, SyncClientConfig, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
32
32
  /** One event pushed on `syncular://event` (the derived client-observable set). */
33
33
  interface SyncularEvent {
34
34
  readonly type: string;
@@ -97,6 +97,8 @@ export interface TauriSyncClientConfig {
97
97
  export type BytesEnvelope = {
98
98
  readonly $bytes: string;
99
99
  };
100
+ /** @internal Shared native row codec. */
101
+ export declare function decodeRow(row: Record<string, unknown>): SqlRow;
100
102
  /**
101
103
  * The webview-side proxy implementing `SyncClientLike` over the plugin. Every
102
104
  * snapshot method returns a promise from an IPC round trip. React hooks accept
@@ -106,6 +108,10 @@ export declare class TauriSyncClient implements PromiseMethods<ClientSnapshotMet
106
108
  #private;
107
109
  /** @internal — use {@link createTauriSyncClient}. */
108
110
  constructor(tauri: TauriApi, unlisten: () => void, securityLifecycle?: SecurityLifecycle);
111
+ /** @internal Shared command gate for explicit bridge extensions. */
112
+ protected command(method: string, params: Record<string, unknown>): Promise<unknown>;
113
+ /** @internal Recheck local close after an extension's asynchronous reply. */
114
+ protected requireOpen(): void;
109
115
  /** @internal — fan an incoming plugin event out to the local listeners. */
110
116
  __dispatchEvent(event: SyncularEvent): void;
111
117
  securityLifecycle(): Promise<SecurityLifecycle>;
@@ -128,7 +134,6 @@ export declare class TauriSyncClient implements PromiseMethods<ClientSnapshotMet
128
134
  onDiagnostics(listener: ClientDiagnosticsListener): () => void;
129
135
  onPresence(listener: (scopeKey: string) => void): () => void;
130
136
  query(sql: string, params?: readonly SqlValue[]): Promise<SqlRow[]>;
131
- authoritySnapshot(): Promise<AuthoritySnapshot>;
132
137
  querySnapshot<Row = SqlRow>(spec: QueryReadSpec): Promise<QuerySnapshot<Row>>;
133
138
  localRevision(): Promise<bigint>;
134
139
  statusSnapshot(): Promise<SyncStatusSnapshot>;
@@ -204,4 +209,6 @@ export declare class TauriSyncError extends Error {
204
209
  * React `<SyncProvider client={…}>`.
205
210
  */
206
211
  export declare function createTauriSyncClient(config: TauriSyncClientConfig): Promise<TauriSyncClient>;
212
+ /** @internal Explicit bridge extension used by the authority entry. */
213
+ export declare function createTauriSyncClient<Client extends TauriSyncClient>(config: TauriSyncClientConfig, createClient: (...args: ConstructorParameters<typeof TauriSyncClient>) => Client): Promise<Client>;
207
214
  export {};
package/dist/index.js CHANGED
@@ -82,7 +82,8 @@ function decodeCell(value) {
82
82
  // string (SQLite json columns arrive as text already; this is defensive).
83
83
  return JSON.stringify(value);
84
84
  }
85
- function decodeRow(row) {
85
+ /** @internal Shared native row codec. */
86
+ export function decodeRow(row) {
86
87
  const out = {};
87
88
  for (const [key, value] of Object.entries(row)) {
88
89
  // Reserved `_sync_*` columns stay engine-internal (parity with the
@@ -190,6 +191,14 @@ export class TauriSyncClient {
190
191
  throw new TauriSyncError('client.closed', 'the Tauri sync client is closed');
191
192
  }
192
193
  }
194
+ /** @internal Shared command gate for explicit bridge extensions. */
195
+ command(method, params) {
196
+ return this.#command(method, params);
197
+ }
198
+ /** @internal Recheck local close after an extension's asynchronous reply. */
199
+ requireOpen() {
200
+ this.#requireOpen();
201
+ }
193
202
  #requireActive() {
194
203
  this.#requireOpen();
195
204
  if (this.#securityLifecycle === 'preflight')
@@ -365,24 +374,6 @@ export class TauriSyncClient {
365
374
  const rows = reply.result.rows ?? [];
366
375
  return rows.map((r) => decodeRow(r));
367
376
  }
368
- async authoritySnapshot() {
369
- this.#requireOpen();
370
- if (arguments.length)
371
- throw new TauriSyncError('client.authority_read_forbidden', 'authoritySnapshot accepts no arguments');
372
- const result = (await this.#command('authoritySnapshot', {}));
373
- this.#requireOpen();
374
- return {
375
- ...result,
376
- revision: BigInt(result.revision),
377
- tables: result.tables.map((table) => ({
378
- ...table,
379
- rows: table.rows.map((row) => ({
380
- ...row,
381
- values: decodeRow(row.values),
382
- })),
383
- })),
384
- };
385
- }
386
377
  async querySnapshot(spec) {
387
378
  this.#requireActive();
388
379
  const reply = await this.#tauri.invoke(`${PLUGIN}syncular_query_snapshot`, {
@@ -720,13 +711,7 @@ function encodeMutation(mutation) {
720
711
  }
721
712
  return { ...mutation, values };
722
713
  }
723
- /**
724
- * Construct the bridge and issue the native `create` (opening/attaching the
725
- * file DB on the Rust side per the plugin config). Returns a ready
726
- * `TauriSyncClient` that satisfies `SyncClientLike` — pass it straight to the
727
- * React `<SyncProvider client={…}>`.
728
- */
729
- export async function createTauriSyncClient(config) {
714
+ export async function createTauriSyncClient(config, createClient = (...args) => new TauriSyncClient(...args)) {
730
715
  const tauri = await resolveTauri(config.tauri);
731
716
  // Wire the event stream BEFORE create, so no early change batch is missed.
732
717
  const clientRef = {
@@ -739,7 +724,7 @@ export async function createTauriSyncClient(config) {
739
724
  unlisten();
740
725
  throw new TauriSyncError('sync.invalid_request', 'securityPreflight and encryption are mutually exclusive; install keys with activateSecurity after preflight');
741
726
  }
742
- const client = new TauriSyncClient(tauri, unlisten, config.securityPreflight === true ? 'preflight' : 'active');
727
+ const client = createClient(tauri, unlisten, config.securityPreflight === true ? 'preflight' : 'active');
743
728
  clientRef.client = client;
744
729
  // The native side owns every path (plugin config); the JS side names a
745
730
  // database and supplies the schema, clientId, and limits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/tauri",
3
- "version": "0.30.15",
3
+ "version": "0.30.16",
4
4
  "description": "Tauri integration for the Syncular client",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -33,6 +33,14 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "default": "./dist/index.js"
35
35
  }
36
+ },
37
+ "./authority": {
38
+ "bun": "./src/authority.ts",
39
+ "browser": "./dist/authority.js",
40
+ "import": {
41
+ "types": "./dist/authority.d.ts",
42
+ "default": "./dist/authority.js"
43
+ }
36
44
  }
37
45
  },
38
46
  "files": [
@@ -48,13 +56,13 @@
48
56
  "test": "bun test"
49
57
  },
50
58
  "dependencies": {
51
- "@syncular/client": "0.30.15"
59
+ "@syncular/client": "0.30.16"
52
60
  },
53
61
  "peerDependencies": {
54
62
  "@tauri-apps/api": ">=2.0.0"
55
63
  },
56
64
  "devDependencies": {
57
- "@syncular/react": "0.30.15",
58
- "@syncular/server": "0.30.15"
65
+ "@syncular/react": "0.30.16",
66
+ "@syncular/server": "0.30.16"
59
67
  }
60
68
  }
@@ -0,0 +1,51 @@
1
+ import type {
2
+ AuthorityReadPolicy,
3
+ AuthoritySnapshot,
4
+ } from '@syncular/client/authority';
5
+ import {
6
+ createTauriSyncClient,
7
+ decodeRow,
8
+ TauriSyncClient,
9
+ TauriSyncError,
10
+ type TauriSyncClientConfig,
11
+ } from './index';
12
+
13
+ /** Explicit native bridge with the zero-argument preflight authority read. */
14
+ export class TauriAuthoritySyncClient extends TauriSyncClient {
15
+ async authoritySnapshot(): Promise<AuthoritySnapshot> {
16
+ this.requireOpen();
17
+ if (arguments.length)
18
+ throw new TauriSyncError(
19
+ 'client.authority_read_forbidden',
20
+ 'authoritySnapshot accepts no arguments',
21
+ );
22
+ const result = (await this.command('authoritySnapshot', {})) as Omit<
23
+ AuthoritySnapshot,
24
+ 'revision'
25
+ > & { revision: string };
26
+ this.requireOpen();
27
+ return {
28
+ ...result,
29
+ revision: BigInt(result.revision),
30
+ tables: result.tables.map((table) => ({
31
+ ...table,
32
+ rows: table.rows.map((row) => ({
33
+ ...row,
34
+ values: decodeRow(row.values),
35
+ })),
36
+ })),
37
+ };
38
+ }
39
+ }
40
+
41
+ /** Construct an authority-enabled bridge; Rust still enforces its independent ceiling. */
42
+ export function createTauriAuthoritySyncClient(
43
+ config: TauriSyncClientConfig & {
44
+ readonly authorityReads: AuthorityReadPolicy;
45
+ },
46
+ ): Promise<TauriAuthoritySyncClient> {
47
+ return createTauriSyncClient(
48
+ config,
49
+ (...args) => new TauriAuthoritySyncClient(...args),
50
+ );
51
+ }
package/src/index.ts CHANGED
@@ -35,7 +35,6 @@ import type { ClientSnapshotMethods, PromiseMethods } from '@syncular/client';
35
35
 
36
36
  import type {
37
37
  AuthorityReadPolicy,
38
- AuthoritySnapshot,
39
38
  ClientChangeBatch,
40
39
  ClientChangeListener,
41
40
  ClientDiagnosticsListener,
@@ -250,7 +249,8 @@ function decodeCell(value: unknown): SqlValue {
250
249
  return JSON.stringify(value);
251
250
  }
252
251
 
253
- function decodeRow(row: Record<string, unknown>): SqlRow {
252
+ /** @internal Shared native row codec. */
253
+ export function decodeRow(row: Record<string, unknown>): SqlRow {
254
254
  const out: SqlRow = {};
255
255
  for (const [key, value] of Object.entries(row)) {
256
256
  // Reserved `_sync_*` columns stay engine-internal (parity with the
@@ -394,6 +394,19 @@ export class TauriSyncClient implements PromiseMethods<ClientSnapshotMethods> {
394
394
  }
395
395
  }
396
396
 
397
+ /** @internal Shared command gate for explicit bridge extensions. */
398
+ protected command(
399
+ method: string,
400
+ params: Record<string, unknown>,
401
+ ): Promise<unknown> {
402
+ return this.#command(method, params);
403
+ }
404
+
405
+ /** @internal Recheck local close after an extension's asynchronous reply. */
406
+ protected requireOpen(): void {
407
+ this.#requireOpen();
408
+ }
409
+
397
410
  #requireActive(): void {
398
411
  this.#requireOpen();
399
412
  if (this.#securityLifecycle === 'preflight') this.#throwSecurityPreflight();
@@ -593,31 +606,6 @@ export class TauriSyncClient implements PromiseMethods<ClientSnapshotMethods> {
593
606
  return rows.map((r) => decodeRow(r as Record<string, unknown>));
594
607
  }
595
608
 
596
- async authoritySnapshot(): Promise<AuthoritySnapshot> {
597
- this.#requireOpen();
598
- if (arguments.length)
599
- throw new TauriSyncError(
600
- 'client.authority_read_forbidden',
601
- 'authoritySnapshot accepts no arguments',
602
- );
603
- const result = (await this.#command('authoritySnapshot', {})) as Omit<
604
- AuthoritySnapshot,
605
- 'revision'
606
- > & { revision: string };
607
- this.#requireOpen();
608
- return {
609
- ...result,
610
- revision: BigInt(result.revision),
611
- tables: result.tables.map((table) => ({
612
- ...table,
613
- rows: table.rows.map((row) => ({
614
- ...row,
615
- values: decodeRow(row.values),
616
- })),
617
- })),
618
- };
619
- }
620
-
621
609
  async querySnapshot<Row = SqlRow>(
622
610
  spec: QueryReadSpec,
623
611
  ): Promise<QuerySnapshot<Row>> {
@@ -1097,8 +1085,20 @@ function encodeMutation(mutation: MutationInput): unknown {
1097
1085
  * `TauriSyncClient` that satisfies `SyncClientLike` — pass it straight to the
1098
1086
  * React `<SyncProvider client={…}>`.
1099
1087
  */
1088
+ export function createTauriSyncClient(
1089
+ config: TauriSyncClientConfig,
1090
+ ): Promise<TauriSyncClient>;
1091
+ /** @internal Explicit bridge extension used by the authority entry. */
1092
+ export function createTauriSyncClient<Client extends TauriSyncClient>(
1093
+ config: TauriSyncClientConfig,
1094
+ createClient: (
1095
+ ...args: ConstructorParameters<typeof TauriSyncClient>
1096
+ ) => Client,
1097
+ ): Promise<Client>;
1100
1098
  export async function createTauriSyncClient(
1101
1099
  config: TauriSyncClientConfig,
1100
+ createClient = (...args: ConstructorParameters<typeof TauriSyncClient>) =>
1101
+ new TauriSyncClient(...args),
1102
1102
  ): Promise<TauriSyncClient> {
1103
1103
  const tauri = await resolveTauri(config.tauri);
1104
1104
 
@@ -1121,7 +1121,7 @@ export async function createTauriSyncClient(
1121
1121
  );
1122
1122
  }
1123
1123
 
1124
- const client = new TauriSyncClient(
1124
+ const client = createClient(
1125
1125
  tauri,
1126
1126
  unlisten,
1127
1127
  config.securityPreflight === true ? 'preflight' : 'active',