@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClientTableRegistry,
|
|
3
|
+
enqueueOutboxCommit,
|
|
4
|
+
type SyncClientDb,
|
|
5
|
+
type SyncOnceOptions,
|
|
6
|
+
type SyncOnceResult,
|
|
7
|
+
type SyncPullOnceOptions,
|
|
8
|
+
type SyncPullResponse,
|
|
9
|
+
type SyncPushOnceOptions,
|
|
10
|
+
type SyncPushOnceResult,
|
|
11
|
+
syncOnce,
|
|
12
|
+
syncPullOnce,
|
|
13
|
+
syncPushOnce,
|
|
14
|
+
} from '@syncular/client';
|
|
15
|
+
import type {
|
|
16
|
+
SyncCombinedRequest,
|
|
17
|
+
SyncCombinedResponse,
|
|
18
|
+
SyncTransport,
|
|
19
|
+
} from '@syncular/core';
|
|
20
|
+
import type { Kysely } from 'kysely';
|
|
21
|
+
|
|
22
|
+
export interface ScenarioFlowClient<DB extends SyncClientDb> {
|
|
23
|
+
db: Kysely<DB>;
|
|
24
|
+
transport: SyncTransport;
|
|
25
|
+
handlers: ClientTableRegistry<DB>;
|
|
26
|
+
clientId: string;
|
|
27
|
+
actorId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PushThenPullOptions<DB extends SyncClientDb> {
|
|
31
|
+
enqueue?: Parameters<typeof enqueueOutboxCommit<DB>>[1];
|
|
32
|
+
push?: Omit<SyncPushOnceOptions, 'clientId' | 'actorId'>;
|
|
33
|
+
pull: Omit<SyncPullOnceOptions, 'clientId' | 'actorId'>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PushThenPullResult<DB extends SyncClientDb> {
|
|
37
|
+
enqueueResult?: { id: string; clientCommitId: string };
|
|
38
|
+
pushResult: SyncPushOnceResult;
|
|
39
|
+
pullResult: SyncPullResponse;
|
|
40
|
+
client: ScenarioFlowClient<DB>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ScenarioFlow<DB extends SyncClientDb> {
|
|
44
|
+
client: ScenarioFlowClient<DB>;
|
|
45
|
+
enqueue: (
|
|
46
|
+
args: Parameters<typeof enqueueOutboxCommit<DB>>[1]
|
|
47
|
+
) => Promise<{ id: string; clientCommitId: string }>;
|
|
48
|
+
push: (
|
|
49
|
+
options?: Omit<SyncPushOnceOptions, 'clientId' | 'actorId'>
|
|
50
|
+
) => Promise<SyncPushOnceResult>;
|
|
51
|
+
pull: (
|
|
52
|
+
options: Omit<SyncPullOnceOptions, 'clientId' | 'actorId'>
|
|
53
|
+
) => Promise<SyncPullResponse>;
|
|
54
|
+
sync: (
|
|
55
|
+
options: Omit<SyncOnceOptions, 'clientId' | 'actorId'>
|
|
56
|
+
) => Promise<SyncOnceResult>;
|
|
57
|
+
transportSync: (
|
|
58
|
+
request: Omit<SyncCombinedRequest, 'clientId'>
|
|
59
|
+
) => Promise<SyncCombinedResponse>;
|
|
60
|
+
pushThenPull: (
|
|
61
|
+
options: PushThenPullOptions<DB>
|
|
62
|
+
) => Promise<PushThenPullResult<DB>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function withClientIdentity<
|
|
66
|
+
DB extends SyncClientDb,
|
|
67
|
+
T extends Record<string, unknown>,
|
|
68
|
+
>(
|
|
69
|
+
values: T,
|
|
70
|
+
client: ScenarioFlowClient<DB>
|
|
71
|
+
): T & { clientId: string; actorId?: string } {
|
|
72
|
+
return {
|
|
73
|
+
...values,
|
|
74
|
+
clientId: client.clientId,
|
|
75
|
+
...(client.actorId ? { actorId: client.actorId } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createScenarioFlow<DB extends SyncClientDb>(
|
|
80
|
+
client: ScenarioFlowClient<DB>
|
|
81
|
+
): ScenarioFlow<DB> {
|
|
82
|
+
const enqueue: ScenarioFlow<DB>['enqueue'] = (args) =>
|
|
83
|
+
enqueueOutboxCommit(client.db, args);
|
|
84
|
+
|
|
85
|
+
const push: ScenarioFlow<DB>['push'] = (options) =>
|
|
86
|
+
syncPushOnce(
|
|
87
|
+
client.db,
|
|
88
|
+
client.transport,
|
|
89
|
+
withClientIdentity(options ?? {}, client)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const pull: ScenarioFlow<DB>['pull'] = (options) =>
|
|
93
|
+
syncPullOnce(
|
|
94
|
+
client.db,
|
|
95
|
+
client.transport,
|
|
96
|
+
client.handlers,
|
|
97
|
+
withClientIdentity(options, client)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const sync: ScenarioFlow<DB>['sync'] = (options) =>
|
|
101
|
+
syncOnce(
|
|
102
|
+
client.db,
|
|
103
|
+
client.transport,
|
|
104
|
+
client.handlers,
|
|
105
|
+
withClientIdentity(options, client)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const transportSync: ScenarioFlow<DB>['transportSync'] = (request) =>
|
|
109
|
+
client.transport.sync({ clientId: client.clientId, ...request });
|
|
110
|
+
|
|
111
|
+
const pushThenPull: ScenarioFlow<DB>['pushThenPull'] = async (options) => {
|
|
112
|
+
const enqueueResult = options.enqueue
|
|
113
|
+
? await enqueueOutboxCommit(client.db, options.enqueue)
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
116
|
+
const pushResult = await push(options.push);
|
|
117
|
+
const pullResult = await pull(options.pull);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
enqueueResult,
|
|
121
|
+
pushResult,
|
|
122
|
+
pullResult,
|
|
123
|
+
client,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
client,
|
|
129
|
+
enqueue,
|
|
130
|
+
push,
|
|
131
|
+
pull,
|
|
132
|
+
sync,
|
|
133
|
+
transportSync,
|
|
134
|
+
pushThenPull,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function runPushPullCycle<DB extends SyncClientDb>(
|
|
139
|
+
client: ScenarioFlowClient<DB>,
|
|
140
|
+
options: PushThenPullOptions<DB>
|
|
141
|
+
): Promise<PushThenPullResult<DB>> {
|
|
142
|
+
return createScenarioFlow(client).pushThenPull(options);
|
|
143
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SyncCombinedRequest,
|
|
3
|
+
SyncOperation,
|
|
4
|
+
SyncPullRequest,
|
|
5
|
+
SyncPushRequest,
|
|
6
|
+
SyncSubscriptionRequest,
|
|
7
|
+
} from '@syncular/core';
|
|
8
|
+
|
|
9
|
+
export interface CreateSyncUpsertOperationOptions {
|
|
10
|
+
table: string;
|
|
11
|
+
rowId: string;
|
|
12
|
+
payload: Record<string, unknown>;
|
|
13
|
+
baseVersion?: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createSyncUpsertOperation(
|
|
17
|
+
options: CreateSyncUpsertOperationOptions
|
|
18
|
+
): SyncOperation {
|
|
19
|
+
return {
|
|
20
|
+
table: options.table,
|
|
21
|
+
row_id: options.rowId,
|
|
22
|
+
op: 'upsert',
|
|
23
|
+
payload: options.payload,
|
|
24
|
+
base_version: options.baseVersion ?? null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CreateSyncDeleteOperationOptions {
|
|
29
|
+
table: string;
|
|
30
|
+
rowId: string;
|
|
31
|
+
baseVersion?: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createSyncDeleteOperation(
|
|
35
|
+
options: CreateSyncDeleteOperationOptions
|
|
36
|
+
): SyncOperation {
|
|
37
|
+
return {
|
|
38
|
+
table: options.table,
|
|
39
|
+
row_id: options.rowId,
|
|
40
|
+
op: 'delete',
|
|
41
|
+
payload: null,
|
|
42
|
+
base_version: options.baseVersion ?? null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CreateSyncSubscriptionOptions {
|
|
47
|
+
id: string;
|
|
48
|
+
table: string;
|
|
49
|
+
scopes: SyncSubscriptionRequest['scopes'];
|
|
50
|
+
params?: SyncSubscriptionRequest['params'];
|
|
51
|
+
cursor?: number;
|
|
52
|
+
bootstrapState?: SyncSubscriptionRequest['bootstrapState'];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createSyncSubscription(
|
|
56
|
+
options: CreateSyncSubscriptionOptions
|
|
57
|
+
): SyncSubscriptionRequest {
|
|
58
|
+
return {
|
|
59
|
+
id: options.id,
|
|
60
|
+
table: options.table,
|
|
61
|
+
scopes: options.scopes,
|
|
62
|
+
...(options.params ? { params: options.params } : {}),
|
|
63
|
+
cursor: options.cursor ?? 0,
|
|
64
|
+
bootstrapState: options.bootstrapState ?? null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CreateSyncPushRequestOptions {
|
|
69
|
+
clientId: string;
|
|
70
|
+
clientCommitId: string;
|
|
71
|
+
operations: SyncOperation[];
|
|
72
|
+
schemaVersion?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createSyncPushRequest(
|
|
76
|
+
options: CreateSyncPushRequestOptions
|
|
77
|
+
): SyncPushRequest {
|
|
78
|
+
return {
|
|
79
|
+
clientId: options.clientId,
|
|
80
|
+
clientCommitId: options.clientCommitId,
|
|
81
|
+
operations: options.operations,
|
|
82
|
+
schemaVersion: options.schemaVersion ?? 1,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CreateSyncPullRequestOptions {
|
|
87
|
+
clientId: string;
|
|
88
|
+
subscriptions: SyncSubscriptionRequest[];
|
|
89
|
+
limitCommits?: number;
|
|
90
|
+
limitSnapshotRows?: number;
|
|
91
|
+
maxSnapshotPages?: number;
|
|
92
|
+
dedupeRows?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createSyncPullRequest(
|
|
96
|
+
options: CreateSyncPullRequestOptions
|
|
97
|
+
): SyncPullRequest {
|
|
98
|
+
return {
|
|
99
|
+
clientId: options.clientId,
|
|
100
|
+
subscriptions: options.subscriptions,
|
|
101
|
+
limitCommits: options.limitCommits ?? 50,
|
|
102
|
+
...(options.limitSnapshotRows !== undefined
|
|
103
|
+
? { limitSnapshotRows: options.limitSnapshotRows }
|
|
104
|
+
: {}),
|
|
105
|
+
...(options.maxSnapshotPages !== undefined
|
|
106
|
+
? { maxSnapshotPages: options.maxSnapshotPages }
|
|
107
|
+
: {}),
|
|
108
|
+
...(options.dedupeRows !== undefined
|
|
109
|
+
? { dedupeRows: options.dedupeRows }
|
|
110
|
+
: {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface CreateSyncCombinedRequestOptions {
|
|
115
|
+
clientId: string;
|
|
116
|
+
push?: Omit<SyncPushRequest, 'clientId'>;
|
|
117
|
+
pull?: Omit<SyncPullRequest, 'clientId'>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createSyncCombinedRequest(
|
|
121
|
+
options: CreateSyncCombinedRequestOptions
|
|
122
|
+
): SyncCombinedRequest {
|
|
123
|
+
return {
|
|
124
|
+
clientId: options.clientId,
|
|
125
|
+
...(options.push ? { push: options.push } : {}),
|
|
126
|
+
...(options.pull ? { pull: options.pull } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/sync-http.ts
CHANGED
|
@@ -6,6 +6,11 @@ import type {
|
|
|
6
6
|
SyncPushRequest,
|
|
7
7
|
SyncPushResponse,
|
|
8
8
|
} from '@syncular/core';
|
|
9
|
+
import {
|
|
10
|
+
parseSyncCombinedResponse,
|
|
11
|
+
parseSyncPullResponse,
|
|
12
|
+
parseSyncPushResponse,
|
|
13
|
+
} from './sync-parse';
|
|
9
14
|
|
|
10
15
|
export interface JsonActorHeadersOptions {
|
|
11
16
|
actorId: string;
|
|
@@ -66,7 +71,11 @@ export interface PostSyncCombinedRequestOptions {
|
|
|
66
71
|
export async function postSyncCombinedRequest(
|
|
67
72
|
options: PostSyncCombinedRequestOptions
|
|
68
73
|
): Promise<PostJsonWithActorResult<SyncCombinedResponse>> {
|
|
69
|
-
|
|
74
|
+
const result = await postJsonWithActor<SyncCombinedRequest, unknown>(options);
|
|
75
|
+
return {
|
|
76
|
+
response: result.response,
|
|
77
|
+
json: parseSyncCombinedResponse(result.json),
|
|
78
|
+
};
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
export interface PostSyncPushRequestOptions {
|
|
@@ -81,7 +90,11 @@ export interface PostSyncPushRequestOptions {
|
|
|
81
90
|
export async function postSyncPushRequest(
|
|
82
91
|
options: PostSyncPushRequestOptions
|
|
83
92
|
): Promise<PostJsonWithActorResult<SyncPushResponse>> {
|
|
84
|
-
|
|
93
|
+
const result = await postJsonWithActor<SyncPushRequest, unknown>(options);
|
|
94
|
+
return {
|
|
95
|
+
response: result.response,
|
|
96
|
+
json: parseSyncPushResponse(result.json),
|
|
97
|
+
};
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
export interface PostSyncPullRequestOptions {
|
|
@@ -96,5 +109,9 @@ export interface PostSyncPullRequestOptions {
|
|
|
96
109
|
export async function postSyncPullRequest(
|
|
97
110
|
options: PostSyncPullRequestOptions
|
|
98
111
|
): Promise<PostJsonWithActorResult<SyncPullResponse>> {
|
|
99
|
-
|
|
112
|
+
const result = await postJsonWithActor<SyncPullRequest, unknown>(options);
|
|
113
|
+
return {
|
|
114
|
+
response: result.response,
|
|
115
|
+
json: parseSyncPullResponse(result.json),
|
|
116
|
+
};
|
|
100
117
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SyncCombinedResponse,
|
|
3
|
+
SyncCombinedResponseSchema,
|
|
4
|
+
type SyncPullResponse,
|
|
5
|
+
SyncPullResponseSchema,
|
|
6
|
+
type SyncPushResponse,
|
|
7
|
+
SyncPushResponseSchema,
|
|
8
|
+
} from '@syncular/core';
|
|
9
|
+
|
|
10
|
+
interface ParseIssue {
|
|
11
|
+
path: ReadonlyArray<PropertyKey>;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ParseResult<T> =
|
|
16
|
+
| { success: true; data: T }
|
|
17
|
+
| { success: false; error: { issues: ReadonlyArray<ParseIssue> } };
|
|
18
|
+
|
|
19
|
+
function formatIssue(issue: ParseIssue): string {
|
|
20
|
+
const path =
|
|
21
|
+
issue.path.length > 0
|
|
22
|
+
? issue.path
|
|
23
|
+
.map((segment) =>
|
|
24
|
+
typeof segment === 'symbol' ? segment.toString() : String(segment)
|
|
25
|
+
)
|
|
26
|
+
.join('.')
|
|
27
|
+
: 'root';
|
|
28
|
+
return `${path}: ${issue.message}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatIssues(issues: ReadonlyArray<ParseIssue>): string {
|
|
32
|
+
return issues.map((issue) => formatIssue(issue)).join('; ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseOrThrow<T>(
|
|
36
|
+
label: string,
|
|
37
|
+
value: unknown,
|
|
38
|
+
parse: (input: unknown) => ParseResult<T>
|
|
39
|
+
): T {
|
|
40
|
+
const parsed = parse(value);
|
|
41
|
+
if (parsed.success) {
|
|
42
|
+
return parsed.data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${label} validation failed: ${formatIssues(parsed.error.issues)}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseSyncCombinedResponse(
|
|
51
|
+
value: unknown
|
|
52
|
+
): SyncCombinedResponse {
|
|
53
|
+
return parseOrThrow('SyncCombinedResponse', value, (input) =>
|
|
54
|
+
SyncCombinedResponseSchema.safeParse(input)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseSyncPushResponse(value: unknown): SyncPushResponse {
|
|
59
|
+
return parseOrThrow('SyncPushResponse', value, (input) =>
|
|
60
|
+
SyncPushResponseSchema.safeParse(input)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseSyncPullResponse(value: unknown): SyncPullResponse {
|
|
65
|
+
return parseOrThrow('SyncPullResponse', value, (input) =>
|
|
66
|
+
SyncPullResponseSchema.safeParse(input)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function readSyncCombinedResponse(
|
|
71
|
+
response: Response
|
|
72
|
+
): Promise<SyncCombinedResponse> {
|
|
73
|
+
return parseSyncCombinedResponse(await response.json());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function readSyncPushResponse(
|
|
77
|
+
response: Response
|
|
78
|
+
): Promise<SyncPushResponse> {
|
|
79
|
+
return parseSyncPushResponse(await response.json());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readSyncPullResponse(
|
|
83
|
+
response: Response
|
|
84
|
+
): Promise<SyncPullResponse> {
|
|
85
|
+
return parseSyncPullResponse(await response.json());
|
|
86
|
+
}
|