@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.
@@ -37,6 +37,7 @@ import type {
37
37
  ConflictInfo,
38
38
  OutboxStats,
39
39
  PresenceEntry,
40
+ PushResultInfo,
40
41
  RealtimeTransportLike,
41
42
  SubscriptionProgress,
42
43
  SyncAwaitBootstrapOptions,
@@ -70,6 +71,7 @@ const REALTIME_RECONNECT_CATCHUP_DELAY_MS = 500;
70
71
  const DEFAULT_AWAIT_TIMEOUT_MS = 60_000;
71
72
  const DEFAULT_INSPECTOR_EVENT_LIMIT = 100;
72
73
  const MAX_INSPECTOR_EVENT_LIMIT = 500;
74
+ const DEFAULT_DATA_CHANGE_DEBOUNCE_MS = 10;
73
75
 
74
76
  function calculateRetryDelay(attemptIndex: number): number {
75
77
  return Math.min(
@@ -303,6 +305,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
303
305
  private dataChangeDebounceTimeoutId: ReturnType<typeof setTimeout> | null =
304
306
  null;
305
307
  private pendingDataChangeScopes = new Set<string>();
308
+ private batchDataChangeUntilReconnectSettles = false;
306
309
  private hasRealtimeConnectedOnce = false;
307
310
  private transportHealth: TransportHealth = {
308
311
  mode: 'disconnected',
@@ -313,6 +316,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
313
316
  };
314
317
  private activeBootstrapSubscriptions = new Set<string>();
315
318
  private bootstrapStartedAt = new Map<string, number>();
319
+ private emittedConflictIds = new Set<string>();
316
320
  private inspectorEvents: SyncInspectorEvent[] = [];
317
321
  private nextInspectorEventId = 1;
318
322
 
@@ -809,19 +813,41 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
809
813
  this.emit('state:change', {});
810
814
  }
811
815
 
812
- private resolveDataChangeDebounceMs(): number {
813
- const normalize = (value: number | undefined): number | undefined => {
814
- if (value === undefined) return undefined;
815
- return Number.isFinite(value) ? Math.max(0, value) : 0;
816
- };
816
+ private normalizeDataChangeDebounceMs(
817
+ value: number | false | undefined
818
+ ): number | false | undefined {
819
+ if (value === undefined) return undefined;
820
+ if (value === false) return false;
821
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
822
+ }
823
+
824
+ private resolveReconnectDataChangeDebounceMs(): number | false {
825
+ const reconnectDebounce = this.normalizeDataChangeDebounceMs(
826
+ this.config.dataChangeDebounceMsWhenReconnecting
827
+ );
828
+ if (reconnectDebounce !== undefined) {
829
+ return reconnectDebounce;
830
+ }
831
+
832
+ const syncingDebounce = this.normalizeDataChangeDebounceMs(
833
+ this.config.dataChangeDebounceMsWhenSyncing
834
+ );
835
+ if (syncingDebounce !== undefined) {
836
+ return syncingDebounce;
837
+ }
838
+
839
+ return (
840
+ this.normalizeDataChangeDebounceMs(this.config.dataChangeDebounceMs) ??
841
+ DEFAULT_DATA_CHANGE_DEBOUNCE_MS
842
+ );
843
+ }
844
+
845
+ private resolveDataChangeDebounceMs(): number | false {
846
+ const normalize = (value: number | false | undefined) =>
847
+ this.normalizeDataChangeDebounceMs(value);
817
848
 
818
849
  if (this.state.connectionState === 'reconnecting') {
819
- const reconnectDebounce = normalize(
820
- this.config.dataChangeDebounceMsWhenReconnecting
821
- );
822
- if (reconnectDebounce !== undefined) {
823
- return reconnectDebounce;
824
- }
850
+ return this.resolveReconnectDataChangeDebounceMs();
825
851
  }
826
852
 
827
853
  if (this.state.isSyncing) {
@@ -833,7 +859,24 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
833
859
  }
834
860
  }
835
861
 
836
- return normalize(this.config.dataChangeDebounceMs) ?? 0;
862
+ return (
863
+ normalize(this.config.dataChangeDebounceMs) ??
864
+ DEFAULT_DATA_CHANGE_DEBOUNCE_MS
865
+ );
866
+ }
867
+
868
+ private flushReconnectBatchedDataChangesIfReady(): void {
869
+ if (!this.batchDataChangeUntilReconnectSettles) return;
870
+ if (this.state.isSyncing || this.state.connectionState === 'reconnecting') {
871
+ return;
872
+ }
873
+
874
+ this.batchDataChangeUntilReconnectSettles = false;
875
+ if (this.dataChangeDebounceTimeoutId) {
876
+ clearTimeout(this.dataChangeDebounceTimeoutId);
877
+ this.dataChangeDebounceTimeoutId = null;
878
+ }
879
+ this.flushDataChange();
837
880
  }
838
881
 
839
882
  private emitDataChange(scopes: Iterable<string>): void {
@@ -847,7 +890,19 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
847
890
  if (normalizedScopes.size === 0) return;
848
891
 
849
892
  const debounceMs = this.resolveDataChangeDebounceMs();
850
- if (debounceMs <= 0) {
893
+ const shouldBatchWithoutTimer =
894
+ (this.batchDataChangeUntilReconnectSettles ||
895
+ this.state.connectionState === 'reconnecting') &&
896
+ debounceMs !== false &&
897
+ debounceMs > 0;
898
+ if (shouldBatchWithoutTimer) {
899
+ for (const scope of normalizedScopes) {
900
+ this.pendingDataChangeScopes.add(scope);
901
+ }
902
+ return;
903
+ }
904
+
905
+ if (debounceMs === false || debounceMs <= 0) {
851
906
  this.flushDataChange(normalizedScopes);
852
907
  return;
853
908
  }
@@ -1133,6 +1188,49 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1133
1188
  }
1134
1189
  }
1135
1190
 
1191
+ private emitPushResult(result: PushResultInfo): void {
1192
+ this.emit('push:result', result);
1193
+ this.config.onPushResult?.(result);
1194
+ }
1195
+
1196
+ private async emitNewConflicts(): Promise<void> {
1197
+ const conflicts = await this.getConflicts();
1198
+ const activeIds = new Set(conflicts.map((conflict) => conflict.id));
1199
+
1200
+ for (const id of this.emittedConflictIds) {
1201
+ if (!activeIds.has(id)) {
1202
+ this.emittedConflictIds.delete(id);
1203
+ }
1204
+ }
1205
+
1206
+ const sorted = [...conflicts].sort((left, right) => {
1207
+ if (left.createdAt !== right.createdAt) {
1208
+ return left.createdAt - right.createdAt;
1209
+ }
1210
+ return left.opIndex - right.opIndex;
1211
+ });
1212
+
1213
+ for (const conflict of sorted) {
1214
+ if (this.emittedConflictIds.has(conflict.id)) {
1215
+ continue;
1216
+ }
1217
+ this.emittedConflictIds.add(conflict.id);
1218
+ this.emit('conflict:new', conflict);
1219
+ this.config.onConflict?.(conflict);
1220
+ }
1221
+ }
1222
+
1223
+ private async emitNewConflictsSafe(context: string): Promise<void> {
1224
+ try {
1225
+ await this.emitNewConflicts();
1226
+ } catch (error) {
1227
+ console.warn(
1228
+ `[SyncEngine] Failed to emit conflict:new during ${context}`,
1229
+ error
1230
+ );
1231
+ }
1232
+ }
1233
+
1136
1234
  private async resolveResetTargets(
1137
1235
  options: SyncResetOptions
1138
1236
  ): Promise<SubscriptionState[]> {
@@ -1279,6 +1377,9 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1279
1377
 
1280
1378
  this.resetLocalState();
1281
1379
  await this.refreshOutboxStats();
1380
+ if (result.deletedConflicts > 0) {
1381
+ this.emittedConflictIds.clear();
1382
+ }
1282
1383
  this.updateState({ error: null });
1283
1384
 
1284
1385
  return result;
@@ -1399,9 +1500,41 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1399
1500
 
1400
1501
  private setConnectionState(state: SyncConnectionState): void {
1401
1502
  const previous = this.state.connectionState;
1402
- if (previous !== state) {
1403
- this.updateState({ connectionState: state });
1404
- this.emit('connection:change', { previous, current: state });
1503
+ if (previous === state) return;
1504
+
1505
+ const reconnectDebounceMs = this.resolveReconnectDataChangeDebounceMs();
1506
+ if (
1507
+ state === 'reconnecting' &&
1508
+ reconnectDebounceMs !== false &&
1509
+ reconnectDebounceMs > 0
1510
+ ) {
1511
+ this.batchDataChangeUntilReconnectSettles = true;
1512
+ if (this.dataChangeDebounceTimeoutId) {
1513
+ clearTimeout(this.dataChangeDebounceTimeoutId);
1514
+ this.dataChangeDebounceTimeoutId = null;
1515
+ }
1516
+ }
1517
+
1518
+ this.updateState({ connectionState: state });
1519
+ this.emit('connection:change', { previous, current: state });
1520
+
1521
+ if (previous === 'reconnecting' && state === 'connected') {
1522
+ queueMicrotask(() => {
1523
+ this.flushReconnectBatchedDataChangesIfReady();
1524
+ });
1525
+ }
1526
+
1527
+ if (previous === 'reconnecting' && state !== 'connected') {
1528
+ this.batchDataChangeUntilReconnectSettles = false;
1529
+ if (this.dataChangeDebounceTimeoutId) {
1530
+ clearTimeout(this.dataChangeDebounceTimeoutId);
1531
+ this.dataChangeDebounceTimeoutId = null;
1532
+ }
1533
+ this.flushDataChange();
1534
+ }
1535
+
1536
+ if (state === 'disconnected') {
1537
+ this.batchDataChangeUntilReconnectSettles = false;
1405
1538
  }
1406
1539
  }
1407
1540
 
@@ -1446,6 +1579,9 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1446
1579
  }
1447
1580
  );
1448
1581
  pushed = result.pushed;
1582
+ if (result.pushResult) {
1583
+ this.emitPushResult(result.pushResult);
1584
+ }
1449
1585
  }
1450
1586
  }
1451
1587
  } catch {
@@ -1636,6 +1772,10 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1636
1772
  )
1637
1773
  );
1638
1774
 
1775
+ for (const pushResult of result.pushResults) {
1776
+ this.emitPushResult(pushResult);
1777
+ }
1778
+
1639
1779
  const syncResult: SyncResult = {
1640
1780
  success: true,
1641
1781
  pushedCommits: result.pushedCommits,
@@ -1674,6 +1814,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1674
1814
  this.emitDataChange(changedTables);
1675
1815
  }
1676
1816
  this.handleBootstrapLifecycle(result.pullResponse);
1817
+ await this.emitNewConflictsSafe('sync success');
1677
1818
 
1678
1819
  // Refresh outbox stats (fire-and-forget — don't block sync:complete)
1679
1820
  this.refreshOutboxStats().catch((error) => {
@@ -1711,6 +1852,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1711
1852
  attributes: { trigger: triggerLabel },
1712
1853
  }
1713
1854
  );
1855
+ this.flushReconnectBatchedDataChangesIfReady();
1714
1856
 
1715
1857
  return syncResult;
1716
1858
  } catch (err) {
@@ -1732,6 +1874,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1732
1874
  });
1733
1875
 
1734
1876
  this.handleError(error);
1877
+ await this.emitNewConflictsSafe('sync error');
1735
1878
 
1736
1879
  const durationMs = Math.max(0, Date.now() - startedAtMs);
1737
1880
  countSyncMetric('sync.client.sync.results', 1, {
@@ -1757,6 +1900,7 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1757
1900
  if (error.retryable && this.state.retryCount < maxRetries) {
1758
1901
  this.scheduleRetry();
1759
1902
  }
1903
+ this.flushReconnectBatchedDataChangesIfReady();
1760
1904
 
1761
1905
  return {
1762
1906
  success: false,
@@ -1797,9 +1941,17 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1797
1941
  */
1798
1942
  private async applyWsDeliveredChanges(
1799
1943
  changes: SyncChange[],
1800
- cursor: number
1944
+ cursor: number,
1945
+ metadata?: {
1946
+ commitSeq?: number;
1947
+ actorId?: string | null;
1948
+ createdAt?: string | null;
1949
+ }
1801
1950
  ): Promise<boolean> {
1802
1951
  try {
1952
+ const commitSeq = metadata?.commitSeq ?? cursor;
1953
+ const actorId = metadata?.actorId ?? null;
1954
+ const createdAt = metadata?.createdAt ?? null;
1803
1955
  await this.config.db.transaction().execute(async (trx) => {
1804
1956
  for (const change of changes) {
1805
1957
  const handler = getClientHandler(this.config.handlers, change.table);
@@ -1808,7 +1960,15 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1808
1960
  `Missing client table handler for WS change table "${change.table}"`
1809
1961
  );
1810
1962
  }
1811
- await handler.applyChange({ trx }, change);
1963
+ await handler.applyChange(
1964
+ {
1965
+ trx,
1966
+ commitSeq,
1967
+ actorId,
1968
+ createdAt,
1969
+ },
1970
+ change
1971
+ );
1812
1972
  }
1813
1973
 
1814
1974
  // Update subscription cursors
@@ -1851,7 +2011,12 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1851
2011
  */
1852
2012
  private async handleWsDelivery(
1853
2013
  changes: SyncChange[],
1854
- cursor: number
2014
+ cursor: number,
2015
+ metadata?: {
2016
+ commitSeq?: number;
2017
+ actorId?: string | null;
2018
+ createdAt?: string | null;
2019
+ }
1855
2020
  ): Promise<void> {
1856
2021
  // If a sync is already in-flight, let it handle everything
1857
2022
  if (this.syncPromise) {
@@ -1895,7 +2060,11 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
1895
2060
 
1896
2061
  // Apply changes + update cursor
1897
2062
  const inlineApplyStartedAtMs = Date.now();
1898
- const applied = await this.applyWsDeliveredChanges(changes, cursor);
2063
+ const applied = await this.applyWsDeliveredChanges(
2064
+ changes,
2065
+ cursor,
2066
+ metadata
2067
+ );
1899
2068
  const inlineApplyDurationMs = Math.max(
1900
2069
  0,
1901
2070
  Date.now() - inlineApplyStartedAtMs
@@ -2161,10 +2330,27 @@ export class SyncEngine<DB extends SyncClientDb = SyncClientDb> {
2161
2330
  const hasInlineChanges =
2162
2331
  Array.isArray(event.data.changes) && event.data.changes.length > 0;
2163
2332
  const cursor = event.data.cursor;
2333
+ const commitSeqRaw = event.data.commitSeq;
2334
+ const commitSeq =
2335
+ typeof commitSeqRaw === 'number'
2336
+ ? commitSeqRaw
2337
+ : typeof cursor === 'number'
2338
+ ? cursor
2339
+ : undefined;
2340
+ const actorId =
2341
+ typeof event.data.actorId === 'string' ? event.data.actorId : null;
2342
+ const createdAt =
2343
+ typeof event.data.createdAt === 'string'
2344
+ ? event.data.createdAt
2345
+ : null;
2164
2346
 
2165
2347
  if (hasInlineChanges && typeof cursor === 'number') {
2166
2348
  // WS delivered changes + cursor — may skip HTTP pull
2167
- this.handleWsDelivery(event.data.changes as SyncChange[], cursor);
2349
+ this.handleWsDelivery(event.data.changes as SyncChange[], cursor, {
2350
+ commitSeq,
2351
+ actorId,
2352
+ createdAt,
2353
+ });
2168
2354
  } else {
2169
2355
  // Cursor-only wake-up or no cursor — must HTTP sync
2170
2356
  countSyncMetric('sync.client.ws.delivery.events', 1, {
@@ -140,14 +140,28 @@ export type SyncEventType =
140
140
  | 'sync:complete'
141
141
  | 'sync:live'
142
142
  | 'sync:error'
143
+ | 'push:result'
143
144
  | 'bootstrap:start'
144
145
  | 'bootstrap:progress'
145
146
  | 'bootstrap:complete'
146
147
  | 'connection:change'
147
148
  | 'outbox:change'
148
149
  | 'data:change'
150
+ | 'conflict:new'
149
151
  | 'presence:change';
150
152
 
153
+ export type PushResultStatus = 'applied' | 'cached' | 'rejected' | 'retriable';
154
+
155
+ export interface PushResultInfo {
156
+ outboxCommitId: string;
157
+ clientCommitId: string;
158
+ status: PushResultStatus;
159
+ commitSeq: number | null;
160
+ results: SyncPushResponse['results'];
161
+ errorCode: string | null;
162
+ timestamp: number;
163
+ }
164
+
151
165
  /**
152
166
  * Presence entry for a client connected to a scope
153
167
  */
@@ -172,6 +186,7 @@ export interface SyncEventPayloads {
172
186
  };
173
187
  'sync:live': { timestamp: number };
174
188
  'sync:error': SyncError;
189
+ 'push:result': PushResultInfo;
175
190
  'bootstrap:start': {
176
191
  timestamp: number;
177
192
  stateId: string;
@@ -203,6 +218,7 @@ export interface SyncEventPayloads {
203
218
  scopes: string[];
204
219
  timestamp: number;
205
220
  };
221
+ 'conflict:new': ConflictInfo;
206
222
  'presence:change': {
207
223
  scopeKey: string;
208
224
  presence: PresenceEntry[];
@@ -260,25 +276,28 @@ export interface SyncEngineConfig<DB extends SyncClientDb = SyncClientDb> {
260
276
  onError?: (error: SyncError) => void;
261
277
  /** Conflict callback */
262
278
  onConflict?: (conflict: ConflictInfo) => void;
279
+ /** Per-commit push outcome callback */
280
+ onPushResult?: (result: PushResultInfo) => void;
263
281
  /** Data change callback */
264
282
  onDataChange?: (scopes: string[]) => void;
265
283
  /**
266
284
  * Debounce window for coalescing `data:change` emissions.
267
- * - `0` (default): emit immediately
285
+ * - default: `10`
286
+ * - `0`/`false`: emit immediately (disable debounce)
268
287
  * - `>0`: merge scopes and emit once per window
269
288
  */
270
- dataChangeDebounceMs?: number;
289
+ dataChangeDebounceMs?: number | false;
271
290
  /**
272
291
  * Override debounce window while `isSyncing === true`.
273
292
  * If omitted, `dataChangeDebounceMs` is used.
274
293
  */
275
- dataChangeDebounceMsWhenSyncing?: number;
294
+ dataChangeDebounceMsWhenSyncing?: number | false;
276
295
  /**
277
296
  * Override debounce window while `connectionState === "reconnecting"`.
278
297
  * If omitted, `dataChangeDebounceMsWhenSyncing` (if syncing) or
279
298
  * `dataChangeDebounceMs` is used.
280
299
  */
281
- dataChangeDebounceMsWhenReconnecting?: number;
300
+ dataChangeDebounceMsWhenReconnecting?: number | false;
282
301
  /** Optional client plugins (e.g. encryption) */
283
302
  plugins?: SyncClientPlugin[];
284
303
  /** Custom SHA-256 hash function (for platforms without crypto.subtle, e.g. React Native) */
@@ -317,8 +336,11 @@ export interface RealtimeTransportLike extends SyncTransport {
317
336
  event: string;
318
337
  data: {
319
338
  cursor?: number;
339
+ commitSeq?: number;
320
340
  changes?: unknown[];
321
341
  error?: string;
342
+ actorId?: string;
343
+ createdAt?: string;
322
344
  timestamp: number;
323
345
  };
324
346
  }) => void,
@@ -16,6 +16,15 @@ import type { Transaction } from 'kysely';
16
16
  export interface ClientHandlerContext<DB> {
17
17
  /** Database transaction */
18
18
  trx: Transaction<DB>;
19
+ /**
20
+ * Commit metadata for server-delivered changes.
21
+ * Undefined for local optimistic changes.
22
+ */
23
+ commitSeq?: number | null;
24
+ /** Actor that authored the server commit, when available. */
25
+ actorId?: string | null;
26
+ /** Commit creation timestamp (ISO string), when available. */
27
+ createdAt?: string | null;
19
28
  }
20
29
 
21
30
  /**
@@ -818,4 +818,98 @@ describe('applyPullResponse chunk streaming', () => {
818
818
  .executeTakeFirst();
819
819
  expect(Number(state?.cursor ?? -1)).toBe(2);
820
820
  });
821
+
822
+ it('passes commit metadata to applyChange handler context', async () => {
823
+ const transport: SyncTransport = {
824
+ async sync() {
825
+ return {};
826
+ },
827
+ async fetchSnapshotChunk() {
828
+ return new Uint8Array();
829
+ },
830
+ };
831
+
832
+ const appliedContexts: Array<{
833
+ commitSeq: number | null | undefined;
834
+ actorId: string | null | undefined;
835
+ createdAt: string | null | undefined;
836
+ }> = [];
837
+
838
+ const handlers: ClientHandlerCollection<TestDb> = [
839
+ {
840
+ table: 'items',
841
+ async applySnapshot() {},
842
+ async clearAll() {},
843
+ async applyChange(ctx) {
844
+ appliedContexts.push({
845
+ commitSeq: ctx.commitSeq,
846
+ actorId: ctx.actorId,
847
+ createdAt: ctx.createdAt,
848
+ });
849
+ },
850
+ },
851
+ ];
852
+
853
+ const options = {
854
+ clientId: 'client-1',
855
+ subscriptions: [
856
+ {
857
+ id: 'items-sub',
858
+ table: 'items',
859
+ scopes: {},
860
+ },
861
+ ],
862
+ stateId: 'default',
863
+ };
864
+
865
+ const pullState = await buildPullRequest(db, options);
866
+ const response: SyncPullResponse = {
867
+ ok: true,
868
+ subscriptions: [
869
+ {
870
+ id: 'items-sub',
871
+ status: 'active',
872
+ scopes: {},
873
+ bootstrap: false,
874
+ bootstrapState: null,
875
+ nextCursor: 7,
876
+ commits: [
877
+ {
878
+ commitSeq: 7,
879
+ actorId: 'remote-user',
880
+ createdAt: '2026-02-28T12:00:00.000Z',
881
+ changes: [
882
+ {
883
+ table: 'items',
884
+ row_id: 'item-ctx',
885
+ op: 'upsert',
886
+ row_version: 1,
887
+ row_json: { id: 'item-ctx', name: 'ctx-test' },
888
+ scopes: {},
889
+ },
890
+ ],
891
+ },
892
+ ],
893
+ snapshots: [],
894
+ },
895
+ ],
896
+ };
897
+
898
+ await applyPullResponse(
899
+ db,
900
+ transport,
901
+ handlers,
902
+ options,
903
+ pullState,
904
+ response
905
+ );
906
+
907
+ expect(appliedContexts).toEqual([
908
+ {
909
+ commitSeq: 7,
910
+ actorId: 'remote-user',
911
+ createdAt: '2026-02-28T12:00:00.000Z',
912
+ },
913
+ ]);
914
+ });
821
915
  });
@@ -888,9 +888,20 @@ export async function applyPullResponse<DB extends SyncClientDb>(
888
888
  } else {
889
889
  // Apply incremental changes
890
890
  for (const commit of sub.commits) {
891
+ const commitSeq = commit.commitSeq ?? null;
892
+ const actorId = commit.actorId ?? null;
893
+ const createdAt = commit.createdAt ?? null;
891
894
  for (const change of commit.changes) {
892
895
  const handler = getClientHandlerOrThrow(handlers, change.table);
893
- await handler.applyChange({ trx }, change);
896
+ await handler.applyChange(
897
+ {
898
+ trx,
899
+ commitSeq,
900
+ actorId,
901
+ createdAt,
902
+ },
903
+ change
904
+ );
894
905
  }
895
906
  }
896
907
  }