@syncular/testkit 0.0.6-96 → 0.1.0

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.
Files changed (71) hide show
  1. package/dist/audit.d.ts +22 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +40 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/client-bridge.d.ts +77 -0
  6. package/dist/client-bridge.d.ts.map +1 -0
  7. package/dist/client-bridge.js +540 -0
  8. package/dist/client-bridge.js.map +1 -0
  9. package/dist/deterministic.js.map +1 -1
  10. package/dist/disposable.js.map +1 -1
  11. package/dist/faults.d.ts +14 -2
  12. package/dist/faults.d.ts.map +1 -1
  13. package/dist/faults.js +104 -42
  14. package/dist/faults.js.map +1 -1
  15. package/dist/hono-node-server.js +2 -2
  16. package/dist/hono-node-server.js.map +1 -1
  17. package/dist/http-fixtures.d.ts +1 -30
  18. package/dist/http-fixtures.d.ts.map +1 -1
  19. package/dist/http-fixtures.js +6 -66
  20. package/dist/http-fixtures.js.map +1 -1
  21. package/dist/index.d.ts +2 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +14 -16
  24. package/dist/index.js.map +1 -1
  25. package/dist/project-scoped-tasks.d.ts.map +1 -1
  26. package/dist/project-scoped-tasks.js +6 -4
  27. package/dist/project-scoped-tasks.js.map +1 -1
  28. package/dist/realtime-ws.js +1 -1
  29. package/dist/realtime-ws.js.map +1 -1
  30. package/dist/runtime-process.js +1 -1
  31. package/dist/runtime-process.js.map +1 -1
  32. package/dist/sync-builders.d.ts +5 -3
  33. package/dist/sync-builders.d.ts.map +1 -1
  34. package/dist/sync-builders.js +9 -3
  35. package/dist/sync-builders.js.map +1 -1
  36. package/dist/sync-http.js +1 -1
  37. package/dist/sync-http.js.map +1 -1
  38. package/dist/sync-parse.js.map +1 -1
  39. package/dist/sync-response.d.ts +3 -4
  40. package/dist/sync-response.d.ts.map +1 -1
  41. package/dist/sync-response.js.map +1 -1
  42. package/package.json +15 -16
  43. package/src/audit.ts +74 -0
  44. package/src/client-bridge.ts +841 -0
  45. package/src/faults.ts +178 -55
  46. package/src/hono-node-server.ts +1 -1
  47. package/src/http-fixtures.ts +6 -127
  48. package/src/index.ts +2 -4
  49. package/src/project-scoped-tasks.ts +6 -4
  50. package/src/sync-builders.ts +14 -6
  51. package/src/sync-response.ts +3 -5
  52. package/dist/assertions.d.ts +0 -61
  53. package/dist/assertions.d.ts.map +0 -1
  54. package/dist/assertions.js +0 -239
  55. package/dist/assertions.js.map +0 -1
  56. package/dist/fixtures.d.ts +0 -104
  57. package/dist/fixtures.d.ts.map +0 -1
  58. package/dist/fixtures.js +0 -558
  59. package/dist/fixtures.js.map +0 -1
  60. package/dist/resources.d.ts +0 -25
  61. package/dist/resources.d.ts.map +0 -1
  62. package/dist/resources.js +0 -64
  63. package/dist/resources.js.map +0 -1
  64. package/dist/scenario-flow.d.ts +0 -39
  65. package/dist/scenario-flow.d.ts.map +0 -1
  66. package/dist/scenario-flow.js +0 -41
  67. package/dist/scenario-flow.js.map +0 -1
  68. package/src/assertions.ts +0 -432
  69. package/src/fixtures.ts +0 -867
  70. package/src/resources.ts +0 -186
  71. 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
- SyncPushResponse,
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
- if (currentOptions.latencyMs && currentOptions.latencyMs > 0) {
51
- await new Promise((resolve) =>
52
- setTimeout(resolve, currentOptions.latencyMs)
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: 'push' | 'pull' | 'fetch',
111
+ operation: FaultTransportOperation,
59
112
  count: number
60
113
  ): boolean => {
61
- if (operation === 'push' && currentOptions.failOnPull) {
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 transport: SyncTransport = {
90
- async sync(request, transportOptions) {
91
- await maybeDelay();
134
+ const fail = (operation: FaultTransportOperation, error: Error): never => {
135
+ state.failureCount++;
136
+ currentOptions.onFail?.(operation, error);
137
+ throw error;
138
+ };
92
139
 
93
- const operation = request.push ? 'push' : 'pull';
94
- const count = operation === 'push' ? state.pushCount : state.pullCount;
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
- if (shouldFail(operation, count)) {
97
- const error = getError();
98
- state.failureCount++;
99
- currentOptions.onFail?.(operation, error);
100
- throw error;
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
- if (operation === 'push') {
104
- state.pushCount++;
105
- } else {
106
- state.pullCount++;
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
- const result = await baseTransport.sync(request, transportOptions);
110
- currentOptions.onSuccess?.(operation);
111
- return result;
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
- await maybeDelay();
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?: SyncPushResponse;
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?: SyncPushResponse;
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
- status: 'applied',
169
- results: request.push.operations.map((_, i) => ({
170
- opIndex: i,
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
  }
@@ -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) => {
@@ -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 { createPostgresServerDialect } from '@syncular/server-dialect-postgres';
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-hono';
31
- import { createHttpTransport } from '@syncular/transport-http';
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 './assertions';
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: `UNKNOWN_TABLE:${op.table}`,
154
- code: 'UNKNOWN_TABLE',
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: 'MISSING_PROJECT_ID',
240
- code: 'INVALID_REQUEST',
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
 
@@ -2,7 +2,7 @@ import type {
2
2
  SyncCombinedRequest,
3
3
  SyncOperation,
4
4
  SyncPullRequest,
5
- SyncPushRequest,
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
- ): SyncPushRequest {
79
+ ): SyncPushBatchRequest {
78
80
  return {
79
81
  clientId: options.clientId,
80
- clientCommitId: options.clientCommitId,
81
- operations: options.operations,
82
- schemaVersion: options.schemaVersion ?? 1,
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<SyncPushRequest, 'clientId'>;
124
+ push?: Omit<SyncPushBatchRequest, 'clientId'>;
117
125
  pull?: Omit<SyncPullRequest, 'clientId'>;
118
126
  }
119
127
 
@@ -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
- ): SyncChangeRecord[] {
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
- ): SyncChangeRecord | undefined {
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: SyncChangeRecord | undefined
36
+ change: SyncChange | undefined
39
37
  ): Record<string, unknown> | undefined {
40
38
  if (!change) {
41
39
  return undefined;