@syncular/testkit 0.1.3 → 0.2.1
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 -11
- package/dist/index.js +18 -11
- package/dist/react.d.ts +10 -0
- package/dist/react.js +41 -0
- package/dist/server.d.ts +44 -4
- package/dist/server.js +68 -4
- package/package.json +41 -57
- 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 -10
- package/src/react.ts +46 -0
- package/src/server.ts +102 -3
- 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/server.d.ts.map +0 -1
- package/dist/server.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/dist/audit.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
export function assertAuditChangeRedacted(change, message = 'Expected audit change to be redacted') {
|
|
3
|
-
if ('rowJson' in change) {
|
|
4
|
-
throw new Error(`${message}: rowJson must be omitted`);
|
|
5
|
-
}
|
|
6
|
-
if ('scopes' in change) {
|
|
7
|
-
throw new Error(`${message}: raw scopes must be omitted`);
|
|
8
|
-
}
|
|
9
|
-
if (!isDeepStrictEqual(change.redaction, {
|
|
10
|
-
payload: 'omitted',
|
|
11
|
-
reason: 'audit_redacted_by_default',
|
|
12
|
-
})) {
|
|
13
|
-
throw new Error(`${message}: missing canonical redaction marker`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function assertAuditJsonExcludes(value, forbiddenNeedles, message = 'Audit JSON leaked forbidden content') {
|
|
17
|
-
const serialized = JSON.stringify(value);
|
|
18
|
-
for (const needle of forbiddenNeedles) {
|
|
19
|
-
if (!needle)
|
|
20
|
-
continue;
|
|
21
|
-
if (serialized.includes(needle)) {
|
|
22
|
-
throw new Error(`${message}: ${needle}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
export function assertAuditDebugExportRedacted(value, forbiddenNeedles = [], message = 'Expected audit debug export to be redacted') {
|
|
27
|
-
for (const commit of value.commits ?? []) {
|
|
28
|
-
for (const change of commit.changes ?? []) {
|
|
29
|
-
assertAuditChangeRedacted(change, message);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
assertAuditJsonExcludes(value, forbiddenNeedles, message);
|
|
33
|
-
}
|
|
34
|
-
export async function readJsonResponse(response) {
|
|
35
|
-
return (await response.json());
|
|
36
|
-
}
|
|
37
|
-
export async function readTextResponse(response) {
|
|
38
|
-
return await response.text();
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=audit.js.map
|
package/dist/audit.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAoB9C,MAAM,UAAU,yBAAyB,CACvC,MAA2B,EAC3B,OAAO,GAAG,sCAAsC;IAEhD,IAAI,SAAS,IAAI,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,2BAA2B,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,8BAA8B,CAAC,CAAC;IAC5D,CAAC;IACD,IACE,CAAC,iBAAiB,CAAC,MAAM,CAAC,SAAS,EAAE;QACnC,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE,2BAA2B;KACpC,CAAC,EACF,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,sCAAsC,CAAC,CAAC;IACpE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,KAAc,EACd,gBAAmC,EACnC,OAAO,GAAG,qCAAqC;IAE/C,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACzC,KAAK,MAAM,MAAM,IAAI,gBAAgB,EAAE,CAAC;QACtC,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,8BAA8B,CAC5C,KAA+B,EAC/B,gBAAgB,GAAsB,EAAE,EACxC,OAAO,GAAG,4CAA4C;IAEtD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACzC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YAC1C,yBAAyB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,uBAAuB,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAI,QAAkB;IAC1D,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,QAAkB;IACvD,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;AAC/B,CAAC"}
|
package/dist/client-bridge.d.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { SyncularAuthLeaseRecord, SyncularBridge, SyncularBridgeMutationBatch, SyncularBridgeQueryRequest, SyncularBridgeQueryResult, SyncularBridgeStatus, SyncularClientEventMap, SyncularClientEventSink, SyncularClientEventType, SyncularConflictResolution, SyncularConflictSummary, SyncularDiagnosticSnapshot, SyncularPresenceEntry, SyncularSubscriptionSpec, SyncularSyncResult } from '@syncular/client';
|
|
2
|
-
import type { SyncAuthLeaseIssueRequest, SyncOperation } from '@syncular/core';
|
|
3
|
-
import type { AsyncDisposableResource } from './disposable';
|
|
4
|
-
export type ClientBridgeSeed = Record<string, readonly Record<string, unknown>[]>;
|
|
5
|
-
export type ClientBridgeTauriInvoke = <TResult>(command: string, args?: Record<string, unknown>) => Promise<TResult>;
|
|
6
|
-
export type ClientBridgeTauriListen = <TPayload>(event: string, handler: (event: {
|
|
7
|
-
payload: TPayload;
|
|
8
|
-
}) => void) => Promise<() => void>;
|
|
9
|
-
export type ClientBridgeNativeEventSubscription = (() => void) | {
|
|
10
|
-
remove(): void;
|
|
11
|
-
};
|
|
12
|
-
export interface ClientBridgeNativeModule {
|
|
13
|
-
executeSql<Row extends Record<string, unknown> = Record<string, unknown>>(request: SyncularBridgeQueryRequest): Promise<SyncularBridgeQueryResult<Row>> | SyncularBridgeQueryResult<Row>;
|
|
14
|
-
applyMutationsCommit(batch: SyncularBridgeMutationBatch): Promise<string> | string;
|
|
15
|
-
applyLeasedMutationsCommit?(batch: SyncularBridgeMutationBatch): Promise<string> | string;
|
|
16
|
-
sync?(): Promise<SyncularSyncResult>;
|
|
17
|
-
resumeFromBackground?(): Promise<SyncularSyncResult>;
|
|
18
|
-
start?(): Promise<void>;
|
|
19
|
-
stop?(): Promise<void>;
|
|
20
|
-
setSubscriptions?(subscriptions: readonly SyncularSubscriptionSpec[]): Promise<void>;
|
|
21
|
-
getStatus?(): SyncularBridgeStatus;
|
|
22
|
-
issueAuthLease?(request: SyncAuthLeaseIssueRequest): Promise<SyncularAuthLeaseRecord>;
|
|
23
|
-
upsertAuthLease?(lease: SyncularAuthLeaseRecord): Promise<void>;
|
|
24
|
-
authLease?(leaseId: string): Promise<SyncularAuthLeaseRecord | null>;
|
|
25
|
-
activeAuthLeases?(actorId?: string | null, nowMs?: number): Promise<SyncularAuthLeaseRecord[]>;
|
|
26
|
-
diagnosticSnapshot?(): Promise<SyncularDiagnosticSnapshot>;
|
|
27
|
-
addListener?<T extends SyncularClientEventType>(event: T, listener: SyncularClientEventSink<T>): ClientBridgeNativeEventSubscription;
|
|
28
|
-
getPresence?<TMetadata = Record<string, unknown>>(scopeKey: string): SyncularPresenceEntry<TMetadata>[];
|
|
29
|
-
joinPresence?(scopeKey: string, metadata?: Record<string, unknown>): void;
|
|
30
|
-
leavePresence?(scopeKey: string): void;
|
|
31
|
-
updatePresenceMetadata?(scopeKey: string, metadata: Record<string, unknown>): void;
|
|
32
|
-
conflictSummaries?(): Promise<SyncularConflictSummary[]>;
|
|
33
|
-
retryConflictKeepLocal?(id: string): Promise<string>;
|
|
34
|
-
resolveConflict?(id: string, resolution: SyncularConflictResolution): Promise<void>;
|
|
35
|
-
}
|
|
36
|
-
export interface CreateClientBridgeHarnessOptions {
|
|
37
|
-
seed?: ClientBridgeSeed;
|
|
38
|
-
idColumn?: string;
|
|
39
|
-
actorId?: string;
|
|
40
|
-
clientId?: string;
|
|
41
|
-
}
|
|
42
|
-
export interface ClientBridgeInvocation {
|
|
43
|
-
command: string;
|
|
44
|
-
args: Record<string, unknown> | undefined;
|
|
45
|
-
}
|
|
46
|
-
export interface ClientBridgeSqlQuery extends SyncularBridgeQueryRequest {
|
|
47
|
-
}
|
|
48
|
-
export interface ClientBridgeHarness {
|
|
49
|
-
bridge: SyncularBridge;
|
|
50
|
-
tauri: {
|
|
51
|
-
invoke: ClientBridgeTauriInvoke;
|
|
52
|
-
listen: ClientBridgeTauriListen;
|
|
53
|
-
invocations(): ClientBridgeInvocation[];
|
|
54
|
-
};
|
|
55
|
-
reactNative: {
|
|
56
|
-
module: ClientBridgeNativeModule;
|
|
57
|
-
};
|
|
58
|
-
queries(): ClientBridgeSqlQuery[];
|
|
59
|
-
batches(): SyncularBridgeMutationBatch[];
|
|
60
|
-
leasedBatches(): SyncularBridgeMutationBatch[];
|
|
61
|
-
operations(): SyncOperation[];
|
|
62
|
-
syncCount(): number;
|
|
63
|
-
listenerCount(event: SyncularClientEventType): number;
|
|
64
|
-
rows(table: string): Record<string, unknown>[];
|
|
65
|
-
setRows(table: string, rows: readonly Record<string, unknown>[]): void;
|
|
66
|
-
setStatus(status: SyncularBridgeStatus): void;
|
|
67
|
-
setConflicts(conflicts: readonly SyncularConflictSummary[]): void;
|
|
68
|
-
authLease(leaseId: string): SyncularAuthLeaseRecord | null;
|
|
69
|
-
authLeases(): SyncularAuthLeaseRecord[];
|
|
70
|
-
diagnosticSnapshot(): SyncularDiagnosticSnapshot;
|
|
71
|
-
emit<T extends SyncularClientEventType>(event: T, payload: SyncularClientEventMap[T]): void;
|
|
72
|
-
emitRowsChanged(event: SyncularClientEventMap['rowsChanged']): void;
|
|
73
|
-
presence<TMetadata = Record<string, unknown>>(scopeKey: string): SyncularPresenceEntry<TMetadata>[];
|
|
74
|
-
close(): void;
|
|
75
|
-
}
|
|
76
|
-
export declare function createClientBridgeHarness(options?: CreateClientBridgeHarnessOptions): Promise<AsyncDisposableResource<ClientBridgeHarness>>;
|
|
77
|
-
//# sourceMappingURL=client-bridge.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client-bridge.d.ts","sourceRoot":"","sources":["../src/client-bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/client-bridge.js
DELETED
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
import { Database } from 'bun:sqlite';
|
|
2
|
-
import { createAsyncDisposableResource } from './disposable.js';
|
|
3
|
-
export async function createClientBridgeHarness(options = {}) {
|
|
4
|
-
const harness = new InProcessClientBridgeHarness(options);
|
|
5
|
-
return createAsyncDisposableResource(harness, () => harness.close());
|
|
6
|
-
}
|
|
7
|
-
class InProcessClientBridgeHarness {
|
|
8
|
-
#db = new Database(':memory:');
|
|
9
|
-
#idColumn;
|
|
10
|
-
#actorId;
|
|
11
|
-
#clientId;
|
|
12
|
-
#queries = [];
|
|
13
|
-
#batches = [];
|
|
14
|
-
#leasedBatches = [];
|
|
15
|
-
#invocations = [];
|
|
16
|
-
#listeners = new Map();
|
|
17
|
-
#tauriListeners = new Map();
|
|
18
|
-
#presenceListeners = new Set();
|
|
19
|
-
#presence = new Map();
|
|
20
|
-
#conflicts = [];
|
|
21
|
-
#status = {};
|
|
22
|
-
#authLeases = new Map();
|
|
23
|
-
#syncCount = 0;
|
|
24
|
-
#commitIndex = 1;
|
|
25
|
-
#leaseIndex = 1;
|
|
26
|
-
constructor(options) {
|
|
27
|
-
this.#idColumn = options.idColumn ?? 'id';
|
|
28
|
-
this.#actorId = options.actorId ?? 'actor-test';
|
|
29
|
-
this.#clientId = options.clientId ?? 'client-test';
|
|
30
|
-
for (const [table, rows] of Object.entries(options.seed ?? {})) {
|
|
31
|
-
this.setRows(table, rows);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
bridge = {
|
|
35
|
-
executeSql: (request) => this.executeSql(request),
|
|
36
|
-
applyMutationsCommit: (batch) => this.applyMutationsCommit(batch),
|
|
37
|
-
applyLeasedMutationsCommit: (batch) => this.applyMutationsCommit(batch, { leased: true }),
|
|
38
|
-
sync: () => this.sync(),
|
|
39
|
-
resumeFromBackground: () => this.resumeFromBackground(),
|
|
40
|
-
start: async () => {
|
|
41
|
-
this.setConnection('connected');
|
|
42
|
-
},
|
|
43
|
-
stop: async () => {
|
|
44
|
-
this.setConnection('disconnected');
|
|
45
|
-
},
|
|
46
|
-
setSubscriptions: async () => undefined,
|
|
47
|
-
getStatus: () => this.#status,
|
|
48
|
-
issueAuthLease: (request) => this.issueAuthLease(request),
|
|
49
|
-
upsertAuthLease: async (lease) => {
|
|
50
|
-
this.#authLeases.set(lease.leaseId, { ...lease });
|
|
51
|
-
},
|
|
52
|
-
authLease: async (leaseId) => this.authLease(leaseId),
|
|
53
|
-
activeAuthLeases: async (actorId, nowMs) => this.activeAuthLeases(actorId, nowMs),
|
|
54
|
-
diagnosticSnapshot: async () => this.diagnosticSnapshot(),
|
|
55
|
-
on: (event, listener) => this.addEventListener(event, listener),
|
|
56
|
-
presence: {
|
|
57
|
-
get: (scopeKey) => this.presence(scopeKey),
|
|
58
|
-
join: (scopeKey, metadata) => this.joinPresence(scopeKey, metadata),
|
|
59
|
-
leave: (scopeKey) => this.leavePresence(scopeKey),
|
|
60
|
-
updateMetadata: (scopeKey, metadata) => this.updatePresenceMetadata(scopeKey, metadata),
|
|
61
|
-
onChange: (listener) => {
|
|
62
|
-
this.#presenceListeners.add(listener);
|
|
63
|
-
return () => this.#presenceListeners.delete(listener);
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
conflicts: {
|
|
67
|
-
list: async () => this.#conflicts,
|
|
68
|
-
retryKeepLocal: async (id) => `retry-${id}`,
|
|
69
|
-
resolve: async (id, resolution) => {
|
|
70
|
-
this.#conflicts = this.#conflicts.map((conflict) => conflict.id === id
|
|
71
|
-
? { ...conflict, resolvedAt: Date.now(), resolution }
|
|
72
|
-
: conflict);
|
|
73
|
-
this.emit('conflictsChanged', conflictStats(this.#conflicts));
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
tauri = {
|
|
78
|
-
invoke: (async (command, args) => {
|
|
79
|
-
this.#invocations.push({ command, args });
|
|
80
|
-
return this.handleTauriCommand(command, args);
|
|
81
|
-
}),
|
|
82
|
-
listen: (async (event, handler) => {
|
|
83
|
-
const listeners = this.#tauriListeners.get(event) ?? new Set();
|
|
84
|
-
const wrapped = handler;
|
|
85
|
-
listeners.add(wrapped);
|
|
86
|
-
this.#tauriListeners.set(event, listeners);
|
|
87
|
-
return () => listeners.delete(wrapped);
|
|
88
|
-
}),
|
|
89
|
-
invocations: () => [...this.#invocations],
|
|
90
|
-
};
|
|
91
|
-
reactNative = {
|
|
92
|
-
module: {
|
|
93
|
-
executeSql: (request) => this.executeSql(request),
|
|
94
|
-
applyMutationsCommit: (batch) => this.applyMutationsCommit(batch).commitId,
|
|
95
|
-
applyLeasedMutationsCommit: (batch) => this.applyMutationsCommit(batch, { leased: true }).commitId,
|
|
96
|
-
sync: () => this.sync(),
|
|
97
|
-
resumeFromBackground: () => this.resumeFromBackground(),
|
|
98
|
-
start: async () => {
|
|
99
|
-
this.setConnection('connected');
|
|
100
|
-
},
|
|
101
|
-
stop: async () => {
|
|
102
|
-
this.setConnection('disconnected');
|
|
103
|
-
},
|
|
104
|
-
setSubscriptions: async (_subscriptions) => undefined,
|
|
105
|
-
getStatus: () => this.#status,
|
|
106
|
-
issueAuthLease: (request) => this.issueAuthLease(request),
|
|
107
|
-
upsertAuthLease: async (lease) => {
|
|
108
|
-
this.#authLeases.set(lease.leaseId, { ...lease });
|
|
109
|
-
},
|
|
110
|
-
authLease: async (leaseId) => this.authLease(leaseId),
|
|
111
|
-
activeAuthLeases: async (actorId, nowMs) => this.activeAuthLeases(actorId, nowMs),
|
|
112
|
-
diagnosticSnapshot: async () => this.diagnosticSnapshot(),
|
|
113
|
-
addListener: (event, listener) => this.reactNativeSubscription(this.addEventListener(event, listener)),
|
|
114
|
-
getPresence: (scopeKey) => this.presence(scopeKey),
|
|
115
|
-
joinPresence: (scopeKey, metadata) => this.joinPresence(scopeKey, metadata),
|
|
116
|
-
leavePresence: (scopeKey) => this.leavePresence(scopeKey),
|
|
117
|
-
updatePresenceMetadata: (scopeKey, metadata) => this.updatePresenceMetadata(scopeKey, metadata),
|
|
118
|
-
conflictSummaries: async () => this.#conflicts,
|
|
119
|
-
retryConflictKeepLocal: async (id) => `retry-${id}`,
|
|
120
|
-
resolveConflict: async (id, resolution) => {
|
|
121
|
-
await this.bridge.conflicts?.resolve(id, resolution);
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
queries() {
|
|
126
|
-
return [...this.#queries];
|
|
127
|
-
}
|
|
128
|
-
batches() {
|
|
129
|
-
return [...this.#batches];
|
|
130
|
-
}
|
|
131
|
-
leasedBatches() {
|
|
132
|
-
return [...this.#leasedBatches];
|
|
133
|
-
}
|
|
134
|
-
operations() {
|
|
135
|
-
return [...this.#batches, ...this.#leasedBatches].flatMap((batch) => batch.operations);
|
|
136
|
-
}
|
|
137
|
-
syncCount() {
|
|
138
|
-
return this.#syncCount;
|
|
139
|
-
}
|
|
140
|
-
listenerCount(event) {
|
|
141
|
-
return this.#listeners.get(event)?.size ?? 0;
|
|
142
|
-
}
|
|
143
|
-
rows(table) {
|
|
144
|
-
this.ensureTable(table);
|
|
145
|
-
return this.#db.query(`select * from ${quoteIdent(table)}`).all();
|
|
146
|
-
}
|
|
147
|
-
setRows(table, rows) {
|
|
148
|
-
this.ensureTable(table, rows);
|
|
149
|
-
this.#db.run(`delete from ${quoteIdent(table)}`);
|
|
150
|
-
for (const row of rows) {
|
|
151
|
-
this.upsertRow(table, row);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
setStatus(status) {
|
|
155
|
-
this.#status = status;
|
|
156
|
-
}
|
|
157
|
-
setConflicts(conflicts) {
|
|
158
|
-
this.#conflicts = [...conflicts];
|
|
159
|
-
this.emit('conflictsChanged', conflictStats(this.#conflicts));
|
|
160
|
-
}
|
|
161
|
-
authLease(leaseId) {
|
|
162
|
-
const lease = this.#authLeases.get(leaseId);
|
|
163
|
-
return lease ? { ...lease } : null;
|
|
164
|
-
}
|
|
165
|
-
authLeases() {
|
|
166
|
-
return [...this.#authLeases.values()].map((lease) => ({ ...lease }));
|
|
167
|
-
}
|
|
168
|
-
emit(event, payload) {
|
|
169
|
-
for (const listener of this.#listeners.get(event) ?? []) {
|
|
170
|
-
listener(payload);
|
|
171
|
-
}
|
|
172
|
-
this.emitTauri(`syncular:${event}`, payload);
|
|
173
|
-
}
|
|
174
|
-
emitRowsChanged(event) {
|
|
175
|
-
this.emit('rowsChanged', event);
|
|
176
|
-
}
|
|
177
|
-
presence(scopeKey) {
|
|
178
|
-
return [
|
|
179
|
-
...(this.#presence.get(scopeKey) ?? []),
|
|
180
|
-
];
|
|
181
|
-
}
|
|
182
|
-
close() {
|
|
183
|
-
this.#db.close();
|
|
184
|
-
this.#listeners.clear();
|
|
185
|
-
this.#tauriListeners.clear();
|
|
186
|
-
this.#presenceListeners.clear();
|
|
187
|
-
}
|
|
188
|
-
executeSql(request) {
|
|
189
|
-
this.#queries.push(request);
|
|
190
|
-
const statement = this.#db.query(request.sql);
|
|
191
|
-
const rows = statement.all(...request.parameters);
|
|
192
|
-
return { rows };
|
|
193
|
-
}
|
|
194
|
-
applyMutationsCommit(batch, options = {}) {
|
|
195
|
-
const target = options.leased ? this.#leasedBatches : this.#batches;
|
|
196
|
-
target.push({
|
|
197
|
-
operations: batch.operations.map((operation) => ({ ...operation })),
|
|
198
|
-
});
|
|
199
|
-
const commitId = `bridge-commit-${this.#commitIndex++}`;
|
|
200
|
-
for (const operation of batch.operations) {
|
|
201
|
-
this.applyOperation(operation);
|
|
202
|
-
}
|
|
203
|
-
return { commitId, clientCommitId: commitId };
|
|
204
|
-
}
|
|
205
|
-
async sync() {
|
|
206
|
-
this.#syncCount += 1;
|
|
207
|
-
return zeroSyncResult();
|
|
208
|
-
}
|
|
209
|
-
async resumeFromBackground() {
|
|
210
|
-
this.setConnection('connected');
|
|
211
|
-
return this.sync();
|
|
212
|
-
}
|
|
213
|
-
async issueAuthLease(request) {
|
|
214
|
-
const nowMs = Date.now();
|
|
215
|
-
const leaseId = `bridge-lease-${this.#leaseIndex++}`;
|
|
216
|
-
const expiresAtMs = nowMs + (request.ttlMs ?? 60_000);
|
|
217
|
-
const payload = {
|
|
218
|
-
version: 1,
|
|
219
|
-
leaseId,
|
|
220
|
-
issuer: 'syncular-testkit',
|
|
221
|
-
audience: 'syncular-testkit',
|
|
222
|
-
actorId: this.#actorId,
|
|
223
|
-
subject: {},
|
|
224
|
-
schemaVersion: request.schemaVersion,
|
|
225
|
-
protocolVersion: 1,
|
|
226
|
-
issuedAtMs: nowMs,
|
|
227
|
-
notBeforeMs: nowMs,
|
|
228
|
-
expiresAtMs,
|
|
229
|
-
maxClockSkewMs: 0,
|
|
230
|
-
scopes: request.scopes,
|
|
231
|
-
capabilities: {
|
|
232
|
-
allowBlobs: true,
|
|
233
|
-
allowCrdt: true,
|
|
234
|
-
allowEncryptedFields: true,
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
const lease = {
|
|
238
|
-
leaseId,
|
|
239
|
-
kid: 'syncular-testkit',
|
|
240
|
-
actorId: this.#actorId,
|
|
241
|
-
issuedAtMs: nowMs,
|
|
242
|
-
notBeforeMs: nowMs,
|
|
243
|
-
expiresAtMs,
|
|
244
|
-
schemaVersion: request.schemaVersion,
|
|
245
|
-
payloadJson: JSON.stringify(payload),
|
|
246
|
-
token: `testkit.${leaseId}`,
|
|
247
|
-
status: 'active',
|
|
248
|
-
lastValidationError: null,
|
|
249
|
-
createdAtMs: nowMs,
|
|
250
|
-
updatedAtMs: nowMs,
|
|
251
|
-
};
|
|
252
|
-
this.#authLeases.set(leaseId, lease);
|
|
253
|
-
return { ...lease };
|
|
254
|
-
}
|
|
255
|
-
handleTauriCommand(command, args) {
|
|
256
|
-
switch (command) {
|
|
257
|
-
case 'syncular_execute_sql':
|
|
258
|
-
return this.executeSql(args?.request);
|
|
259
|
-
case 'syncular_apply_mutations_commit':
|
|
260
|
-
return this.applyMutationsCommit(args?.batch).commitId;
|
|
261
|
-
case 'syncular_apply_leased_mutations_commit':
|
|
262
|
-
return this.applyMutationsCommit(args?.batch, { leased: true }).commitId;
|
|
263
|
-
case 'syncular_sync':
|
|
264
|
-
return this.sync();
|
|
265
|
-
case 'syncular_resume_from_background':
|
|
266
|
-
return this.resumeFromBackground();
|
|
267
|
-
case 'syncular_start':
|
|
268
|
-
return this.bridge.start?.();
|
|
269
|
-
case 'syncular_stop':
|
|
270
|
-
return this.bridge.stop?.();
|
|
271
|
-
case 'syncular_set_subscriptions':
|
|
272
|
-
return undefined;
|
|
273
|
-
case 'syncular_issue_auth_lease':
|
|
274
|
-
return this.issueAuthLease(args?.request);
|
|
275
|
-
case 'syncular_upsert_auth_lease':
|
|
276
|
-
this.#authLeases.set((args?.lease).leaseId, {
|
|
277
|
-
...args?.lease,
|
|
278
|
-
});
|
|
279
|
-
return undefined;
|
|
280
|
-
case 'syncular_auth_lease':
|
|
281
|
-
return this.authLease(String(args?.leaseId));
|
|
282
|
-
case 'syncular_active_auth_leases':
|
|
283
|
-
return this.activeAuthLeases(args?.actorId == null ? null : String(args.actorId), typeof args?.nowMs === 'number' ? args.nowMs : undefined);
|
|
284
|
-
case 'syncular_diagnostic_snapshot':
|
|
285
|
-
return this.diagnosticSnapshot();
|
|
286
|
-
case 'syncular_join_presence':
|
|
287
|
-
return this.joinPresence(String(args?.scopeKey), args?.metadata);
|
|
288
|
-
case 'syncular_leave_presence':
|
|
289
|
-
return this.leavePresence(String(args?.scopeKey));
|
|
290
|
-
case 'syncular_update_presence_metadata':
|
|
291
|
-
return this.updatePresenceMetadata(String(args?.scopeKey), args?.metadata);
|
|
292
|
-
case 'syncular_conflict_summaries':
|
|
293
|
-
return this.#conflicts;
|
|
294
|
-
case 'syncular_retry_conflict_keep_local':
|
|
295
|
-
return `retry-${String(args?.id)}`;
|
|
296
|
-
case 'syncular_resolve_conflict':
|
|
297
|
-
return this.bridge.conflicts?.resolve(String(args?.id), args?.resolution);
|
|
298
|
-
default:
|
|
299
|
-
throw new Error(`Unknown Syncular test bridge command: ${command}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
addEventListener(event, listener) {
|
|
303
|
-
const listeners = this.#listeners.get(event) ?? new Set();
|
|
304
|
-
listeners.add(listener);
|
|
305
|
-
this.#listeners.set(event, listeners);
|
|
306
|
-
return () => listeners.delete(listener);
|
|
307
|
-
}
|
|
308
|
-
emitTauri(event, payload) {
|
|
309
|
-
for (const listener of this.#tauriListeners.get(event) ?? []) {
|
|
310
|
-
listener({ payload });
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
reactNativeSubscription(unsubscribe) {
|
|
314
|
-
return { remove: unsubscribe };
|
|
315
|
-
}
|
|
316
|
-
setConnection(realtime) {
|
|
317
|
-
const connection = {
|
|
318
|
-
closed: false,
|
|
319
|
-
pendingRequests: 0,
|
|
320
|
-
realtime,
|
|
321
|
-
};
|
|
322
|
-
const lifecycle = {
|
|
323
|
-
phase: realtime === 'connected' ? 'complete' : 'offline',
|
|
324
|
-
realtime,
|
|
325
|
-
online: realtime === 'connected',
|
|
326
|
-
requiresAction: false,
|
|
327
|
-
pendingRequests: 0,
|
|
328
|
-
};
|
|
329
|
-
this.#status = { ...this.#status, connection, lifecycle };
|
|
330
|
-
this.emit('lifecycleChanged', lifecycle);
|
|
331
|
-
}
|
|
332
|
-
joinPresence(scopeKey, metadata) {
|
|
333
|
-
this.#presence.set(scopeKey, [
|
|
334
|
-
{
|
|
335
|
-
clientId: this.#clientId,
|
|
336
|
-
actorId: this.#actorId,
|
|
337
|
-
joinedAt: Date.now(),
|
|
338
|
-
metadata,
|
|
339
|
-
},
|
|
340
|
-
]);
|
|
341
|
-
this.emitPresence(scopeKey);
|
|
342
|
-
}
|
|
343
|
-
leavePresence(scopeKey) {
|
|
344
|
-
this.#presence.set(scopeKey, []);
|
|
345
|
-
this.emitPresence(scopeKey);
|
|
346
|
-
}
|
|
347
|
-
updatePresenceMetadata(scopeKey, metadata) {
|
|
348
|
-
this.#presence.set(scopeKey, this.presence(scopeKey).map((entry) => ({ ...entry, metadata })));
|
|
349
|
-
this.emitPresence(scopeKey);
|
|
350
|
-
}
|
|
351
|
-
emitPresence(scopeKey) {
|
|
352
|
-
const event = {
|
|
353
|
-
scopeKey,
|
|
354
|
-
presence: this.presence(scopeKey),
|
|
355
|
-
};
|
|
356
|
-
for (const listener of this.#presenceListeners)
|
|
357
|
-
listener(event);
|
|
358
|
-
this.emit('presenceChanged', event);
|
|
359
|
-
}
|
|
360
|
-
activeAuthLeases(actorId, nowMs = Date.now()) {
|
|
361
|
-
return this.authLeases().filter((lease) => lease.status === 'active' &&
|
|
362
|
-
lease.notBeforeMs <= nowMs &&
|
|
363
|
-
lease.expiresAtMs > nowMs &&
|
|
364
|
-
(actorId == null || lease.actorId === actorId));
|
|
365
|
-
}
|
|
366
|
-
diagnosticSnapshot() {
|
|
367
|
-
const status = this.#status;
|
|
368
|
-
return {
|
|
369
|
-
generatedAt: Date.now(),
|
|
370
|
-
runtime: {
|
|
371
|
-
packageName: '@syncular/testkit',
|
|
372
|
-
packageVersion: '0.0.0',
|
|
373
|
-
workerProtocolVersion: 0,
|
|
374
|
-
wasmGlueUrl: '',
|
|
375
|
-
wasmUrl: '',
|
|
376
|
-
},
|
|
377
|
-
connection: status.connection ?? {
|
|
378
|
-
closed: false,
|
|
379
|
-
pendingRequests: 0,
|
|
380
|
-
realtime: 'disconnected',
|
|
381
|
-
},
|
|
382
|
-
subscriptions: [],
|
|
383
|
-
recentDiagnostics: [],
|
|
384
|
-
recentSyncTimings: [],
|
|
385
|
-
...(status.lifecycle
|
|
386
|
-
? {
|
|
387
|
-
bootstrap: {
|
|
388
|
-
channelPhase: 'idle',
|
|
389
|
-
progressPercent: 100,
|
|
390
|
-
isBootstrapping: false,
|
|
391
|
-
criticalReady: true,
|
|
392
|
-
interactiveReady: true,
|
|
393
|
-
complete: true,
|
|
394
|
-
activePhase: null,
|
|
395
|
-
expectedSubscriptionIds: [],
|
|
396
|
-
readySubscriptionIds: [],
|
|
397
|
-
pendingSubscriptionIds: [],
|
|
398
|
-
subscriptions: [],
|
|
399
|
-
phases: [],
|
|
400
|
-
},
|
|
401
|
-
}
|
|
402
|
-
: {}),
|
|
403
|
-
...(status.outbox ? { outboxStats: status.outbox } : {}),
|
|
404
|
-
...(status.conflicts ? { conflictStats: status.conflicts } : {}),
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
applyOperation(operation) {
|
|
408
|
-
if (operation.op === 'delete') {
|
|
409
|
-
this.ensureTable(operation.table);
|
|
410
|
-
this.#db
|
|
411
|
-
.query(`delete from ${quoteIdent(operation.table)} where ${quoteIdent(this.#idColumn)} = ?`)
|
|
412
|
-
.run(operation.row_id);
|
|
413
|
-
this.emitOperationChange(operation);
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
this.upsertRow(operation.table, {
|
|
417
|
-
[this.#idColumn]: operation.row_id,
|
|
418
|
-
...(operation.payload ?? {}),
|
|
419
|
-
});
|
|
420
|
-
this.emitOperationChange(operation);
|
|
421
|
-
}
|
|
422
|
-
emitOperationChange(operation) {
|
|
423
|
-
this.emitRowsChanged({
|
|
424
|
-
source: 'localWrite',
|
|
425
|
-
changedTables: [operation.table],
|
|
426
|
-
changedRows: [
|
|
427
|
-
{
|
|
428
|
-
table: operation.table,
|
|
429
|
-
rowId: operation.row_id,
|
|
430
|
-
operation: operation.op === 'delete' ? 'delete' : 'update',
|
|
431
|
-
changedFields: Object.keys(operation.payload ?? {}),
|
|
432
|
-
crdtFields: [],
|
|
433
|
-
},
|
|
434
|
-
],
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
upsertRow(table, row) {
|
|
438
|
-
this.ensureTable(table, [row]);
|
|
439
|
-
const columns = Object.keys(row);
|
|
440
|
-
const assignments = columns
|
|
441
|
-
.filter((column) => column !== this.#idColumn)
|
|
442
|
-
.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`)
|
|
443
|
-
.join(', ');
|
|
444
|
-
const sql = `insert into ${quoteIdent(table)} (${columns
|
|
445
|
-
.map(quoteIdent)
|
|
446
|
-
.join(', ')}) values (${columns
|
|
447
|
-
.map(() => '?')
|
|
448
|
-
.join(', ')}) on conflict(${quoteIdent(this.#idColumn)}) do update set ${assignments ||
|
|
449
|
-
`${quoteIdent(this.#idColumn)} = excluded.${quoteIdent(this.#idColumn)}`}`;
|
|
450
|
-
const statement = this.#db.query(sql);
|
|
451
|
-
statement.run(...columns.map((column) => encodeSqlValue(row[column])));
|
|
452
|
-
}
|
|
453
|
-
ensureTable(table, rows = []) {
|
|
454
|
-
this.#db.run(`create table if not exists ${quoteIdent(table)} (${quoteIdent(this.#idColumn)} text primary key)`);
|
|
455
|
-
const columns = new Set([this.#idColumn]);
|
|
456
|
-
for (const row of rows) {
|
|
457
|
-
for (const column of Object.keys(row))
|
|
458
|
-
columns.add(column);
|
|
459
|
-
}
|
|
460
|
-
const existing = new Set(this.#db
|
|
461
|
-
.query(`pragma table_info(${quoteIdent(table)})`)
|
|
462
|
-
.all().map((column) => column.name));
|
|
463
|
-
for (const column of columns) {
|
|
464
|
-
if (!existing.has(column)) {
|
|
465
|
-
this.#db.run(`alter table ${quoteIdent(table)} add column ${quoteIdent(column)} ${column === this.#idColumn ? 'text' : 'any'}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
function quoteIdent(identifier) {
|
|
471
|
-
return `"${identifier.replaceAll('"', '""')}"`;
|
|
472
|
-
}
|
|
473
|
-
function encodeSqlValue(value) {
|
|
474
|
-
if (value == null)
|
|
475
|
-
return null;
|
|
476
|
-
if (typeof value === 'string' ||
|
|
477
|
-
typeof value === 'number' ||
|
|
478
|
-
typeof value === 'bigint' ||
|
|
479
|
-
typeof value === 'boolean' ||
|
|
480
|
-
value instanceof Uint8Array) {
|
|
481
|
-
return value;
|
|
482
|
-
}
|
|
483
|
-
return JSON.stringify(value);
|
|
484
|
-
}
|
|
485
|
-
function conflictStats(conflicts) {
|
|
486
|
-
const resolved = conflicts.filter((conflict) => conflict.resolvedAt != null);
|
|
487
|
-
return {
|
|
488
|
-
unresolved: conflicts.length - resolved.length,
|
|
489
|
-
resolved: resolved.length,
|
|
490
|
-
total: conflicts.length,
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
function zeroSyncResult() {
|
|
494
|
-
return {
|
|
495
|
-
changedTables: [],
|
|
496
|
-
changedRows: [],
|
|
497
|
-
changedRowsTruncated: false,
|
|
498
|
-
subscriptions: [],
|
|
499
|
-
bootstrap: {
|
|
500
|
-
channelPhase: 'idle',
|
|
501
|
-
progressPercent: 100,
|
|
502
|
-
isBootstrapping: false,
|
|
503
|
-
criticalReady: true,
|
|
504
|
-
interactiveReady: true,
|
|
505
|
-
complete: true,
|
|
506
|
-
activePhase: null,
|
|
507
|
-
expectedSubscriptionIds: [],
|
|
508
|
-
readySubscriptionIds: [],
|
|
509
|
-
pendingSubscriptionIds: [],
|
|
510
|
-
subscriptions: [],
|
|
511
|
-
phases: [],
|
|
512
|
-
},
|
|
513
|
-
pushedCommits: 0,
|
|
514
|
-
timings: {
|
|
515
|
-
totalMs: 0,
|
|
516
|
-
pushMs: 0,
|
|
517
|
-
pullMs: 0,
|
|
518
|
-
pullRequestMs: 0,
|
|
519
|
-
syncPackDecodeMs: 0,
|
|
520
|
-
pullTransformMs: 0,
|
|
521
|
-
integrityVerifyMs: 0,
|
|
522
|
-
snapshotFetchMs: 0,
|
|
523
|
-
pullApplyMs: 0,
|
|
524
|
-
scopeClearMs: 0,
|
|
525
|
-
snapshotRowApplyMs: 0,
|
|
526
|
-
snapshotArtifactApplyMs: 0,
|
|
527
|
-
snapshotArtifactCheckpointMs: 0,
|
|
528
|
-
snapshotArtifactCheckpointCount: 0,
|
|
529
|
-
snapshotChunkApplyMs: 0,
|
|
530
|
-
snapshotChunkMaterializeMs: 0,
|
|
531
|
-
snapshotChunkResetMs: 0,
|
|
532
|
-
snapshotChunkBindMs: 0,
|
|
533
|
-
snapshotChunkStepMs: 0,
|
|
534
|
-
commitApplyMs: 0,
|
|
535
|
-
subscriptionStateMs: 0,
|
|
536
|
-
notifyMs: 0,
|
|
537
|
-
},
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
//# sourceMappingURL=client-bridge.js.map
|