@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/dist/faults.d.ts
CHANGED
|
@@ -1,52 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Fault injection at the transport seam (test doctrine, REVISE B4).
|
|
3
|
+
*
|
|
4
|
+
* Canonical home: the PUBLIC app test kit. The private conformance harness
|
|
5
|
+
* re-exports from here so both arm the SAME fault vocabulary — one
|
|
6
|
+
* implementation, publishable dependency direction (testkit ships to npm;
|
|
7
|
+
* conformance stays private).
|
|
8
|
+
*
|
|
9
|
+
* The harness owns the loopback between a ClientInstance and a
|
|
10
|
+
* ServerInstance; scenarios arm faults on this controller and the next
|
|
11
|
+
* matching exchange misbehaves — deterministically, with no timers. The
|
|
12
|
+
* only randomness is the truncation offset, drawn from a seeded PRNG so
|
|
13
|
+
* every run of a scenario byte-truncates at the same place.
|
|
14
|
+
*
|
|
15
|
+
* Fault vocabulary (§2.3, §1.4, §5.1):
|
|
16
|
+
* - drop request — the request never reaches the server;
|
|
17
|
+
* - drop response — the server processed it, the ack is lost;
|
|
18
|
+
* - duplicate — the request is delivered twice (replayed bytes);
|
|
19
|
+
* - truncate — the response is cut short (decode-error surfacing);
|
|
20
|
+
* - segment faults — the same, at the segment download hop.
|
|
21
|
+
*/
|
|
22
|
+
/** Deterministic PRNG (mulberry32) — seeded per scenario. */
|
|
23
|
+
export declare function seededRandom(seed: number): () => number;
|
|
24
|
+
export declare function seedFromName(name: string): number;
|
|
25
|
+
/** The error a faulted transport rejects with (client-observable). */
|
|
26
|
+
export declare class TransportFault extends Error {
|
|
27
|
+
readonly name = "TransportFault";
|
|
28
|
+
/** Client drivers surface transport losses under this code. */
|
|
29
|
+
readonly code = "transport.lost";
|
|
12
30
|
}
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
export declare class TransportFaults {
|
|
32
|
+
#private;
|
|
33
|
+
/** Fail the next N sync requests before they reach the server. */
|
|
34
|
+
dropNextRequests: number;
|
|
35
|
+
/** Deliver the next N sync requests, then lose their responses. */
|
|
36
|
+
dropNextResponses: number;
|
|
37
|
+
/** Deliver the next sync request twice; return the second response. */
|
|
38
|
+
duplicateNextRequest: boolean;
|
|
39
|
+
/** Truncate the next sync response at a seeded offset. */
|
|
40
|
+
truncateNextResponse: boolean;
|
|
41
|
+
/** Fail the next N segment downloads before they reach the server. */
|
|
42
|
+
dropNextSegmentRequests: number;
|
|
43
|
+
/** Truncate the next segment download at a seeded offset. */
|
|
44
|
+
truncateNextSegmentDownload: boolean;
|
|
45
|
+
/** Fail the next N signed-URL fetches (the CDN hop, §5.4). */
|
|
46
|
+
dropNextUrlFetches: number;
|
|
47
|
+
/** Corrupt the next signed-URL fetch's bytes (§5.1 tamper). */
|
|
48
|
+
corruptNextUrlFetch: boolean;
|
|
49
|
+
constructor(random: () => number);
|
|
50
|
+
/** Seeded cut point: at least 1 byte kept, at least 1 byte removed. */
|
|
51
|
+
truncate(bytes: Uint8Array): Uint8Array;
|
|
52
|
+
/** Flip one seeded byte — same length, different content address. */
|
|
53
|
+
corrupt(bytes: Uint8Array): Uint8Array;
|
|
24
54
|
}
|
|
25
|
-
export interface FaultTransportState {
|
|
26
|
-
pushCount: number;
|
|
27
|
-
pullCount: number;
|
|
28
|
-
fetchCount: number;
|
|
29
|
-
failureCount: number;
|
|
30
|
-
}
|
|
31
|
-
export interface FaultTransportResult {
|
|
32
|
-
transport: SyncTransport;
|
|
33
|
-
getState: () => FaultTransportState;
|
|
34
|
-
reset: () => void;
|
|
35
|
-
setOptions: (options: Partial<FaultTransportOptions>) => void;
|
|
36
|
-
}
|
|
37
|
-
export declare function withFaults(baseTransport: SyncTransport, options?: FaultTransportOptions): FaultTransportResult;
|
|
38
|
-
export declare function createMockTransport(options?: {
|
|
39
|
-
pullResponse?: SyncPullResponse;
|
|
40
|
-
pushResponse?: SyncPushBatchResponse;
|
|
41
|
-
chunkData?: Uint8Array;
|
|
42
|
-
}): SyncTransport;
|
|
43
|
-
export interface RecordingTransportResult {
|
|
44
|
-
transport: SyncTransport;
|
|
45
|
-
syncRequests: SyncCombinedRequest[];
|
|
46
|
-
fetchRequests: {
|
|
47
|
-
chunkId: string;
|
|
48
|
-
}[];
|
|
49
|
-
clear: () => void;
|
|
50
|
-
}
|
|
51
|
-
export declare function withRecording(baseTransport: SyncTransport): RecordingTransportResult;
|
|
52
|
-
//# sourceMappingURL=faults.d.ts.map
|
package/dist/faults.js
CHANGED
|
@@ -1,198 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
31
|
-
};
|
|
32
|
-
const shouldFail = (operation, count) => {
|
|
33
|
-
if (!shouldTargetOperation(operation)) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
if (currentOptions.failAfter !== undefined &&
|
|
37
|
-
count >= currentOptions.failAfter) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
if (currentOptions.flaky !== undefined && currentOptions.flaky > 0) {
|
|
41
|
-
return Math.random() < currentOptions.flaky;
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
};
|
|
45
|
-
const getError = () => currentOptions.failWith ?? defaultError;
|
|
46
|
-
const fail = (operation, error) => {
|
|
47
|
-
state.failureCount++;
|
|
48
|
-
currentOptions.onFail?.(operation, error);
|
|
49
|
-
throw error;
|
|
50
|
-
};
|
|
51
|
-
const recordSuccess = (operation) => {
|
|
52
|
-
if (operation === 'push') {
|
|
53
|
-
state.pushCount++;
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (operation === 'pull') {
|
|
57
|
-
state.pullCount++;
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
state.fetchCount++;
|
|
61
|
-
};
|
|
62
|
-
const takePlannedDecision = (operation) => {
|
|
63
|
-
const step = currentPlan.find((candidate) => candidate.remaining > 0 &&
|
|
64
|
-
(candidate.operation === 'any' || candidate.operation === operation));
|
|
65
|
-
if (!step) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
step.remaining -= 1;
|
|
69
|
-
return {
|
|
70
|
-
action: step.action,
|
|
71
|
-
phase: step.phase,
|
|
72
|
-
error: step.failWith ?? getError(),
|
|
73
|
-
latencyMs: step.latencyMs ?? 0,
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
const runWithFaults = async (operation, count, run) => {
|
|
77
|
-
const plannedDecision = takePlannedDecision(operation);
|
|
78
|
-
await maybeDelay(plannedDecision?.latencyMs ?? 0);
|
|
79
|
-
if (plannedDecision?.action === 'fail' &&
|
|
80
|
-
plannedDecision.phase === 'before') {
|
|
81
|
-
return fail(operation, plannedDecision.error);
|
|
82
|
-
}
|
|
83
|
-
if (!plannedDecision && shouldFail(operation, count)) {
|
|
84
|
-
return fail(operation, getError());
|
|
85
|
-
}
|
|
86
|
-
const result = await run();
|
|
87
|
-
recordSuccess(operation);
|
|
88
|
-
if (plannedDecision?.action === 'fail' &&
|
|
89
|
-
plannedDecision.phase === 'after') {
|
|
90
|
-
return fail(operation, plannedDecision.error);
|
|
91
|
-
}
|
|
92
|
-
currentOptions.onSuccess?.(operation);
|
|
93
|
-
return result;
|
|
94
|
-
};
|
|
95
|
-
const transport = {
|
|
96
|
-
async sync(request, transportOptions) {
|
|
97
|
-
const operation = request.push ? 'push' : 'pull';
|
|
98
|
-
const count = operation === 'push' ? state.pushCount : state.pullCount;
|
|
99
|
-
return runWithFaults(operation, count, () => baseTransport.sync(request, transportOptions));
|
|
100
|
-
},
|
|
101
|
-
async fetchSnapshotChunk(request, transportOptions) {
|
|
102
|
-
return runWithFaults('fetch', state.fetchCount, () => baseTransport.fetchSnapshotChunk(request, transportOptions));
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
return {
|
|
106
|
-
transport,
|
|
107
|
-
getState: () => ({ ...state }),
|
|
108
|
-
reset: () => {
|
|
109
|
-
state.pushCount = 0;
|
|
110
|
-
state.pullCount = 0;
|
|
111
|
-
state.fetchCount = 0;
|
|
112
|
-
state.failureCount = 0;
|
|
113
|
-
currentPlan = createActivePlan(currentOptions.plan);
|
|
114
|
-
},
|
|
115
|
-
setOptions: (newOptions) => {
|
|
116
|
-
currentOptions = { ...currentOptions, ...newOptions };
|
|
117
|
-
if ('plan' in newOptions) {
|
|
118
|
-
currentPlan = createActivePlan(currentOptions.plan);
|
|
119
|
-
}
|
|
120
|
-
},
|
|
1
|
+
/**
|
|
2
|
+
* Fault injection at the transport seam (test doctrine, REVISE B4).
|
|
3
|
+
*
|
|
4
|
+
* Canonical home: the PUBLIC app test kit. The private conformance harness
|
|
5
|
+
* re-exports from here so both arm the SAME fault vocabulary — one
|
|
6
|
+
* implementation, publishable dependency direction (testkit ships to npm;
|
|
7
|
+
* conformance stays private).
|
|
8
|
+
*
|
|
9
|
+
* The harness owns the loopback between a ClientInstance and a
|
|
10
|
+
* ServerInstance; scenarios arm faults on this controller and the next
|
|
11
|
+
* matching exchange misbehaves — deterministically, with no timers. The
|
|
12
|
+
* only randomness is the truncation offset, drawn from a seeded PRNG so
|
|
13
|
+
* every run of a scenario byte-truncates at the same place.
|
|
14
|
+
*
|
|
15
|
+
* Fault vocabulary (§2.3, §1.4, §5.1):
|
|
16
|
+
* - drop request — the request never reaches the server;
|
|
17
|
+
* - drop response — the server processed it, the ack is lost;
|
|
18
|
+
* - duplicate — the request is delivered twice (replayed bytes);
|
|
19
|
+
* - truncate — the response is cut short (decode-error surfacing);
|
|
20
|
+
* - segment faults — the same, at the segment download hop.
|
|
21
|
+
*/
|
|
22
|
+
/** Deterministic PRNG (mulberry32) — seeded per scenario. */
|
|
23
|
+
export function seededRandom(seed) {
|
|
24
|
+
let state = seed >>> 0;
|
|
25
|
+
return () => {
|
|
26
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
27
|
+
let t = state;
|
|
28
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
29
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
30
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
121
31
|
};
|
|
122
32
|
}
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
33
|
+
export function seedFromName(name) {
|
|
34
|
+
let hash = 2166136261;
|
|
35
|
+
for (let i = 0; i < name.length; i++) {
|
|
36
|
+
hash ^= name.charCodeAt(i);
|
|
37
|
+
hash = Math.imul(hash, 16777619);
|
|
126
38
|
}
|
|
127
|
-
return
|
|
128
|
-
const remaining = step.repeat ?? 1;
|
|
129
|
-
if (remaining <= 0) {
|
|
130
|
-
return [];
|
|
131
|
-
}
|
|
132
|
-
return [
|
|
133
|
-
{
|
|
134
|
-
operation: step.operation,
|
|
135
|
-
action: step.action ?? 'fail',
|
|
136
|
-
phase: step.phase ?? 'before',
|
|
137
|
-
remaining,
|
|
138
|
-
failWith: step.failWith,
|
|
139
|
-
latencyMs: step.latencyMs,
|
|
140
|
-
},
|
|
141
|
-
];
|
|
142
|
-
});
|
|
39
|
+
return hash >>> 0;
|
|
143
40
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
result.push = options?.pushResponse ?? {
|
|
150
|
-
ok: true,
|
|
151
|
-
commits: request.push.commits.map((commit) => ({
|
|
152
|
-
ok: true,
|
|
153
|
-
clientCommitId: commit.clientCommitId,
|
|
154
|
-
status: 'applied',
|
|
155
|
-
results: commit.operations.map((_, i) => ({
|
|
156
|
-
opIndex: i,
|
|
157
|
-
status: 'applied',
|
|
158
|
-
})),
|
|
159
|
-
})),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
if (request.pull) {
|
|
163
|
-
result.pull = options?.pullResponse ?? {
|
|
164
|
-
ok: true,
|
|
165
|
-
subscriptions: [],
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
return result;
|
|
169
|
-
},
|
|
170
|
-
async fetchSnapshotChunk() {
|
|
171
|
-
return options?.chunkData ?? new Uint8Array();
|
|
172
|
-
},
|
|
173
|
-
};
|
|
41
|
+
/** The error a faulted transport rejects with (client-observable). */
|
|
42
|
+
export class TransportFault extends Error {
|
|
43
|
+
name = 'TransportFault';
|
|
44
|
+
/** Client drivers surface transport losses under this code. */
|
|
45
|
+
code = 'transport.lost';
|
|
174
46
|
}
|
|
175
|
-
export
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
47
|
+
export class TransportFaults {
|
|
48
|
+
/** Fail the next N sync requests before they reach the server. */
|
|
49
|
+
dropNextRequests = 0;
|
|
50
|
+
/** Deliver the next N sync requests, then lose their responses. */
|
|
51
|
+
dropNextResponses = 0;
|
|
52
|
+
/** Deliver the next sync request twice; return the second response. */
|
|
53
|
+
duplicateNextRequest = false;
|
|
54
|
+
/** Truncate the next sync response at a seeded offset. */
|
|
55
|
+
truncateNextResponse = false;
|
|
56
|
+
/** Fail the next N segment downloads before they reach the server. */
|
|
57
|
+
dropNextSegmentRequests = 0;
|
|
58
|
+
/** Truncate the next segment download at a seeded offset. */
|
|
59
|
+
truncateNextSegmentDownload = false;
|
|
60
|
+
/** Fail the next N signed-URL fetches (the CDN hop, §5.4). */
|
|
61
|
+
dropNextUrlFetches = 0;
|
|
62
|
+
/** Corrupt the next signed-URL fetch's bytes (§5.1 tamper). */
|
|
63
|
+
corruptNextUrlFetch = false;
|
|
64
|
+
#random;
|
|
65
|
+
constructor(random) {
|
|
66
|
+
this.#random = random;
|
|
67
|
+
}
|
|
68
|
+
/** Seeded cut point: at least 1 byte kept, at least 1 byte removed. */
|
|
69
|
+
truncate(bytes) {
|
|
70
|
+
const cut = 1 + Math.floor(this.#random() * (bytes.length - 1));
|
|
71
|
+
return bytes.slice(0, cut);
|
|
72
|
+
}
|
|
73
|
+
/** Flip one seeded byte — same length, different content address. */
|
|
74
|
+
corrupt(bytes) {
|
|
75
|
+
const out = bytes.slice();
|
|
76
|
+
const index = Math.floor(this.#random() * out.length);
|
|
77
|
+
out[index] = (out[index] ?? 0) ^ 0xff;
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
197
80
|
}
|
|
198
|
-
//# sourceMappingURL=faults.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/testkit — the app-developer test kit (TODO §2).
|
|
3
|
+
*
|
|
4
|
+
* A small, documented surface over the shipped Syncular core: an in-memory
|
|
5
|
+
* server, N real clients on bun:sqlite through an in-process loopback (no
|
|
6
|
+
* HTTP), a shared virtual clock, per-client offline/online, and the
|
|
7
|
+
* conformance harness's transport-fault vocabulary. Designed for APP tests
|
|
8
|
+
* (readable, minimal) — not conformance (no driver/pairing machinery).
|
|
9
|
+
*
|
|
10
|
+
* The React helper lives behind `@syncular/testkit/react` so the core
|
|
11
|
+
* pulls in no React. See README.md for paste-ready examples.
|
|
12
|
+
*/
|
|
13
|
+
export type { TestClient, TestClientOptions, } from './client.js';
|
|
14
|
+
export { createVirtualClock, DEFAULT_EPOCH_MS, type VirtualClock, } from './clock.js';
|
|
15
|
+
export { type CreateTestSyncOptions, createTestSync, type TestClientOverrides, type TestSync, } from './create-test-sync.js';
|
|
16
|
+
export { seededRandom, seedFromName, TransportFault, TransportFaults, } from './faults.js';
|
|
17
|
+
export { allowAllScopes, createTestServer, DEFAULT_ACTOR, DEFAULT_PARTITION, type TestServer, type TestServerOptions, } from './server.js';
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/testkit — the app-developer test kit (TODO §2).
|
|
3
|
+
*
|
|
4
|
+
* A small, documented surface over the shipped Syncular core: an in-memory
|
|
5
|
+
* server, N real clients on bun:sqlite through an in-process loopback (no
|
|
6
|
+
* HTTP), a shared virtual clock, per-client offline/online, and the
|
|
7
|
+
* conformance harness's transport-fault vocabulary. Designed for APP tests
|
|
8
|
+
* (readable, minimal) — not conformance (no driver/pairing machinery).
|
|
9
|
+
*
|
|
10
|
+
* The React helper lives behind `@syncular/testkit/react` so the core
|
|
11
|
+
* pulls in no React. See README.md for paste-ready examples.
|
|
12
|
+
*/
|
|
13
|
+
export { createVirtualClock, DEFAULT_EPOCH_MS, } from './clock.js';
|
|
14
|
+
export { createTestSync, } from './create-test-sync.js';
|
|
15
|
+
// The transport-fault controller, re-exported from the conformance harness
|
|
16
|
+
// (never duplicated) — the SAME vocabulary the reference pairing arms.
|
|
17
|
+
export { seededRandom, seedFromName, TransportFault, TransportFaults, } from './faults.js';
|
|
18
|
+
export { allowAllScopes, createTestServer, DEFAULT_ACTOR, DEFAULT_PARTITION, } from './server.js';
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { TestClient } from './client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build the `wrapper` component `renderHook` / `render` take, providing the
|
|
5
|
+
* given test client to the Syncular hook tree. Pass a `TestClient` — its
|
|
6
|
+
* `.api` (the real `SyncClient`) is supplied to `SyncProvider`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function syncWrapper(client: TestClient): (props: {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}) => ReactNode;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React helper for the test kit (`@syncular/testkit/react`).
|
|
3
|
+
*
|
|
4
|
+
* `@syncular/react`'s `SyncProvider` already accepts any `SyncClient`, so
|
|
5
|
+
* mounting hooks against a test client is just:
|
|
6
|
+
*
|
|
7
|
+
* <SyncProvider client={testClient.api}>…</SyncProvider>
|
|
8
|
+
*
|
|
9
|
+
* This module is the tiny convenience over that pattern: `syncWrapper`
|
|
10
|
+
* returns the `wrapper` prop `@testing-library/react`'s `renderHook` /
|
|
11
|
+
* `render` want, so a test never hand-writes the provider element.
|
|
12
|
+
*
|
|
13
|
+
* import { renderHook, waitFor } from '@testing-library/react';
|
|
14
|
+
* import { useSyncQuery } from '@syncular/react';
|
|
15
|
+
* import { createTestSync } from '@syncular/testkit';
|
|
16
|
+
* import { syncWrapper } from '@syncular/testkit/react';
|
|
17
|
+
*
|
|
18
|
+
* const sync = await createTestSync({ schema });
|
|
19
|
+
* const client = await sync.client('a');
|
|
20
|
+
* const { result } = renderHook(
|
|
21
|
+
* () => useSyncQuery('SELECT * FROM notes'),
|
|
22
|
+
* { wrapper: syncWrapper(client) },
|
|
23
|
+
* );
|
|
24
|
+
*
|
|
25
|
+
* `react` is an OPTIONAL peer of the kit: import this subpath only from a
|
|
26
|
+
* test that already depends on React. The core (`@syncular/testkit`)
|
|
27
|
+
* pulls in no React.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: this file uses `createElement` (no JSX) so the kit typechecks under
|
|
30
|
+
* the repo's `.ts`-only root tsconfig — same posture as `SyncProvider`.
|
|
31
|
+
*/
|
|
32
|
+
import { SyncProvider } from '@syncular/react';
|
|
33
|
+
import { createElement } from 'react';
|
|
34
|
+
/**
|
|
35
|
+
* Build the `wrapper` component `renderHook` / `render` take, providing the
|
|
36
|
+
* given test client to the Syncular hook tree. Pass a `TestClient` — its
|
|
37
|
+
* `.api` (the real `SyncClient`) is supplied to `SyncProvider`.
|
|
38
|
+
*/
|
|
39
|
+
export function syncWrapper(client) {
|
|
40
|
+
return ({ children }) => createElement(SyncProvider, { client: client.api }, children);
|
|
41
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-memory test server: one `SqliteServerStorage` (`:memory:`), a
|
|
3
|
+
* `MemorySegmentStore`, and a `RealtimeHub`, all sharing the test's virtual
|
|
4
|
+
* clock. It is the SHIPPED `@syncular/server` core — the kit adds no
|
|
5
|
+
* server logic, it only wires the pieces an app would wire in production
|
|
6
|
+
* (minus the HTTP adapter). Clients reach it through the loopback seam in
|
|
7
|
+
* `client.ts`, calling `handleSyncRequest` / `handleSegmentDownload`
|
|
8
|
+
* directly.
|
|
9
|
+
*/
|
|
10
|
+
import { type ClientSchema } from '@syncular/client';
|
|
11
|
+
import { MemorySegmentStore, type RealtimeHub, type ResolveScopes, SqliteServerStorage, type SyncRequestContext } from '@syncular/server';
|
|
12
|
+
import type { VirtualClock } from './clock.js';
|
|
13
|
+
/** Default partition every actor lives in (apps override via options). */
|
|
14
|
+
export declare const DEFAULT_PARTITION = "test";
|
|
15
|
+
/** Default actor a client is bound to when none is given. */
|
|
16
|
+
export declare const DEFAULT_ACTOR = "test-actor";
|
|
17
|
+
/**
|
|
18
|
+
* `resolveScopes` that grants every value (`'*'`) for every scope variable
|
|
19
|
+
* the schema declares — the "no authorization, just make it converge"
|
|
20
|
+
* default an app test wants. Override with your own resolver to exercise
|
|
21
|
+
* scope-scoping / revocation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function allowAllScopes(schema: ClientSchema): ResolveScopes;
|
|
24
|
+
export interface TestServerOptions {
|
|
25
|
+
readonly schema: ClientSchema;
|
|
26
|
+
readonly clock: VirtualClock;
|
|
27
|
+
readonly partition: string;
|
|
28
|
+
/** Defaults to {@link allowAllScopes}. */
|
|
29
|
+
readonly resolveScopes?: ResolveScopes;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The server half of a test sync. `ctxFor(actorId)` builds the per-request
|
|
33
|
+
* context the loopback transport feeds `handleSyncRequest`; `hub` is the
|
|
34
|
+
* realtime fanout every client's socket attaches to.
|
|
35
|
+
*/
|
|
36
|
+
export interface TestServer {
|
|
37
|
+
readonly storage: SqliteServerStorage;
|
|
38
|
+
readonly segments: MemorySegmentStore;
|
|
39
|
+
readonly hub: RealtimeHub;
|
|
40
|
+
readonly partition: string;
|
|
41
|
+
ctxFor(actorId: string): SyncRequestContext;
|
|
42
|
+
close(): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function createTestServer(options: TestServerOptions): TestServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-memory test server: one `SqliteServerStorage` (`:memory:`), a
|
|
3
|
+
* `MemorySegmentStore`, and a `RealtimeHub`, all sharing the test's virtual
|
|
4
|
+
* clock. It is the SHIPPED `@syncular/server` core — the kit adds no
|
|
5
|
+
* server logic, it only wires the pieces an app would wire in production
|
|
6
|
+
* (minus the HTTP adapter). Clients reach it through the loopback seam in
|
|
7
|
+
* `client.ts`, calling `handleSyncRequest` / `handleSegmentDownload`
|
|
8
|
+
* directly.
|
|
9
|
+
*/
|
|
10
|
+
import { compileClientSchema } from '@syncular/client';
|
|
11
|
+
import { createRealtimeHub, MemorySegmentStore, SqliteServerStorage, } from '@syncular/server';
|
|
12
|
+
/** Default partition every actor lives in (apps override via options). */
|
|
13
|
+
export const DEFAULT_PARTITION = 'test';
|
|
14
|
+
/** Default actor a client is bound to when none is given. */
|
|
15
|
+
export const DEFAULT_ACTOR = 'test-actor';
|
|
16
|
+
/**
|
|
17
|
+
* `resolveScopes` that grants every value (`'*'`) for every scope variable
|
|
18
|
+
* the schema declares — the "no authorization, just make it converge"
|
|
19
|
+
* default an app test wants. Override with your own resolver to exercise
|
|
20
|
+
* scope-scoping / revocation.
|
|
21
|
+
*/
|
|
22
|
+
export function allowAllScopes(schema) {
|
|
23
|
+
const compiled = compileClientSchema(schema);
|
|
24
|
+
const variables = new Set();
|
|
25
|
+
for (const table of compiled.tables.values()) {
|
|
26
|
+
for (const variable of table.scopeColumnByVariable.keys()) {
|
|
27
|
+
variables.add(variable);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const allowed = {};
|
|
31
|
+
for (const variable of variables)
|
|
32
|
+
allowed[variable] = ['*'];
|
|
33
|
+
return () => allowed;
|
|
34
|
+
}
|
|
35
|
+
export function createTestServer(options) {
|
|
36
|
+
const { schema, clock, partition } = options;
|
|
37
|
+
const serverSchema = schema;
|
|
38
|
+
const storage = new SqliteServerStorage();
|
|
39
|
+
const segments = new MemorySegmentStore();
|
|
40
|
+
const resolveScopes = options.resolveScopes ?? allowAllScopes(schema);
|
|
41
|
+
const clockFn = () => clock.now();
|
|
42
|
+
const hub = createRealtimeHub({
|
|
43
|
+
schema: serverSchema,
|
|
44
|
+
storage,
|
|
45
|
+
resolveScopes,
|
|
46
|
+
clock: clockFn,
|
|
47
|
+
segments,
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
storage,
|
|
51
|
+
segments,
|
|
52
|
+
hub,
|
|
53
|
+
partition,
|
|
54
|
+
ctxFor: (actorId) => ({
|
|
55
|
+
partition,
|
|
56
|
+
actorId,
|
|
57
|
+
schema: serverSchema,
|
|
58
|
+
storage,
|
|
59
|
+
segments,
|
|
60
|
+
resolveScopes,
|
|
61
|
+
clock: clockFn,
|
|
62
|
+
realtime: hub,
|
|
63
|
+
}),
|
|
64
|
+
close: () => {
|
|
65
|
+
storage.db.close();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|