@syncular/testkit 0.1.3 → 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.
Files changed (94) hide show
  1. package/README.md +210 -0
  2. package/dist/client.d.ts +68 -0
  3. package/dist/client.js +145 -0
  4. package/dist/clock.d.ts +24 -0
  5. package/dist/clock.js +32 -0
  6. package/dist/create-test-sync.d.ts +67 -0
  7. package/dist/create-test-sync.js +90 -0
  8. package/dist/faults.d.ts +52 -50
  9. package/dist/faults.js +74 -192
  10. package/dist/index.d.ts +17 -11
  11. package/dist/index.js +18 -11
  12. package/dist/react.d.ts +10 -0
  13. package/dist/react.js +41 -0
  14. package/dist/server.d.ts +44 -4
  15. package/dist/server.js +68 -4
  16. package/package.json +41 -57
  17. package/src/client.ts +218 -0
  18. package/src/clock.ts +45 -0
  19. package/src/create-test-sync.ts +158 -0
  20. package/src/faults.ts +77 -338
  21. package/src/index.ts +44 -10
  22. package/src/react.ts +46 -0
  23. package/src/server.ts +102 -3
  24. package/dist/audit.d.ts +0 -22
  25. package/dist/audit.d.ts.map +0 -1
  26. package/dist/audit.js +0 -40
  27. package/dist/audit.js.map +0 -1
  28. package/dist/client-bridge.d.ts +0 -77
  29. package/dist/client-bridge.d.ts.map +0 -1
  30. package/dist/client-bridge.js +0 -540
  31. package/dist/client-bridge.js.map +0 -1
  32. package/dist/deterministic.d.ts +0 -31
  33. package/dist/deterministic.d.ts.map +0 -1
  34. package/dist/deterministic.js +0 -69
  35. package/dist/deterministic.js.map +0 -1
  36. package/dist/disposable.d.ts +0 -10
  37. package/dist/disposable.d.ts.map +0 -1
  38. package/dist/disposable.js +0 -28
  39. package/dist/disposable.js.map +0 -1
  40. package/dist/faults.d.ts.map +0 -1
  41. package/dist/faults.js.map +0 -1
  42. package/dist/hono-node-server.d.ts +0 -20
  43. package/dist/hono-node-server.d.ts.map +0 -1
  44. package/dist/hono-node-server.js +0 -90
  45. package/dist/hono-node-server.js.map +0 -1
  46. package/dist/http-fixtures.d.ts +0 -32
  47. package/dist/http-fixtures.d.ts.map +0 -1
  48. package/dist/http-fixtures.js +0 -117
  49. package/dist/http-fixtures.js.map +0 -1
  50. package/dist/index.d.ts.map +0 -1
  51. package/dist/index.js.map +0 -1
  52. package/dist/project-scoped-tasks.d.ts +0 -40
  53. package/dist/project-scoped-tasks.d.ts.map +0 -1
  54. package/dist/project-scoped-tasks.js +0 -247
  55. package/dist/project-scoped-tasks.js.map +0 -1
  56. package/dist/realtime-ws.d.ts +0 -48
  57. package/dist/realtime-ws.d.ts.map +0 -1
  58. package/dist/realtime-ws.js +0 -203
  59. package/dist/realtime-ws.js.map +0 -1
  60. package/dist/runtime-process.d.ts +0 -14
  61. package/dist/runtime-process.d.ts.map +0 -1
  62. package/dist/runtime-process.js +0 -99
  63. package/dist/runtime-process.js.map +0 -1
  64. package/dist/server.d.ts.map +0 -1
  65. package/dist/server.js.map +0 -1
  66. package/dist/sync-builders.d.ts +0 -48
  67. package/dist/sync-builders.d.ts.map +0 -1
  68. package/dist/sync-builders.js +0 -66
  69. package/dist/sync-builders.js.map +0 -1
  70. package/dist/sync-http.d.ts +0 -48
  71. package/dist/sync-http.d.ts.map +0 -1
  72. package/dist/sync-http.js +0 -43
  73. package/dist/sync-http.js.map +0 -1
  74. package/dist/sync-parse.d.ts +0 -8
  75. package/dist/sync-parse.d.ts.map +0 -1
  76. package/dist/sync-parse.js +0 -38
  77. package/dist/sync-parse.js.map +0 -1
  78. package/dist/sync-response.d.ts +0 -6
  79. package/dist/sync-response.d.ts.map +0 -1
  80. package/dist/sync-response.js +0 -19
  81. package/dist/sync-response.js.map +0 -1
  82. package/src/audit.ts +0 -74
  83. package/src/client-bridge.ts +0 -841
  84. package/src/deterministic.ts +0 -121
  85. package/src/disposable.ts +0 -50
  86. package/src/hono-node-server.ts +0 -144
  87. package/src/http-fixtures.ts +0 -187
  88. package/src/project-scoped-tasks.ts +0 -351
  89. package/src/realtime-ws.ts +0 -354
  90. package/src/runtime-process.ts +0 -159
  91. package/src/sync-builders.ts +0 -136
  92. package/src/sync-http.ts +0 -117
  93. package/src/sync-parse.ts +0 -86
  94. 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"}
@@ -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":"AACA,OAAO,KAAK,EACV,uBAAuB,EACvB,cAAc,EACd,2BAA2B,EAC3B,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,0BAA0B,EAE1B,uBAAuB,EAEvB,0BAA0B,EAG1B,qBAAqB,EAErB,wBAAwB,EACxB,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAG5D,MAAM,MAAM,gBAAgB,GAAG,MAAM,CACnC,MAAM,EACN,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CACnC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,EAC5C,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB,MAAM,MAAM,uBAAuB,GAAG,CAAC,QAAQ,EAC7C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,QAAQ,CAAA;CAAE,KAAK,IAAI,KAC5C,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;AAEzB,MAAM,MAAM,mCAAmC,GAC3C,CAAC,MAAM,IAAI,CAAC,GACZ;IAAE,MAAM,IAAI,IAAI,CAAA;CAAE,CAAC;AAEvB,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtE,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC;IAC5E,oBAAoB,CAClB,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC5B,0BAA0B,CAAC,CACzB,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAC5B,IAAI,CAAC,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACrC,oBAAoB,CAAC,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACrD,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,gBAAgB,CAAC,CACf,aAAa,EAAE,SAAS,wBAAwB,EAAE,GACjD,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,SAAS,CAAC,IAAI,oBAAoB,CAAC;IACnC,cAAc,CAAC,CACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACpC,eAAe,CAAC,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IACrE,gBAAgB,CAAC,CACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,EACvB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;IACtC,kBAAkB,CAAC,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC3D,WAAW,CAAC,CAAC,CAAC,SAAS,uBAAuB,EAC5C,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,uBAAuB,CAAC,CAAC,CAAC,GACnC,mCAAmC,CAAC;IACvC,WAAW,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9C,QAAQ,EAAE,MAAM,GACf,qBAAqB,CAAC,SAAS,CAAC,EAAE,CAAC;IACtC,YAAY,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1E,aAAa,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,sBAAsB,CAAC,CACrB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,IAAI,CAAC;IACR,iBAAiB,CAAC,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;IACzD,sBAAsB,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,eAAe,CAAC,CACd,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,0BAA0B,GACrC,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,oBAAqB,SAAQ,0BAA0B;CAAG;AAE3E,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE;QACL,MAAM,EAAE,uBAAuB,CAAC;QAChC,MAAM,EAAE,uBAAuB,CAAC;QAChC,WAAW,IAAI,sBAAsB,EAAE,CAAC;KACzC,CAAC;IACF,WAAW,EAAE;QACX,MAAM,EAAE,wBAAwB,CAAC;KAClC,CAAC;IACF,OAAO,IAAI,oBAAoB,EAAE,CAAC;IAClC,OAAO,IAAI,2BAA2B,EAAE,CAAC;IACzC,aAAa,IAAI,2BAA2B,EAAE,CAAC;IAC/C,UAAU,IAAI,aAAa,EAAE,CAAC;IAC9B,SAAS,IAAI,MAAM,CAAC;IACpB,aAAa,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAAC;IACtD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC/C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC9C,YAAY,CAAC,SAAS,EAAE,SAAS,uBAAuB,EAAE,GAAG,IAAI,CAAC;IAClE,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAAC;IAC3D,UAAU,IAAI,uBAAuB,EAAE,CAAC;IACxC,kBAAkB,IAAI,0BAA0B,CAAC;IACjD,IAAI,CAAC,CAAC,SAAS,uBAAuB,EACpC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GACjC,IAAI,CAAC;IACR,eAAe,CAAC,KAAK,EAAE,sBAAsB,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;IACpE,QAAQ,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1C,QAAQ,EAAE,MAAM,GACf,qBAAqB,CAAC,SAAS,CAAC,EAAE,CAAC;IACtC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAsB,yBAAyB,CAC7C,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,uBAAuB,CAAC,mBAAmB,CAAC,CAAC,CAGvD"}
@@ -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