@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,192 @@
|
|
|
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';
|
|
9
|
+
|
|
10
|
+
export const CLIENT_DIAGNOSTICS_VERSION = 1 as const;
|
|
11
|
+
export const MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS = 256;
|
|
12
|
+
export const MAX_DIAGNOSTIC_DOMAINS = 256;
|
|
13
|
+
|
|
14
|
+
export type ClientDiagnosticsHostKind =
|
|
15
|
+
| 'direct'
|
|
16
|
+
| 'worker'
|
|
17
|
+
| 'tauri'
|
|
18
|
+
| 'react-native';
|
|
19
|
+
export type ClientDiagnosticsHostRole =
|
|
20
|
+
| 'single'
|
|
21
|
+
| 'leader'
|
|
22
|
+
| 'follower'
|
|
23
|
+
| 'unknown';
|
|
24
|
+
export type ClientDiagnosticsConnectivity = 'online' | 'offline' | 'unknown';
|
|
25
|
+
export type ClientDiagnosticsRealtime =
|
|
26
|
+
| 'connected'
|
|
27
|
+
| 'disconnected'
|
|
28
|
+
| 'unsupported'
|
|
29
|
+
| 'unknown';
|
|
30
|
+
|
|
31
|
+
export interface ClientDiagnosticsHost {
|
|
32
|
+
readonly kind: ClientDiagnosticsHostKind;
|
|
33
|
+
readonly role: ClientDiagnosticsHostRole;
|
|
34
|
+
readonly connectivity: ClientDiagnosticsConnectivity;
|
|
35
|
+
readonly realtime: ClientDiagnosticsRealtime;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ExpectedDiagnosticSubscription {
|
|
39
|
+
/** Application-owned stable identifier. It must never contain PHI. */
|
|
40
|
+
readonly id: string;
|
|
41
|
+
/** Generated schema table name. */
|
|
42
|
+
readonly table: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ClientDiagnosticsRequest {
|
|
46
|
+
/**
|
|
47
|
+
* Optional bounded intent list. Missing registrations are returned as
|
|
48
|
+
* `unregistered`, allowing a zero-row security scope to fail closed without
|
|
49
|
+
* reading private Syncular tables. Values/scopes are intentionally absent.
|
|
50
|
+
*/
|
|
51
|
+
readonly expectedSubscriptions?: readonly ExpectedDiagnosticSubscription[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type DiagnosticSubscriptionState =
|
|
55
|
+
| 'unregistered'
|
|
56
|
+
| 'bootstrapping'
|
|
57
|
+
| 'complete'
|
|
58
|
+
| 'reset'
|
|
59
|
+
| 'revoked'
|
|
60
|
+
| 'failed';
|
|
61
|
+
|
|
62
|
+
export interface DiagnosticSubscription {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly table: string;
|
|
65
|
+
readonly state: DiagnosticSubscriptionState;
|
|
66
|
+
readonly complete: boolean;
|
|
67
|
+
/** Last fully applied local commit sequence; absent when unregistered. */
|
|
68
|
+
readonly cursor?: number;
|
|
69
|
+
readonly reasonCode?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DiagnosticRoundCounters {
|
|
73
|
+
readonly pushed: number;
|
|
74
|
+
readonly applied: number;
|
|
75
|
+
readonly rejected: number;
|
|
76
|
+
readonly retryable: number;
|
|
77
|
+
readonly conflicts: number;
|
|
78
|
+
readonly commitsApplied: number;
|
|
79
|
+
readonly segmentRowsApplied: number;
|
|
80
|
+
readonly bootstrapping: number;
|
|
81
|
+
readonly resets: number;
|
|
82
|
+
readonly revoked: number;
|
|
83
|
+
readonly failed: number;
|
|
84
|
+
readonly deferredCommits: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type DiagnosticLastRound =
|
|
88
|
+
| {
|
|
89
|
+
readonly status: 'succeeded';
|
|
90
|
+
readonly startedAtMs: number;
|
|
91
|
+
readonly completedAtMs: number;
|
|
92
|
+
readonly durationMs: number;
|
|
93
|
+
readonly counters: DiagnosticRoundCounters;
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
readonly status: 'failed';
|
|
97
|
+
readonly startedAtMs: number;
|
|
98
|
+
readonly completedAtMs: number;
|
|
99
|
+
readonly durationMs: number;
|
|
100
|
+
/** Stable code only; never arbitrary transport/server prose. */
|
|
101
|
+
readonly errorCode: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface DiagnosticLastChange {
|
|
105
|
+
/** Decimal u64 for JSON/IPC parity. */
|
|
106
|
+
readonly revision: string;
|
|
107
|
+
readonly recordedAtMs: number;
|
|
108
|
+
/** Generated table names only; no scope keys or row ids. */
|
|
109
|
+
readonly tables: readonly string[];
|
|
110
|
+
/** Generated table names for changed window registrations/completeness. */
|
|
111
|
+
readonly windows: readonly string[];
|
|
112
|
+
readonly domainsTruncated: boolean;
|
|
113
|
+
readonly statusChanged: boolean;
|
|
114
|
+
readonly conflictsChanged: boolean;
|
|
115
|
+
readonly rejectionsChanged: boolean;
|
|
116
|
+
readonly outcomesChanged: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ClientDiagnosticsStorage {
|
|
120
|
+
readonly status: 'healthy' | 'pressure' | 'unreadable';
|
|
121
|
+
/** SQLite page estimate. No path, filename, or per-domain row counts. */
|
|
122
|
+
readonly databaseBytesApprox?: number;
|
|
123
|
+
readonly pendingOutboxBytesApprox?: number;
|
|
124
|
+
readonly retainedOutcomeBytesApprox?: number;
|
|
125
|
+
readonly retainedOutcomeEntries?: number;
|
|
126
|
+
readonly blobCacheBytesApprox?: number;
|
|
127
|
+
readonly pressureReasonCode?: 'client.blob_cache_over_limit';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ClientDiagnosticsSnapshot {
|
|
131
|
+
readonly version: typeof CLIENT_DIAGNOSTICS_VERSION;
|
|
132
|
+
readonly capturedAtMs: number;
|
|
133
|
+
readonly host: ClientDiagnosticsHost;
|
|
134
|
+
readonly securityLifecycle: SecurityLifecycle;
|
|
135
|
+
readonly schema: {
|
|
136
|
+
readonly currentVersion: number;
|
|
137
|
+
readonly upgrading: boolean;
|
|
138
|
+
readonly requiredVersion?: number;
|
|
139
|
+
readonly latestVersion?: number;
|
|
140
|
+
};
|
|
141
|
+
readonly replica: {
|
|
142
|
+
/** Decimal u64 for JSON/IPC parity. */
|
|
143
|
+
readonly localRevision: string;
|
|
144
|
+
readonly syncNeeded: boolean;
|
|
145
|
+
readonly pendingOutbox: number;
|
|
146
|
+
};
|
|
147
|
+
readonly lease: {
|
|
148
|
+
readonly state: 'none' | 'active' | 'expired' | 'stopped';
|
|
149
|
+
readonly expiresAtMs?: number;
|
|
150
|
+
readonly errorCode?: string;
|
|
151
|
+
};
|
|
152
|
+
readonly subscriptions: readonly DiagnosticSubscription[];
|
|
153
|
+
readonly subscriptionsTruncated: boolean;
|
|
154
|
+
readonly lastRound?: DiagnosticLastRound;
|
|
155
|
+
readonly lastChange?: DiagnosticLastChange;
|
|
156
|
+
readonly storage: ClientDiagnosticsStorage;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type ClientDiagnosticsListener = (
|
|
160
|
+
snapshot: ClientDiagnosticsSnapshot,
|
|
161
|
+
) => void;
|
|
162
|
+
|
|
163
|
+
export class ClientDiagnosticsEmitter {
|
|
164
|
+
readonly #listeners = new Set<ClientDiagnosticsListener>();
|
|
165
|
+
|
|
166
|
+
get observed(): boolean {
|
|
167
|
+
return this.#listeners.size > 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
on(listener: ClientDiagnosticsListener): () => void {
|
|
171
|
+
this.#listeners.add(listener);
|
|
172
|
+
return () => this.#listeners.delete(listener);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
emit(snapshot: ClientDiagnosticsSnapshot): void {
|
|
176
|
+
for (const listener of this.#listeners) {
|
|
177
|
+
try {
|
|
178
|
+
listener(snapshot);
|
|
179
|
+
} catch {
|
|
180
|
+
// Diagnostics observers cannot alter sync correctness.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Host wrappers replace topology facts without changing core evidence. */
|
|
187
|
+
export function withClientDiagnosticsHost(
|
|
188
|
+
snapshot: ClientDiagnosticsSnapshot,
|
|
189
|
+
host: ClientDiagnosticsHost,
|
|
190
|
+
): ClientDiagnosticsSnapshot {
|
|
191
|
+
return { ...snapshot, host };
|
|
192
|
+
}
|
package/src/index.ts
CHANGED
package/src/worker-entry.ts
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { SyncClient } from './client';
|
|
17
17
|
import type { ClientDatabase } from './database';
|
|
18
|
+
import {
|
|
19
|
+
type ClientDiagnosticsSnapshot,
|
|
20
|
+
withClientDiagnosticsHost,
|
|
21
|
+
} from './diagnostics';
|
|
18
22
|
import { encryptionConfigFromKeyring } from './encryption';
|
|
19
23
|
import { ClientSyncError } from './errors';
|
|
20
24
|
import {
|
|
@@ -338,6 +342,22 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
|
338
342
|
started.onChange((batch) => {
|
|
339
343
|
if (!closed) post({ t: 'event', event: { kind: 'change', batch } });
|
|
340
344
|
});
|
|
345
|
+
const postDiagnostics = (snapshot: ClientDiagnosticsSnapshot): void => {
|
|
346
|
+
if (closed) return;
|
|
347
|
+
post({
|
|
348
|
+
t: 'event',
|
|
349
|
+
event: {
|
|
350
|
+
kind: 'diagnostics',
|
|
351
|
+
snapshot: withClientDiagnosticsHost(snapshot, {
|
|
352
|
+
kind: 'worker',
|
|
353
|
+
role: 'leader',
|
|
354
|
+
connectivity: offline ? 'offline' : snapshot.host.connectivity,
|
|
355
|
+
realtime: snapshot.host.realtime,
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
started.onDiagnostics(postDiagnostics);
|
|
341
361
|
await started.start();
|
|
342
362
|
realtimeConnector =
|
|
343
363
|
overrides.createRealtime !== undefined
|
|
@@ -411,6 +431,15 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
|
411
431
|
querySnapshot: (spec) => requireClient().querySnapshot(spec),
|
|
412
432
|
localRevision: () => requireClient().localRevision,
|
|
413
433
|
statusSnapshot: () => requireClient().statusSnapshot(),
|
|
434
|
+
diagnosticsSnapshot: (request) => {
|
|
435
|
+
const snapshot = requireClient().diagnosticsSnapshot(request);
|
|
436
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
437
|
+
kind: 'worker',
|
|
438
|
+
role: 'leader',
|
|
439
|
+
connectivity: offline ? 'offline' : snapshot.host.connectivity,
|
|
440
|
+
realtime: snapshot.host.realtime,
|
|
441
|
+
});
|
|
442
|
+
},
|
|
414
443
|
conflicts: () => requireClient().conflicts,
|
|
415
444
|
rejections: () => requireClient().rejections,
|
|
416
445
|
commitOutcome: (clientCommitId) =>
|
|
@@ -434,6 +463,21 @@ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
|
|
|
434
463
|
setOffline: (value) => {
|
|
435
464
|
offline = value;
|
|
436
465
|
if (offline) client?.disconnectRealtime();
|
|
466
|
+
else if (client !== undefined) {
|
|
467
|
+
const snapshot = client.diagnosticsSnapshot();
|
|
468
|
+
post({
|
|
469
|
+
t: 'event',
|
|
470
|
+
event: {
|
|
471
|
+
kind: 'diagnostics',
|
|
472
|
+
snapshot: withClientDiagnosticsHost(snapshot, {
|
|
473
|
+
kind: 'worker',
|
|
474
|
+
role: 'leader',
|
|
475
|
+
connectivity: snapshot.host.connectivity,
|
|
476
|
+
realtime: snapshot.host.realtime,
|
|
477
|
+
}),
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
}
|
|
437
481
|
},
|
|
438
482
|
close: async () => {
|
|
439
483
|
closed = true;
|
package/src/worker-host.ts
CHANGED
|
@@ -40,6 +40,13 @@ import type {
|
|
|
40
40
|
} from './client';
|
|
41
41
|
import type { SqlRow, SqlValue } from './database';
|
|
42
42
|
import { registerDevtools } from './devtools';
|
|
43
|
+
import {
|
|
44
|
+
ClientDiagnosticsEmitter,
|
|
45
|
+
type ClientDiagnosticsListener,
|
|
46
|
+
type ClientDiagnosticsRequest,
|
|
47
|
+
type ClientDiagnosticsSnapshot,
|
|
48
|
+
withClientDiagnosticsHost,
|
|
49
|
+
} from './diagnostics';
|
|
43
50
|
import type { EncryptionKeyringConfig } from './encryption';
|
|
44
51
|
import { ClientSyncError } from './errors';
|
|
45
52
|
import {
|
|
@@ -180,6 +187,8 @@ export interface SyncClientHandleConfig {
|
|
|
180
187
|
readonly onUpgrading?: (upgrading: boolean) => void;
|
|
181
188
|
/** §8.6: presence on a scope key changed. */
|
|
182
189
|
readonly onPresence?: (scopeKey: string) => void;
|
|
190
|
+
/** Atomic privacy-safe diagnostics changed. */
|
|
191
|
+
readonly onDiagnostics?: (snapshot: ClientDiagnosticsSnapshot) => void;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
interface Pending {
|
|
@@ -247,6 +256,7 @@ export class SyncClientHandle {
|
|
|
247
256
|
readonly #invalidation: InvalidationEmitter;
|
|
248
257
|
readonly #changes: ChangeEmitter;
|
|
249
258
|
readonly #presence: Set<(scopeKey: string) => void>;
|
|
259
|
+
readonly #diagnostics: ClientDiagnosticsEmitter;
|
|
250
260
|
readonly #roleListeners: Set<(role: HandleRole) => void>;
|
|
251
261
|
readonly #leadershipListeners: Set<(state: LeadershipState) => void>;
|
|
252
262
|
readonly #devtoolsUnregister: () => void;
|
|
@@ -262,6 +272,7 @@ export class SyncClientHandle {
|
|
|
262
272
|
invalidation: InvalidationEmitter;
|
|
263
273
|
changes: ChangeEmitter;
|
|
264
274
|
presence: Set<(scopeKey: string) => void>;
|
|
275
|
+
diagnostics: ClientDiagnosticsEmitter;
|
|
265
276
|
roleListeners?: Set<(role: HandleRole) => void>;
|
|
266
277
|
leadershipListeners?: Set<(state: LeadershipState) => void>;
|
|
267
278
|
leadership?: LeadershipState;
|
|
@@ -284,6 +295,7 @@ export class SyncClientHandle {
|
|
|
284
295
|
this.#invalidation = internals.invalidation;
|
|
285
296
|
this.#changes = internals.changes;
|
|
286
297
|
this.#presence = internals.presence;
|
|
298
|
+
this.#diagnostics = internals.diagnostics;
|
|
287
299
|
this.#roleListeners = internals.roleListeners ?? new Set();
|
|
288
300
|
this.#leadershipListeners = internals.leadershipListeners ?? new Set();
|
|
289
301
|
// RFC 0002 §3.2: console introspection — a no-op outside a dev page.
|
|
@@ -346,6 +358,14 @@ export class SyncClientHandle {
|
|
|
346
358
|
this.#changes.emit(event.batch);
|
|
347
359
|
const legacy = invalidationFromChange(event.batch);
|
|
348
360
|
if (legacy !== undefined) this.#invalidation.emit(legacy);
|
|
361
|
+
} else if (event.kind === 'diagnostics') {
|
|
362
|
+
this.#diagnostics.emit(
|
|
363
|
+
withClientDiagnosticsHost(event.snapshot, {
|
|
364
|
+
...event.snapshot.host,
|
|
365
|
+
kind: 'worker',
|
|
366
|
+
role: this.#role,
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
349
369
|
}
|
|
350
370
|
}
|
|
351
371
|
|
|
@@ -363,6 +383,10 @@ export class SyncClientHandle {
|
|
|
363
383
|
return this.#changes.on(listener);
|
|
364
384
|
}
|
|
365
385
|
|
|
386
|
+
onDiagnostics(listener: ClientDiagnosticsListener): () => void {
|
|
387
|
+
return this.#diagnostics.on(listener);
|
|
388
|
+
}
|
|
389
|
+
|
|
366
390
|
/**
|
|
367
391
|
* §8.6: subscribe to presence changes — the identical surface as
|
|
368
392
|
* `SyncClient.onPresence`. Returns an unsubscribe function.
|
|
@@ -495,6 +519,17 @@ export class SyncClientHandle {
|
|
|
495
519
|
return this.#call('statusSnapshot', []);
|
|
496
520
|
}
|
|
497
521
|
|
|
522
|
+
async diagnosticsSnapshot(
|
|
523
|
+
request: ClientDiagnosticsRequest = {},
|
|
524
|
+
): Promise<ClientDiagnosticsSnapshot> {
|
|
525
|
+
const snapshot = await this.#call('diagnosticsSnapshot', [request]);
|
|
526
|
+
return withClientDiagnosticsHost(snapshot, {
|
|
527
|
+
...snapshot.host,
|
|
528
|
+
kind: 'worker',
|
|
529
|
+
role: this.#role,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
498
533
|
conflicts(): Promise<readonly ConflictRecord[]> {
|
|
499
534
|
return this.#call('conflicts', []);
|
|
500
535
|
}
|
|
@@ -767,6 +802,8 @@ function fireConfigCallbacks(
|
|
|
767
802
|
...(event.summary !== undefined ? { summary: event.summary } : {}),
|
|
768
803
|
...(event.error !== undefined ? { error: event.error } : {}),
|
|
769
804
|
});
|
|
805
|
+
} else if (event.kind === 'diagnostics') {
|
|
806
|
+
config.onDiagnostics?.(event.snapshot);
|
|
770
807
|
}
|
|
771
808
|
}
|
|
772
809
|
|
|
@@ -786,6 +823,7 @@ export async function createSyncClientHandle(
|
|
|
786
823
|
const invalidation = new InvalidationEmitter();
|
|
787
824
|
const changes = new ChangeEmitter();
|
|
788
825
|
const presence = new Set<(scopeKey: string) => void>();
|
|
826
|
+
const diagnostics = new ClientDiagnosticsEmitter();
|
|
789
827
|
const roleListeners = new Set<(role: HandleRole) => void>();
|
|
790
828
|
if (resolvedConfig.onRoleChange !== undefined)
|
|
791
829
|
roleListeners.add(resolvedConfig.onRoleChange);
|
|
@@ -811,6 +849,7 @@ export async function createSyncClientHandle(
|
|
|
811
849
|
invalidation,
|
|
812
850
|
changes,
|
|
813
851
|
presence,
|
|
852
|
+
diagnostics,
|
|
814
853
|
roleListeners,
|
|
815
854
|
leadershipListeners,
|
|
816
855
|
});
|
|
@@ -826,6 +865,7 @@ export async function createSyncClientHandle(
|
|
|
826
865
|
invalidation,
|
|
827
866
|
changes,
|
|
828
867
|
presence,
|
|
868
|
+
diagnostics,
|
|
829
869
|
roleListeners,
|
|
830
870
|
leadershipListeners,
|
|
831
871
|
});
|
|
@@ -836,6 +876,7 @@ export async function createSyncClientHandle(
|
|
|
836
876
|
invalidation,
|
|
837
877
|
changes,
|
|
838
878
|
presence,
|
|
879
|
+
diagnostics,
|
|
839
880
|
roleListeners,
|
|
840
881
|
leadershipListeners,
|
|
841
882
|
});
|
|
@@ -846,6 +887,7 @@ interface HandleParts {
|
|
|
846
887
|
invalidation: InvalidationEmitter;
|
|
847
888
|
changes: ChangeEmitter;
|
|
848
889
|
presence: Set<(scopeKey: string) => void>;
|
|
890
|
+
diagnostics: ClientDiagnosticsEmitter;
|
|
849
891
|
roleListeners: Set<(role: HandleRole) => void>;
|
|
850
892
|
leadershipListeners: Set<(state: LeadershipState) => void>;
|
|
851
893
|
}
|
|
@@ -904,6 +946,7 @@ async function bootLeader(
|
|
|
904
946
|
invalidation: parts.invalidation,
|
|
905
947
|
changes: parts.changes,
|
|
906
948
|
presence: parts.presence,
|
|
949
|
+
diagnostics: parts.diagnostics,
|
|
907
950
|
roleListeners: parts.roleListeners,
|
|
908
951
|
leadershipListeners: parts.leadershipListeners,
|
|
909
952
|
});
|
|
@@ -1009,6 +1052,7 @@ async function bootFollower(
|
|
|
1009
1052
|
invalidation: parts.invalidation,
|
|
1010
1053
|
changes: parts.changes,
|
|
1011
1054
|
presence: parts.presence,
|
|
1055
|
+
diagnostics: parts.diagnostics,
|
|
1012
1056
|
roleListeners: parts.roleListeners,
|
|
1013
1057
|
leadershipListeners: parts.leadershipListeners,
|
|
1014
1058
|
});
|
package/src/worker-protocol.ts
CHANGED
|
@@ -34,6 +34,10 @@ import type {
|
|
|
34
34
|
WindowState,
|
|
35
35
|
} from './client';
|
|
36
36
|
import type { SqlRow, SqlValue } from './database';
|
|
37
|
+
import type {
|
|
38
|
+
ClientDiagnosticsRequest,
|
|
39
|
+
ClientDiagnosticsSnapshot,
|
|
40
|
+
} from './diagnostics';
|
|
37
41
|
import type { EncryptionKeyringConfig } from './encryption';
|
|
38
42
|
import type {
|
|
39
43
|
ClientChangeBatch,
|
|
@@ -153,6 +157,9 @@ export interface WorkerApi {
|
|
|
153
157
|
querySnapshot(spec: QueryReadSpec): QuerySnapshot;
|
|
154
158
|
localRevision(): LocalRevision;
|
|
155
159
|
statusSnapshot(): SyncStatusSnapshot;
|
|
160
|
+
diagnosticsSnapshot(
|
|
161
|
+
request?: ClientDiagnosticsRequest,
|
|
162
|
+
): ClientDiagnosticsSnapshot;
|
|
156
163
|
conflicts(): readonly ConflictRecord[];
|
|
157
164
|
rejections(): readonly RejectionRecord[];
|
|
158
165
|
commitOutcome(clientCommitId: string): CommitOutcome | undefined;
|
|
@@ -240,6 +247,11 @@ export type SyncWorkerEvent =
|
|
|
240
247
|
/** Exact revisioned core transaction; Sets and bigint clone directly. */
|
|
241
248
|
readonly kind: 'change';
|
|
242
249
|
readonly batch: ClientChangeBatch;
|
|
250
|
+
}
|
|
251
|
+
| {
|
|
252
|
+
/** Atomic privacy-safe health/support evidence from the worker core. */
|
|
253
|
+
readonly kind: 'diagnostics';
|
|
254
|
+
readonly snapshot: ClientDiagnosticsSnapshot;
|
|
243
255
|
};
|
|
244
256
|
|
|
245
257
|
export type WorkerToMainMessage =
|