@syncular/testkit 0.0.2-135 → 0.0.2-137
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/deterministic.d.ts +31 -0
- package/dist/deterministic.d.ts.map +1 -0
- package/dist/deterministic.js +69 -0
- package/dist/deterministic.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/realtime-ws.d.ts +40 -0
- package/dist/realtime-ws.d.ts.map +1 -0
- package/dist/realtime-ws.js +192 -0
- package/dist/realtime-ws.js.map +1 -0
- package/dist/scenario-flow.d.ts +39 -0
- package/dist/scenario-flow.d.ts.map +1 -0
- package/dist/scenario-flow.js +41 -0
- package/dist/scenario-flow.js.map +1 -0
- package/dist/sync-builders.d.ts +46 -0
- package/dist/sync-builders.d.ts.map +1 -0
- package/dist/sync-builders.js +60 -0
- package/dist/sync-builders.js.map +1 -0
- package/dist/sync-http.d.ts.map +1 -1
- package/dist/sync-http.js +16 -3
- package/dist/sync-http.js.map +1 -1
- package/dist/sync-parse.d.ts +8 -0
- package/dist/sync-parse.d.ts.map +1 -0
- package/dist/sync-parse.js +38 -0
- package/dist/sync-parse.js.map +1 -0
- package/package.json +12 -12
- package/src/deterministic.ts +121 -0
- package/src/index.ts +5 -0
- package/src/realtime-ws.ts +321 -0
- package/src/scenario-flow.ts +143 -0
- package/src/sync-builders.ts +128 -0
- package/src/sync-http.ts +20 -3
- package/src/sync-parse.ts +86 -0
package/dist/sync-http.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseSyncCombinedResponse, parseSyncPullResponse, parseSyncPushResponse, } from './sync-parse.js';
|
|
1
2
|
export function createJsonActorHeaders(options) {
|
|
2
3
|
return {
|
|
3
4
|
'content-type': 'application/json',
|
|
@@ -19,12 +20,24 @@ export async function postJsonWithActor(options) {
|
|
|
19
20
|
return { response, json };
|
|
20
21
|
}
|
|
21
22
|
export async function postSyncCombinedRequest(options) {
|
|
22
|
-
|
|
23
|
+
const result = await postJsonWithActor(options);
|
|
24
|
+
return {
|
|
25
|
+
response: result.response,
|
|
26
|
+
json: parseSyncCombinedResponse(result.json),
|
|
27
|
+
};
|
|
23
28
|
}
|
|
24
29
|
export async function postSyncPushRequest(options) {
|
|
25
|
-
|
|
30
|
+
const result = await postJsonWithActor(options);
|
|
31
|
+
return {
|
|
32
|
+
response: result.response,
|
|
33
|
+
json: parseSyncPushResponse(result.json),
|
|
34
|
+
};
|
|
26
35
|
}
|
|
27
36
|
export async function postSyncPullRequest(options) {
|
|
28
|
-
|
|
37
|
+
const result = await postJsonWithActor(options);
|
|
38
|
+
return {
|
|
39
|
+
response: result.response,
|
|
40
|
+
json: parseSyncPullResponse(result.json),
|
|
41
|
+
};
|
|
29
42
|
}
|
|
30
43
|
//# sourceMappingURL=sync-http.js.map
|
package/dist/sync-http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-http.js","sourceRoot":"","sources":["../src/sync-http.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sync-http.js","sourceRoot":"","sources":["../src/sync-http.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAQtB,MAAM,UAAU,sBAAsB,CACpC,OAAgC,EACR;IACxB,OAAO;QACL,cAAc,EAAE,kBAAkB;QAClC,CAAC,OAAO,CAAC,WAAW,IAAI,YAAY,CAAC,EAAE,OAAO,CAAC,OAAO;QACtD,GAAG,OAAO,CAAC,YAAY;KACxB,CAAC;AAAA,CACH;AAgBD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAwC,EACK;IAC7C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QAChD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,sBAAsB,CAAC;YAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,YAAY,EAAE,OAAO,CAAC,YAAY;SACnC,CAAC;QACF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;KACnC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAc,CAAC;IAClD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAAA,CAC3B;AAWD,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,OAAuC,EACiB;IACxD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAA+B,OAAO,CAAC,CAAC;IAC9E,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC,IAAI,CAAC;KAC7C,CAAC;AAAA,CACH;AAWD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAmC,EACiB;IACpD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAA2B,OAAO,CAAC,CAAC;IAC1E,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,IAAI,CAAC;KACzC,CAAC;AAAA,CACH;AAWD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAmC,EACiB;IACpD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAA2B,OAAO,CAAC,CAAC;IAC1E,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,IAAI,CAAC;KACzC,CAAC;AAAA,CACH"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SyncCombinedResponse, type SyncPullResponse, type SyncPushResponse } from '@syncular/core';
|
|
2
|
+
export declare function parseSyncCombinedResponse(value: unknown): SyncCombinedResponse;
|
|
3
|
+
export declare function parseSyncPushResponse(value: unknown): SyncPushResponse;
|
|
4
|
+
export declare function parseSyncPullResponse(value: unknown): SyncPullResponse;
|
|
5
|
+
export declare function readSyncCombinedResponse(response: Response): Promise<SyncCombinedResponse>;
|
|
6
|
+
export declare function readSyncPushResponse(response: Response): Promise<SyncPushResponse>;
|
|
7
|
+
export declare function readSyncPullResponse(response: Response): Promise<SyncPullResponse>;
|
|
8
|
+
//# sourceMappingURL=sync-parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-parse.d.ts","sourceRoot":"","sources":["../src/sync-parse.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,oBAAoB,EAEzB,KAAK,gBAAgB,EAErB,KAAK,gBAAgB,EAEtB,MAAM,gBAAgB,CAAC;AA0CxB,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,OAAO,GACb,oBAAoB,CAItB;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAItE;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAItE;AAED,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,gBAAgB,CAAC,CAE3B;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,gBAAgB,CAAC,CAE3B"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SyncCombinedResponseSchema, SyncPullResponseSchema, SyncPushResponseSchema, } from '@syncular/core';
|
|
2
|
+
function formatIssue(issue) {
|
|
3
|
+
const path = issue.path.length > 0
|
|
4
|
+
? issue.path
|
|
5
|
+
.map((segment) => typeof segment === 'symbol' ? segment.toString() : String(segment))
|
|
6
|
+
.join('.')
|
|
7
|
+
: 'root';
|
|
8
|
+
return `${path}: ${issue.message}`;
|
|
9
|
+
}
|
|
10
|
+
function formatIssues(issues) {
|
|
11
|
+
return issues.map((issue) => formatIssue(issue)).join('; ');
|
|
12
|
+
}
|
|
13
|
+
function parseOrThrow(label, value, parse) {
|
|
14
|
+
const parsed = parse(value);
|
|
15
|
+
if (parsed.success) {
|
|
16
|
+
return parsed.data;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`${label} validation failed: ${formatIssues(parsed.error.issues)}`);
|
|
19
|
+
}
|
|
20
|
+
export function parseSyncCombinedResponse(value) {
|
|
21
|
+
return parseOrThrow('SyncCombinedResponse', value, (input) => SyncCombinedResponseSchema.safeParse(input));
|
|
22
|
+
}
|
|
23
|
+
export function parseSyncPushResponse(value) {
|
|
24
|
+
return parseOrThrow('SyncPushResponse', value, (input) => SyncPushResponseSchema.safeParse(input));
|
|
25
|
+
}
|
|
26
|
+
export function parseSyncPullResponse(value) {
|
|
27
|
+
return parseOrThrow('SyncPullResponse', value, (input) => SyncPullResponseSchema.safeParse(input));
|
|
28
|
+
}
|
|
29
|
+
export async function readSyncCombinedResponse(response) {
|
|
30
|
+
return parseSyncCombinedResponse(await response.json());
|
|
31
|
+
}
|
|
32
|
+
export async function readSyncPushResponse(response) {
|
|
33
|
+
return parseSyncPushResponse(await response.json());
|
|
34
|
+
}
|
|
35
|
+
export async function readSyncPullResponse(response) {
|
|
36
|
+
return parseSyncPullResponse(await response.json());
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=sync-parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-parse.js","sourceRoot":"","sources":["../src/sync-parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,0BAA0B,EAE1B,sBAAsB,EAEtB,sBAAsB,GACvB,MAAM,gBAAgB,CAAC;AAWxB,SAAS,WAAW,CAAC,KAAiB,EAAU;IAC9C,MAAM,IAAI,GACR,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QACnB,CAAC,CAAC,KAAK,CAAC,IAAI;aACP,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CACf,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CACnE;aACA,IAAI,CAAC,GAAG,CAAC;QACd,CAAC,CAAC,MAAM,CAAC;IACb,OAAO,GAAG,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;AAAA,CACpC;AAED,SAAS,YAAY,CAAC,MAAiC,EAAU;IAC/D,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CAC7D;AAED,SAAS,YAAY,CACnB,KAAa,EACb,KAAc,EACd,KAAyC,EACtC;IACH,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,uBAAuB,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CACnE,CAAC;AAAA,CACH;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAc,EACQ;IACtB,OAAO,YAAY,CAAC,sBAAsB,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAC3D,0BAA0B,CAAC,SAAS,CAAC,KAAK,CAAC,CAC5C,CAAC;AAAA,CACH;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAc,EAAoB;IACtE,OAAO,YAAY,CAAC,kBAAkB,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACvD,sBAAsB,CAAC,SAAS,CAAC,KAAK,CAAC,CACxC,CAAC;AAAA,CACH;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAc,EAAoB;IACtE,OAAO,YAAY,CAAC,kBAAkB,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACvD,sBAAsB,CAAC,SAAS,CAAC,KAAK,CAAC,CACxC,CAAC;AAAA,CACH;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,QAAkB,EACa;IAC/B,OAAO,yBAAyB,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;AAAA,CACzD;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,QAAkB,EACS;IAC3B,OAAO,qBAAqB,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;AAAA,CACrD;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,QAAkB,EACS;IAC3B,OAAO,qBAAqB,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;AAAA,CACrD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/testkit",
|
|
3
|
-
"version": "0.0.2-
|
|
3
|
+
"version": "0.0.2-137",
|
|
4
4
|
"description": "Testing fixtures and utilities for Syncular",
|
|
5
5
|
"license": "MIT",
|
|
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.2-
|
|
45
|
-
"@syncular/core": "0.0.2-
|
|
46
|
-
"@syncular/dialect-bun-sqlite": "0.0.2-
|
|
47
|
-
"@syncular/dialect-libsql": "0.0.2-
|
|
48
|
-
"@syncular/dialect-pglite": "0.0.2-
|
|
49
|
-
"@syncular/dialect-sqlite3": "0.0.2-
|
|
50
|
-
"@syncular/server": "0.0.2-
|
|
51
|
-
"@syncular/server-dialect-postgres": "0.0.2-
|
|
52
|
-
"@syncular/server-dialect-sqlite": "0.0.2-
|
|
53
|
-
"@syncular/server-hono": "0.0.2-
|
|
54
|
-
"@syncular/transport-http": "0.0.2-
|
|
44
|
+
"@syncular/client": "0.0.2-137",
|
|
45
|
+
"@syncular/core": "0.0.2-137",
|
|
46
|
+
"@syncular/dialect-bun-sqlite": "0.0.2-137",
|
|
47
|
+
"@syncular/dialect-libsql": "0.0.2-137",
|
|
48
|
+
"@syncular/dialect-pglite": "0.0.2-137",
|
|
49
|
+
"@syncular/dialect-sqlite3": "0.0.2-137",
|
|
50
|
+
"@syncular/server": "0.0.2-137",
|
|
51
|
+
"@syncular/server-dialect-postgres": "0.0.2-137",
|
|
52
|
+
"@syncular/server-dialect-sqlite": "0.0.2-137",
|
|
53
|
+
"@syncular/server-hono": "0.0.2-137",
|
|
54
|
+
"@syncular/transport-http": "0.0.2-137",
|
|
55
55
|
"hono": "^4.11.9"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export interface CreateIdFactoryOptions {
|
|
2
|
+
prefix?: string;
|
|
3
|
+
separator?: string;
|
|
4
|
+
startAt?: number;
|
|
5
|
+
step?: number;
|
|
6
|
+
padLength?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface IdFactory {
|
|
10
|
+
next: () => string;
|
|
11
|
+
peek: () => string;
|
|
12
|
+
current: () => number;
|
|
13
|
+
reset: (startAt?: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeCounter(value: number, label: string): number {
|
|
17
|
+
if (!Number.isFinite(value)) {
|
|
18
|
+
throw new Error(`${label} must be a finite number`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return Math.trunc(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatCounter(value: number, padLength: number): string {
|
|
25
|
+
const text = String(value);
|
|
26
|
+
return padLength > text.length ? text.padStart(padLength, '0') : text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createIdFactory(
|
|
30
|
+
options: CreateIdFactoryOptions = {}
|
|
31
|
+
): IdFactory {
|
|
32
|
+
const prefix = options.prefix ?? '';
|
|
33
|
+
const separator = options.separator ?? '-';
|
|
34
|
+
const step = normalizeCounter(options.step ?? 1, 'step');
|
|
35
|
+
const padLength = Math.max(
|
|
36
|
+
0,
|
|
37
|
+
normalizeCounter(options.padLength ?? 0, 'padLength')
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (step === 0) {
|
|
41
|
+
throw new Error('step must not be 0');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let counter = normalizeCounter(options.startAt ?? 1, 'startAt');
|
|
45
|
+
|
|
46
|
+
const format = (value: number): string => {
|
|
47
|
+
const token = formatCounter(value, padLength);
|
|
48
|
+
if (prefix.length === 0) {
|
|
49
|
+
return token;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `${prefix}${separator}${token}`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
next: () => {
|
|
57
|
+
const value = format(counter);
|
|
58
|
+
counter += step;
|
|
59
|
+
return value;
|
|
60
|
+
},
|
|
61
|
+
peek: () => format(counter),
|
|
62
|
+
current: () => counter,
|
|
63
|
+
reset: (nextStartAt = options.startAt ?? 1) => {
|
|
64
|
+
counter = normalizeCounter(nextStartAt, 'startAt');
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CreateCommitIdFactoryOptions
|
|
70
|
+
extends Omit<CreateIdFactoryOptions, 'prefix'> {
|
|
71
|
+
prefix?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createCommitIdFactory(
|
|
75
|
+
options: CreateCommitIdFactoryOptions = {}
|
|
76
|
+
): IdFactory {
|
|
77
|
+
return createIdFactory({
|
|
78
|
+
prefix: options.prefix ?? 'commit',
|
|
79
|
+
separator: options.separator,
|
|
80
|
+
startAt: options.startAt,
|
|
81
|
+
step: options.step,
|
|
82
|
+
padLength: options.padLength,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CreateFakeClockOptions {
|
|
87
|
+
startMs?: number;
|
|
88
|
+
tickMs?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface FakeClock {
|
|
92
|
+
now: () => number;
|
|
93
|
+
iso: () => string;
|
|
94
|
+
set: (nextMs: number) => number;
|
|
95
|
+
advance: (deltaMs: number) => number;
|
|
96
|
+
tick: (stepMs?: number) => number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createFakeClock(
|
|
100
|
+
options: CreateFakeClockOptions = {}
|
|
101
|
+
): FakeClock {
|
|
102
|
+
const defaultTickMs = normalizeCounter(options.tickMs ?? 1, 'tickMs');
|
|
103
|
+
let nowMs = normalizeCounter(options.startMs ?? Date.now(), 'startMs');
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
now: () => nowMs,
|
|
107
|
+
iso: () => new Date(nowMs).toISOString(),
|
|
108
|
+
set: (nextMs: number) => {
|
|
109
|
+
nowMs = normalizeCounter(nextMs, 'nextMs');
|
|
110
|
+
return nowMs;
|
|
111
|
+
},
|
|
112
|
+
advance: (deltaMs: number) => {
|
|
113
|
+
nowMs += normalizeCounter(deltaMs, 'deltaMs');
|
|
114
|
+
return nowMs;
|
|
115
|
+
},
|
|
116
|
+
tick: (stepMs = defaultTickMs) => {
|
|
117
|
+
nowMs += normalizeCounter(stepMs, 'stepMs');
|
|
118
|
+
return nowMs;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export * from './assertions';
|
|
2
|
+
export * from './deterministic';
|
|
2
3
|
export * from './faults';
|
|
3
4
|
export * from './fixtures';
|
|
4
5
|
export * from './hono-node-server';
|
|
5
6
|
export * from './http-fixtures';
|
|
6
7
|
export * from './project-scoped-tasks';
|
|
8
|
+
export * from './realtime-ws';
|
|
7
9
|
export * from './runtime-process';
|
|
10
|
+
export * from './scenario-flow';
|
|
11
|
+
export * from './sync-builders';
|
|
8
12
|
export * from './sync-http';
|
|
13
|
+
export * from './sync-parse';
|
|
9
14
|
export * from './sync-response';
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { isRecord } from '@syncular/core';
|
|
2
|
+
|
|
3
|
+
export type RealtimeWsQueryValue = string | number | boolean | null | undefined;
|
|
4
|
+
|
|
5
|
+
export interface CreateRealtimeWsUrlOptions {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
actorId: string;
|
|
9
|
+
actorQueryParam?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
query?: Record<string, RealtimeWsQueryValue>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WebSocketConstructor {
|
|
15
|
+
new (url: string, protocols?: string | string[]): WebSocket;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OpenRealtimeWsOptions extends CreateRealtimeWsUrlOptions {
|
|
19
|
+
WebSocketCtor?: WebSocketConstructor;
|
|
20
|
+
protocols?: string | string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function appendQueryParam(
|
|
24
|
+
url: URL,
|
|
25
|
+
key: string,
|
|
26
|
+
value: RealtimeWsQueryValue
|
|
27
|
+
): void {
|
|
28
|
+
if (value === null || value === undefined) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
url.searchParams.set(key, String(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toWsProtocol(protocol: string): string {
|
|
36
|
+
if (protocol === 'http:') {
|
|
37
|
+
return 'ws:';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (protocol === 'https:') {
|
|
41
|
+
return 'wss:';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return protocol;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createRealtimeWsUrl(
|
|
48
|
+
options: CreateRealtimeWsUrlOptions
|
|
49
|
+
): string {
|
|
50
|
+
const url = new URL(options.baseUrl);
|
|
51
|
+
url.protocol = toWsProtocol(url.protocol);
|
|
52
|
+
|
|
53
|
+
const path = options.path ?? '/sync/realtime';
|
|
54
|
+
url.pathname = path.startsWith('/') ? path : `/${path}`;
|
|
55
|
+
url.search = '';
|
|
56
|
+
|
|
57
|
+
appendQueryParam(url, 'clientId', options.clientId);
|
|
58
|
+
appendQueryParam(url, options.actorQueryParam ?? 'userId', options.actorId);
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
61
|
+
appendQueryParam(url, key, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return url.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function openRealtimeWs(options: OpenRealtimeWsOptions): WebSocket {
|
|
68
|
+
const WebSocketCtor = options.WebSocketCtor ?? globalThis.WebSocket;
|
|
69
|
+
if (!WebSocketCtor) {
|
|
70
|
+
throw new Error('WebSocket constructor is unavailable in this runtime');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new WebSocketCtor(createRealtimeWsUrl(options), options.protocols);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface WaitForWsOpenOptions {
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function waitForWsOpen(
|
|
81
|
+
ws: WebSocket,
|
|
82
|
+
options: WaitForWsOpenOptions = {}
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
85
|
+
|
|
86
|
+
return new Promise<void>((resolve, reject) => {
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
ws.removeEventListener('open', onOpen);
|
|
90
|
+
ws.removeEventListener('error', onError);
|
|
91
|
+
ws.removeEventListener('close', onClose);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onOpen = () => {
|
|
95
|
+
cleanup();
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onError = () => {
|
|
100
|
+
cleanup();
|
|
101
|
+
reject(new Error('WebSocket emitted an error before opening'));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onClose = (event: CloseEvent) => {
|
|
105
|
+
cleanup();
|
|
106
|
+
reject(
|
|
107
|
+
new Error(
|
|
108
|
+
`WebSocket closed before opening (code=${event.code} reason=${event.reason || 'none'})`
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const timeout = setTimeout(() => {
|
|
114
|
+
cleanup();
|
|
115
|
+
reject(
|
|
116
|
+
new Error(`Timed out waiting for WebSocket open (${timeoutMs}ms)`)
|
|
117
|
+
);
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
|
|
120
|
+
ws.addEventListener('open', onOpen);
|
|
121
|
+
ws.addEventListener('error', onError);
|
|
122
|
+
ws.addEventListener('close', onClose);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface WaitForWsMessageOptions {
|
|
127
|
+
timeoutMs?: number;
|
|
128
|
+
predicate?: (event: MessageEvent) => boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function waitForWsMessage(
|
|
132
|
+
ws: WebSocket,
|
|
133
|
+
options: WaitForWsMessageOptions = {}
|
|
134
|
+
): Promise<MessageEvent> {
|
|
135
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
136
|
+
|
|
137
|
+
return new Promise<MessageEvent>((resolve, reject) => {
|
|
138
|
+
const cleanup = () => {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
ws.removeEventListener('message', onMessage);
|
|
141
|
+
ws.removeEventListener('error', onError);
|
|
142
|
+
ws.removeEventListener('close', onClose);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const onMessage = (event: MessageEvent) => {
|
|
146
|
+
if (options.predicate && !options.predicate(event)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
cleanup();
|
|
151
|
+
resolve(event);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const onError = () => {
|
|
155
|
+
cleanup();
|
|
156
|
+
reject(new Error('WebSocket emitted an error before a message arrived'));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const onClose = (event: CloseEvent) => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(
|
|
162
|
+
new Error(
|
|
163
|
+
`WebSocket closed before receiving a matching message (code=${event.code} reason=${event.reason || 'none'})`
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const timeout = setTimeout(() => {
|
|
169
|
+
cleanup();
|
|
170
|
+
reject(
|
|
171
|
+
new Error(`Timed out waiting for WebSocket message (${timeoutMs}ms)`)
|
|
172
|
+
);
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
|
|
175
|
+
ws.addEventListener('message', onMessage);
|
|
176
|
+
ws.addEventListener('error', onError);
|
|
177
|
+
ws.addEventListener('close', onClose);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readTextPayload(data: MessageEvent['data']): string | null {
|
|
182
|
+
if (typeof data === 'string') {
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (data instanceof ArrayBuffer) {
|
|
187
|
+
return new TextDecoder().decode(new Uint8Array(data));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (ArrayBuffer.isView(data)) {
|
|
191
|
+
return new TextDecoder().decode(
|
|
192
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function parseWsJsonMessage(
|
|
200
|
+
event: MessageEvent
|
|
201
|
+
): Record<string, unknown> | null {
|
|
202
|
+
const payload = readTextPayload(event.data);
|
|
203
|
+
if (!payload) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(payload);
|
|
209
|
+
return isRecord(parsed) ? parsed : null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface WaitForWsJsonMessageOptions {
|
|
216
|
+
timeoutMs?: number;
|
|
217
|
+
predicate?: (message: Record<string, unknown>) => boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function waitForWsJsonMessage(
|
|
221
|
+
ws: WebSocket,
|
|
222
|
+
options: WaitForWsJsonMessageOptions = {}
|
|
223
|
+
): Promise<Record<string, unknown>> {
|
|
224
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
225
|
+
|
|
226
|
+
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
227
|
+
const cleanup = () => {
|
|
228
|
+
clearTimeout(timeout);
|
|
229
|
+
ws.removeEventListener('message', onMessage);
|
|
230
|
+
ws.removeEventListener('error', onError);
|
|
231
|
+
ws.removeEventListener('close', onClose);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const onMessage = (event: MessageEvent) => {
|
|
235
|
+
const parsed = parseWsJsonMessage(event);
|
|
236
|
+
if (!parsed) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (options.predicate && !options.predicate(parsed)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
cleanup();
|
|
245
|
+
resolve(parsed);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const onError = () => {
|
|
249
|
+
cleanup();
|
|
250
|
+
reject(new Error('WebSocket emitted an error before JSON message'));
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const onClose = (event: CloseEvent) => {
|
|
254
|
+
cleanup();
|
|
255
|
+
reject(
|
|
256
|
+
new Error(
|
|
257
|
+
`WebSocket closed before receiving JSON message (code=${event.code} reason=${event.reason || 'none'})`
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const timeout = setTimeout(() => {
|
|
263
|
+
cleanup();
|
|
264
|
+
reject(
|
|
265
|
+
new Error(
|
|
266
|
+
`Timed out waiting for WebSocket JSON message (${timeoutMs}ms)`
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}, timeoutMs);
|
|
270
|
+
|
|
271
|
+
ws.addEventListener('message', onMessage);
|
|
272
|
+
ws.addEventListener('error', onError);
|
|
273
|
+
ws.addEventListener('close', onClose);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface CloseWsSafeOptions {
|
|
278
|
+
code?: number;
|
|
279
|
+
reason?: string;
|
|
280
|
+
timeoutMs?: number;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const WS_CLOSING = 2;
|
|
284
|
+
const WS_CLOSED = 3;
|
|
285
|
+
|
|
286
|
+
export async function closeWsSafe(
|
|
287
|
+
ws: WebSocket,
|
|
288
|
+
options: CloseWsSafeOptions = {}
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
if (ws.readyState === WS_CLOSED) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const timeoutMs = options.timeoutMs ?? 2_000;
|
|
295
|
+
|
|
296
|
+
await new Promise<void>((resolve) => {
|
|
297
|
+
const cleanup = () => {
|
|
298
|
+
clearTimeout(timeout);
|
|
299
|
+
ws.removeEventListener('close', onClose);
|
|
300
|
+
resolve();
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const onClose = () => {
|
|
304
|
+
cleanup();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const timeout = setTimeout(() => {
|
|
308
|
+
cleanup();
|
|
309
|
+
}, timeoutMs);
|
|
310
|
+
|
|
311
|
+
ws.addEventListener('close', onClose);
|
|
312
|
+
|
|
313
|
+
if (ws.readyState !== WS_CLOSING) {
|
|
314
|
+
try {
|
|
315
|
+
ws.close(options.code, options.reason);
|
|
316
|
+
} catch {
|
|
317
|
+
cleanup();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|