@syncular/testkit 0.0.6-168 → 0.0.6-177
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/faults.d.ts +12 -0
- package/dist/faults.d.ts.map +1 -1
- package/dist/faults.js +97 -39
- 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/package.json +12 -12
- package/src/faults.test.ts +123 -0
- package/src/faults.ts +168 -49
- package/src/hono-node-server.test.ts +53 -0
- package/src/hono-node-server.ts +1 -1
package/dist/faults.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { SyncCombinedRequest, SyncPullResponse, SyncPushResponse, SyncTransport } from '@syncular/core';
|
|
2
|
+
export type FaultTransportOperation = 'push' | 'pull' | 'fetch';
|
|
3
|
+
export type FaultTransportAction = 'pass' | 'fail';
|
|
4
|
+
export type FaultTransportPhase = 'before' | 'after';
|
|
5
|
+
export interface FaultPlanStep {
|
|
6
|
+
operation: FaultTransportOperation | 'any';
|
|
7
|
+
action?: FaultTransportAction;
|
|
8
|
+
phase?: FaultTransportPhase;
|
|
9
|
+
repeat?: number;
|
|
10
|
+
failWith?: Error;
|
|
11
|
+
latencyMs?: number;
|
|
12
|
+
}
|
|
2
13
|
export interface FaultTransportOptions {
|
|
3
14
|
failAfter?: number;
|
|
4
15
|
failWith?: Error;
|
|
@@ -7,6 +18,7 @@ export interface FaultTransportOptions {
|
|
|
7
18
|
failOnPush?: boolean;
|
|
8
19
|
failOnPull?: boolean;
|
|
9
20
|
failOnFetch?: boolean;
|
|
21
|
+
plan?: FaultPlanStep[];
|
|
10
22
|
onFail?: (operation: 'push' | 'pull' | 'fetch', error: Error) => void;
|
|
11
23
|
onSuccess?: (operation: 'push' | 'pull' | 'fetch') => void;
|
|
12
24
|
}
|
package/dist/faults.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"faults.d.ts","sourceRoot":"","sources":["../src/faults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EAEd,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtE,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,CAAC;CAC5D;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;IACpC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,KAAK,IAAI,CAAC;CAC/D;
|
|
1
|
+
{"version":3,"file":"faults.d.ts","sourceRoot":"","sources":["../src/faults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EAEd,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEhE,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,uBAAuB,GAAG,KAAK,CAAC;IAC3C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,aAAa,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtE,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,KAAK,IAAI,CAAC;CAC5D;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;IACpC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,qBAAqB,CAAC,KAAK,IAAI,CAAC;CAC/D;AAkBD,wBAAgB,UAAU,CACxB,aAAa,EAAE,aAAa,EAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CA6KtB;AA4BD,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAC5C,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,SAAS,CAAC,EAAE,UAAU,CAAC;CACxB,GAAG,aAAa,CAkChB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,aAAa,CAAC;IACzB,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,aAAa,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACrC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,wBAAgB,aAAa,CAC3B,aAAa,EAAE,aAAa,GAC3B,wBAAwB,CAyB1B"}
|
package/dist/faults.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export function withFaults(baseTransport, options = {}) {
|
|
2
2
|
let currentOptions = { ...options };
|
|
3
|
+
let currentPlan = createActivePlan(currentOptions.plan);
|
|
3
4
|
const state = {
|
|
4
5
|
pushCount: 0,
|
|
5
6
|
pullCount: 0,
|
|
@@ -7,22 +8,30 @@ export function withFaults(baseTransport, options = {}) {
|
|
|
7
8
|
failureCount: 0,
|
|
8
9
|
};
|
|
9
10
|
const defaultError = new Error('Simulated transport error');
|
|
10
|
-
const maybeDelay = async () => {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const maybeDelay = async (extraLatencyMs = 0) => {
|
|
12
|
+
const latencyMs = (currentOptions.latencyMs ?? 0) + extraLatencyMs;
|
|
13
|
+
if (latencyMs > 0) {
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, latencyMs));
|
|
13
15
|
}
|
|
14
16
|
};
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const shouldTargetOperation = (operation) => {
|
|
18
|
+
const hasTargets = currentOptions.failOnPush === true ||
|
|
19
|
+
currentOptions.failOnPull === true ||
|
|
20
|
+
currentOptions.failOnFetch === true;
|
|
21
|
+
if (!hasTargets) {
|
|
22
|
+
return true;
|
|
18
23
|
}
|
|
19
|
-
if (operation === '
|
|
20
|
-
return
|
|
24
|
+
if (operation === 'push') {
|
|
25
|
+
return currentOptions.failOnPush === true;
|
|
21
26
|
}
|
|
22
|
-
if (operation === '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
if (operation === 'pull') {
|
|
28
|
+
return currentOptions.failOnPull === true;
|
|
29
|
+
}
|
|
30
|
+
return currentOptions.failOnFetch === true;
|
|
31
|
+
};
|
|
32
|
+
const shouldFail = (operation, count) => {
|
|
33
|
+
if (!shouldTargetOperation(operation)) {
|
|
34
|
+
return false;
|
|
26
35
|
}
|
|
27
36
|
if (currentOptions.failAfter !== undefined &&
|
|
28
37
|
count >= currentOptions.failAfter) {
|
|
@@ -34,39 +43,63 @@ export function withFaults(baseTransport, options = {}) {
|
|
|
34
43
|
return false;
|
|
35
44
|
};
|
|
36
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
|
+
};
|
|
37
95
|
const transport = {
|
|
38
96
|
async sync(request, transportOptions) {
|
|
39
|
-
await maybeDelay();
|
|
40
97
|
const operation = request.push ? 'push' : 'pull';
|
|
41
98
|
const count = operation === 'push' ? state.pushCount : state.pullCount;
|
|
42
|
-
|
|
43
|
-
const error = getError();
|
|
44
|
-
state.failureCount++;
|
|
45
|
-
currentOptions.onFail?.(operation, error);
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
if (operation === 'push') {
|
|
49
|
-
state.pushCount++;
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
state.pullCount++;
|
|
53
|
-
}
|
|
54
|
-
const result = await baseTransport.sync(request, transportOptions);
|
|
55
|
-
currentOptions.onSuccess?.(operation);
|
|
56
|
-
return result;
|
|
99
|
+
return runWithFaults(operation, count, () => baseTransport.sync(request, transportOptions));
|
|
57
100
|
},
|
|
58
101
|
async fetchSnapshotChunk(request, transportOptions) {
|
|
59
|
-
|
|
60
|
-
if (shouldFail('fetch', state.fetchCount)) {
|
|
61
|
-
const error = getError();
|
|
62
|
-
state.failureCount++;
|
|
63
|
-
currentOptions.onFail?.('fetch', error);
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
state.fetchCount++;
|
|
67
|
-
const result = await baseTransport.fetchSnapshotChunk(request, transportOptions);
|
|
68
|
-
currentOptions.onSuccess?.('fetch');
|
|
69
|
-
return result;
|
|
102
|
+
return runWithFaults('fetch', state.fetchCount, () => baseTransport.fetchSnapshotChunk(request, transportOptions));
|
|
70
103
|
},
|
|
71
104
|
};
|
|
72
105
|
return {
|
|
@@ -77,12 +110,37 @@ export function withFaults(baseTransport, options = {}) {
|
|
|
77
110
|
state.pullCount = 0;
|
|
78
111
|
state.fetchCount = 0;
|
|
79
112
|
state.failureCount = 0;
|
|
113
|
+
currentPlan = createActivePlan(currentOptions.plan);
|
|
80
114
|
},
|
|
81
115
|
setOptions: (newOptions) => {
|
|
82
116
|
currentOptions = { ...currentOptions, ...newOptions };
|
|
117
|
+
if ('plan' in newOptions) {
|
|
118
|
+
currentPlan = createActivePlan(currentOptions.plan);
|
|
119
|
+
}
|
|
83
120
|
},
|
|
84
121
|
};
|
|
85
122
|
}
|
|
123
|
+
function createActivePlan(plan) {
|
|
124
|
+
if (!plan || plan.length === 0) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return plan.flatMap((step) => {
|
|
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
|
+
});
|
|
143
|
+
}
|
|
86
144
|
export function createMockTransport(options) {
|
|
87
145
|
return {
|
|
88
146
|
async sync(request) {
|
package/dist/faults.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"faults.js","sourceRoot":"","sources":["../src/faults.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"faults.js","sourceRoot":"","sources":["../src/faults.ts"],"names":[],"mappings":"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"}
|
package/dist/hono-node-server.js
CHANGED
|
@@ -4,7 +4,7 @@ export function createNodeHonoServer(app, options) {
|
|
|
4
4
|
const corsEnabled = options?.cors ?? true;
|
|
5
5
|
const corsAllowMethods = options?.corsAllowMethods ?? 'GET, POST, PUT, DELETE, OPTIONS';
|
|
6
6
|
const corsAllowHeaders = options?.corsAllowHeaders ??
|
|
7
|
-
'content-type, x-actor-id, x-syncular-transport-path, x-user-id';
|
|
7
|
+
'content-type, x-actor-id, x-syncular-snapshot-scopes, x-syncular-transport-path, x-user-id';
|
|
8
8
|
const corsMaxAgeSeconds = options?.corsMaxAgeSeconds ?? 86400;
|
|
9
9
|
return createServer(async (req, res) => {
|
|
10
10
|
const url = `http://localhost${req.url ?? '/'}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono-node-server.js","sourceRoot":"","sources":["../src/hono-node-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA6B,MAAM,WAAW,CAAC;AAEpE,OAAO,EAEL,6BAA6B,EAE7B,0BAA0B,GAC3B,MAAM,cAAc,CAAC;AAgBtB,MAAM,UAAU,oBAAoB,CAClC,GAAS,EACT,OAA+B,EACnB;IACZ,MAAM,WAAW,GAAG,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;IAC1C,MAAM,gBAAgB,GACpB,OAAO,EAAE,gBAAgB,IAAI,iCAAiC,CAAC;IACjE,MAAM,gBAAgB,GACpB,OAAO,EAAE,gBAAgB;QACzB,
|
|
1
|
+
{"version":3,"file":"hono-node-server.js","sourceRoot":"","sources":["../src/hono-node-server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/testkit",
|
|
3
|
-
"version": "0.0.6-
|
|
3
|
+
"version": "0.0.6-177",
|
|
4
4
|
"description": "Testing fixtures and utilities for Syncular",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"release": "bunx syncular-publish"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@syncular/client": "0.0.6-
|
|
45
|
-
"@syncular/core": "0.0.6-
|
|
46
|
-
"@syncular/dialect-bun-sqlite": "0.0.6-
|
|
47
|
-
"@syncular/dialect-libsql": "0.0.6-
|
|
48
|
-
"@syncular/dialect-pglite": "0.0.6-
|
|
49
|
-
"@syncular/dialect-sqlite3": "0.0.6-
|
|
50
|
-
"@syncular/server": "0.0.6-
|
|
51
|
-
"@syncular/server-dialect-postgres": "0.0.6-
|
|
52
|
-
"@syncular/server-dialect-sqlite": "0.0.6-
|
|
53
|
-
"@syncular/server-hono": "0.0.6-
|
|
54
|
-
"@syncular/transport-http": "0.0.6-
|
|
44
|
+
"@syncular/client": "0.0.6-177",
|
|
45
|
+
"@syncular/core": "0.0.6-177",
|
|
46
|
+
"@syncular/dialect-bun-sqlite": "0.0.6-177",
|
|
47
|
+
"@syncular/dialect-libsql": "0.0.6-177",
|
|
48
|
+
"@syncular/dialect-pglite": "0.0.6-177",
|
|
49
|
+
"@syncular/dialect-sqlite3": "0.0.6-177",
|
|
50
|
+
"@syncular/server": "0.0.6-177",
|
|
51
|
+
"@syncular/server-dialect-postgres": "0.0.6-177",
|
|
52
|
+
"@syncular/server-dialect-sqlite": "0.0.6-177",
|
|
53
|
+
"@syncular/server-hono": "0.0.6-177",
|
|
54
|
+
"@syncular/transport-http": "0.0.6-177",
|
|
55
55
|
"hono": "^4.12.3"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import type { SyncCombinedRequest } from '@syncular/core';
|
|
3
|
+
import { createMockTransport, withFaults } from './faults';
|
|
4
|
+
|
|
5
|
+
function createPushRequest(
|
|
6
|
+
clientCommitId: string
|
|
7
|
+
): Pick<SyncCombinedRequest, 'clientId' | 'push'> {
|
|
8
|
+
return {
|
|
9
|
+
clientId: 'client-1',
|
|
10
|
+
push: {
|
|
11
|
+
clientCommitId,
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
operations: [
|
|
14
|
+
{
|
|
15
|
+
table: 'tasks',
|
|
16
|
+
row_id: `row-${clientCommitId}`,
|
|
17
|
+
op: 'upsert',
|
|
18
|
+
payload: { title: clientCommitId, project_id: 'p1' },
|
|
19
|
+
base_version: null,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createPullRequest(): Pick<SyncCombinedRequest, 'clientId' | 'pull'> {
|
|
27
|
+
return {
|
|
28
|
+
clientId: 'client-1',
|
|
29
|
+
pull: {
|
|
30
|
+
limitCommits: 10,
|
|
31
|
+
subscriptions: [
|
|
32
|
+
{
|
|
33
|
+
id: 'tasks-p1',
|
|
34
|
+
table: 'tasks',
|
|
35
|
+
scopes: { project_id: 'p1', user_id: 'u1' },
|
|
36
|
+
cursor: 0,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('withFaults', () => {
|
|
44
|
+
it('supports deterministic post-success failures for ack-loss scenarios', async () => {
|
|
45
|
+
const wrapped = withFaults(createMockTransport(), {
|
|
46
|
+
plan: [
|
|
47
|
+
{
|
|
48
|
+
operation: 'push',
|
|
49
|
+
phase: 'after',
|
|
50
|
+
failWith: new Error('SIMULATED_ACK_LOSS'),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
wrapped.transport.sync(createPushRequest('push-1'))
|
|
57
|
+
).rejects.toThrow('SIMULATED_ACK_LOSS');
|
|
58
|
+
|
|
59
|
+
const retry = await wrapped.transport.sync(createPushRequest('push-2'));
|
|
60
|
+
expect(retry.push?.status).toBe('applied');
|
|
61
|
+
expect(wrapped.getState()).toEqual({
|
|
62
|
+
pushCount: 2,
|
|
63
|
+
pullCount: 0,
|
|
64
|
+
fetchCount: 0,
|
|
65
|
+
failureCount: 1,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('consumes pass and fail steps in order for matching operations', async () => {
|
|
70
|
+
const wrapped = withFaults(createMockTransport(), {
|
|
71
|
+
plan: [
|
|
72
|
+
{ operation: 'push', action: 'pass' },
|
|
73
|
+
{
|
|
74
|
+
operation: 'pull',
|
|
75
|
+
phase: 'before',
|
|
76
|
+
failWith: new Error('SIMULATED_PULL_DROP'),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const pushResult = await wrapped.transport.sync(
|
|
82
|
+
createPushRequest('push-1')
|
|
83
|
+
);
|
|
84
|
+
expect(pushResult.push?.status).toBe('applied');
|
|
85
|
+
|
|
86
|
+
await expect(wrapped.transport.sync(createPullRequest())).rejects.toThrow(
|
|
87
|
+
'SIMULATED_PULL_DROP'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const retry = await wrapped.transport.sync(createPullRequest());
|
|
91
|
+
expect(retry.pull?.ok).toBe(true);
|
|
92
|
+
expect(wrapped.getState()).toEqual({
|
|
93
|
+
pushCount: 1,
|
|
94
|
+
pullCount: 1,
|
|
95
|
+
fetchCount: 0,
|
|
96
|
+
failureCount: 1,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('reset restores the original plan for deterministic reruns', async () => {
|
|
101
|
+
const wrapped = withFaults(createMockTransport(), {
|
|
102
|
+
plan: [
|
|
103
|
+
{
|
|
104
|
+
operation: 'fetch',
|
|
105
|
+
phase: 'before',
|
|
106
|
+
failWith: new Error('SIMULATED_FETCH_FAILURE'),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await expect(
|
|
112
|
+
wrapped.transport.fetchSnapshotChunk({ chunkId: 'chunk-1' })
|
|
113
|
+
).rejects.toThrow('SIMULATED_FETCH_FAILURE');
|
|
114
|
+
|
|
115
|
+
await wrapped.transport.fetchSnapshotChunk({ chunkId: 'chunk-1' });
|
|
116
|
+
|
|
117
|
+
wrapped.reset();
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
wrapped.transport.fetchSnapshotChunk({ chunkId: 'chunk-1' })
|
|
121
|
+
).rejects.toThrow('SIMULATED_FETCH_FAILURE');
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/faults.ts
CHANGED
|
@@ -6,6 +6,21 @@ import type {
|
|
|
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,13 +231,43 @@ 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
273
|
pushResponse?: SyncPushResponse;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { closeNodeServer, createNodeHonoServer } from './hono-node-server';
|
|
4
|
+
|
|
5
|
+
const servers: Array<ReturnType<typeof createNodeHonoServer>> = [];
|
|
6
|
+
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
while (servers.length > 0) {
|
|
9
|
+
const server = servers.pop();
|
|
10
|
+
if (!server) continue;
|
|
11
|
+
await closeNodeServer(server);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('createNodeHonoServer', () => {
|
|
16
|
+
it('allows snapshot scope headers in CORS preflight responses', async () => {
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
app.get('/sync/snapshot-chunks/:chunkId', (c) =>
|
|
19
|
+
c.body(new Uint8Array([1, 2, 3]))
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const server = createNodeHonoServer(app);
|
|
23
|
+
servers.push(server);
|
|
24
|
+
|
|
25
|
+
await new Promise<void>((resolve) => {
|
|
26
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const address = server.address();
|
|
30
|
+
if (typeof address !== 'object' || !address) {
|
|
31
|
+
throw new Error('Failed to resolve test server address');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const response = await fetch(
|
|
35
|
+
`http://127.0.0.1:${address.port}/sync/snapshot-chunks/chunk-1`,
|
|
36
|
+
{
|
|
37
|
+
method: 'OPTIONS',
|
|
38
|
+
headers: {
|
|
39
|
+
origin: 'http://127.0.0.1:4173',
|
|
40
|
+
'access-control-request-method': 'GET',
|
|
41
|
+
'access-control-request-headers':
|
|
42
|
+
'x-actor-id, x-syncular-snapshot-scopes',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(response.status).toBe(204);
|
|
48
|
+
expect(response.headers.get('access-control-allow-origin')).toBe('*');
|
|
49
|
+
expect(response.headers.get('access-control-allow-headers')).toContain(
|
|
50
|
+
'x-syncular-snapshot-scopes'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|
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
|
+
'content-type, x-actor-id, x-syncular-snapshot-scopes, x-syncular-transport-path, x-user-id';
|
|
34
34
|
const corsMaxAgeSeconds = options?.corsMaxAgeSeconds ?? 86400;
|
|
35
35
|
|
|
36
36
|
return createServer(async (req, res) => {
|