@syncular/tauri 0.15.16 → 0.15.17
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 +15 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +43 -1
- package/package.json +3 -3
- package/src/index.ts +53 -1
package/README.md
CHANGED
|
@@ -59,6 +59,21 @@ preflight at both the JavaScript and native command boundaries. Supply bootstrap
|
|
|
59
59
|
headers through trusted plugin configuration; rotate them only after successful
|
|
60
60
|
activation.
|
|
61
61
|
|
|
62
|
+
## Privacy-safe diagnostics
|
|
63
|
+
|
|
64
|
+
`diagnosticsSnapshot({ expectedSubscriptions })` and `onDiagnostics(listener)`
|
|
65
|
+
carry the native Rust core's versioned support evidence through the Tauri event
|
|
66
|
+
channel. The bridge marks the host as `{ kind: 'tauri', role: 'single' }`; it
|
|
67
|
+
does not infer state from IPC commands. Expected subscriptions accept only
|
|
68
|
+
stable PHI-free ids and generated table names, never scope values.
|
|
69
|
+
|
|
70
|
+
The snapshot is suitable for a redacted “copy diagnostics” workflow: it omits
|
|
71
|
+
rows, clinical row counts, scopes, SQL, paths, client/actor/lease ids, auth,
|
|
72
|
+
keys, mutations, stack traces, and arbitrary prose. Do not supplement it with
|
|
73
|
+
the SQLite file, WebView console dump, or application state. Diagnostics stays
|
|
74
|
+
blocked during security preflight because subscription/table evidence is
|
|
75
|
+
protected. See SPEC §7.6 and `@syncular/react`'s `useDiagnostics`.
|
|
76
|
+
|
|
62
77
|
## React availability guard
|
|
63
78
|
|
|
64
79
|
The Tauri bridge carries `currentSchemaVersion`, `schemaFloor`, and migration
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* `invoke`/`listen` either from its ESM entry points, from the ambient
|
|
27
27
|
* `window.__TAURI__`, or via injected doubles (tests).
|
|
28
28
|
*/
|
|
29
|
-
import type { ClientChangeListener, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LeaseState, LocalDataPurgeInput, LocalDataPurgeResult, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SchemaFloor, SecurityLifecycle, SqlRow, SqlValue, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
|
|
29
|
+
import type { ClientChangeListener, ClientDiagnosticsListener, ClientDiagnosticsRequest, ClientDiagnosticsSnapshot, CommitOutcome, CommitOutcomeQuery, ConflictRecord, EncryptionKeyringConfig, InvalidationListener, LeaseState, LocalDataPurgeInput, LocalDataPurgeResult, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, ResolveCommitOutcomeInput, SchemaFloor, SecurityLifecycle, SqlRow, SqlValue, SyncStatusSnapshot, WindowBase, WindowState } from '@syncular/client';
|
|
30
30
|
/** One event pushed on `syncular://event` (the derived client-observable set). */
|
|
31
31
|
interface SyncularEvent {
|
|
32
32
|
readonly type: string;
|
|
@@ -89,11 +89,13 @@ export declare class TauriSyncClient {
|
|
|
89
89
|
}): Promise<void>;
|
|
90
90
|
onInvalidate(listener: InvalidationListener): () => void;
|
|
91
91
|
onChange(listener: ClientChangeListener): () => void;
|
|
92
|
+
onDiagnostics(listener: ClientDiagnosticsListener): () => void;
|
|
92
93
|
onPresence(listener: (scopeKey: string) => void): () => void;
|
|
93
94
|
query(sql: string, params?: readonly SqlValue[]): Promise<SqlRow[]>;
|
|
94
95
|
querySnapshot<Row = SqlRow>(spec: QueryReadSpec): Promise<QuerySnapshot<Row>>;
|
|
95
96
|
localRevision(): Promise<bigint>;
|
|
96
97
|
statusSnapshot(): Promise<SyncStatusSnapshot>;
|
|
98
|
+
diagnosticsSnapshot(request?: ClientDiagnosticsRequest): Promise<ClientDiagnosticsSnapshot>;
|
|
97
99
|
mutate(mutations: readonly MutationInput[]): Promise<string>;
|
|
98
100
|
patch(table: string, rowId: string, partial: Readonly<Record<string, unknown>>, options?: {
|
|
99
101
|
readonly baseVersion?: number;
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// -- Types the bridge speaks (structurally the web-client's) -----------------
|
|
30
30
|
// Most imports stay type-only; the stable preflight error code is shared at
|
|
31
31
|
// runtime so every host surfaces byte-identical policy evidence.
|
|
32
|
-
import { SECURITY_PREFLIGHT_REQUIRED_CODE } from '@syncular/client';
|
|
32
|
+
import { SECURITY_PREFLIGHT_REQUIRED_CODE, withClientDiagnosticsHost, } from '@syncular/client';
|
|
33
33
|
/** The plugin's Tauri event name — mirror of `tauri-plugin-syncular`. */
|
|
34
34
|
export const SYNCULAR_EVENT = 'syncular://event';
|
|
35
35
|
const PLUGIN = 'plugin:syncular|';
|
|
@@ -152,6 +152,7 @@ export class TauriSyncClient {
|
|
|
152
152
|
#tauri;
|
|
153
153
|
#invalidationListeners = new Set();
|
|
154
154
|
#changeListeners = new Set();
|
|
155
|
+
#diagnosticsListeners = new Set();
|
|
155
156
|
#presenceListeners = new Set();
|
|
156
157
|
#unlisten;
|
|
157
158
|
#closed = false;
|
|
@@ -233,6 +234,20 @@ export class TauriSyncClient {
|
|
|
233
234
|
}
|
|
234
235
|
break;
|
|
235
236
|
}
|
|
237
|
+
case 'diagnostics': {
|
|
238
|
+
const snapshot = decodeDiagnosticsSnapshot(event.snapshot, 'tauri');
|
|
239
|
+
if (snapshot === undefined)
|
|
240
|
+
break;
|
|
241
|
+
for (const listener of this.#diagnosticsListeners) {
|
|
242
|
+
try {
|
|
243
|
+
listener(snapshot);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* a diagnostics observer must never break event dispatch */
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
236
251
|
default:
|
|
237
252
|
// Unknown extension events are deliberately ignored. All durable
|
|
238
253
|
// observable state arrives in the revisioned `change` batch.
|
|
@@ -282,6 +297,10 @@ export class TauriSyncClient {
|
|
|
282
297
|
this.#changeListeners.add(listener);
|
|
283
298
|
return () => this.#changeListeners.delete(listener);
|
|
284
299
|
}
|
|
300
|
+
onDiagnostics(listener) {
|
|
301
|
+
this.#diagnosticsListeners.add(listener);
|
|
302
|
+
return () => this.#diagnosticsListeners.delete(listener);
|
|
303
|
+
}
|
|
285
304
|
onPresence(listener) {
|
|
286
305
|
this.#presenceListeners.add(listener);
|
|
287
306
|
return () => this.#presenceListeners.delete(listener);
|
|
@@ -319,6 +338,16 @@ export class TauriSyncClient {
|
|
|
319
338
|
async statusSnapshot() {
|
|
320
339
|
return (await this.#command('statusSnapshot', {}));
|
|
321
340
|
}
|
|
341
|
+
async diagnosticsSnapshot(request = {}) {
|
|
342
|
+
const snapshot = (await this.#command('diagnosticsSnapshot', {
|
|
343
|
+
expectedSubscriptions: request.expectedSubscriptions ?? [],
|
|
344
|
+
}));
|
|
345
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
346
|
+
...snapshot.host,
|
|
347
|
+
kind: 'tauri',
|
|
348
|
+
role: 'single',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
322
351
|
async mutate(mutations) {
|
|
323
352
|
const result = (await this.#command('mutate', {
|
|
324
353
|
mutations: mutations.map(encodeMutation),
|
|
@@ -504,9 +533,22 @@ export class TauriSyncClient {
|
|
|
504
533
|
this.#unlisten = undefined;
|
|
505
534
|
this.#invalidationListeners.clear();
|
|
506
535
|
this.#changeListeners.clear();
|
|
536
|
+
this.#diagnosticsListeners.clear();
|
|
507
537
|
this.#presenceListeners.clear();
|
|
508
538
|
}
|
|
509
539
|
}
|
|
540
|
+
function decodeDiagnosticsSnapshot(value, kind) {
|
|
541
|
+
if (value === null || typeof value !== 'object')
|
|
542
|
+
return undefined;
|
|
543
|
+
const snapshot = value;
|
|
544
|
+
if (snapshot.version !== 1 || snapshot.host === undefined)
|
|
545
|
+
return undefined;
|
|
546
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
547
|
+
...snapshot.host,
|
|
548
|
+
kind,
|
|
549
|
+
role: 'single',
|
|
550
|
+
});
|
|
551
|
+
}
|
|
510
552
|
/** The error a `{error}` reply surfaces (mirrors the web-client `ClientSyncError`). */
|
|
511
553
|
export class TauriSyncError extends Error {
|
|
512
554
|
code;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/tauri",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.17",
|
|
4
4
|
"description": "Tauri integration for the Syncular client",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"test": "bun test"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@syncular/client": "0.15.
|
|
51
|
+
"@syncular/client": "0.15.17"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@tauri-apps/api": ">=2.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@syncular/react": "0.15.
|
|
57
|
+
"@syncular/react": "0.15.17"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
import type {
|
|
31
31
|
ClientChangeBatch,
|
|
32
32
|
ClientChangeListener,
|
|
33
|
+
ClientDiagnosticsListener,
|
|
34
|
+
ClientDiagnosticsRequest,
|
|
35
|
+
ClientDiagnosticsSnapshot,
|
|
33
36
|
CommitOutcome,
|
|
34
37
|
CommitOutcomeQuery,
|
|
35
38
|
ConflictRecord,
|
|
@@ -56,7 +59,10 @@ import type {
|
|
|
56
59
|
// -- Types the bridge speaks (structurally the web-client's) -----------------
|
|
57
60
|
// Most imports stay type-only; the stable preflight error code is shared at
|
|
58
61
|
// runtime so every host surfaces byte-identical policy evidence.
|
|
59
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
SECURITY_PREFLIGHT_REQUIRED_CODE,
|
|
64
|
+
withClientDiagnosticsHost,
|
|
65
|
+
} from '@syncular/client';
|
|
60
66
|
|
|
61
67
|
/** A driver-protocol reply: `{result}` on success or `{error}` on failure. */
|
|
62
68
|
interface CommandReply {
|
|
@@ -250,6 +256,7 @@ export class TauriSyncClient {
|
|
|
250
256
|
readonly #tauri: TauriApi;
|
|
251
257
|
readonly #invalidationListeners = new Set<InvalidationListener>();
|
|
252
258
|
readonly #changeListeners = new Set<ClientChangeListener>();
|
|
259
|
+
readonly #diagnosticsListeners = new Set<ClientDiagnosticsListener>();
|
|
253
260
|
readonly #presenceListeners = new Set<(scopeKey: string) => void>();
|
|
254
261
|
#unlisten: (() => void) | undefined;
|
|
255
262
|
#closed = false;
|
|
@@ -349,6 +356,18 @@ export class TauriSyncClient {
|
|
|
349
356
|
}
|
|
350
357
|
break;
|
|
351
358
|
}
|
|
359
|
+
case 'diagnostics': {
|
|
360
|
+
const snapshot = decodeDiagnosticsSnapshot(event.snapshot, 'tauri');
|
|
361
|
+
if (snapshot === undefined) break;
|
|
362
|
+
for (const listener of this.#diagnosticsListeners) {
|
|
363
|
+
try {
|
|
364
|
+
listener(snapshot);
|
|
365
|
+
} catch {
|
|
366
|
+
/* a diagnostics observer must never break event dispatch */
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
352
371
|
default:
|
|
353
372
|
// Unknown extension events are deliberately ignored. All durable
|
|
354
373
|
// observable state arrives in the revisioned `change` batch.
|
|
@@ -414,6 +433,11 @@ export class TauriSyncClient {
|
|
|
414
433
|
return () => this.#changeListeners.delete(listener);
|
|
415
434
|
}
|
|
416
435
|
|
|
436
|
+
onDiagnostics(listener: ClientDiagnosticsListener): () => void {
|
|
437
|
+
this.#diagnosticsListeners.add(listener);
|
|
438
|
+
return () => this.#diagnosticsListeners.delete(listener);
|
|
439
|
+
}
|
|
440
|
+
|
|
417
441
|
onPresence(listener: (scopeKey: string) => void): () => void {
|
|
418
442
|
this.#presenceListeners.add(listener);
|
|
419
443
|
return () => this.#presenceListeners.delete(listener);
|
|
@@ -470,6 +494,19 @@ export class TauriSyncClient {
|
|
|
470
494
|
return (await this.#command('statusSnapshot', {})) as SyncStatusSnapshot;
|
|
471
495
|
}
|
|
472
496
|
|
|
497
|
+
async diagnosticsSnapshot(
|
|
498
|
+
request: ClientDiagnosticsRequest = {},
|
|
499
|
+
): Promise<ClientDiagnosticsSnapshot> {
|
|
500
|
+
const snapshot = (await this.#command('diagnosticsSnapshot', {
|
|
501
|
+
expectedSubscriptions: request.expectedSubscriptions ?? [],
|
|
502
|
+
})) as ClientDiagnosticsSnapshot;
|
|
503
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
504
|
+
...snapshot.host,
|
|
505
|
+
kind: 'tauri',
|
|
506
|
+
role: 'single',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
473
510
|
async mutate(mutations: readonly MutationInput[]): Promise<string> {
|
|
474
511
|
const result = (await this.#command('mutate', {
|
|
475
512
|
mutations: mutations.map(encodeMutation),
|
|
@@ -746,10 +783,25 @@ export class TauriSyncClient {
|
|
|
746
783
|
this.#unlisten = undefined;
|
|
747
784
|
this.#invalidationListeners.clear();
|
|
748
785
|
this.#changeListeners.clear();
|
|
786
|
+
this.#diagnosticsListeners.clear();
|
|
749
787
|
this.#presenceListeners.clear();
|
|
750
788
|
}
|
|
751
789
|
}
|
|
752
790
|
|
|
791
|
+
function decodeDiagnosticsSnapshot(
|
|
792
|
+
value: unknown,
|
|
793
|
+
kind: 'tauri',
|
|
794
|
+
): ClientDiagnosticsSnapshot | undefined {
|
|
795
|
+
if (value === null || typeof value !== 'object') return undefined;
|
|
796
|
+
const snapshot = value as ClientDiagnosticsSnapshot;
|
|
797
|
+
if (snapshot.version !== 1 || snapshot.host === undefined) return undefined;
|
|
798
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
799
|
+
...snapshot.host,
|
|
800
|
+
kind,
|
|
801
|
+
role: 'single',
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
753
805
|
/** The error a `{error}` reply surfaces (mirrors the web-client `ClientSyncError`). */
|
|
754
806
|
export class TauriSyncError extends Error {
|
|
755
807
|
readonly code: string;
|