@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.
Files changed (92) 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 -15
  11. package/dist/index.js +18 -15
  12. package/dist/react.d.ts +10 -0
  13. package/dist/react.js +41 -0
  14. package/dist/server.d.ts +44 -0
  15. package/dist/server.js +68 -0
  16. package/package.json +52 -29
  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 -14
  22. package/src/react.ts +46 -0
  23. package/src/server.ts +102 -0
  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/sync-builders.d.ts +0 -48
  65. package/dist/sync-builders.d.ts.map +0 -1
  66. package/dist/sync-builders.js +0 -66
  67. package/dist/sync-builders.js.map +0 -1
  68. package/dist/sync-http.d.ts +0 -48
  69. package/dist/sync-http.d.ts.map +0 -1
  70. package/dist/sync-http.js +0 -43
  71. package/dist/sync-http.js.map +0 -1
  72. package/dist/sync-parse.d.ts +0 -8
  73. package/dist/sync-parse.d.ts.map +0 -1
  74. package/dist/sync-parse.js +0 -38
  75. package/dist/sync-parse.js.map +0 -1
  76. package/dist/sync-response.d.ts +0 -6
  77. package/dist/sync-response.d.ts.map +0 -1
  78. package/dist/sync-response.js +0 -19
  79. package/dist/sync-response.js.map +0 -1
  80. package/src/audit.ts +0 -74
  81. package/src/client-bridge.ts +0 -841
  82. package/src/deterministic.ts +0 -121
  83. package/src/disposable.ts +0 -50
  84. package/src/hono-node-server.ts +0 -144
  85. package/src/http-fixtures.ts +0 -187
  86. package/src/project-scoped-tasks.ts +0 -351
  87. package/src/realtime-ws.ts +0 -354
  88. package/src/runtime-process.ts +0 -159
  89. package/src/sync-builders.ts +0 -136
  90. package/src/sync-http.ts +0 -117
  91. package/src/sync-parse.ts +0 -86
  92. package/src/sync-response.ts +0 -43
package/src/faults.ts CHANGED
@@ -1,349 +1,88 @@
1
- import type {
2
- SyncCombinedRequest,
3
- SyncPullResponse,
4
- SyncPushBatchResponse,
5
- SyncTransport,
6
- SyncTransportOptions,
7
- } from '@syncular/core';
8
-
9
- export type FaultTransportOperation = 'push' | 'pull' | 'fetch';
10
-
11
- export type FaultTransportAction = 'pass' | 'fail';
12
-
13
- export type FaultTransportPhase = 'before' | 'after';
14
-
15
- export interface FaultPlanStep {
16
- operation: FaultTransportOperation | 'any';
17
- action?: FaultTransportAction;
18
- phase?: FaultTransportPhase;
19
- repeat?: number;
20
- failWith?: Error;
21
- latencyMs?: number;
22
- }
23
-
24
- export interface FaultTransportOptions {
25
- failAfter?: number;
26
- failWith?: Error;
27
- latencyMs?: number;
28
- flaky?: number;
29
- failOnPush?: boolean;
30
- failOnPull?: boolean;
31
- failOnFetch?: boolean;
32
- plan?: FaultPlanStep[];
33
- onFail?: (operation: 'push' | 'pull' | 'fetch', error: Error) => void;
34
- onSuccess?: (operation: 'push' | 'pull' | 'fetch') => void;
35
- }
36
-
37
- export interface FaultTransportState {
38
- pushCount: number;
39
- pullCount: number;
40
- fetchCount: number;
41
- failureCount: number;
42
- }
43
-
44
- export interface FaultTransportResult {
45
- transport: SyncTransport;
46
- getState: () => FaultTransportState;
47
- reset: () => void;
48
- setOptions: (options: Partial<FaultTransportOptions>) => void;
49
- }
50
-
51
- interface ActiveFaultPlanStep {
52
- operation: FaultPlanStep['operation'];
53
- action: FaultTransportAction;
54
- phase: FaultTransportPhase;
55
- remaining: number;
56
- failWith?: Error;
57
- latencyMs?: number;
58
- }
59
-
60
- interface PlannedFaultDecision {
61
- action: FaultTransportAction;
62
- phase: FaultTransportPhase;
63
- error: Error;
64
- latencyMs: number;
65
- }
66
-
67
- export function withFaults(
68
- baseTransport: SyncTransport,
69
- options: FaultTransportOptions = {}
70
- ): FaultTransportResult {
71
- let currentOptions = { ...options };
72
- let currentPlan = createActivePlan(currentOptions.plan);
73
- const state: FaultTransportState = {
74
- pushCount: 0,
75
- pullCount: 0,
76
- fetchCount: 0,
77
- failureCount: 0,
78
- };
79
-
80
- const defaultError = new Error('Simulated transport error');
81
-
82
- const maybeDelay = async (extraLatencyMs = 0): Promise<void> => {
83
- const latencyMs = (currentOptions.latencyMs ?? 0) + extraLatencyMs;
84
- if (latencyMs > 0) {
85
- await new Promise((resolve) => setTimeout(resolve, latencyMs));
86
- }
87
- };
88
-
89
- const shouldTargetOperation = (
90
- operation: FaultTransportOperation
91
- ): boolean => {
92
- const hasTargets =
93
- currentOptions.failOnPush === true ||
94
- currentOptions.failOnPull === true ||
95
- currentOptions.failOnFetch === true;
96
-
97
- if (!hasTargets) {
98
- return true;
99
- }
100
-
101
- if (operation === 'push') {
102
- return currentOptions.failOnPush === true;
103
- }
104
- if (operation === 'pull') {
105
- return currentOptions.failOnPull === true;
106
- }
107
- return currentOptions.failOnFetch === true;
108
- };
109
-
110
- const shouldFail = (
111
- operation: FaultTransportOperation,
112
- count: number
113
- ): boolean => {
114
- if (!shouldTargetOperation(operation)) {
115
- return false;
116
- }
117
-
118
- if (
119
- currentOptions.failAfter !== undefined &&
120
- count >= currentOptions.failAfter
121
- ) {
122
- return true;
123
- }
124
-
125
- if (currentOptions.flaky !== undefined && currentOptions.flaky > 0) {
126
- return Math.random() < currentOptions.flaky;
127
- }
128
-
129
- return false;
130
- };
131
-
132
- const getError = (): Error => currentOptions.failWith ?? defaultError;
133
-
134
- const fail = (operation: FaultTransportOperation, error: Error): never => {
135
- state.failureCount++;
136
- currentOptions.onFail?.(operation, error);
137
- throw error;
138
- };
139
-
140
- const recordSuccess = (operation: FaultTransportOperation): void => {
141
- if (operation === 'push') {
142
- state.pushCount++;
143
- return;
144
- }
145
- if (operation === 'pull') {
146
- state.pullCount++;
147
- return;
148
- }
149
- state.fetchCount++;
150
- };
151
-
152
- const takePlannedDecision = (
153
- operation: FaultTransportOperation
154
- ): PlannedFaultDecision | null => {
155
- const step = currentPlan.find(
156
- (candidate) =>
157
- candidate.remaining > 0 &&
158
- (candidate.operation === 'any' || candidate.operation === operation)
159
- );
160
- if (!step) {
161
- return null;
162
- }
163
-
164
- step.remaining -= 1;
165
- return {
166
- action: step.action,
167
- phase: step.phase,
168
- error: step.failWith ?? getError(),
169
- latencyMs: step.latencyMs ?? 0,
170
- };
171
- };
172
-
173
- const runWithFaults = async <Result>(
174
- operation: FaultTransportOperation,
175
- count: number,
176
- run: () => Promise<Result>
177
- ): Promise<Result> => {
178
- const plannedDecision = takePlannedDecision(operation);
179
- await maybeDelay(plannedDecision?.latencyMs ?? 0);
180
-
181
- if (
182
- plannedDecision?.action === 'fail' &&
183
- plannedDecision.phase === 'before'
184
- ) {
185
- return fail(operation, plannedDecision.error);
186
- }
187
-
188
- if (!plannedDecision && shouldFail(operation, count)) {
189
- return fail(operation, getError());
190
- }
191
-
192
- const result = await run();
193
- recordSuccess(operation);
194
-
195
- if (
196
- plannedDecision?.action === 'fail' &&
197
- plannedDecision.phase === 'after'
198
- ) {
199
- return fail(operation, plannedDecision.error);
200
- }
201
-
202
- currentOptions.onSuccess?.(operation);
203
- return result;
204
- };
205
-
206
- const transport: SyncTransport = {
207
- async sync(request, transportOptions) {
208
- const operation: FaultTransportOperation = request.push ? 'push' : 'pull';
209
- const count = operation === 'push' ? state.pushCount : state.pullCount;
210
-
211
- return runWithFaults(operation, count, () =>
212
- baseTransport.sync(request, transportOptions)
213
- );
214
- },
215
-
216
- async fetchSnapshotChunk(
217
- request: { chunkId: string },
218
- transportOptions?: SyncTransportOptions
219
- ): Promise<Uint8Array> {
220
- return runWithFaults('fetch', state.fetchCount, () =>
221
- baseTransport.fetchSnapshotChunk(request, transportOptions)
222
- );
223
- },
224
- };
225
-
226
- return {
227
- transport,
228
- getState: () => ({ ...state }),
229
- reset: () => {
230
- state.pushCount = 0;
231
- state.pullCount = 0;
232
- state.fetchCount = 0;
233
- state.failureCount = 0;
234
- currentPlan = createActivePlan(currentOptions.plan);
235
- },
236
- setOptions: (newOptions) => {
237
- currentOptions = { ...currentOptions, ...newOptions };
238
- if ('plan' in newOptions) {
239
- currentPlan = createActivePlan(currentOptions.plan);
240
- }
241
- },
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
+
23
+ /** Deterministic PRNG (mulberry32) — seeded per scenario. */
24
+ export function seededRandom(seed: number): () => number {
25
+ let state = seed >>> 0;
26
+ return () => {
27
+ state = (state + 0x6d2b79f5) >>> 0;
28
+ let t = state;
29
+ t = Math.imul(t ^ (t >>> 15), t | 1);
30
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
31
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
242
32
  };
243
33
  }
244
34
 
245
- function createActivePlan(
246
- plan: FaultPlanStep[] | undefined
247
- ): ActiveFaultPlanStep[] {
248
- if (!plan || plan.length === 0) {
249
- return [];
35
+ export function seedFromName(name: string): number {
36
+ let hash = 2166136261;
37
+ for (let i = 0; i < name.length; i++) {
38
+ hash ^= name.charCodeAt(i);
39
+ hash = Math.imul(hash, 16777619);
250
40
  }
251
-
252
- return plan.flatMap((step) => {
253
- const remaining = step.repeat ?? 1;
254
- if (remaining <= 0) {
255
- return [];
256
- }
257
-
258
- return [
259
- {
260
- operation: step.operation,
261
- action: step.action ?? 'fail',
262
- phase: step.phase ?? 'before',
263
- remaining,
264
- failWith: step.failWith,
265
- latencyMs: step.latencyMs,
266
- },
267
- ];
268
- });
269
- }
270
-
271
- export function createMockTransport(options?: {
272
- pullResponse?: SyncPullResponse;
273
- pushResponse?: SyncPushBatchResponse;
274
- chunkData?: Uint8Array;
275
- }): SyncTransport {
276
- return {
277
- async sync(request) {
278
- const result: {
279
- ok: true;
280
- push?: SyncPushBatchResponse;
281
- pull?: SyncPullResponse;
282
- } = { ok: true };
283
-
284
- if (request.push) {
285
- result.push = options?.pushResponse ?? {
286
- ok: true,
287
- commits: request.push.commits.map((commit) => ({
288
- ok: true,
289
- clientCommitId: commit.clientCommitId,
290
- status: 'applied',
291
- results: commit.operations.map((_: unknown, i: number) => ({
292
- opIndex: i,
293
- status: 'applied',
294
- })),
295
- })),
296
- };
297
- }
298
-
299
- if (request.pull) {
300
- result.pull = options?.pullResponse ?? {
301
- ok: true,
302
- subscriptions: [],
303
- };
304
- }
305
-
306
- return result;
307
- },
308
-
309
- async fetchSnapshotChunk(): Promise<Uint8Array> {
310
- return options?.chunkData ?? new Uint8Array();
311
- },
312
- };
41
+ return hash >>> 0;
313
42
  }
314
43
 
315
- export interface RecordingTransportResult {
316
- transport: SyncTransport;
317
- syncRequests: SyncCombinedRequest[];
318
- fetchRequests: { chunkId: string }[];
319
- clear: () => void;
44
+ /** The error a faulted transport rejects with (client-observable). */
45
+ export class TransportFault extends Error {
46
+ override readonly name = 'TransportFault';
47
+ /** Client drivers surface transport losses under this code. */
48
+ readonly code = 'transport.lost';
320
49
  }
321
50
 
322
- export function withRecording(
323
- baseTransport: SyncTransport
324
- ): RecordingTransportResult {
325
- const syncRequests: SyncCombinedRequest[] = [];
326
- const fetchRequests: { chunkId: string }[] = [];
327
-
328
- const transport: SyncTransport = {
329
- async sync(request, options) {
330
- syncRequests.push(structuredClone(request));
331
- return baseTransport.sync(request, options);
332
- },
51
+ export class TransportFaults {
52
+ /** Fail the next N sync requests before they reach the server. */
53
+ dropNextRequests = 0;
54
+ /** Deliver the next N sync requests, then lose their responses. */
55
+ dropNextResponses = 0;
56
+ /** Deliver the next sync request twice; return the second response. */
57
+ duplicateNextRequest = false;
58
+ /** Truncate the next sync response at a seeded offset. */
59
+ truncateNextResponse = false;
60
+ /** Fail the next N segment downloads before they reach the server. */
61
+ dropNextSegmentRequests = 0;
62
+ /** Truncate the next segment download at a seeded offset. */
63
+ truncateNextSegmentDownload = false;
64
+ /** Fail the next N signed-URL fetches (the CDN hop, §5.4). */
65
+ dropNextUrlFetches = 0;
66
+ /** Corrupt the next signed-URL fetch's bytes (§5.1 tamper). */
67
+ corruptNextUrlFetch = false;
68
+
69
+ readonly #random: () => number;
70
+
71
+ constructor(random: () => number) {
72
+ this.#random = random;
73
+ }
333
74
 
334
- async fetchSnapshotChunk(request, options) {
335
- fetchRequests.push({ ...request });
336
- return baseTransport.fetchSnapshotChunk(request, options);
337
- },
338
- };
75
+ /** Seeded cut point: at least 1 byte kept, at least 1 byte removed. */
76
+ truncate(bytes: Uint8Array): Uint8Array {
77
+ const cut = 1 + Math.floor(this.#random() * (bytes.length - 1));
78
+ return bytes.slice(0, cut);
79
+ }
339
80
 
340
- return {
341
- transport,
342
- syncRequests,
343
- fetchRequests,
344
- clear: () => {
345
- syncRequests.length = 0;
346
- fetchRequests.length = 0;
347
- },
348
- };
81
+ /** Flip one seeded byte — same length, different content address. */
82
+ corrupt(bytes: Uint8Array): Uint8Array {
83
+ const out = bytes.slice();
84
+ const index = Math.floor(this.#random() * out.length);
85
+ out[index] = (out[index] ?? 0) ^ 0xff;
86
+ return out;
87
+ }
349
88
  }
package/src/index.ts CHANGED
@@ -1,14 +1,44 @@
1
- export * from './audit';
2
- export * from './client-bridge';
3
- export * from './deterministic';
4
- export * from './disposable';
5
- export * from './faults';
6
- export * from './hono-node-server';
7
- export * from './http-fixtures';
8
- export * from './project-scoped-tasks';
9
- export * from './realtime-ws';
10
- export * from './runtime-process';
11
- export * from './sync-builders';
12
- export * from './sync-http';
13
- export * from './sync-parse';
14
- export * from './sync-response';
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
+
14
+ export type {
15
+ TestClient,
16
+ TestClientOptions,
17
+ } from './client';
18
+ export {
19
+ createVirtualClock,
20
+ DEFAULT_EPOCH_MS,
21
+ type VirtualClock,
22
+ } from './clock';
23
+ export {
24
+ type CreateTestSyncOptions,
25
+ createTestSync,
26
+ type TestClientOverrides,
27
+ type TestSync,
28
+ } from './create-test-sync';
29
+ // The transport-fault controller, re-exported from the conformance harness
30
+ // (never duplicated) — the SAME vocabulary the reference pairing arms.
31
+ export {
32
+ seededRandom,
33
+ seedFromName,
34
+ TransportFault,
35
+ TransportFaults,
36
+ } from './faults';
37
+ export {
38
+ allowAllScopes,
39
+ createTestServer,
40
+ DEFAULT_ACTOR,
41
+ DEFAULT_PARTITION,
42
+ type TestServer,
43
+ type TestServerOptions,
44
+ } from './server';
package/src/react.ts ADDED
@@ -0,0 +1,46 @@
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, type ReactNode } from 'react';
34
+ import type { TestClient } from './client';
35
+
36
+ /**
37
+ * Build the `wrapper` component `renderHook` / `render` take, providing the
38
+ * given test client to the Syncular hook tree. Pass a `TestClient` — its
39
+ * `.api` (the real `SyncClient`) is supplied to `SyncProvider`.
40
+ */
41
+ export function syncWrapper(
42
+ client: TestClient,
43
+ ): (props: { children: ReactNode }) => ReactNode {
44
+ return ({ children }) =>
45
+ createElement(SyncProvider, { client: client.api }, children);
46
+ }
package/src/server.ts ADDED
@@ -0,0 +1,102 @@
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
+
11
+ import { type ClientSchema, compileClientSchema } from '@syncular/client';
12
+ import {
13
+ createRealtimeHub,
14
+ MemorySegmentStore,
15
+ type RealtimeHub,
16
+ type ResolveScopes,
17
+ type ServerSchema,
18
+ SqliteServerStorage,
19
+ type SyncRequestContext,
20
+ } from '@syncular/server';
21
+ import type { VirtualClock } from './clock';
22
+
23
+ /** Default partition every actor lives in (apps override via options). */
24
+ export const DEFAULT_PARTITION = 'test';
25
+ /** Default actor a client is bound to when none is given. */
26
+ export const DEFAULT_ACTOR = 'test-actor';
27
+
28
+ /**
29
+ * `resolveScopes` that grants every value (`'*'`) for every scope variable
30
+ * the schema declares — the "no authorization, just make it converge"
31
+ * default an app test wants. Override with your own resolver to exercise
32
+ * scope-scoping / revocation.
33
+ */
34
+ export function allowAllScopes(schema: ClientSchema): ResolveScopes {
35
+ const compiled = compileClientSchema(schema);
36
+ const variables = new Set<string>();
37
+ for (const table of compiled.tables.values()) {
38
+ for (const variable of table.scopeColumnByVariable.keys()) {
39
+ variables.add(variable);
40
+ }
41
+ }
42
+ const allowed: Record<string, string[]> = {};
43
+ for (const variable of variables) allowed[variable] = ['*'];
44
+ return () => allowed;
45
+ }
46
+
47
+ export interface TestServerOptions {
48
+ readonly schema: ClientSchema;
49
+ readonly clock: VirtualClock;
50
+ readonly partition: string;
51
+ /** Defaults to {@link allowAllScopes}. */
52
+ readonly resolveScopes?: ResolveScopes;
53
+ }
54
+
55
+ /**
56
+ * The server half of a test sync. `ctxFor(actorId)` builds the per-request
57
+ * context the loopback transport feeds `handleSyncRequest`; `hub` is the
58
+ * realtime fanout every client's socket attaches to.
59
+ */
60
+ export interface TestServer {
61
+ readonly storage: SqliteServerStorage;
62
+ readonly segments: MemorySegmentStore;
63
+ readonly hub: RealtimeHub;
64
+ readonly partition: string;
65
+ ctxFor(actorId: string): SyncRequestContext;
66
+ close(): void;
67
+ }
68
+
69
+ export function createTestServer(options: TestServerOptions): TestServer {
70
+ const { schema, clock, partition } = options;
71
+ const serverSchema = schema as ServerSchema;
72
+ const storage = new SqliteServerStorage();
73
+ const segments = new MemorySegmentStore();
74
+ const resolveScopes = options.resolveScopes ?? allowAllScopes(schema);
75
+ const clockFn = () => clock.now();
76
+ const hub = createRealtimeHub({
77
+ schema: serverSchema,
78
+ storage,
79
+ resolveScopes,
80
+ clock: clockFn,
81
+ segments,
82
+ });
83
+ return {
84
+ storage,
85
+ segments,
86
+ hub,
87
+ partition,
88
+ ctxFor: (actorId) => ({
89
+ partition,
90
+ actorId,
91
+ schema: serverSchema,
92
+ storage,
93
+ segments,
94
+ resolveScopes,
95
+ clock: clockFn,
96
+ realtime: hub,
97
+ }),
98
+ close: () => {
99
+ storage.db.close();
100
+ },
101
+ };
102
+ }
package/dist/audit.d.ts DELETED
@@ -1,22 +0,0 @@
1
- export interface RedactedAuditChange {
2
- redaction?: {
3
- payload?: unknown;
4
- reason?: unknown;
5
- };
6
- rowJson?: unknown;
7
- scopes?: unknown;
8
- [key: string]: unknown;
9
- }
10
- export interface RedactedAuditDebugExport {
11
- commits?: Array<{
12
- changes?: RedactedAuditChange[];
13
- [key: string]: unknown;
14
- }>;
15
- [key: string]: unknown;
16
- }
17
- export declare function assertAuditChangeRedacted(change: RedactedAuditChange, message?: string): void;
18
- export declare function assertAuditJsonExcludes(value: unknown, forbiddenNeedles: readonly string[], message?: string): void;
19
- export declare function assertAuditDebugExportRedacted(value: RedactedAuditDebugExport, forbiddenNeedles?: readonly string[], message?: string): void;
20
- export declare function readJsonResponse<T>(response: Response): Promise<T>;
21
- export declare function readTextResponse(response: Response): Promise<string>;
22
- //# sourceMappingURL=audit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,KAAK,CAAC;QACd,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;QAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,SAAyC,GAC/C,IAAI,CAeN;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,gBAAgB,EAAE,SAAS,MAAM,EAAE,EACnC,OAAO,SAAwC,GAC9C,IAAI,CAQN;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,wBAAwB,EAC/B,gBAAgB,GAAE,SAAS,MAAM,EAAO,EACxC,OAAO,SAA+C,GACrD,IAAI,CAON;AAED,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAExE;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}