@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.
@@ -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
- return postJsonWithActor<SyncCombinedRequest, SyncCombinedResponse>(options);
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
- return postJsonWithActor<SyncPushRequest, SyncPushResponse>(options);
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
- return postJsonWithActor<SyncPullRequest, SyncPullResponse>(options);
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
+ }