@syncular/testkit 0.0.6-96 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.d.ts +22 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +40 -0
- package/dist/audit.js.map +1 -0
- package/dist/client-bridge.d.ts +77 -0
- package/dist/client-bridge.d.ts.map +1 -0
- package/dist/client-bridge.js +540 -0
- package/dist/client-bridge.js.map +1 -0
- package/dist/deterministic.js.map +1 -1
- package/dist/disposable.js.map +1 -1
- package/dist/faults.d.ts +14 -2
- package/dist/faults.d.ts.map +1 -1
- package/dist/faults.js +104 -42
- package/dist/faults.js.map +1 -1
- package/dist/hono-node-server.js +1 -1
- package/dist/hono-node-server.js.map +1 -1
- package/dist/http-fixtures.d.ts +1 -30
- package/dist/http-fixtures.d.ts.map +1 -1
- package/dist/http-fixtures.js +5 -65
- package/dist/http-fixtures.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/project-scoped-tasks.d.ts.map +1 -1
- package/dist/project-scoped-tasks.js +6 -4
- package/dist/project-scoped-tasks.js.map +1 -1
- package/dist/realtime-ws.js.map +1 -1
- package/dist/runtime-process.js.map +1 -1
- package/dist/sync-builders.d.ts +5 -3
- package/dist/sync-builders.d.ts.map +1 -1
- package/dist/sync-builders.js +9 -3
- package/dist/sync-builders.js.map +1 -1
- package/dist/sync-http.js.map +1 -1
- package/dist/sync-parse.js.map +1 -1
- package/dist/sync-response.d.ts +3 -4
- package/dist/sync-response.d.ts.map +1 -1
- package/dist/sync-response.js.map +1 -1
- package/package.json +14 -15
- package/src/audit.ts +74 -0
- package/src/client-bridge.ts +841 -0
- package/src/faults.ts +178 -55
- package/src/hono-node-server.ts +1 -1
- package/src/http-fixtures.ts +6 -127
- package/src/index.ts +2 -4
- package/src/project-scoped-tasks.ts +6 -4
- package/src/sync-builders.ts +14 -6
- package/src/sync-response.ts +3 -5
- package/dist/assertions.d.ts +0 -61
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -239
- package/dist/assertions.js.map +0 -1
- package/dist/fixtures.d.ts +0 -104
- package/dist/fixtures.d.ts.map +0 -1
- package/dist/fixtures.js +0 -558
- package/dist/fixtures.js.map +0 -1
- package/dist/resources.d.ts +0 -25
- package/dist/resources.d.ts.map +0 -1
- package/dist/resources.js +0 -64
- package/dist/resources.js.map +0 -1
- package/dist/scenario-flow.d.ts +0 -39
- package/dist/scenario-flow.d.ts.map +0 -1
- package/dist/scenario-flow.js +0 -41
- package/dist/scenario-flow.js.map +0 -1
- package/src/assertions.ts +0 -432
- package/src/fixtures.ts +0 -867
- package/src/resources.ts +0 -186
- package/src/scenario-flow.ts +0 -143
package/src/faults.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
SyncCombinedRequest,
|
|
3
3
|
SyncPullResponse,
|
|
4
|
-
|
|
4
|
+
SyncPushBatchResponse,
|
|
5
5
|
SyncTransport,
|
|
6
6
|
SyncTransportOptions,
|
|
7
7
|
} from '@syncular/core';
|
|
8
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
|
+
|
|
9
24
|
export interface FaultTransportOptions {
|
|
10
25
|
failAfter?: number;
|
|
11
26
|
failWith?: Error;
|
|
@@ -14,6 +29,7 @@ export interface FaultTransportOptions {
|
|
|
14
29
|
failOnPush?: boolean;
|
|
15
30
|
failOnPull?: boolean;
|
|
16
31
|
failOnFetch?: boolean;
|
|
32
|
+
plan?: FaultPlanStep[];
|
|
17
33
|
onFail?: (operation: 'push' | 'pull' | 'fetch', error: Error) => void;
|
|
18
34
|
onSuccess?: (operation: 'push' | 'pull' | 'fetch') => void;
|
|
19
35
|
}
|
|
@@ -32,11 +48,28 @@ export interface FaultTransportResult {
|
|
|
32
48
|
setOptions: (options: Partial<FaultTransportOptions>) => void;
|
|
33
49
|
}
|
|
34
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
|
+
|
|
35
67
|
export function withFaults(
|
|
36
68
|
baseTransport: SyncTransport,
|
|
37
69
|
options: FaultTransportOptions = {}
|
|
38
70
|
): FaultTransportResult {
|
|
39
71
|
let currentOptions = { ...options };
|
|
72
|
+
let currentPlan = createActivePlan(currentOptions.plan);
|
|
40
73
|
const state: FaultTransportState = {
|
|
41
74
|
pushCount: 0,
|
|
42
75
|
pullCount: 0,
|
|
@@ -46,29 +79,41 @@ export function withFaults(
|
|
|
46
79
|
|
|
47
80
|
const defaultError = new Error('Simulated transport error');
|
|
48
81
|
|
|
49
|
-
const maybeDelay = async (): Promise<void> => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
54
103
|
}
|
|
104
|
+
if (operation === 'pull') {
|
|
105
|
+
return currentOptions.failOnPull === true;
|
|
106
|
+
}
|
|
107
|
+
return currentOptions.failOnFetch === true;
|
|
55
108
|
};
|
|
56
109
|
|
|
57
110
|
const shouldFail = (
|
|
58
|
-
operation:
|
|
111
|
+
operation: FaultTransportOperation,
|
|
59
112
|
count: number
|
|
60
113
|
): boolean => {
|
|
61
|
-
if (operation
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
if (operation === 'pull' && currentOptions.failOnPush) {
|
|
114
|
+
if (!shouldTargetOperation(operation)) {
|
|
65
115
|
return false;
|
|
66
116
|
}
|
|
67
|
-
if (operation === 'fetch' && !currentOptions.failOnFetch) {
|
|
68
|
-
if (currentOptions.failOnPush || currentOptions.failOnPull) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
117
|
|
|
73
118
|
if (
|
|
74
119
|
currentOptions.failAfter !== undefined &&
|
|
@@ -86,51 +131,95 @@ export function withFaults(
|
|
|
86
131
|
|
|
87
132
|
const getError = (): Error => currentOptions.failWith ?? defaultError;
|
|
88
133
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
134
|
+
const fail = (operation: FaultTransportOperation, error: Error): never => {
|
|
135
|
+
state.failureCount++;
|
|
136
|
+
currentOptions.onFail?.(operation, error);
|
|
137
|
+
throw error;
|
|
138
|
+
};
|
|
92
139
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
};
|
|
95
151
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
}
|
|
102
163
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
};
|
|
108
172
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
);
|
|
112
214
|
},
|
|
113
215
|
|
|
114
216
|
async fetchSnapshotChunk(
|
|
115
217
|
request: { chunkId: string },
|
|
116
218
|
transportOptions?: SyncTransportOptions
|
|
117
219
|
): Promise<Uint8Array> {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (shouldFail('fetch', state.fetchCount)) {
|
|
121
|
-
const error = getError();
|
|
122
|
-
state.failureCount++;
|
|
123
|
-
currentOptions.onFail?.('fetch', error);
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
state.fetchCount++;
|
|
128
|
-
const result = await baseTransport.fetchSnapshotChunk(
|
|
129
|
-
request,
|
|
130
|
-
transportOptions
|
|
220
|
+
return runWithFaults('fetch', state.fetchCount, () =>
|
|
221
|
+
baseTransport.fetchSnapshotChunk(request, transportOptions)
|
|
131
222
|
);
|
|
132
|
-
currentOptions.onSuccess?.('fetch');
|
|
133
|
-
return result;
|
|
134
223
|
},
|
|
135
224
|
};
|
|
136
225
|
|
|
@@ -142,33 +231,67 @@ export function withFaults(
|
|
|
142
231
|
state.pullCount = 0;
|
|
143
232
|
state.fetchCount = 0;
|
|
144
233
|
state.failureCount = 0;
|
|
234
|
+
currentPlan = createActivePlan(currentOptions.plan);
|
|
145
235
|
},
|
|
146
236
|
setOptions: (newOptions) => {
|
|
147
237
|
currentOptions = { ...currentOptions, ...newOptions };
|
|
238
|
+
if ('plan' in newOptions) {
|
|
239
|
+
currentPlan = createActivePlan(currentOptions.plan);
|
|
240
|
+
}
|
|
148
241
|
},
|
|
149
242
|
};
|
|
150
243
|
}
|
|
151
244
|
|
|
245
|
+
function createActivePlan(
|
|
246
|
+
plan: FaultPlanStep[] | undefined
|
|
247
|
+
): ActiveFaultPlanStep[] {
|
|
248
|
+
if (!plan || plan.length === 0) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
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
|
+
|
|
152
271
|
export function createMockTransport(options?: {
|
|
153
272
|
pullResponse?: SyncPullResponse;
|
|
154
|
-
pushResponse?:
|
|
273
|
+
pushResponse?: SyncPushBatchResponse;
|
|
155
274
|
chunkData?: Uint8Array;
|
|
156
275
|
}): SyncTransport {
|
|
157
276
|
return {
|
|
158
277
|
async sync(request) {
|
|
159
278
|
const result: {
|
|
160
279
|
ok: true;
|
|
161
|
-
push?:
|
|
280
|
+
push?: SyncPushBatchResponse;
|
|
162
281
|
pull?: SyncPullResponse;
|
|
163
282
|
} = { ok: true };
|
|
164
283
|
|
|
165
284
|
if (request.push) {
|
|
166
285
|
result.push = options?.pushResponse ?? {
|
|
167
286
|
ok: true,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
287
|
+
commits: request.push.commits.map((commit) => ({
|
|
288
|
+
ok: true,
|
|
289
|
+
clientCommitId: commit.clientCommitId,
|
|
171
290
|
status: 'applied',
|
|
291
|
+
results: commit.operations.map((_: unknown, i: number) => ({
|
|
292
|
+
opIndex: i,
|
|
293
|
+
status: 'applied',
|
|
294
|
+
})),
|
|
172
295
|
})),
|
|
173
296
|
};
|
|
174
297
|
}
|
package/src/hono-node-server.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function createNodeHonoServer(
|
|
|
30
30
|
options?.corsAllowMethods ?? 'GET, POST, PUT, DELETE, OPTIONS';
|
|
31
31
|
const corsAllowHeaders =
|
|
32
32
|
options?.corsAllowHeaders ??
|
|
33
|
-
'content-type, x-actor-id, x-syncular-transport-path, x-user-id';
|
|
33
|
+
'authorization, content-type, x-actor-id, x-partition-id, x-syncular-schema-version, x-syncular-snapshot-scopes, x-syncular-transport-path, x-upload-token, x-user-id';
|
|
34
34
|
const corsMaxAgeSeconds = options?.corsMaxAgeSeconds ?? 86400;
|
|
35
35
|
|
|
36
36
|
return createServer(async (req, res) => {
|
package/src/http-fixtures.ts
CHANGED
|
@@ -1,40 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ClientHandlerCollection,
|
|
3
|
-
enqueueOutboxCommit,
|
|
4
|
-
ensureClientSyncSchema,
|
|
5
|
-
type SyncClientDb,
|
|
6
|
-
type SyncOnceOptions,
|
|
7
|
-
type SyncOnceResult,
|
|
8
|
-
type SyncPullOnceOptions,
|
|
9
|
-
type SyncPullResponse,
|
|
10
|
-
type SyncPushOnceOptions,
|
|
11
|
-
type SyncPushOnceResult,
|
|
12
|
-
syncOnce,
|
|
13
|
-
syncPullOnce,
|
|
14
|
-
syncPushOnce,
|
|
15
|
-
} from '@syncular/client';
|
|
16
|
-
import { createDatabase, type SyncTransport } from '@syncular/core';
|
|
17
|
-
import { createBunSqliteDialect } from '@syncular/dialect-bun-sqlite';
|
|
18
|
-
import { createPgliteDialect } from '@syncular/dialect-pglite';
|
|
1
|
+
import { createDatabase } from '@syncular/core';
|
|
19
2
|
import {
|
|
20
3
|
ensureSyncSchema,
|
|
21
4
|
type ServerSyncDialect,
|
|
22
5
|
type ServerTableHandler,
|
|
23
6
|
type SyncCoreDb,
|
|
24
7
|
} from '@syncular/server';
|
|
25
|
-
import {
|
|
26
|
-
import { createSqliteServerDialect } from '@syncular/server-dialect-sqlite';
|
|
8
|
+
import { createBunSqliteDialect } from '@syncular/server/bun-sqlite';
|
|
27
9
|
import {
|
|
28
10
|
type CreateSyncRoutesOptions,
|
|
29
11
|
createSyncRoutes,
|
|
30
|
-
} from '@syncular/server
|
|
31
|
-
import {
|
|
12
|
+
} from '@syncular/server/hono';
|
|
13
|
+
import { createPgliteDialect } from '@syncular/server/pglite';
|
|
14
|
+
import { createPostgresServerDialect } from '@syncular/server/postgres';
|
|
15
|
+
import { createSqliteServerDialect } from '@syncular/server/sqlite';
|
|
32
16
|
import { Hono } from 'hono';
|
|
33
17
|
import type { Kysely } from 'kysely';
|
|
34
18
|
import { createNodeHonoServer } from './hono-node-server';
|
|
35
19
|
|
|
36
20
|
export type HttpServerDialect = 'sqlite' | 'pglite';
|
|
37
|
-
export type HttpClientDialect = 'bun-sqlite' | 'pglite';
|
|
38
21
|
|
|
39
22
|
export interface HttpServerFixture<DB extends SyncCoreDb> {
|
|
40
23
|
db: Kysely<DB>;
|
|
@@ -61,38 +44,6 @@ export interface CreateHttpServerFixtureOptions<DB extends SyncCoreDb> {
|
|
|
61
44
|
corsMaxAgeSeconds?: number;
|
|
62
45
|
}
|
|
63
46
|
|
|
64
|
-
export interface HttpClientFixture<DB extends SyncClientDb> {
|
|
65
|
-
db: Kysely<DB>;
|
|
66
|
-
transport: SyncTransport;
|
|
67
|
-
handlers: ClientHandlerCollection<DB>;
|
|
68
|
-
actorId: string;
|
|
69
|
-
clientId: string;
|
|
70
|
-
enqueue: (
|
|
71
|
-
args: Parameters<typeof enqueueOutboxCommit<DB>>[1]
|
|
72
|
-
) => Promise<{ id: string; clientCommitId: string }>;
|
|
73
|
-
push: (
|
|
74
|
-
options?: Omit<SyncPushOnceOptions, 'clientId' | 'actorId'>
|
|
75
|
-
) => Promise<SyncPushOnceResult>;
|
|
76
|
-
pull: (
|
|
77
|
-
options: Omit<SyncPullOnceOptions, 'clientId' | 'actorId'>
|
|
78
|
-
) => Promise<SyncPullResponse>;
|
|
79
|
-
syncOnce: (
|
|
80
|
-
options: Omit<SyncOnceOptions, 'clientId' | 'actorId'>
|
|
81
|
-
) => Promise<SyncOnceResult>;
|
|
82
|
-
destroy: () => Promise<void>;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface CreateHttpClientFixtureOptions<DB extends SyncClientDb> {
|
|
86
|
-
clientDialect: HttpClientDialect;
|
|
87
|
-
baseUrl: string;
|
|
88
|
-
actorId: string;
|
|
89
|
-
clientId: string;
|
|
90
|
-
createTables: (db: Kysely<DB>) => Promise<void>;
|
|
91
|
-
registerHandlers: (handlers: ClientHandlerCollection<DB>) => void;
|
|
92
|
-
fetch?: typeof globalThis.fetch;
|
|
93
|
-
getHeaders?: () => Record<string, string>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
47
|
const PGLITE_INIT_ATTEMPTS = 3;
|
|
97
48
|
|
|
98
49
|
function isTransientPgliteInitError(error: Error): boolean {
|
|
@@ -234,75 +185,3 @@ export async function createHttpServerFixture<DB extends SyncCoreDb>(
|
|
|
234
185
|
},
|
|
235
186
|
};
|
|
236
187
|
}
|
|
237
|
-
|
|
238
|
-
export async function createHttpClientFixture<DB extends SyncClientDb>(
|
|
239
|
-
options: CreateHttpClientFixtureOptions<DB>
|
|
240
|
-
): Promise<HttpClientFixture<DB>> {
|
|
241
|
-
const db = await withPgliteInitRetry(
|
|
242
|
-
options.clientDialect === 'pglite',
|
|
243
|
-
async () => {
|
|
244
|
-
const candidateDb =
|
|
245
|
-
options.clientDialect === 'pglite'
|
|
246
|
-
? createDatabase<DB>({
|
|
247
|
-
dialect: createPgliteDialect(),
|
|
248
|
-
family: 'postgres',
|
|
249
|
-
})
|
|
250
|
-
: createDatabase<DB>({
|
|
251
|
-
dialect: createBunSqliteDialect({ path: ':memory:' }),
|
|
252
|
-
family: 'sqlite',
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
await ensureClientSyncSchema(candidateDb);
|
|
257
|
-
await options.createTables(candidateDb);
|
|
258
|
-
return candidateDb;
|
|
259
|
-
} catch (error) {
|
|
260
|
-
await candidateDb.destroy();
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
const handlers: ClientHandlerCollection<DB> = [];
|
|
267
|
-
options.registerHandlers(handlers);
|
|
268
|
-
|
|
269
|
-
const transport = createHttpTransport({
|
|
270
|
-
baseUrl: options.baseUrl,
|
|
271
|
-
getHeaders:
|
|
272
|
-
options.getHeaders ??
|
|
273
|
-
(() => ({
|
|
274
|
-
'x-actor-id': options.actorId,
|
|
275
|
-
})),
|
|
276
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
db,
|
|
281
|
-
transport,
|
|
282
|
-
handlers,
|
|
283
|
-
actorId: options.actorId,
|
|
284
|
-
clientId: options.clientId,
|
|
285
|
-
enqueue: (args) => enqueueOutboxCommit(db, args),
|
|
286
|
-
push: (pushOptions) =>
|
|
287
|
-
syncPushOnce(db, transport, {
|
|
288
|
-
clientId: options.clientId,
|
|
289
|
-
actorId: options.actorId,
|
|
290
|
-
plugins: pushOptions?.plugins,
|
|
291
|
-
}),
|
|
292
|
-
pull: (pullOptions) =>
|
|
293
|
-
syncPullOnce(db, transport, handlers, {
|
|
294
|
-
...pullOptions,
|
|
295
|
-
clientId: options.clientId,
|
|
296
|
-
actorId: options.actorId,
|
|
297
|
-
}),
|
|
298
|
-
syncOnce: (syncOptions) =>
|
|
299
|
-
syncOnce(db, transport, handlers, {
|
|
300
|
-
...syncOptions,
|
|
301
|
-
clientId: options.clientId,
|
|
302
|
-
actorId: options.actorId,
|
|
303
|
-
}),
|
|
304
|
-
destroy: async () => {
|
|
305
|
-
await db.destroy();
|
|
306
|
-
},
|
|
307
|
-
};
|
|
308
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './audit';
|
|
2
|
+
export * from './client-bridge';
|
|
2
3
|
export * from './deterministic';
|
|
3
4
|
export * from './disposable';
|
|
4
5
|
export * from './faults';
|
|
5
|
-
export * from './fixtures';
|
|
6
6
|
export * from './hono-node-server';
|
|
7
7
|
export * from './http-fixtures';
|
|
8
8
|
export * from './project-scoped-tasks';
|
|
9
9
|
export * from './realtime-ws';
|
|
10
|
-
export * from './resources';
|
|
11
10
|
export * from './runtime-process';
|
|
12
|
-
export * from './scenario-flow';
|
|
13
11
|
export * from './sync-builders';
|
|
14
12
|
export * from './sync-http';
|
|
15
13
|
export * from './sync-parse';
|
|
@@ -150,8 +150,8 @@ export function createProjectScopedTasksHandler<
|
|
|
150
150
|
result: {
|
|
151
151
|
opIndex,
|
|
152
152
|
status: 'error',
|
|
153
|
-
error: `
|
|
154
|
-
code: '
|
|
153
|
+
error: `Unknown table: ${op.table}`,
|
|
154
|
+
code: 'sync.unknown_table',
|
|
155
155
|
retriable: false,
|
|
156
156
|
},
|
|
157
157
|
emittedChanges: [],
|
|
@@ -216,6 +216,7 @@ export function createProjectScopedTasksHandler<
|
|
|
216
216
|
opIndex,
|
|
217
217
|
status: 'conflict',
|
|
218
218
|
message: `Version conflict: server=${existing.server_version}, base=${op.base_version}`,
|
|
219
|
+
code: 'sync.version_conflict',
|
|
219
220
|
server_version: existing.server_version,
|
|
220
221
|
server_row: {
|
|
221
222
|
id: existing.id,
|
|
@@ -236,8 +237,8 @@ export function createProjectScopedTasksHandler<
|
|
|
236
237
|
result: {
|
|
237
238
|
opIndex,
|
|
238
239
|
status: 'error',
|
|
239
|
-
error: '
|
|
240
|
-
code: '
|
|
240
|
+
error: 'Missing project id',
|
|
241
|
+
code: 'sync.invalid_request',
|
|
241
242
|
retriable: false,
|
|
242
243
|
},
|
|
243
244
|
emittedChanges: [],
|
|
@@ -321,6 +322,7 @@ export function createProjectScopedTasksSubscription(
|
|
|
321
322
|
},
|
|
322
323
|
cursor: options.cursor ?? 0,
|
|
323
324
|
bootstrapState: null,
|
|
325
|
+
crdtStateVectors: [],
|
|
324
326
|
};
|
|
325
327
|
}
|
|
326
328
|
|
package/src/sync-builders.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
SyncCombinedRequest,
|
|
3
3
|
SyncOperation,
|
|
4
4
|
SyncPullRequest,
|
|
5
|
-
|
|
5
|
+
SyncPushBatchRequest,
|
|
6
6
|
SyncSubscriptionRequest,
|
|
7
7
|
} from '@syncular/core';
|
|
8
8
|
|
|
@@ -50,6 +50,7 @@ export interface CreateSyncSubscriptionOptions {
|
|
|
50
50
|
params?: SyncSubscriptionRequest['params'];
|
|
51
51
|
cursor?: number;
|
|
52
52
|
bootstrapState?: SyncSubscriptionRequest['bootstrapState'];
|
|
53
|
+
crdtStateVectors?: SyncSubscriptionRequest['crdtStateVectors'];
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function createSyncSubscription(
|
|
@@ -62,6 +63,7 @@ export function createSyncSubscription(
|
|
|
62
63
|
...(options.params ? { params: options.params } : {}),
|
|
63
64
|
cursor: options.cursor ?? 0,
|
|
64
65
|
bootstrapState: options.bootstrapState ?? null,
|
|
66
|
+
crdtStateVectors: options.crdtStateVectors ?? [],
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -74,17 +76,22 @@ export interface CreateSyncPushRequestOptions {
|
|
|
74
76
|
|
|
75
77
|
export function createSyncPushRequest(
|
|
76
78
|
options: CreateSyncPushRequestOptions
|
|
77
|
-
):
|
|
79
|
+
): SyncPushBatchRequest {
|
|
78
80
|
return {
|
|
79
81
|
clientId: options.clientId,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
commits: [
|
|
83
|
+
{
|
|
84
|
+
clientCommitId: options.clientCommitId,
|
|
85
|
+
operations: options.operations,
|
|
86
|
+
schemaVersion: options.schemaVersion ?? 1,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
export interface CreateSyncPullRequestOptions {
|
|
87
93
|
clientId: string;
|
|
94
|
+
schemaVersion?: number;
|
|
88
95
|
subscriptions: SyncSubscriptionRequest[];
|
|
89
96
|
limitCommits?: number;
|
|
90
97
|
limitSnapshotRows?: number;
|
|
@@ -97,6 +104,7 @@ export function createSyncPullRequest(
|
|
|
97
104
|
): SyncPullRequest {
|
|
98
105
|
return {
|
|
99
106
|
clientId: options.clientId,
|
|
107
|
+
schemaVersion: options.schemaVersion ?? 1,
|
|
100
108
|
subscriptions: options.subscriptions,
|
|
101
109
|
limitCommits: options.limitCommits ?? 50,
|
|
102
110
|
...(options.limitSnapshotRows !== undefined
|
|
@@ -113,7 +121,7 @@ export function createSyncPullRequest(
|
|
|
113
121
|
|
|
114
122
|
export interface CreateSyncCombinedRequestOptions {
|
|
115
123
|
clientId: string;
|
|
116
|
-
push?: Omit<
|
|
124
|
+
push?: Omit<SyncPushBatchRequest, 'clientId'>;
|
|
117
125
|
pull?: Omit<SyncPullRequest, 'clientId'>;
|
|
118
126
|
}
|
|
119
127
|
|
package/src/sync-response.ts
CHANGED
|
@@ -4,8 +4,6 @@ import {
|
|
|
4
4
|
type SyncPullResponse,
|
|
5
5
|
} from '@syncular/core';
|
|
6
6
|
|
|
7
|
-
export type SyncChangeRecord = SyncChange;
|
|
8
|
-
|
|
9
7
|
export type SyncSubscriptionRecord = Pick<
|
|
10
8
|
SyncPullResponse['subscriptions'][number],
|
|
11
9
|
'id' | 'commits'
|
|
@@ -14,7 +12,7 @@ export type SyncSubscriptionRecord = Pick<
|
|
|
14
12
|
export function subscriptionChanges(
|
|
15
13
|
subscriptions: SyncSubscriptionRecord[] | undefined,
|
|
16
14
|
subscriptionId: string
|
|
17
|
-
):
|
|
15
|
+
): SyncChange[] {
|
|
18
16
|
const subscription = subscriptions?.find(
|
|
19
17
|
(item) => item.id === subscriptionId
|
|
20
18
|
);
|
|
@@ -29,13 +27,13 @@ export function findSubscriptionChange(
|
|
|
29
27
|
subscriptions: SyncSubscriptionRecord[] | undefined,
|
|
30
28
|
subscriptionId: string,
|
|
31
29
|
rowId: string
|
|
32
|
-
):
|
|
30
|
+
): SyncChange | undefined {
|
|
33
31
|
const changes = subscriptionChanges(subscriptions, subscriptionId);
|
|
34
32
|
return changes.find((change) => change.row_id === rowId);
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export function subscriptionChangeRow(
|
|
38
|
-
change:
|
|
36
|
+
change: SyncChange | undefined
|
|
39
37
|
): Record<string, unknown> | undefined {
|
|
40
38
|
if (!change) {
|
|
41
39
|
return undefined;
|