@syncular/client 0.15.16 → 0.15.18
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 +29 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.js +345 -23
- package/dist/diagnostics.d.ts +134 -0
- package/dist/diagnostics.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/worker-entry.js +42 -0
- package/dist/worker-host.d.ts +6 -0
- package/dist/worker-host.js +30 -0
- package/dist/worker-protocol.d.ts +6 -0
- package/package.json +3 -3
- package/src/client.ts +442 -32
- package/src/diagnostics.ts +192 -0
- package/src/index.ts +1 -0
- package/src/worker-entry.ts +44 -0
- package/src/worker-host.ts +44 -0
- package/src/worker-protocol.ts +12 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy-safe client diagnostics shared by direct, Worker, Tauri, React
|
|
3
|
+
* Native, and normalized React hosts. The snapshot deliberately excludes
|
|
4
|
+
* requested/effective scope values, row/cardinality data, SQL, database paths,
|
|
5
|
+
* auth material, lease ids, encryption keys, mutation bodies, and arbitrary
|
|
6
|
+
* diagnostic prose.
|
|
7
|
+
*/
|
|
8
|
+
import type { SecurityLifecycle } from './client.js';
|
|
9
|
+
export declare const CLIENT_DIAGNOSTICS_VERSION: 1;
|
|
10
|
+
export declare const MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS = 256;
|
|
11
|
+
export declare const MAX_DIAGNOSTIC_DOMAINS = 256;
|
|
12
|
+
export type ClientDiagnosticsHostKind = 'direct' | 'worker' | 'tauri' | 'react-native';
|
|
13
|
+
export type ClientDiagnosticsHostRole = 'single' | 'leader' | 'follower' | 'unknown';
|
|
14
|
+
export type ClientDiagnosticsConnectivity = 'online' | 'offline' | 'unknown';
|
|
15
|
+
export type ClientDiagnosticsRealtime = 'connected' | 'disconnected' | 'unsupported' | 'unknown';
|
|
16
|
+
export interface ClientDiagnosticsHost {
|
|
17
|
+
readonly kind: ClientDiagnosticsHostKind;
|
|
18
|
+
readonly role: ClientDiagnosticsHostRole;
|
|
19
|
+
readonly connectivity: ClientDiagnosticsConnectivity;
|
|
20
|
+
readonly realtime: ClientDiagnosticsRealtime;
|
|
21
|
+
}
|
|
22
|
+
export interface ExpectedDiagnosticSubscription {
|
|
23
|
+
/** Application-owned stable identifier. It must never contain PHI. */
|
|
24
|
+
readonly id: string;
|
|
25
|
+
/** Generated schema table name. */
|
|
26
|
+
readonly table: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ClientDiagnosticsRequest {
|
|
29
|
+
/**
|
|
30
|
+
* Optional bounded intent list. Missing registrations are returned as
|
|
31
|
+
* `unregistered`, allowing a zero-row security scope to fail closed without
|
|
32
|
+
* reading private Syncular tables. Values/scopes are intentionally absent.
|
|
33
|
+
*/
|
|
34
|
+
readonly expectedSubscriptions?: readonly ExpectedDiagnosticSubscription[];
|
|
35
|
+
}
|
|
36
|
+
export type DiagnosticSubscriptionState = 'unregistered' | 'bootstrapping' | 'complete' | 'reset' | 'revoked' | 'failed';
|
|
37
|
+
export interface DiagnosticSubscription {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly table: string;
|
|
40
|
+
readonly state: DiagnosticSubscriptionState;
|
|
41
|
+
readonly complete: boolean;
|
|
42
|
+
/** Last fully applied local commit sequence; absent when unregistered. */
|
|
43
|
+
readonly cursor?: number;
|
|
44
|
+
readonly reasonCode?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface DiagnosticRoundCounters {
|
|
47
|
+
readonly pushed: number;
|
|
48
|
+
readonly applied: number;
|
|
49
|
+
readonly rejected: number;
|
|
50
|
+
readonly retryable: number;
|
|
51
|
+
readonly conflicts: number;
|
|
52
|
+
readonly commitsApplied: number;
|
|
53
|
+
readonly segmentRowsApplied: number;
|
|
54
|
+
readonly bootstrapping: number;
|
|
55
|
+
readonly resets: number;
|
|
56
|
+
readonly revoked: number;
|
|
57
|
+
readonly failed: number;
|
|
58
|
+
readonly deferredCommits: number;
|
|
59
|
+
}
|
|
60
|
+
export type DiagnosticLastRound = {
|
|
61
|
+
readonly status: 'succeeded';
|
|
62
|
+
readonly startedAtMs: number;
|
|
63
|
+
readonly completedAtMs: number;
|
|
64
|
+
readonly durationMs: number;
|
|
65
|
+
readonly counters: DiagnosticRoundCounters;
|
|
66
|
+
} | {
|
|
67
|
+
readonly status: 'failed';
|
|
68
|
+
readonly startedAtMs: number;
|
|
69
|
+
readonly completedAtMs: number;
|
|
70
|
+
readonly durationMs: number;
|
|
71
|
+
/** Stable code only; never arbitrary transport/server prose. */
|
|
72
|
+
readonly errorCode: string;
|
|
73
|
+
};
|
|
74
|
+
export interface DiagnosticLastChange {
|
|
75
|
+
/** Decimal u64 for JSON/IPC parity. */
|
|
76
|
+
readonly revision: string;
|
|
77
|
+
readonly recordedAtMs: number;
|
|
78
|
+
/** Generated table names only; no scope keys or row ids. */
|
|
79
|
+
readonly tables: readonly string[];
|
|
80
|
+
/** Generated table names for changed window registrations/completeness. */
|
|
81
|
+
readonly windows: readonly string[];
|
|
82
|
+
readonly domainsTruncated: boolean;
|
|
83
|
+
readonly statusChanged: boolean;
|
|
84
|
+
readonly conflictsChanged: boolean;
|
|
85
|
+
readonly rejectionsChanged: boolean;
|
|
86
|
+
readonly outcomesChanged: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface ClientDiagnosticsStorage {
|
|
89
|
+
readonly status: 'healthy' | 'pressure' | 'unreadable';
|
|
90
|
+
/** SQLite page estimate. No path, filename, or per-domain row counts. */
|
|
91
|
+
readonly databaseBytesApprox?: number;
|
|
92
|
+
readonly pendingOutboxBytesApprox?: number;
|
|
93
|
+
readonly retainedOutcomeBytesApprox?: number;
|
|
94
|
+
readonly retainedOutcomeEntries?: number;
|
|
95
|
+
readonly blobCacheBytesApprox?: number;
|
|
96
|
+
readonly pressureReasonCode?: 'client.blob_cache_over_limit';
|
|
97
|
+
}
|
|
98
|
+
export interface ClientDiagnosticsSnapshot {
|
|
99
|
+
readonly version: typeof CLIENT_DIAGNOSTICS_VERSION;
|
|
100
|
+
readonly capturedAtMs: number;
|
|
101
|
+
readonly host: ClientDiagnosticsHost;
|
|
102
|
+
readonly securityLifecycle: SecurityLifecycle;
|
|
103
|
+
readonly schema: {
|
|
104
|
+
readonly currentVersion: number;
|
|
105
|
+
readonly upgrading: boolean;
|
|
106
|
+
readonly requiredVersion?: number;
|
|
107
|
+
readonly latestVersion?: number;
|
|
108
|
+
};
|
|
109
|
+
readonly replica: {
|
|
110
|
+
/** Decimal u64 for JSON/IPC parity. */
|
|
111
|
+
readonly localRevision: string;
|
|
112
|
+
readonly syncNeeded: boolean;
|
|
113
|
+
readonly pendingOutbox: number;
|
|
114
|
+
};
|
|
115
|
+
readonly lease: {
|
|
116
|
+
readonly state: 'none' | 'active' | 'expired' | 'stopped';
|
|
117
|
+
readonly expiresAtMs?: number;
|
|
118
|
+
readonly errorCode?: string;
|
|
119
|
+
};
|
|
120
|
+
readonly subscriptions: readonly DiagnosticSubscription[];
|
|
121
|
+
readonly subscriptionsTruncated: boolean;
|
|
122
|
+
readonly lastRound?: DiagnosticLastRound;
|
|
123
|
+
readonly lastChange?: DiagnosticLastChange;
|
|
124
|
+
readonly storage: ClientDiagnosticsStorage;
|
|
125
|
+
}
|
|
126
|
+
export type ClientDiagnosticsListener = (snapshot: ClientDiagnosticsSnapshot) => void;
|
|
127
|
+
export declare class ClientDiagnosticsEmitter {
|
|
128
|
+
#private;
|
|
129
|
+
get observed(): boolean;
|
|
130
|
+
on(listener: ClientDiagnosticsListener): () => void;
|
|
131
|
+
emit(snapshot: ClientDiagnosticsSnapshot): void;
|
|
132
|
+
}
|
|
133
|
+
/** Host wrappers replace topology facts without changing core evidence. */
|
|
134
|
+
export declare function withClientDiagnosticsHost(snapshot: ClientDiagnosticsSnapshot, host: ClientDiagnosticsHost): ClientDiagnosticsSnapshot;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const CLIENT_DIAGNOSTICS_VERSION = 1;
|
|
2
|
+
export const MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS = 256;
|
|
3
|
+
export const MAX_DIAGNOSTIC_DOMAINS = 256;
|
|
4
|
+
export class ClientDiagnosticsEmitter {
|
|
5
|
+
#listeners = new Set();
|
|
6
|
+
get observed() {
|
|
7
|
+
return this.#listeners.size > 0;
|
|
8
|
+
}
|
|
9
|
+
on(listener) {
|
|
10
|
+
this.#listeners.add(listener);
|
|
11
|
+
return () => this.#listeners.delete(listener);
|
|
12
|
+
}
|
|
13
|
+
emit(snapshot) {
|
|
14
|
+
for (const listener of this.#listeners) {
|
|
15
|
+
try {
|
|
16
|
+
listener(snapshot);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Diagnostics observers cannot alter sync correctness.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Host wrappers replace topology facts without changing core evidence. */
|
|
25
|
+
export function withClientDiagnosticsHost(snapshot, host) {
|
|
26
|
+
return { ...snapshot, host };
|
|
27
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from './client.js';
|
|
|
15
15
|
export * from './content-type.js';
|
|
16
16
|
export * from './database.js';
|
|
17
17
|
export * from './devtools.js';
|
|
18
|
+
export * from './diagnostics.js';
|
|
18
19
|
export * from './encryption.js';
|
|
19
20
|
export * from './errors.js';
|
|
20
21
|
export * from './http.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * from './client.js';
|
|
|
15
15
|
export * from './content-type.js';
|
|
16
16
|
export * from './database.js';
|
|
17
17
|
export * from './devtools.js';
|
|
18
|
+
export * from './diagnostics.js';
|
|
18
19
|
export * from './encryption.js';
|
|
19
20
|
export * from './errors.js';
|
|
20
21
|
export * from './http.js';
|
package/dist/worker-entry.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* the core owns exactly one loop.
|
|
15
15
|
*/
|
|
16
16
|
import { SyncClient } from './client.js';
|
|
17
|
+
import { withClientDiagnosticsHost, } from './diagnostics.js';
|
|
17
18
|
import { encryptionConfigFromKeyring } from './encryption.js';
|
|
18
19
|
import { ClientSyncError } from './errors.js';
|
|
19
20
|
import { httpBlobTransport, httpSegmentDownloader, httpSyncTransport, webSocketRealtimeConnector, } from './http.js';
|
|
@@ -251,6 +252,23 @@ export function startSyncWorker(overrides = {}) {
|
|
|
251
252
|
if (!closed)
|
|
252
253
|
post({ t: 'event', event: { kind: 'change', batch } });
|
|
253
254
|
});
|
|
255
|
+
const postDiagnostics = (snapshot) => {
|
|
256
|
+
if (closed)
|
|
257
|
+
return;
|
|
258
|
+
post({
|
|
259
|
+
t: 'event',
|
|
260
|
+
event: {
|
|
261
|
+
kind: 'diagnostics',
|
|
262
|
+
snapshot: withClientDiagnosticsHost(snapshot, {
|
|
263
|
+
kind: 'worker',
|
|
264
|
+
role: 'leader',
|
|
265
|
+
connectivity: offline ? 'offline' : snapshot.host.connectivity,
|
|
266
|
+
realtime: snapshot.host.realtime,
|
|
267
|
+
}),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
started.onDiagnostics(postDiagnostics);
|
|
254
272
|
await started.start();
|
|
255
273
|
realtimeConnector =
|
|
256
274
|
overrides.createRealtime !== undefined
|
|
@@ -315,6 +333,15 @@ export function startSyncWorker(overrides = {}) {
|
|
|
315
333
|
querySnapshot: (spec) => requireClient().querySnapshot(spec),
|
|
316
334
|
localRevision: () => requireClient().localRevision,
|
|
317
335
|
statusSnapshot: () => requireClient().statusSnapshot(),
|
|
336
|
+
diagnosticsSnapshot: (request) => {
|
|
337
|
+
const snapshot = requireClient().diagnosticsSnapshot(request);
|
|
338
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
339
|
+
kind: 'worker',
|
|
340
|
+
role: 'leader',
|
|
341
|
+
connectivity: offline ? 'offline' : snapshot.host.connectivity,
|
|
342
|
+
realtime: snapshot.host.realtime,
|
|
343
|
+
});
|
|
344
|
+
},
|
|
318
345
|
conflicts: () => requireClient().conflicts,
|
|
319
346
|
rejections: () => requireClient().rejections,
|
|
320
347
|
commitOutcome: (clientCommitId) => requireClient().commitOutcome(clientCommitId),
|
|
@@ -337,6 +364,21 @@ export function startSyncWorker(overrides = {}) {
|
|
|
337
364
|
offline = value;
|
|
338
365
|
if (offline)
|
|
339
366
|
client?.disconnectRealtime();
|
|
367
|
+
else if (client !== undefined) {
|
|
368
|
+
const snapshot = client.diagnosticsSnapshot();
|
|
369
|
+
post({
|
|
370
|
+
t: 'event',
|
|
371
|
+
event: {
|
|
372
|
+
kind: 'diagnostics',
|
|
373
|
+
snapshot: withClientDiagnosticsHost(snapshot, {
|
|
374
|
+
kind: 'worker',
|
|
375
|
+
role: 'leader',
|
|
376
|
+
connectivity: snapshot.host.connectivity,
|
|
377
|
+
realtime: snapshot.host.realtime,
|
|
378
|
+
}),
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
340
382
|
},
|
|
341
383
|
close: async () => {
|
|
342
384
|
closed = true;
|
package/dist/worker-host.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { WakeReason } from '@syncular/core';
|
|
|
25
25
|
import type { BlobRef, CachedBlob } from './blob.js';
|
|
26
26
|
import type { ConflictRecord, LeaseState, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SchemaFloor, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
|
|
27
27
|
import type { SqlRow, SqlValue } from './database.js';
|
|
28
|
+
import { ClientDiagnosticsEmitter, type ClientDiagnosticsListener, type ClientDiagnosticsRequest, type ClientDiagnosticsSnapshot } from './diagnostics.js';
|
|
28
29
|
import type { EncryptionKeyringConfig } from './encryption.js';
|
|
29
30
|
import { ChangeEmitter, type ClientChangeListener, InvalidationEmitter, type InvalidationListener, type LocalRevision, type SyncStatusSnapshot } from './invalidation.js';
|
|
30
31
|
import { type LeaderLease, type LeaderLock } from './leader-lock.js';
|
|
@@ -107,6 +108,8 @@ export interface SyncClientHandleConfig {
|
|
|
107
108
|
readonly onUpgrading?: (upgrading: boolean) => void;
|
|
108
109
|
/** §8.6: presence on a scope key changed. */
|
|
109
110
|
readonly onPresence?: (scopeKey: string) => void;
|
|
111
|
+
/** Atomic privacy-safe diagnostics changed. */
|
|
112
|
+
readonly onDiagnostics?: (snapshot: ClientDiagnosticsSnapshot) => void;
|
|
110
113
|
}
|
|
111
114
|
/**
|
|
112
115
|
* A running worker core owned by THIS tab (the leader). Wraps the worker,
|
|
@@ -145,6 +148,7 @@ export declare class SyncClientHandle {
|
|
|
145
148
|
invalidation: InvalidationEmitter;
|
|
146
149
|
changes: ChangeEmitter;
|
|
147
150
|
presence: Set<(scopeKey: string) => void>;
|
|
151
|
+
diagnostics: ClientDiagnosticsEmitter;
|
|
148
152
|
roleListeners?: Set<(role: HandleRole) => void>;
|
|
149
153
|
leadershipListeners?: Set<(state: LeadershipState) => void>;
|
|
150
154
|
leadership?: LeadershipState;
|
|
@@ -163,6 +167,7 @@ export declare class SyncClientHandle {
|
|
|
163
167
|
*/
|
|
164
168
|
onInvalidate(listener: InvalidationListener): () => void;
|
|
165
169
|
onChange(listener: ClientChangeListener): () => void;
|
|
170
|
+
onDiagnostics(listener: ClientDiagnosticsListener): () => void;
|
|
166
171
|
/**
|
|
167
172
|
* §8.6: subscribe to presence changes — the identical surface as
|
|
168
173
|
* `SyncClient.onPresence`. Returns an unsubscribe function.
|
|
@@ -190,6 +195,7 @@ export declare class SyncClientHandle {
|
|
|
190
195
|
querySnapshot<Row = SqlRow>(spec: QueryReadSpec): Promise<QuerySnapshot<Row>>;
|
|
191
196
|
localRevision(): Promise<LocalRevision>;
|
|
192
197
|
statusSnapshot(): Promise<SyncStatusSnapshot>;
|
|
198
|
+
diagnosticsSnapshot(request?: ClientDiagnosticsRequest): Promise<ClientDiagnosticsSnapshot>;
|
|
193
199
|
conflicts(): Promise<readonly ConflictRecord[]>;
|
|
194
200
|
rejections(): Promise<readonly RejectionRecord[]>;
|
|
195
201
|
commitOutcome(clientCommitId: string): Promise<CommitOutcome | undefined>;
|
package/dist/worker-host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerDevtools } from './devtools.js';
|
|
2
|
+
import { ClientDiagnosticsEmitter, withClientDiagnosticsHost, } from './diagnostics.js';
|
|
2
3
|
import { ClientSyncError } from './errors.js';
|
|
3
4
|
import { ChangeEmitter, InvalidationEmitter, invalidationFromChange, } from './invalidation.js';
|
|
4
5
|
import { singleOwnerLock, webLocksLeaderLock, } from './leader-lock.js';
|
|
@@ -60,6 +61,7 @@ export class SyncClientHandle {
|
|
|
60
61
|
#invalidation;
|
|
61
62
|
#changes;
|
|
62
63
|
#presence;
|
|
64
|
+
#diagnostics;
|
|
63
65
|
#roleListeners;
|
|
64
66
|
#leadershipListeners;
|
|
65
67
|
#devtoolsUnregister;
|
|
@@ -84,6 +86,7 @@ export class SyncClientHandle {
|
|
|
84
86
|
this.#invalidation = internals.invalidation;
|
|
85
87
|
this.#changes = internals.changes;
|
|
86
88
|
this.#presence = internals.presence;
|
|
89
|
+
this.#diagnostics = internals.diagnostics;
|
|
87
90
|
this.#roleListeners = internals.roleListeners ?? new Set();
|
|
88
91
|
this.#leadershipListeners = internals.leadershipListeners ?? new Set();
|
|
89
92
|
// RFC 0002 §3.2: console introspection — a no-op outside a dev page.
|
|
@@ -150,6 +153,13 @@ export class SyncClientHandle {
|
|
|
150
153
|
if (legacy !== undefined)
|
|
151
154
|
this.#invalidation.emit(legacy);
|
|
152
155
|
}
|
|
156
|
+
else if (event.kind === 'diagnostics') {
|
|
157
|
+
this.#diagnostics.emit(withClientDiagnosticsHost(event.snapshot, {
|
|
158
|
+
...event.snapshot.host,
|
|
159
|
+
kind: 'worker',
|
|
160
|
+
role: this.#role,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
153
163
|
}
|
|
154
164
|
/**
|
|
155
165
|
* TODO 3.1 / I1: subscribe to fine-grained invalidation — the identical
|
|
@@ -163,6 +173,9 @@ export class SyncClientHandle {
|
|
|
163
173
|
onChange(listener) {
|
|
164
174
|
return this.#changes.on(listener);
|
|
165
175
|
}
|
|
176
|
+
onDiagnostics(listener) {
|
|
177
|
+
return this.#diagnostics.on(listener);
|
|
178
|
+
}
|
|
166
179
|
/**
|
|
167
180
|
* §8.6: subscribe to presence changes — the identical surface as
|
|
168
181
|
* `SyncClient.onPresence`. Returns an unsubscribe function.
|
|
@@ -252,6 +265,14 @@ export class SyncClientHandle {
|
|
|
252
265
|
statusSnapshot() {
|
|
253
266
|
return this.#call('statusSnapshot', []);
|
|
254
267
|
}
|
|
268
|
+
async diagnosticsSnapshot(request = {}) {
|
|
269
|
+
const snapshot = await this.#call('diagnosticsSnapshot', [request]);
|
|
270
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
271
|
+
...snapshot.host,
|
|
272
|
+
kind: 'worker',
|
|
273
|
+
role: this.#role,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
255
276
|
conflicts() {
|
|
256
277
|
return this.#call('conflicts', []);
|
|
257
278
|
}
|
|
@@ -470,6 +491,9 @@ function fireConfigCallbacks(config, event) {
|
|
|
470
491
|
...(event.error !== undefined ? { error: event.error } : {}),
|
|
471
492
|
});
|
|
472
493
|
}
|
|
494
|
+
else if (event.kind === 'diagnostics') {
|
|
495
|
+
config.onDiagnostics?.(event.snapshot);
|
|
496
|
+
}
|
|
473
497
|
}
|
|
474
498
|
/**
|
|
475
499
|
* Acquire leadership, spawn the worker, initialize the core inside it.
|
|
@@ -485,6 +509,7 @@ export async function createSyncClientHandle(config) {
|
|
|
485
509
|
const invalidation = new InvalidationEmitter();
|
|
486
510
|
const changes = new ChangeEmitter();
|
|
487
511
|
const presence = new Set();
|
|
512
|
+
const diagnostics = new ClientDiagnosticsEmitter();
|
|
488
513
|
const roleListeners = new Set();
|
|
489
514
|
if (resolvedConfig.onRoleChange !== undefined)
|
|
490
515
|
roleListeners.add(resolvedConfig.onRoleChange);
|
|
@@ -507,6 +532,7 @@ export async function createSyncClientHandle(config) {
|
|
|
507
532
|
invalidation,
|
|
508
533
|
changes,
|
|
509
534
|
presence,
|
|
535
|
+
diagnostics,
|
|
510
536
|
roleListeners,
|
|
511
537
|
leadershipListeners,
|
|
512
538
|
});
|
|
@@ -521,6 +547,7 @@ export async function createSyncClientHandle(config) {
|
|
|
521
547
|
invalidation,
|
|
522
548
|
changes,
|
|
523
549
|
presence,
|
|
550
|
+
diagnostics,
|
|
524
551
|
roleListeners,
|
|
525
552
|
leadershipListeners,
|
|
526
553
|
});
|
|
@@ -530,6 +557,7 @@ export async function createSyncClientHandle(config) {
|
|
|
530
557
|
invalidation,
|
|
531
558
|
changes,
|
|
532
559
|
presence,
|
|
560
|
+
diagnostics,
|
|
533
561
|
roleListeners,
|
|
534
562
|
leadershipListeners,
|
|
535
563
|
});
|
|
@@ -571,6 +599,7 @@ async function bootLeader(config, lockName, lease, parts) {
|
|
|
571
599
|
invalidation: parts.invalidation,
|
|
572
600
|
changes: parts.changes,
|
|
573
601
|
presence: parts.presence,
|
|
602
|
+
diagnostics: parts.diagnostics,
|
|
574
603
|
roleListeners: parts.roleListeners,
|
|
575
604
|
leadershipListeners: parts.leadershipListeners,
|
|
576
605
|
});
|
|
@@ -659,6 +688,7 @@ async function bootFollower(config, lockName, lock, parts) {
|
|
|
659
688
|
invalidation: parts.invalidation,
|
|
660
689
|
changes: parts.changes,
|
|
661
690
|
presence: parts.presence,
|
|
691
|
+
diagnostics: parts.diagnostics,
|
|
662
692
|
roleListeners: parts.roleListeners,
|
|
663
693
|
leadershipListeners: parts.leadershipListeners,
|
|
664
694
|
});
|
|
@@ -20,6 +20,7 @@ import type { WakeReason } from '@syncular/core';
|
|
|
20
20
|
import type { BlobRef, CachedBlob } from './blob.js';
|
|
21
21
|
import type { ConflictRecord, LeaseState, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SchemaFloor, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
|
|
22
22
|
import type { SqlRow, SqlValue } from './database.js';
|
|
23
|
+
import type { ClientDiagnosticsRequest, ClientDiagnosticsSnapshot } from './diagnostics.js';
|
|
23
24
|
import type { EncryptionKeyringConfig } from './encryption.js';
|
|
24
25
|
import type { ClientChangeBatch, LocalRevision, SyncStatusSnapshot } from './invalidation.js';
|
|
25
26
|
import type { LocalDataPurgeInput, LocalDataPurgeResult } from './local-purge.js';
|
|
@@ -107,6 +108,7 @@ export interface WorkerApi {
|
|
|
107
108
|
querySnapshot(spec: QueryReadSpec): QuerySnapshot;
|
|
108
109
|
localRevision(): LocalRevision;
|
|
109
110
|
statusSnapshot(): SyncStatusSnapshot;
|
|
111
|
+
diagnosticsSnapshot(request?: ClientDiagnosticsRequest): ClientDiagnosticsSnapshot;
|
|
110
112
|
conflicts(): readonly ConflictRecord[];
|
|
111
113
|
rejections(): readonly RejectionRecord[];
|
|
112
114
|
commitOutcome(clientCommitId: string): CommitOutcome | undefined;
|
|
@@ -180,6 +182,10 @@ export type SyncWorkerEvent = {
|
|
|
180
182
|
/** Exact revisioned core transaction; Sets and bigint clone directly. */
|
|
181
183
|
readonly kind: 'change';
|
|
182
184
|
readonly batch: ClientChangeBatch;
|
|
185
|
+
} | {
|
|
186
|
+
/** Atomic privacy-safe health/support evidence from the worker core. */
|
|
187
|
+
readonly kind: 'diagnostics';
|
|
188
|
+
readonly snapshot: ClientDiagnosticsSnapshot;
|
|
183
189
|
};
|
|
184
190
|
export type WorkerToMainMessage = {
|
|
185
191
|
readonly t: 'ready';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.18",
|
|
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.
|
|
84
|
+
"@syncular/core": "0.15.18"
|
|
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.
|
|
95
|
+
"@syncular/server": "0.15.18",
|
|
96
96
|
"@types/better-sqlite3": "^7.6.13",
|
|
97
97
|
"better-sqlite3": "^12.11.1"
|
|
98
98
|
}
|