@syncular/client 0.0.6-136 → 0.0.6-139

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.
@@ -10,6 +10,7 @@ import type {
10
10
  import { countSyncMetric } from '@syncular/core';
11
11
  import type { Kysely } from 'kysely';
12
12
  import { upsertConflictsForRejectedCommit } from './conflicts';
13
+ import type { PushResultInfo } from './engine/types';
13
14
  import {
14
15
  getNextSendableOutboxCommit,
15
16
  markOutboxCommitAcked,
@@ -31,6 +32,7 @@ export interface SyncPushOnceOptions {
31
32
  export interface SyncPushOnceResult {
32
33
  pushed: boolean;
33
34
  response?: SyncPushResponse;
35
+ pushResult?: PushResultInfo;
34
36
  }
35
37
 
36
38
  interface TransportWithWsPush extends SyncTransport {
@@ -48,6 +50,41 @@ function clonePushRequest(request: SyncPushRequest): SyncPushRequest {
48
50
  return JSON.parse(JSON.stringify(request)) as SyncPushRequest;
49
51
  }
50
52
 
53
+ function firstPushErrorCode(response: SyncPushResponse): string | null {
54
+ const firstError = response.results.find(
55
+ (result) => result.status === 'error'
56
+ );
57
+ if (
58
+ firstError &&
59
+ 'code' in firstError &&
60
+ typeof firstError.code === 'string' &&
61
+ firstError.code
62
+ ) {
63
+ return firstError.code;
64
+ }
65
+ const hasConflict = response.results.some(
66
+ (result) => result.status === 'conflict'
67
+ );
68
+ return hasConflict ? 'CONFLICT' : null;
69
+ }
70
+
71
+ function buildPushResult(args: {
72
+ outboxCommitId: string;
73
+ clientCommitId: string;
74
+ status: PushResultInfo['status'];
75
+ response: SyncPushResponse;
76
+ }): PushResultInfo {
77
+ return {
78
+ outboxCommitId: args.outboxCommitId,
79
+ clientCommitId: args.clientCommitId,
80
+ status: args.status,
81
+ commitSeq: args.response.commitSeq ?? null,
82
+ results: args.response.results,
83
+ errorCode: firstPushErrorCode(args.response),
84
+ timestamp: Date.now(),
85
+ };
86
+ }
87
+
51
88
  export async function syncPushOnce<DB extends SyncClientDb>(
52
89
  db: Kysely<DB>,
53
90
  transport: SyncTransport,
@@ -178,7 +215,16 @@ export async function syncPushOnce<DB extends SyncClientDb>(
178
215
  commitSeq: responseToUse.commitSeq ?? null,
179
216
  responseJson,
180
217
  });
181
- return { pushed: true, response: responseToUse };
218
+ return {
219
+ pushed: true,
220
+ response: responseToUse,
221
+ pushResult: buildPushResult({
222
+ outboxCommitId: next.id,
223
+ clientCommitId: next.client_commit_id,
224
+ status: responseToUse.status,
225
+ response: responseToUse,
226
+ }),
227
+ };
182
228
  }
183
229
 
184
230
  // Check if all errors are retriable - if so, keep pending for retry
@@ -198,7 +244,16 @@ export async function syncPushOnce<DB extends SyncClientDb>(
198
244
  error: `Retriable: ${errorMessages}`,
199
245
  responseJson,
200
246
  });
201
- return { pushed: true, response: responseToUse };
247
+ return {
248
+ pushed: true,
249
+ response: responseToUse,
250
+ pushResult: buildPushResult({
251
+ outboxCommitId: next.id,
252
+ clientCommitId: next.client_commit_id,
253
+ status: 'retriable',
254
+ response: responseToUse,
255
+ }),
256
+ };
202
257
  }
203
258
 
204
259
  // Terminal rejection - mark as failed and record conflicts
@@ -212,5 +267,14 @@ export async function syncPushOnce<DB extends SyncClientDb>(
212
267
  error: 'REJECTED',
213
268
  responseJson,
214
269
  });
215
- return { pushed: true, response: responseToUse };
270
+ return {
271
+ pushed: true,
272
+ response: responseToUse,
273
+ pushResult: buildPushResult({
274
+ outboxCommitId: next.id,
275
+ clientCommitId: next.client_commit_id,
276
+ status: 'rejected',
277
+ response: responseToUse,
278
+ }),
279
+ };
216
280
  }
package/src/sync-loop.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  } from '@syncular/core';
16
16
  import type { Kysely } from 'kysely';
17
17
  import { upsertConflictsForRejectedCommit } from './conflicts';
18
+ import type { PushResultInfo } from './engine/types';
18
19
  import type { ClientHandlerCollection } from './handlers/collection';
19
20
  import {
20
21
  getNextSendableOutboxCommit,
@@ -41,6 +42,42 @@ interface SyncPushUntilSettledOptions extends SyncPushOnceOptions {
41
42
 
42
43
  interface SyncPushUntilSettledResult {
43
44
  pushedCount: number;
45
+ pushResults: PushResultInfo[];
46
+ }
47
+
48
+ function firstPushErrorCode(response: SyncPushResponse): string | null {
49
+ const firstError = response.results.find(
50
+ (result) => result.status === 'error'
51
+ );
52
+ if (
53
+ firstError &&
54
+ 'code' in firstError &&
55
+ typeof firstError.code === 'string' &&
56
+ firstError.code
57
+ ) {
58
+ return firstError.code;
59
+ }
60
+ const hasConflict = response.results.some(
61
+ (result) => result.status === 'conflict'
62
+ );
63
+ return hasConflict ? 'CONFLICT' : null;
64
+ }
65
+
66
+ function buildPushResult(args: {
67
+ outboxCommitId: string;
68
+ clientCommitId: string;
69
+ status: PushResultInfo['status'];
70
+ response: SyncPushResponse;
71
+ }): PushResultInfo {
72
+ return {
73
+ outboxCommitId: args.outboxCommitId,
74
+ clientCommitId: args.clientCommitId,
75
+ status: args.status,
76
+ commitSeq: args.response.commitSeq ?? null,
77
+ results: args.response.results,
78
+ errorCode: firstPushErrorCode(args.response),
79
+ timestamp: Date.now(),
80
+ };
44
81
  }
45
82
 
46
83
  async function syncPushUntilSettled<DB extends SyncClientDb>(
@@ -51,6 +88,7 @@ async function syncPushUntilSettled<DB extends SyncClientDb>(
51
88
  const maxCommits = Math.max(1, Math.min(1000, options.maxCommits ?? 20));
52
89
 
53
90
  let pushedCount = 0;
91
+ const pushResults: PushResultInfo[] = [];
54
92
  for (let i = 0; i < maxCommits; i++) {
55
93
  const res = await syncPushOnce(db, transport, {
56
94
  clientId: options.clientId,
@@ -59,9 +97,12 @@ async function syncPushUntilSettled<DB extends SyncClientDb>(
59
97
  });
60
98
  if (!res.pushed) break;
61
99
  pushedCount += 1;
100
+ if (res.pushResult) {
101
+ pushResults.push(res.pushResult);
102
+ }
62
103
  }
63
104
 
64
- return { pushedCount };
105
+ return { pushedCount, pushResults };
65
106
  }
66
107
 
67
108
  interface SyncPullUntilSettledOptions extends SyncPullOnceOptions {
@@ -176,6 +217,7 @@ export interface SyncOnceResult {
176
217
  pushedCommits: number;
177
218
  pullRounds: number;
178
219
  pullResponse: SyncPullResponse;
220
+ pushResults: PushResultInfo[];
179
221
  }
180
222
 
181
223
  /**
@@ -276,6 +318,7 @@ async function syncOnceCombined<DB extends SyncClientDb>(
276
318
 
277
319
  // Process push response
278
320
  let pushedCommits = 0;
321
+ const pushResults: PushResultInfo[] = [];
279
322
  if (outbox && pushRequest) {
280
323
  let pushRes = wsPushResponse ?? combined.push;
281
324
  if (!pushRes) {
@@ -303,6 +346,14 @@ async function syncOnceCombined<DB extends SyncClientDb>(
303
346
  commitSeq: pushRes.commitSeq ?? null,
304
347
  responseJson,
305
348
  });
349
+ pushResults.push(
350
+ buildPushResult({
351
+ outboxCommitId: outbox.id,
352
+ clientCommitId: outbox.client_commit_id,
353
+ status: pushRes.status,
354
+ response: pushRes,
355
+ })
356
+ );
306
357
  pushedCommits = 1;
307
358
  } else {
308
359
  // Check if all errors are retriable
@@ -317,6 +368,14 @@ async function syncOnceCombined<DB extends SyncClientDb>(
317
368
  error: 'Retriable',
318
369
  responseJson,
319
370
  });
371
+ pushResults.push(
372
+ buildPushResult({
373
+ outboxCommitId: outbox.id,
374
+ clientCommitId: outbox.client_commit_id,
375
+ status: 'retriable',
376
+ response: pushRes,
377
+ })
378
+ );
320
379
  pushedCommits = 1;
321
380
  } else {
322
381
  await upsertConflictsForRejectedCommit(db, {
@@ -329,6 +388,14 @@ async function syncOnceCombined<DB extends SyncClientDb>(
329
388
  error: 'REJECTED',
330
389
  responseJson,
331
390
  });
391
+ pushResults.push(
392
+ buildPushResult({
393
+ outboxCommitId: outbox.id,
394
+ clientCommitId: outbox.client_commit_id,
395
+ status: 'rejected',
396
+ response: pushRes,
397
+ })
398
+ );
332
399
  pushedCommits = 1;
333
400
  }
334
401
  }
@@ -341,6 +408,7 @@ async function syncOnceCombined<DB extends SyncClientDb>(
341
408
  maxCommits: (options.maxPushCommits ?? 20) - 1,
342
409
  });
343
410
  pushedCommits += remaining.pushedCount;
411
+ pushResults.push(...remaining.pushResults);
344
412
  }
345
413
 
346
414
  // Process pull response
@@ -376,7 +444,7 @@ async function syncOnceCombined<DB extends SyncClientDb>(
376
444
  }
377
445
  }
378
446
 
379
- return { pushedCommits, pullRounds, pullResponse };
447
+ return { pushedCommits, pullRounds, pullResponse, pushResults };
380
448
  }
381
449
 
382
450
  export async function syncOnce<DB extends SyncClientDb>(