@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.
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
@@ -1,159 +0,0 @@
1
- import type { ChildProcess } from 'node:child_process';
2
- import {
3
- type AsyncDisposableResource,
4
- createAsyncDisposableResource,
5
- type ResourceRunner,
6
- withAsyncDisposableFactory,
7
- } from './disposable';
8
-
9
- export interface WaitForJsonPortOptions {
10
- timeoutMs?: number;
11
- processName?: string;
12
- }
13
-
14
- export async function waitForJsonPortFromStdout(
15
- process: ChildProcess,
16
- options: WaitForJsonPortOptions = {}
17
- ): Promise<number> {
18
- const timeoutMs = options.timeoutMs ?? 30_000;
19
- const processName = options.processName ?? 'Child process';
20
-
21
- return new Promise<number>((resolve, reject) => {
22
- const stdout = process.stdout;
23
- const stderr = process.stderr;
24
-
25
- if (!stdout || !stderr) {
26
- reject(new Error(`${processName} has no stdout/stderr pipes`));
27
- return;
28
- }
29
-
30
- let stdoutBuffer = '';
31
- let stderrBuffer = '';
32
-
33
- const cleanup = () => {
34
- clearTimeout(timeout);
35
- stdout.off('data', onStdoutData);
36
- stderr.off('data', onStderrData);
37
- process.off('exit', onExit);
38
- };
39
-
40
- const resolvePort = (port: number) => {
41
- cleanup();
42
- resolve(port);
43
- };
44
-
45
- const rejectWith = (error: Error) => {
46
- cleanup();
47
- reject(error);
48
- };
49
-
50
- const onStdoutData = (chunk: Buffer | string) => {
51
- stdoutBuffer += chunk.toString();
52
-
53
- const lines = stdoutBuffer.split('\n');
54
- stdoutBuffer = lines.pop() ?? '';
55
-
56
- for (const line of lines) {
57
- const trimmed = line.trim();
58
- if (!trimmed) {
59
- continue;
60
- }
61
-
62
- try {
63
- const parsed = JSON.parse(trimmed) as { port?: number };
64
- if (parsed.port && parsed.port > 0) {
65
- resolvePort(parsed.port);
66
- return;
67
- }
68
- } catch {
69
- // keep reading until a valid JSON line is emitted
70
- }
71
- }
72
- };
73
-
74
- const onStderrData = (chunk: Buffer | string) => {
75
- stderrBuffer += chunk.toString();
76
- };
77
-
78
- const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
79
- rejectWith(
80
- new Error(
81
- `${processName} exited before reporting port (code=${String(code)} signal=${String(signal)})\nstderr: ${stderrBuffer}`
82
- )
83
- );
84
- };
85
-
86
- const timeout = setTimeout(() => {
87
- rejectWith(
88
- new Error(
89
- `${processName} startup timed out after ${timeoutMs}ms\nstderr: ${stderrBuffer}`
90
- )
91
- );
92
- }, timeoutMs);
93
-
94
- stdout.on('data', onStdoutData);
95
- stderr.on('data', onStderrData);
96
- process.on('exit', onExit);
97
- });
98
- }
99
-
100
- export interface StopChildProcessOptions {
101
- gracePeriodMs?: number;
102
- }
103
-
104
- export async function stopChildProcess(
105
- process: ChildProcess,
106
- options: StopChildProcessOptions = {}
107
- ): Promise<void> {
108
- if (process.exitCode != null) {
109
- return;
110
- }
111
-
112
- const gracePeriodMs = options.gracePeriodMs ?? 5000;
113
-
114
- try {
115
- process.kill('SIGTERM');
116
- } catch {
117
- return;
118
- }
119
-
120
- await new Promise<void>((resolve) => {
121
- const onExit = () => {
122
- clearTimeout(timeout);
123
- process.off('exit', onExit);
124
- resolve();
125
- };
126
-
127
- const timeout = setTimeout(() => {
128
- try {
129
- process.kill('SIGKILL');
130
- } catch {
131
- // ignore
132
- }
133
- process.off('exit', onExit);
134
- resolve();
135
- }, gracePeriodMs);
136
-
137
- process.on('exit', onExit);
138
- });
139
- }
140
-
141
- export function createChildProcessResource(
142
- process: ChildProcess,
143
- options: StopChildProcessOptions = {}
144
- ): AsyncDisposableResource<ChildProcess> {
145
- return createAsyncDisposableResource(process, () =>
146
- stopChildProcess(process, options)
147
- );
148
- }
149
-
150
- export async function withChildProcess<TResult>(
151
- process: ChildProcess,
152
- run: ResourceRunner<ChildProcess, TResult>,
153
- options: StopChildProcessOptions = {}
154
- ): Promise<TResult> {
155
- return withAsyncDisposableFactory(
156
- async () => createChildProcessResource(process, options),
157
- run
158
- );
159
- }
@@ -1,136 +0,0 @@
1
- import type {
2
- SyncCombinedRequest,
3
- SyncOperation,
4
- SyncPullRequest,
5
- SyncPushBatchRequest,
6
- SyncSubscriptionRequest,
7
- } from '@syncular/core';
8
-
9
- export interface CreateSyncUpsertOperationOptions {
10
- table: string;
11
- rowId: string;
12
- payload: Record<string, unknown>;
13
- baseVersion?: number | null;
14
- }
15
-
16
- export function createSyncUpsertOperation(
17
- options: CreateSyncUpsertOperationOptions
18
- ): SyncOperation {
19
- return {
20
- table: options.table,
21
- row_id: options.rowId,
22
- op: 'upsert',
23
- payload: options.payload,
24
- base_version: options.baseVersion ?? null,
25
- };
26
- }
27
-
28
- export interface CreateSyncDeleteOperationOptions {
29
- table: string;
30
- rowId: string;
31
- baseVersion?: number | null;
32
- }
33
-
34
- export function createSyncDeleteOperation(
35
- options: CreateSyncDeleteOperationOptions
36
- ): SyncOperation {
37
- return {
38
- table: options.table,
39
- row_id: options.rowId,
40
- op: 'delete',
41
- payload: null,
42
- base_version: options.baseVersion ?? null,
43
- };
44
- }
45
-
46
- export interface CreateSyncSubscriptionOptions {
47
- id: string;
48
- table: string;
49
- scopes: SyncSubscriptionRequest['scopes'];
50
- params?: SyncSubscriptionRequest['params'];
51
- cursor?: number;
52
- bootstrapState?: SyncSubscriptionRequest['bootstrapState'];
53
- crdtStateVectors?: SyncSubscriptionRequest['crdtStateVectors'];
54
- }
55
-
56
- export function createSyncSubscription(
57
- options: CreateSyncSubscriptionOptions
58
- ): SyncSubscriptionRequest {
59
- return {
60
- id: options.id,
61
- table: options.table,
62
- scopes: options.scopes,
63
- ...(options.params ? { params: options.params } : {}),
64
- cursor: options.cursor ?? 0,
65
- bootstrapState: options.bootstrapState ?? null,
66
- crdtStateVectors: options.crdtStateVectors ?? [],
67
- };
68
- }
69
-
70
- export interface CreateSyncPushRequestOptions {
71
- clientId: string;
72
- clientCommitId: string;
73
- operations: SyncOperation[];
74
- schemaVersion?: number;
75
- }
76
-
77
- export function createSyncPushRequest(
78
- options: CreateSyncPushRequestOptions
79
- ): SyncPushBatchRequest {
80
- return {
81
- clientId: options.clientId,
82
- commits: [
83
- {
84
- clientCommitId: options.clientCommitId,
85
- operations: options.operations,
86
- schemaVersion: options.schemaVersion ?? 1,
87
- },
88
- ],
89
- };
90
- }
91
-
92
- export interface CreateSyncPullRequestOptions {
93
- clientId: string;
94
- schemaVersion?: number;
95
- subscriptions: SyncSubscriptionRequest[];
96
- limitCommits?: number;
97
- limitSnapshotRows?: number;
98
- maxSnapshotPages?: number;
99
- dedupeRows?: boolean;
100
- }
101
-
102
- export function createSyncPullRequest(
103
- options: CreateSyncPullRequestOptions
104
- ): SyncPullRequest {
105
- return {
106
- clientId: options.clientId,
107
- schemaVersion: options.schemaVersion ?? 1,
108
- subscriptions: options.subscriptions,
109
- limitCommits: options.limitCommits ?? 50,
110
- ...(options.limitSnapshotRows !== undefined
111
- ? { limitSnapshotRows: options.limitSnapshotRows }
112
- : {}),
113
- ...(options.maxSnapshotPages !== undefined
114
- ? { maxSnapshotPages: options.maxSnapshotPages }
115
- : {}),
116
- ...(options.dedupeRows !== undefined
117
- ? { dedupeRows: options.dedupeRows }
118
- : {}),
119
- };
120
- }
121
-
122
- export interface CreateSyncCombinedRequestOptions {
123
- clientId: string;
124
- push?: Omit<SyncPushBatchRequest, 'clientId'>;
125
- pull?: Omit<SyncPullRequest, 'clientId'>;
126
- }
127
-
128
- export function createSyncCombinedRequest(
129
- options: CreateSyncCombinedRequestOptions
130
- ): SyncCombinedRequest {
131
- return {
132
- clientId: options.clientId,
133
- ...(options.push ? { push: options.push } : {}),
134
- ...(options.pull ? { pull: options.pull } : {}),
135
- };
136
- }
package/src/sync-http.ts DELETED
@@ -1,117 +0,0 @@
1
- import type {
2
- SyncCombinedRequest,
3
- SyncCombinedResponse,
4
- SyncPullRequest,
5
- SyncPullResponse,
6
- SyncPushRequest,
7
- SyncPushResponse,
8
- } from '@syncular/core';
9
- import {
10
- parseSyncCombinedResponse,
11
- parseSyncPullResponse,
12
- parseSyncPushResponse,
13
- } from './sync-parse';
14
-
15
- export interface JsonActorHeadersOptions {
16
- actorId: string;
17
- actorHeader?: string;
18
- extraHeaders?: Record<string, string>;
19
- }
20
-
21
- export function createJsonActorHeaders(
22
- options: JsonActorHeadersOptions
23
- ): Record<string, string> {
24
- return {
25
- 'content-type': 'application/json',
26
- [options.actorHeader ?? 'x-actor-id']: options.actorId,
27
- ...options.extraHeaders,
28
- };
29
- }
30
-
31
- export interface PostJsonWithActorOptions<TBody> {
32
- fetch: typeof globalThis.fetch;
33
- url: string;
34
- actorId: string;
35
- actorHeader?: string;
36
- extraHeaders?: Record<string, string>;
37
- body: TBody;
38
- }
39
-
40
- export interface PostJsonWithActorResult<TResponse> {
41
- response: Response;
42
- json: TResponse;
43
- }
44
-
45
- export async function postJsonWithActor<TBody, TResponse>(
46
- options: PostJsonWithActorOptions<TBody>
47
- ): Promise<PostJsonWithActorResult<TResponse>> {
48
- const response = await options.fetch(options.url, {
49
- method: 'POST',
50
- headers: createJsonActorHeaders({
51
- actorId: options.actorId,
52
- actorHeader: options.actorHeader,
53
- extraHeaders: options.extraHeaders,
54
- }),
55
- body: JSON.stringify(options.body),
56
- });
57
-
58
- const json = (await response.json()) as TResponse;
59
- return { response, json };
60
- }
61
-
62
- export interface PostSyncCombinedRequestOptions {
63
- fetch: typeof globalThis.fetch;
64
- url: string;
65
- actorId: string;
66
- actorHeader?: string;
67
- extraHeaders?: Record<string, string>;
68
- body: SyncCombinedRequest;
69
- }
70
-
71
- export async function postSyncCombinedRequest(
72
- options: PostSyncCombinedRequestOptions
73
- ): Promise<PostJsonWithActorResult<SyncCombinedResponse>> {
74
- const result = await postJsonWithActor<SyncCombinedRequest, unknown>(options);
75
- return {
76
- response: result.response,
77
- json: parseSyncCombinedResponse(result.json),
78
- };
79
- }
80
-
81
- export interface PostSyncPushRequestOptions {
82
- fetch: typeof globalThis.fetch;
83
- url: string;
84
- actorId: string;
85
- actorHeader?: string;
86
- extraHeaders?: Record<string, string>;
87
- body: SyncPushRequest;
88
- }
89
-
90
- export async function postSyncPushRequest(
91
- options: PostSyncPushRequestOptions
92
- ): Promise<PostJsonWithActorResult<SyncPushResponse>> {
93
- const result = await postJsonWithActor<SyncPushRequest, unknown>(options);
94
- return {
95
- response: result.response,
96
- json: parseSyncPushResponse(result.json),
97
- };
98
- }
99
-
100
- export interface PostSyncPullRequestOptions {
101
- fetch: typeof globalThis.fetch;
102
- url: string;
103
- actorId: string;
104
- actorHeader?: string;
105
- extraHeaders?: Record<string, string>;
106
- body: SyncPullRequest;
107
- }
108
-
109
- export async function postSyncPullRequest(
110
- options: PostSyncPullRequestOptions
111
- ): Promise<PostJsonWithActorResult<SyncPullResponse>> {
112
- const result = await postJsonWithActor<SyncPullRequest, unknown>(options);
113
- return {
114
- response: result.response,
115
- json: parseSyncPullResponse(result.json),
116
- };
117
- }
package/src/sync-parse.ts DELETED
@@ -1,86 +0,0 @@
1
- import {
2
- type SyncCombinedResponse,
3
- SyncCombinedResponseSchema,
4
- type SyncPullResponse,
5
- SyncPullResponseSchema,
6
- type SyncPushResponse,
7
- SyncPushResponseSchema,
8
- } from '@syncular/core';
9
-
10
- interface ParseIssue {
11
- path: ReadonlyArray<PropertyKey>;
12
- message: string;
13
- }
14
-
15
- type ParseResult<T> =
16
- | { success: true; data: T }
17
- | { success: false; error: { issues: ReadonlyArray<ParseIssue> } };
18
-
19
- function formatIssue(issue: ParseIssue): string {
20
- const path =
21
- issue.path.length > 0
22
- ? issue.path
23
- .map((segment) =>
24
- typeof segment === 'symbol' ? segment.toString() : String(segment)
25
- )
26
- .join('.')
27
- : 'root';
28
- return `${path}: ${issue.message}`;
29
- }
30
-
31
- function formatIssues(issues: ReadonlyArray<ParseIssue>): string {
32
- return issues.map((issue) => formatIssue(issue)).join('; ');
33
- }
34
-
35
- function parseOrThrow<T>(
36
- label: string,
37
- value: unknown,
38
- parse: (input: unknown) => ParseResult<T>
39
- ): T {
40
- const parsed = parse(value);
41
- if (parsed.success) {
42
- return parsed.data;
43
- }
44
-
45
- throw new Error(
46
- `${label} validation failed: ${formatIssues(parsed.error.issues)}`
47
- );
48
- }
49
-
50
- export function parseSyncCombinedResponse(
51
- value: unknown
52
- ): SyncCombinedResponse {
53
- return parseOrThrow('SyncCombinedResponse', value, (input) =>
54
- SyncCombinedResponseSchema.safeParse(input)
55
- );
56
- }
57
-
58
- export function parseSyncPushResponse(value: unknown): SyncPushResponse {
59
- return parseOrThrow('SyncPushResponse', value, (input) =>
60
- SyncPushResponseSchema.safeParse(input)
61
- );
62
- }
63
-
64
- export function parseSyncPullResponse(value: unknown): SyncPullResponse {
65
- return parseOrThrow('SyncPullResponse', value, (input) =>
66
- SyncPullResponseSchema.safeParse(input)
67
- );
68
- }
69
-
70
- export async function readSyncCombinedResponse(
71
- response: Response
72
- ): Promise<SyncCombinedResponse> {
73
- return parseSyncCombinedResponse(await response.json());
74
- }
75
-
76
- export async function readSyncPushResponse(
77
- response: Response
78
- ): Promise<SyncPushResponse> {
79
- return parseSyncPushResponse(await response.json());
80
- }
81
-
82
- export async function readSyncPullResponse(
83
- response: Response
84
- ): Promise<SyncPullResponse> {
85
- return parseSyncPullResponse(await response.json());
86
- }
@@ -1,43 +0,0 @@
1
- import {
2
- isRecord,
3
- type SyncChange,
4
- type SyncPullResponse,
5
- } from '@syncular/core';
6
-
7
- export type SyncSubscriptionRecord = Pick<
8
- SyncPullResponse['subscriptions'][number],
9
- 'id' | 'commits'
10
- >;
11
-
12
- export function subscriptionChanges(
13
- subscriptions: SyncSubscriptionRecord[] | undefined,
14
- subscriptionId: string
15
- ): SyncChange[] {
16
- const subscription = subscriptions?.find(
17
- (item) => item.id === subscriptionId
18
- );
19
- if (!subscription) {
20
- return [];
21
- }
22
-
23
- return subscription.commits?.flatMap((commit) => commit.changes) ?? [];
24
- }
25
-
26
- export function findSubscriptionChange(
27
- subscriptions: SyncSubscriptionRecord[] | undefined,
28
- subscriptionId: string,
29
- rowId: string
30
- ): SyncChange | undefined {
31
- const changes = subscriptionChanges(subscriptions, subscriptionId);
32
- return changes.find((change) => change.row_id === rowId);
33
- }
34
-
35
- export function subscriptionChangeRow(
36
- change: SyncChange | undefined
37
- ): Record<string, unknown> | undefined {
38
- if (!change) {
39
- return undefined;
40
- }
41
-
42
- return isRecord(change.row_json) ? change.row_json : undefined;
43
- }