@syncular/testkit 0.1.2 → 0.2.0
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 +210 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.js +145 -0
- package/dist/clock.d.ts +24 -0
- package/dist/clock.js +32 -0
- package/dist/create-test-sync.d.ts +67 -0
- package/dist/create-test-sync.js +90 -0
- package/dist/faults.d.ts +52 -50
- package/dist/faults.js +74 -192
- package/dist/index.d.ts +17 -15
- package/dist/index.js +18 -15
- package/dist/react.d.ts +10 -0
- package/dist/react.js +41 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +68 -0
- package/package.json +52 -29
- package/src/client.ts +218 -0
- package/src/clock.ts +45 -0
- package/src/create-test-sync.ts +158 -0
- package/src/faults.ts +77 -338
- package/src/index.ts +44 -14
- package/src/react.ts +46 -0
- package/src/server.ts +102 -0
- package/dist/audit.d.ts +0 -22
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js +0 -40
- package/dist/audit.js.map +0 -1
- package/dist/client-bridge.d.ts +0 -77
- package/dist/client-bridge.d.ts.map +0 -1
- package/dist/client-bridge.js +0 -540
- package/dist/client-bridge.js.map +0 -1
- package/dist/deterministic.d.ts +0 -31
- package/dist/deterministic.d.ts.map +0 -1
- package/dist/deterministic.js +0 -69
- package/dist/deterministic.js.map +0 -1
- package/dist/disposable.d.ts +0 -10
- package/dist/disposable.d.ts.map +0 -1
- package/dist/disposable.js +0 -28
- package/dist/disposable.js.map +0 -1
- package/dist/faults.d.ts.map +0 -1
- package/dist/faults.js.map +0 -1
- package/dist/hono-node-server.d.ts +0 -20
- package/dist/hono-node-server.d.ts.map +0 -1
- package/dist/hono-node-server.js +0 -90
- package/dist/hono-node-server.js.map +0 -1
- package/dist/http-fixtures.d.ts +0 -32
- package/dist/http-fixtures.d.ts.map +0 -1
- package/dist/http-fixtures.js +0 -117
- package/dist/http-fixtures.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/project-scoped-tasks.d.ts +0 -40
- package/dist/project-scoped-tasks.d.ts.map +0 -1
- package/dist/project-scoped-tasks.js +0 -247
- package/dist/project-scoped-tasks.js.map +0 -1
- package/dist/realtime-ws.d.ts +0 -48
- package/dist/realtime-ws.d.ts.map +0 -1
- package/dist/realtime-ws.js +0 -203
- package/dist/realtime-ws.js.map +0 -1
- package/dist/runtime-process.d.ts +0 -14
- package/dist/runtime-process.d.ts.map +0 -1
- package/dist/runtime-process.js +0 -99
- package/dist/runtime-process.js.map +0 -1
- package/dist/sync-builders.d.ts +0 -48
- package/dist/sync-builders.d.ts.map +0 -1
- package/dist/sync-builders.js +0 -66
- package/dist/sync-builders.js.map +0 -1
- package/dist/sync-http.d.ts +0 -48
- package/dist/sync-http.d.ts.map +0 -1
- package/dist/sync-http.js +0 -43
- package/dist/sync-http.js.map +0 -1
- package/dist/sync-parse.d.ts +0 -8
- package/dist/sync-parse.d.ts.map +0 -1
- package/dist/sync-parse.js +0 -38
- package/dist/sync-parse.js.map +0 -1
- package/dist/sync-response.d.ts +0 -6
- package/dist/sync-response.d.ts.map +0 -1
- package/dist/sync-response.js +0 -19
- package/dist/sync-response.js.map +0 -1
- package/src/audit.ts +0 -74
- package/src/client-bridge.ts +0 -841
- package/src/deterministic.ts +0 -121
- package/src/disposable.ts +0 -50
- package/src/hono-node-server.ts +0 -144
- package/src/http-fixtures.ts +0 -187
- package/src/project-scoped-tasks.ts +0 -351
- package/src/realtime-ws.ts +0 -354
- package/src/runtime-process.ts +0 -159
- package/src/sync-builders.ts +0 -136
- package/src/sync-http.ts +0 -117
- package/src/sync-parse.ts +0 -86
- package/src/sync-response.ts +0 -43
package/src/client-bridge.ts
DELETED
|
@@ -1,841 +0,0 @@
|
|
|
1
|
-
import { Database } from 'bun:sqlite';
|
|
2
|
-
import type {
|
|
3
|
-
SyncularAuthLeaseRecord,
|
|
4
|
-
SyncularBridge,
|
|
5
|
-
SyncularBridgeMutationBatch,
|
|
6
|
-
SyncularBridgeQueryRequest,
|
|
7
|
-
SyncularBridgeQueryResult,
|
|
8
|
-
SyncularBridgeStatus,
|
|
9
|
-
SyncularClientEventMap,
|
|
10
|
-
SyncularClientEventSink,
|
|
11
|
-
SyncularClientEventType,
|
|
12
|
-
SyncularConflictResolution,
|
|
13
|
-
SyncularConflictStats,
|
|
14
|
-
SyncularConflictSummary,
|
|
15
|
-
SyncularConnectionState,
|
|
16
|
-
SyncularDiagnosticSnapshot,
|
|
17
|
-
SyncularLifecycleState,
|
|
18
|
-
SyncularPresenceChangeEvent,
|
|
19
|
-
SyncularPresenceEntry,
|
|
20
|
-
SyncularPresenceSink,
|
|
21
|
-
SyncularSubscriptionSpec,
|
|
22
|
-
SyncularSyncResult,
|
|
23
|
-
} from '@syncular/client';
|
|
24
|
-
import type { SyncAuthLeaseIssueRequest, SyncOperation } from '@syncular/core';
|
|
25
|
-
import type { AsyncDisposableResource } from './disposable';
|
|
26
|
-
import { createAsyncDisposableResource } from './disposable';
|
|
27
|
-
|
|
28
|
-
export type ClientBridgeSeed = Record<
|
|
29
|
-
string,
|
|
30
|
-
readonly Record<string, unknown>[]
|
|
31
|
-
>;
|
|
32
|
-
|
|
33
|
-
export type ClientBridgeTauriInvoke = <TResult>(
|
|
34
|
-
command: string,
|
|
35
|
-
args?: Record<string, unknown>
|
|
36
|
-
) => Promise<TResult>;
|
|
37
|
-
|
|
38
|
-
export type ClientBridgeTauriListen = <TPayload>(
|
|
39
|
-
event: string,
|
|
40
|
-
handler: (event: { payload: TPayload }) => void
|
|
41
|
-
) => Promise<() => void>;
|
|
42
|
-
|
|
43
|
-
export type ClientBridgeNativeEventSubscription =
|
|
44
|
-
| (() => void)
|
|
45
|
-
| { remove(): void };
|
|
46
|
-
|
|
47
|
-
export interface ClientBridgeNativeModule {
|
|
48
|
-
executeSql<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
49
|
-
request: SyncularBridgeQueryRequest
|
|
50
|
-
): Promise<SyncularBridgeQueryResult<Row>> | SyncularBridgeQueryResult<Row>;
|
|
51
|
-
applyMutationsCommit(
|
|
52
|
-
batch: SyncularBridgeMutationBatch
|
|
53
|
-
): Promise<string> | string;
|
|
54
|
-
applyLeasedMutationsCommit?(
|
|
55
|
-
batch: SyncularBridgeMutationBatch
|
|
56
|
-
): Promise<string> | string;
|
|
57
|
-
sync?(): Promise<SyncularSyncResult>;
|
|
58
|
-
resumeFromBackground?(): Promise<SyncularSyncResult>;
|
|
59
|
-
start?(): Promise<void>;
|
|
60
|
-
stop?(): Promise<void>;
|
|
61
|
-
setSubscriptions?(
|
|
62
|
-
subscriptions: readonly SyncularSubscriptionSpec[]
|
|
63
|
-
): Promise<void>;
|
|
64
|
-
getStatus?(): SyncularBridgeStatus;
|
|
65
|
-
issueAuthLease?(
|
|
66
|
-
request: SyncAuthLeaseIssueRequest
|
|
67
|
-
): Promise<SyncularAuthLeaseRecord>;
|
|
68
|
-
upsertAuthLease?(lease: SyncularAuthLeaseRecord): Promise<void>;
|
|
69
|
-
authLease?(leaseId: string): Promise<SyncularAuthLeaseRecord | null>;
|
|
70
|
-
activeAuthLeases?(
|
|
71
|
-
actorId?: string | null,
|
|
72
|
-
nowMs?: number
|
|
73
|
-
): Promise<SyncularAuthLeaseRecord[]>;
|
|
74
|
-
diagnosticSnapshot?(): Promise<SyncularDiagnosticSnapshot>;
|
|
75
|
-
addListener?<T extends SyncularClientEventType>(
|
|
76
|
-
event: T,
|
|
77
|
-
listener: SyncularClientEventSink<T>
|
|
78
|
-
): ClientBridgeNativeEventSubscription;
|
|
79
|
-
getPresence?<TMetadata = Record<string, unknown>>(
|
|
80
|
-
scopeKey: string
|
|
81
|
-
): SyncularPresenceEntry<TMetadata>[];
|
|
82
|
-
joinPresence?(scopeKey: string, metadata?: Record<string, unknown>): void;
|
|
83
|
-
leavePresence?(scopeKey: string): void;
|
|
84
|
-
updatePresenceMetadata?(
|
|
85
|
-
scopeKey: string,
|
|
86
|
-
metadata: Record<string, unknown>
|
|
87
|
-
): void;
|
|
88
|
-
conflictSummaries?(): Promise<SyncularConflictSummary[]>;
|
|
89
|
-
retryConflictKeepLocal?(id: string): Promise<string>;
|
|
90
|
-
resolveConflict?(
|
|
91
|
-
id: string,
|
|
92
|
-
resolution: SyncularConflictResolution
|
|
93
|
-
): Promise<void>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface CreateClientBridgeHarnessOptions {
|
|
97
|
-
seed?: ClientBridgeSeed;
|
|
98
|
-
idColumn?: string;
|
|
99
|
-
actorId?: string;
|
|
100
|
-
clientId?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface ClientBridgeInvocation {
|
|
104
|
-
command: string;
|
|
105
|
-
args: Record<string, unknown> | undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface ClientBridgeSqlQuery extends SyncularBridgeQueryRequest {}
|
|
109
|
-
|
|
110
|
-
export interface ClientBridgeHarness {
|
|
111
|
-
bridge: SyncularBridge;
|
|
112
|
-
tauri: {
|
|
113
|
-
invoke: ClientBridgeTauriInvoke;
|
|
114
|
-
listen: ClientBridgeTauriListen;
|
|
115
|
-
invocations(): ClientBridgeInvocation[];
|
|
116
|
-
};
|
|
117
|
-
reactNative: {
|
|
118
|
-
module: ClientBridgeNativeModule;
|
|
119
|
-
};
|
|
120
|
-
queries(): ClientBridgeSqlQuery[];
|
|
121
|
-
batches(): SyncularBridgeMutationBatch[];
|
|
122
|
-
leasedBatches(): SyncularBridgeMutationBatch[];
|
|
123
|
-
operations(): SyncOperation[];
|
|
124
|
-
syncCount(): number;
|
|
125
|
-
listenerCount(event: SyncularClientEventType): number;
|
|
126
|
-
rows(table: string): Record<string, unknown>[];
|
|
127
|
-
setRows(table: string, rows: readonly Record<string, unknown>[]): void;
|
|
128
|
-
setStatus(status: SyncularBridgeStatus): void;
|
|
129
|
-
setConflicts(conflicts: readonly SyncularConflictSummary[]): void;
|
|
130
|
-
authLease(leaseId: string): SyncularAuthLeaseRecord | null;
|
|
131
|
-
authLeases(): SyncularAuthLeaseRecord[];
|
|
132
|
-
diagnosticSnapshot(): SyncularDiagnosticSnapshot;
|
|
133
|
-
emit<T extends SyncularClientEventType>(
|
|
134
|
-
event: T,
|
|
135
|
-
payload: SyncularClientEventMap[T]
|
|
136
|
-
): void;
|
|
137
|
-
emitRowsChanged(event: SyncularClientEventMap['rowsChanged']): void;
|
|
138
|
-
presence<TMetadata = Record<string, unknown>>(
|
|
139
|
-
scopeKey: string
|
|
140
|
-
): SyncularPresenceEntry<TMetadata>[];
|
|
141
|
-
close(): void;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function createClientBridgeHarness(
|
|
145
|
-
options: CreateClientBridgeHarnessOptions = {}
|
|
146
|
-
): Promise<AsyncDisposableResource<ClientBridgeHarness>> {
|
|
147
|
-
const harness = new InProcessClientBridgeHarness(options);
|
|
148
|
-
return createAsyncDisposableResource(harness, () => harness.close());
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
class InProcessClientBridgeHarness implements ClientBridgeHarness {
|
|
152
|
-
readonly #db = new Database(':memory:');
|
|
153
|
-
readonly #idColumn: string;
|
|
154
|
-
readonly #actorId: string;
|
|
155
|
-
readonly #clientId: string;
|
|
156
|
-
readonly #queries: ClientBridgeSqlQuery[] = [];
|
|
157
|
-
readonly #batches: SyncularBridgeMutationBatch[] = [];
|
|
158
|
-
readonly #leasedBatches: SyncularBridgeMutationBatch[] = [];
|
|
159
|
-
readonly #invocations: ClientBridgeInvocation[] = [];
|
|
160
|
-
readonly #listeners = new Map<
|
|
161
|
-
SyncularClientEventType,
|
|
162
|
-
Set<SyncularClientEventSink<SyncularClientEventType>>
|
|
163
|
-
>();
|
|
164
|
-
readonly #tauriListeners = new Map<
|
|
165
|
-
string,
|
|
166
|
-
Set<(event: { payload: unknown }) => void>
|
|
167
|
-
>();
|
|
168
|
-
readonly #presenceListeners = new Set<SyncularPresenceSink>();
|
|
169
|
-
readonly #presence = new Map<string, SyncularPresenceEntry[]>();
|
|
170
|
-
#conflicts: SyncularConflictSummary[] = [];
|
|
171
|
-
#status: SyncularBridgeStatus = {};
|
|
172
|
-
#authLeases = new Map<string, SyncularAuthLeaseRecord>();
|
|
173
|
-
#syncCount = 0;
|
|
174
|
-
#commitIndex = 1;
|
|
175
|
-
#leaseIndex = 1;
|
|
176
|
-
|
|
177
|
-
constructor(options: CreateClientBridgeHarnessOptions) {
|
|
178
|
-
this.#idColumn = options.idColumn ?? 'id';
|
|
179
|
-
this.#actorId = options.actorId ?? 'actor-test';
|
|
180
|
-
this.#clientId = options.clientId ?? 'client-test';
|
|
181
|
-
for (const [table, rows] of Object.entries(options.seed ?? {})) {
|
|
182
|
-
this.setRows(table, rows);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
readonly bridge: SyncularBridge = {
|
|
187
|
-
executeSql: (request) => this.executeSql(request),
|
|
188
|
-
applyMutationsCommit: (batch) => this.applyMutationsCommit(batch),
|
|
189
|
-
applyLeasedMutationsCommit: (batch) =>
|
|
190
|
-
this.applyMutationsCommit(batch, { leased: true }),
|
|
191
|
-
sync: () => this.sync(),
|
|
192
|
-
resumeFromBackground: () => this.resumeFromBackground(),
|
|
193
|
-
start: async () => {
|
|
194
|
-
this.setConnection('connected');
|
|
195
|
-
},
|
|
196
|
-
stop: async () => {
|
|
197
|
-
this.setConnection('disconnected');
|
|
198
|
-
},
|
|
199
|
-
setSubscriptions: async () => undefined,
|
|
200
|
-
getStatus: () => this.#status,
|
|
201
|
-
issueAuthLease: (request) => this.issueAuthLease(request),
|
|
202
|
-
upsertAuthLease: async (lease) => {
|
|
203
|
-
this.#authLeases.set(lease.leaseId, { ...lease });
|
|
204
|
-
},
|
|
205
|
-
authLease: async (leaseId) => this.authLease(leaseId),
|
|
206
|
-
activeAuthLeases: async (actorId, nowMs) =>
|
|
207
|
-
this.activeAuthLeases(actorId, nowMs),
|
|
208
|
-
diagnosticSnapshot: async () => this.diagnosticSnapshot(),
|
|
209
|
-
on: (event, listener) => this.addEventListener(event, listener),
|
|
210
|
-
presence: {
|
|
211
|
-
get: (scopeKey) => this.presence(scopeKey),
|
|
212
|
-
join: (scopeKey, metadata) => this.joinPresence(scopeKey, metadata),
|
|
213
|
-
leave: (scopeKey) => this.leavePresence(scopeKey),
|
|
214
|
-
updateMetadata: (scopeKey, metadata) =>
|
|
215
|
-
this.updatePresenceMetadata(scopeKey, metadata),
|
|
216
|
-
onChange: (listener) => {
|
|
217
|
-
this.#presenceListeners.add(listener as SyncularPresenceSink);
|
|
218
|
-
return () =>
|
|
219
|
-
this.#presenceListeners.delete(listener as SyncularPresenceSink);
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
conflicts: {
|
|
223
|
-
list: async () => this.#conflicts,
|
|
224
|
-
retryKeepLocal: async (id) => `retry-${id}`,
|
|
225
|
-
resolve: async (id, resolution) => {
|
|
226
|
-
this.#conflicts = this.#conflicts.map((conflict) =>
|
|
227
|
-
conflict.id === id
|
|
228
|
-
? { ...conflict, resolvedAt: Date.now(), resolution }
|
|
229
|
-
: conflict
|
|
230
|
-
);
|
|
231
|
-
this.emit('conflictsChanged', conflictStats(this.#conflicts));
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
readonly tauri = {
|
|
237
|
-
invoke: (async <TResult>(
|
|
238
|
-
command: string,
|
|
239
|
-
args?: Record<string, unknown>
|
|
240
|
-
) => {
|
|
241
|
-
this.#invocations.push({ command, args });
|
|
242
|
-
return this.handleTauriCommand(command, args) as TResult;
|
|
243
|
-
}) satisfies ClientBridgeTauriInvoke,
|
|
244
|
-
listen: (async <TPayload>(
|
|
245
|
-
event: string,
|
|
246
|
-
handler: (event: { payload: TPayload }) => void
|
|
247
|
-
) => {
|
|
248
|
-
const listeners = this.#tauriListeners.get(event) ?? new Set();
|
|
249
|
-
const wrapped = handler as (event: { payload: unknown }) => void;
|
|
250
|
-
listeners.add(wrapped);
|
|
251
|
-
this.#tauriListeners.set(event, listeners);
|
|
252
|
-
return () => listeners.delete(wrapped);
|
|
253
|
-
}) satisfies ClientBridgeTauriListen,
|
|
254
|
-
invocations: () => [...this.#invocations],
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
readonly reactNative = {
|
|
258
|
-
module: {
|
|
259
|
-
executeSql: (request) => this.executeSql(request),
|
|
260
|
-
applyMutationsCommit: (batch) =>
|
|
261
|
-
this.applyMutationsCommit(batch).commitId,
|
|
262
|
-
applyLeasedMutationsCommit: (batch) =>
|
|
263
|
-
this.applyMutationsCommit(batch, { leased: true }).commitId,
|
|
264
|
-
sync: () => this.sync(),
|
|
265
|
-
resumeFromBackground: () => this.resumeFromBackground(),
|
|
266
|
-
start: async () => {
|
|
267
|
-
this.setConnection('connected');
|
|
268
|
-
},
|
|
269
|
-
stop: async () => {
|
|
270
|
-
this.setConnection('disconnected');
|
|
271
|
-
},
|
|
272
|
-
setSubscriptions: async (_subscriptions) => undefined,
|
|
273
|
-
getStatus: () => this.#status,
|
|
274
|
-
issueAuthLease: (request) => this.issueAuthLease(request),
|
|
275
|
-
upsertAuthLease: async (lease) => {
|
|
276
|
-
this.#authLeases.set(lease.leaseId, { ...lease });
|
|
277
|
-
},
|
|
278
|
-
authLease: async (leaseId) => this.authLease(leaseId),
|
|
279
|
-
activeAuthLeases: async (actorId, nowMs) =>
|
|
280
|
-
this.activeAuthLeases(actorId, nowMs),
|
|
281
|
-
diagnosticSnapshot: async () => this.diagnosticSnapshot(),
|
|
282
|
-
addListener: (event, listener) =>
|
|
283
|
-
this.reactNativeSubscription(this.addEventListener(event, listener)),
|
|
284
|
-
getPresence: (scopeKey) => this.presence(scopeKey),
|
|
285
|
-
joinPresence: (scopeKey, metadata) =>
|
|
286
|
-
this.joinPresence(scopeKey, metadata),
|
|
287
|
-
leavePresence: (scopeKey) => this.leavePresence(scopeKey),
|
|
288
|
-
updatePresenceMetadata: (scopeKey, metadata) =>
|
|
289
|
-
this.updatePresenceMetadata(scopeKey, metadata),
|
|
290
|
-
conflictSummaries: async () => this.#conflicts,
|
|
291
|
-
retryConflictKeepLocal: async (id) => `retry-${id}`,
|
|
292
|
-
resolveConflict: async (id, resolution) => {
|
|
293
|
-
await this.bridge.conflicts?.resolve(id, resolution);
|
|
294
|
-
},
|
|
295
|
-
} satisfies ClientBridgeNativeModule,
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
queries(): ClientBridgeSqlQuery[] {
|
|
299
|
-
return [...this.#queries];
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
batches(): SyncularBridgeMutationBatch[] {
|
|
303
|
-
return [...this.#batches];
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
leasedBatches(): SyncularBridgeMutationBatch[] {
|
|
307
|
-
return [...this.#leasedBatches];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
operations(): SyncOperation[] {
|
|
311
|
-
return [...this.#batches, ...this.#leasedBatches].flatMap(
|
|
312
|
-
(batch) => batch.operations
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
syncCount(): number {
|
|
317
|
-
return this.#syncCount;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
listenerCount(event: SyncularClientEventType): number {
|
|
321
|
-
return this.#listeners.get(event)?.size ?? 0;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
rows(table: string): Record<string, unknown>[] {
|
|
325
|
-
this.ensureTable(table);
|
|
326
|
-
return this.#db.query(`select * from ${quoteIdent(table)}`).all() as Record<
|
|
327
|
-
string,
|
|
328
|
-
unknown
|
|
329
|
-
>[];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
setRows(table: string, rows: readonly Record<string, unknown>[]): void {
|
|
333
|
-
this.ensureTable(table, rows);
|
|
334
|
-
this.#db.run(`delete from ${quoteIdent(table)}`);
|
|
335
|
-
for (const row of rows) {
|
|
336
|
-
this.upsertRow(table, row);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
setStatus(status: SyncularBridgeStatus): void {
|
|
341
|
-
this.#status = status;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
setConflicts(conflicts: readonly SyncularConflictSummary[]): void {
|
|
345
|
-
this.#conflicts = [...conflicts];
|
|
346
|
-
this.emit('conflictsChanged', conflictStats(this.#conflicts));
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
authLease(leaseId: string): SyncularAuthLeaseRecord | null {
|
|
350
|
-
const lease = this.#authLeases.get(leaseId);
|
|
351
|
-
return lease ? { ...lease } : null;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
authLeases(): SyncularAuthLeaseRecord[] {
|
|
355
|
-
return [...this.#authLeases.values()].map((lease) => ({ ...lease }));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
emit<T extends SyncularClientEventType>(
|
|
359
|
-
event: T,
|
|
360
|
-
payload: SyncularClientEventMap[T]
|
|
361
|
-
): void {
|
|
362
|
-
for (const listener of this.#listeners.get(event) ?? []) {
|
|
363
|
-
listener(payload as never);
|
|
364
|
-
}
|
|
365
|
-
this.emitTauri(`syncular:${event}`, payload);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
emitRowsChanged(event: SyncularClientEventMap['rowsChanged']): void {
|
|
369
|
-
this.emit('rowsChanged', event);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
presence<TMetadata = Record<string, unknown>>(
|
|
373
|
-
scopeKey: string
|
|
374
|
-
): SyncularPresenceEntry<TMetadata>[] {
|
|
375
|
-
return [
|
|
376
|
-
...(this.#presence.get(scopeKey) ?? []),
|
|
377
|
-
] as SyncularPresenceEntry<TMetadata>[];
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
close(): void {
|
|
381
|
-
this.#db.close();
|
|
382
|
-
this.#listeners.clear();
|
|
383
|
-
this.#tauriListeners.clear();
|
|
384
|
-
this.#presenceListeners.clear();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
private executeSql<Row extends Record<string, unknown>>(
|
|
388
|
-
request: SyncularBridgeQueryRequest
|
|
389
|
-
): SyncularBridgeQueryResult<Row> {
|
|
390
|
-
this.#queries.push(request);
|
|
391
|
-
const statement = this.#db.query(request.sql);
|
|
392
|
-
const rows = statement.all(...(request.parameters as never[])) as Row[];
|
|
393
|
-
return { rows };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private applyMutationsCommit(
|
|
397
|
-
batch: SyncularBridgeMutationBatch,
|
|
398
|
-
options: { leased?: boolean } = {}
|
|
399
|
-
): {
|
|
400
|
-
commitId: string;
|
|
401
|
-
clientCommitId: string;
|
|
402
|
-
} {
|
|
403
|
-
const target = options.leased ? this.#leasedBatches : this.#batches;
|
|
404
|
-
target.push({
|
|
405
|
-
operations: batch.operations.map((operation) => ({ ...operation })),
|
|
406
|
-
});
|
|
407
|
-
const commitId = `bridge-commit-${this.#commitIndex++}`;
|
|
408
|
-
for (const operation of batch.operations) {
|
|
409
|
-
this.applyOperation(operation);
|
|
410
|
-
}
|
|
411
|
-
return { commitId, clientCommitId: commitId };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
private async sync(): Promise<SyncularSyncResult> {
|
|
415
|
-
this.#syncCount += 1;
|
|
416
|
-
return zeroSyncResult();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private async resumeFromBackground(): Promise<SyncularSyncResult> {
|
|
420
|
-
this.setConnection('connected');
|
|
421
|
-
return this.sync();
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private async issueAuthLease(
|
|
425
|
-
request: SyncAuthLeaseIssueRequest
|
|
426
|
-
): Promise<SyncularAuthLeaseRecord> {
|
|
427
|
-
const nowMs = Date.now();
|
|
428
|
-
const leaseId = `bridge-lease-${this.#leaseIndex++}`;
|
|
429
|
-
const expiresAtMs = nowMs + (request.ttlMs ?? 60_000);
|
|
430
|
-
const payload = {
|
|
431
|
-
version: 1,
|
|
432
|
-
leaseId,
|
|
433
|
-
issuer: 'syncular-testkit',
|
|
434
|
-
audience: 'syncular-testkit',
|
|
435
|
-
actorId: this.#actorId,
|
|
436
|
-
subject: {},
|
|
437
|
-
schemaVersion: request.schemaVersion,
|
|
438
|
-
protocolVersion: 1,
|
|
439
|
-
issuedAtMs: nowMs,
|
|
440
|
-
notBeforeMs: nowMs,
|
|
441
|
-
expiresAtMs,
|
|
442
|
-
maxClockSkewMs: 0,
|
|
443
|
-
scopes: request.scopes,
|
|
444
|
-
capabilities: {
|
|
445
|
-
allowBlobs: true,
|
|
446
|
-
allowCrdt: true,
|
|
447
|
-
allowEncryptedFields: true,
|
|
448
|
-
},
|
|
449
|
-
};
|
|
450
|
-
const lease: SyncularAuthLeaseRecord = {
|
|
451
|
-
leaseId,
|
|
452
|
-
kid: 'syncular-testkit',
|
|
453
|
-
actorId: this.#actorId,
|
|
454
|
-
issuedAtMs: nowMs,
|
|
455
|
-
notBeforeMs: nowMs,
|
|
456
|
-
expiresAtMs,
|
|
457
|
-
schemaVersion: request.schemaVersion,
|
|
458
|
-
payloadJson: JSON.stringify(payload),
|
|
459
|
-
token: `testkit.${leaseId}`,
|
|
460
|
-
status: 'active',
|
|
461
|
-
lastValidationError: null,
|
|
462
|
-
createdAtMs: nowMs,
|
|
463
|
-
updatedAtMs: nowMs,
|
|
464
|
-
};
|
|
465
|
-
this.#authLeases.set(leaseId, lease);
|
|
466
|
-
return { ...lease };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
private handleTauriCommand(
|
|
470
|
-
command: string,
|
|
471
|
-
args: Record<string, unknown> | undefined
|
|
472
|
-
): unknown {
|
|
473
|
-
switch (command) {
|
|
474
|
-
case 'syncular_execute_sql':
|
|
475
|
-
return this.executeSql(args?.request as SyncularBridgeQueryRequest);
|
|
476
|
-
case 'syncular_apply_mutations_commit':
|
|
477
|
-
return this.applyMutationsCommit(
|
|
478
|
-
args?.batch as SyncularBridgeMutationBatch
|
|
479
|
-
).commitId;
|
|
480
|
-
case 'syncular_apply_leased_mutations_commit':
|
|
481
|
-
return this.applyMutationsCommit(
|
|
482
|
-
args?.batch as SyncularBridgeMutationBatch,
|
|
483
|
-
{ leased: true }
|
|
484
|
-
).commitId;
|
|
485
|
-
case 'syncular_sync':
|
|
486
|
-
return this.sync();
|
|
487
|
-
case 'syncular_resume_from_background':
|
|
488
|
-
return this.resumeFromBackground();
|
|
489
|
-
case 'syncular_start':
|
|
490
|
-
return this.bridge.start?.();
|
|
491
|
-
case 'syncular_stop':
|
|
492
|
-
return this.bridge.stop?.();
|
|
493
|
-
case 'syncular_set_subscriptions':
|
|
494
|
-
return undefined;
|
|
495
|
-
case 'syncular_issue_auth_lease':
|
|
496
|
-
return this.issueAuthLease(args?.request as SyncAuthLeaseIssueRequest);
|
|
497
|
-
case 'syncular_upsert_auth_lease':
|
|
498
|
-
this.#authLeases.set((args?.lease as SyncularAuthLeaseRecord).leaseId, {
|
|
499
|
-
...(args?.lease as SyncularAuthLeaseRecord),
|
|
500
|
-
});
|
|
501
|
-
return undefined;
|
|
502
|
-
case 'syncular_auth_lease':
|
|
503
|
-
return this.authLease(String(args?.leaseId));
|
|
504
|
-
case 'syncular_active_auth_leases':
|
|
505
|
-
return this.activeAuthLeases(
|
|
506
|
-
args?.actorId == null ? null : String(args.actorId),
|
|
507
|
-
typeof args?.nowMs === 'number' ? args.nowMs : undefined
|
|
508
|
-
);
|
|
509
|
-
case 'syncular_diagnostic_snapshot':
|
|
510
|
-
return this.diagnosticSnapshot();
|
|
511
|
-
case 'syncular_join_presence':
|
|
512
|
-
return this.joinPresence(
|
|
513
|
-
String(args?.scopeKey),
|
|
514
|
-
args?.metadata as Record<string, unknown> | undefined
|
|
515
|
-
);
|
|
516
|
-
case 'syncular_leave_presence':
|
|
517
|
-
return this.leavePresence(String(args?.scopeKey));
|
|
518
|
-
case 'syncular_update_presence_metadata':
|
|
519
|
-
return this.updatePresenceMetadata(
|
|
520
|
-
String(args?.scopeKey),
|
|
521
|
-
args?.metadata as Record<string, unknown>
|
|
522
|
-
);
|
|
523
|
-
case 'syncular_conflict_summaries':
|
|
524
|
-
return this.#conflicts;
|
|
525
|
-
case 'syncular_retry_conflict_keep_local':
|
|
526
|
-
return `retry-${String(args?.id)}`;
|
|
527
|
-
case 'syncular_resolve_conflict':
|
|
528
|
-
return this.bridge.conflicts?.resolve(
|
|
529
|
-
String(args?.id),
|
|
530
|
-
args?.resolution as SyncularConflictResolution
|
|
531
|
-
);
|
|
532
|
-
default:
|
|
533
|
-
throw new Error(`Unknown Syncular test bridge command: ${command}`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
private addEventListener<T extends SyncularClientEventType>(
|
|
538
|
-
event: T,
|
|
539
|
-
listener: SyncularClientEventSink<T>
|
|
540
|
-
): () => void {
|
|
541
|
-
const listeners = this.#listeners.get(event) ?? new Set();
|
|
542
|
-
listeners.add(listener as SyncularClientEventSink<SyncularClientEventType>);
|
|
543
|
-
this.#listeners.set(event, listeners);
|
|
544
|
-
return () =>
|
|
545
|
-
listeners.delete(
|
|
546
|
-
listener as SyncularClientEventSink<SyncularClientEventType>
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
private emitTauri(event: string, payload: unknown): void {
|
|
551
|
-
for (const listener of this.#tauriListeners.get(event) ?? []) {
|
|
552
|
-
listener({ payload });
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
private reactNativeSubscription(
|
|
557
|
-
unsubscribe: () => void
|
|
558
|
-
): ClientBridgeNativeEventSubscription {
|
|
559
|
-
return { remove: unsubscribe };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
private setConnection(realtime: SyncularConnectionState['realtime']): void {
|
|
563
|
-
const connection: SyncularConnectionState = {
|
|
564
|
-
closed: false,
|
|
565
|
-
pendingRequests: 0,
|
|
566
|
-
realtime,
|
|
567
|
-
};
|
|
568
|
-
const lifecycle: SyncularLifecycleState = {
|
|
569
|
-
phase: realtime === 'connected' ? 'complete' : 'offline',
|
|
570
|
-
realtime,
|
|
571
|
-
online: realtime === 'connected',
|
|
572
|
-
requiresAction: false,
|
|
573
|
-
pendingRequests: 0,
|
|
574
|
-
};
|
|
575
|
-
this.#status = { ...this.#status, connection, lifecycle };
|
|
576
|
-
this.emit('lifecycleChanged', lifecycle);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
private joinPresence(
|
|
580
|
-
scopeKey: string,
|
|
581
|
-
metadata?: Record<string, unknown>
|
|
582
|
-
): void {
|
|
583
|
-
this.#presence.set(scopeKey, [
|
|
584
|
-
{
|
|
585
|
-
clientId: this.#clientId,
|
|
586
|
-
actorId: this.#actorId,
|
|
587
|
-
joinedAt: Date.now(),
|
|
588
|
-
metadata,
|
|
589
|
-
},
|
|
590
|
-
]);
|
|
591
|
-
this.emitPresence(scopeKey);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
private leavePresence(scopeKey: string): void {
|
|
595
|
-
this.#presence.set(scopeKey, []);
|
|
596
|
-
this.emitPresence(scopeKey);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
private updatePresenceMetadata(
|
|
600
|
-
scopeKey: string,
|
|
601
|
-
metadata: Record<string, unknown>
|
|
602
|
-
): void {
|
|
603
|
-
this.#presence.set(
|
|
604
|
-
scopeKey,
|
|
605
|
-
this.presence(scopeKey).map((entry) => ({ ...entry, metadata }))
|
|
606
|
-
);
|
|
607
|
-
this.emitPresence(scopeKey);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private emitPresence(scopeKey: string): void {
|
|
611
|
-
const event: SyncularPresenceChangeEvent = {
|
|
612
|
-
scopeKey,
|
|
613
|
-
presence: this.presence(scopeKey),
|
|
614
|
-
};
|
|
615
|
-
for (const listener of this.#presenceListeners) listener(event);
|
|
616
|
-
this.emit('presenceChanged', event);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
private activeAuthLeases(
|
|
620
|
-
actorId?: string | null,
|
|
621
|
-
nowMs = Date.now()
|
|
622
|
-
): SyncularAuthLeaseRecord[] {
|
|
623
|
-
return this.authLeases().filter(
|
|
624
|
-
(lease) =>
|
|
625
|
-
lease.status === 'active' &&
|
|
626
|
-
lease.notBeforeMs <= nowMs &&
|
|
627
|
-
lease.expiresAtMs > nowMs &&
|
|
628
|
-
(actorId == null || lease.actorId === actorId)
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
diagnosticSnapshot(): SyncularDiagnosticSnapshot {
|
|
633
|
-
const status = this.#status;
|
|
634
|
-
return {
|
|
635
|
-
generatedAt: Date.now(),
|
|
636
|
-
runtime: {
|
|
637
|
-
packageName: '@syncular/testkit',
|
|
638
|
-
packageVersion: '0.0.0',
|
|
639
|
-
workerProtocolVersion: 0,
|
|
640
|
-
wasmGlueUrl: '',
|
|
641
|
-
wasmUrl: '',
|
|
642
|
-
},
|
|
643
|
-
connection: status.connection ?? {
|
|
644
|
-
closed: false,
|
|
645
|
-
pendingRequests: 0,
|
|
646
|
-
realtime: 'disconnected',
|
|
647
|
-
},
|
|
648
|
-
subscriptions: [],
|
|
649
|
-
recentDiagnostics: [],
|
|
650
|
-
recentSyncTimings: [],
|
|
651
|
-
...(status.lifecycle
|
|
652
|
-
? {
|
|
653
|
-
bootstrap: {
|
|
654
|
-
channelPhase: 'idle',
|
|
655
|
-
progressPercent: 100,
|
|
656
|
-
isBootstrapping: false,
|
|
657
|
-
criticalReady: true,
|
|
658
|
-
interactiveReady: true,
|
|
659
|
-
complete: true,
|
|
660
|
-
activePhase: null,
|
|
661
|
-
expectedSubscriptionIds: [],
|
|
662
|
-
readySubscriptionIds: [],
|
|
663
|
-
pendingSubscriptionIds: [],
|
|
664
|
-
subscriptions: [],
|
|
665
|
-
phases: [],
|
|
666
|
-
},
|
|
667
|
-
}
|
|
668
|
-
: {}),
|
|
669
|
-
...(status.outbox ? { outboxStats: status.outbox } : {}),
|
|
670
|
-
...(status.conflicts ? { conflictStats: status.conflicts } : {}),
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
private applyOperation(operation: SyncOperation): void {
|
|
675
|
-
if (operation.op === 'delete') {
|
|
676
|
-
this.ensureTable(operation.table);
|
|
677
|
-
this.#db
|
|
678
|
-
.query(
|
|
679
|
-
`delete from ${quoteIdent(operation.table)} where ${quoteIdent(
|
|
680
|
-
this.#idColumn
|
|
681
|
-
)} = ?`
|
|
682
|
-
)
|
|
683
|
-
.run(operation.row_id);
|
|
684
|
-
this.emitOperationChange(operation);
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
this.upsertRow(operation.table, {
|
|
689
|
-
[this.#idColumn]: operation.row_id,
|
|
690
|
-
...(operation.payload ?? {}),
|
|
691
|
-
});
|
|
692
|
-
this.emitOperationChange(operation);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
private emitOperationChange(operation: SyncOperation): void {
|
|
696
|
-
this.emitRowsChanged({
|
|
697
|
-
source: 'localWrite',
|
|
698
|
-
changedTables: [operation.table],
|
|
699
|
-
changedRows: [
|
|
700
|
-
{
|
|
701
|
-
table: operation.table,
|
|
702
|
-
rowId: operation.row_id,
|
|
703
|
-
operation: operation.op === 'delete' ? 'delete' : 'update',
|
|
704
|
-
changedFields: Object.keys(operation.payload ?? {}),
|
|
705
|
-
crdtFields: [],
|
|
706
|
-
},
|
|
707
|
-
],
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
private upsertRow(table: string, row: Record<string, unknown>): void {
|
|
712
|
-
this.ensureTable(table, [row]);
|
|
713
|
-
const columns = Object.keys(row);
|
|
714
|
-
const assignments = columns
|
|
715
|
-
.filter((column) => column !== this.#idColumn)
|
|
716
|
-
.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`)
|
|
717
|
-
.join(', ');
|
|
718
|
-
const sql = `insert into ${quoteIdent(table)} (${columns
|
|
719
|
-
.map(quoteIdent)
|
|
720
|
-
.join(', ')}) values (${columns
|
|
721
|
-
.map(() => '?')
|
|
722
|
-
.join(', ')}) on conflict(${quoteIdent(this.#idColumn)}) do update set ${
|
|
723
|
-
assignments ||
|
|
724
|
-
`${quoteIdent(this.#idColumn)} = excluded.${quoteIdent(this.#idColumn)}`
|
|
725
|
-
}`;
|
|
726
|
-
const statement = this.#db.query(sql) as unknown as {
|
|
727
|
-
run: (...bindings: unknown[]) => unknown;
|
|
728
|
-
};
|
|
729
|
-
statement.run(...columns.map((column) => encodeSqlValue(row[column])));
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
private ensureTable(
|
|
733
|
-
table: string,
|
|
734
|
-
rows: readonly Record<string, unknown>[] = []
|
|
735
|
-
): void {
|
|
736
|
-
this.#db.run(
|
|
737
|
-
`create table if not exists ${quoteIdent(table)} (${quoteIdent(
|
|
738
|
-
this.#idColumn
|
|
739
|
-
)} text primary key)`
|
|
740
|
-
);
|
|
741
|
-
const columns = new Set<string>([this.#idColumn]);
|
|
742
|
-
for (const row of rows) {
|
|
743
|
-
for (const column of Object.keys(row)) columns.add(column);
|
|
744
|
-
}
|
|
745
|
-
const existing = new Set(
|
|
746
|
-
(
|
|
747
|
-
this.#db
|
|
748
|
-
.query(`pragma table_info(${quoteIdent(table)})`)
|
|
749
|
-
.all() as Array<{
|
|
750
|
-
name: string;
|
|
751
|
-
}>
|
|
752
|
-
).map((column) => column.name)
|
|
753
|
-
);
|
|
754
|
-
for (const column of columns) {
|
|
755
|
-
if (!existing.has(column)) {
|
|
756
|
-
this.#db.run(
|
|
757
|
-
`alter table ${quoteIdent(table)} add column ${quoteIdent(
|
|
758
|
-
column
|
|
759
|
-
)} ${column === this.#idColumn ? 'text' : 'any'}`
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function quoteIdent(identifier: string): string {
|
|
767
|
-
return `"${identifier.replaceAll('"', '""')}"`;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function encodeSqlValue(value: unknown): unknown {
|
|
771
|
-
if (value == null) return null;
|
|
772
|
-
if (
|
|
773
|
-
typeof value === 'string' ||
|
|
774
|
-
typeof value === 'number' ||
|
|
775
|
-
typeof value === 'bigint' ||
|
|
776
|
-
typeof value === 'boolean' ||
|
|
777
|
-
value instanceof Uint8Array
|
|
778
|
-
) {
|
|
779
|
-
return value;
|
|
780
|
-
}
|
|
781
|
-
return JSON.stringify(value);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function conflictStats(
|
|
785
|
-
conflicts: readonly SyncularConflictSummary[]
|
|
786
|
-
): SyncularConflictStats {
|
|
787
|
-
const resolved = conflicts.filter((conflict) => conflict.resolvedAt != null);
|
|
788
|
-
return {
|
|
789
|
-
unresolved: conflicts.length - resolved.length,
|
|
790
|
-
resolved: resolved.length,
|
|
791
|
-
total: conflicts.length,
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function zeroSyncResult(): SyncularSyncResult {
|
|
796
|
-
return {
|
|
797
|
-
changedTables: [],
|
|
798
|
-
changedRows: [],
|
|
799
|
-
changedRowsTruncated: false,
|
|
800
|
-
subscriptions: [],
|
|
801
|
-
bootstrap: {
|
|
802
|
-
channelPhase: 'idle',
|
|
803
|
-
progressPercent: 100,
|
|
804
|
-
isBootstrapping: false,
|
|
805
|
-
criticalReady: true,
|
|
806
|
-
interactiveReady: true,
|
|
807
|
-
complete: true,
|
|
808
|
-
activePhase: null,
|
|
809
|
-
expectedSubscriptionIds: [],
|
|
810
|
-
readySubscriptionIds: [],
|
|
811
|
-
pendingSubscriptionIds: [],
|
|
812
|
-
subscriptions: [],
|
|
813
|
-
phases: [],
|
|
814
|
-
},
|
|
815
|
-
pushedCommits: 0,
|
|
816
|
-
timings: {
|
|
817
|
-
totalMs: 0,
|
|
818
|
-
pushMs: 0,
|
|
819
|
-
pullMs: 0,
|
|
820
|
-
pullRequestMs: 0,
|
|
821
|
-
syncPackDecodeMs: 0,
|
|
822
|
-
pullTransformMs: 0,
|
|
823
|
-
integrityVerifyMs: 0,
|
|
824
|
-
snapshotFetchMs: 0,
|
|
825
|
-
pullApplyMs: 0,
|
|
826
|
-
scopeClearMs: 0,
|
|
827
|
-
snapshotRowApplyMs: 0,
|
|
828
|
-
snapshotArtifactApplyMs: 0,
|
|
829
|
-
snapshotArtifactCheckpointMs: 0,
|
|
830
|
-
snapshotArtifactCheckpointCount: 0,
|
|
831
|
-
snapshotChunkApplyMs: 0,
|
|
832
|
-
snapshotChunkMaterializeMs: 0,
|
|
833
|
-
snapshotChunkResetMs: 0,
|
|
834
|
-
snapshotChunkBindMs: 0,
|
|
835
|
-
snapshotChunkStepMs: 0,
|
|
836
|
-
commitApplyMs: 0,
|
|
837
|
-
subscriptionStateMs: 0,
|
|
838
|
-
notifyMs: 0,
|
|
839
|
-
},
|
|
840
|
-
};
|
|
841
|
-
}
|