velocious 1.0.663 → 1.0.665

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 (50) hide show
  1. package/README.md +2 -2
  2. package/build/background-jobs/client.js +36 -6
  3. package/build/configuration-types.js +3 -3
  4. package/build/database/query/alter-table-base.js +11 -1
  5. package/build/src/background-jobs/client.d.ts.map +1 -1
  6. package/build/src/background-jobs/client.js +35 -6
  7. package/build/src/configuration-types.d.ts +8 -4
  8. package/build/src/configuration-types.d.ts.map +1 -1
  9. package/build/src/configuration-types.js +4 -4
  10. package/build/src/database/query/alter-table-base.d.ts.map +1 -1
  11. package/build/src/database/query/alter-table-base.js +12 -2
  12. package/build/src/sync/sync-api-client.d.ts +36 -10
  13. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  14. package/build/src/sync/sync-api-client.js +58 -14
  15. package/build/src/sync/sync-client-types.d.ts +41 -2
  16. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  17. package/build/src/sync/sync-client-types.js +20 -3
  18. package/build/src/sync/sync-client.d.ts +112 -3
  19. package/build/src/sync/sync-client.d.ts.map +1 -1
  20. package/build/src/sync/sync-client.js +306 -60
  21. package/build/src/sync/sync-publisher-types.d.ts +36 -3
  22. package/build/src/sync/sync-publisher-types.d.ts.map +1 -1
  23. package/build/src/sync/sync-publisher-types.js +16 -2
  24. package/build/src/sync/sync-publisher.d.ts +22 -3
  25. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  26. package/build/src/sync/sync-publisher.js +134 -13
  27. package/build/src/sync/sync-realtime-bridge.d.ts +11 -5
  28. package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
  29. package/build/src/sync/sync-realtime-bridge.js +37 -13
  30. package/build/src/sync/sync-scope-store.d.ts +32 -0
  31. package/build/src/sync/sync-scope-store.d.ts.map +1 -1
  32. package/build/src/sync/sync-scope-store.js +74 -4
  33. package/build/sync/sync-api-client.js +58 -13
  34. package/build/sync/sync-client-types.js +22 -2
  35. package/build/sync/sync-client.js +333 -63
  36. package/build/sync/sync-publisher-types.js +17 -1
  37. package/build/sync/sync-publisher.js +156 -15
  38. package/build/sync/sync-realtime-bridge.js +37 -12
  39. package/build/sync/sync-scope-store.js +79 -3
  40. package/package.json +1 -1
  41. package/src/background-jobs/client.js +36 -6
  42. package/src/configuration-types.js +3 -3
  43. package/src/database/query/alter-table-base.js +11 -1
  44. package/src/sync/sync-api-client.js +58 -13
  45. package/src/sync/sync-client-types.js +22 -2
  46. package/src/sync/sync-client.js +333 -63
  47. package/src/sync/sync-publisher-types.js +17 -1
  48. package/src/sync/sync-publisher.js +156 -15
  49. package/src/sync/sync-realtime-bridge.js +37 -12
  50. package/src/sync/sync-scope-store.js +79 -3
@@ -71,16 +71,19 @@ export default class SyncApiClient {
71
71
  * @param {string} args.authenticationToken - Authentication token.
72
72
  * @param {number} [args.batchSize] - Batch size.
73
73
  * @param {import("./sync-client-types.js").SyncClientConflictTrackingConfig} args.conflictTracking - Tracking configuration.
74
- * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}) => Promise<SyncReplayResponse>} args.postReplay - Transport boundary.
74
+ * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}, options?: {signal?: AbortSignal}) => Promise<SyncReplayResponse>} args.postReplay - Transport boundary.
75
75
  * @param {(identity: string) => number} args.remoteGeneration - Current remote generation.
76
76
  * @param {string} args.resourceType - Resource whose log records should drain.
77
+ * @param {AbortSignal} [args.signal] - Lifecycle cancellation signal.
77
78
  * @returns {Promise<void>} Resolves when no ready intent remains.
78
79
  */
79
- static async replayConflictTrackedSyncs({authenticationToken, batchSize, conflictTracking, postReplay, remoteGeneration, resourceType}) {
80
+ static async replayConflictTrackedSyncs({authenticationToken, batchSize, conflictTracking, postReplay, remoteGeneration, resourceType, signal}) {
80
81
  const maxBatchSize = this.normalizedBatchSize(batchSize)
81
82
 
82
83
  while (true) {
84
+ throwIfSyncAborted(signal)
83
85
  const records = await conflictTracking.mutationLog.records()
86
+ throwIfSyncAborted(signal)
84
87
  const statuses = new Map(records.map((record) => [record.mutation.clientMutationId, record.status]))
85
88
  const pending = records.filter((record) => record.status === "pending" && record.mutation.model === resourceType)
86
89
  const ready = pending.filter((record) => record.dependencies.every((dependency) => statuses.get(dependency.clientMutationId) === "synced"))
@@ -92,13 +95,15 @@ export default class SyncApiClient {
92
95
  const response = await postReplay({
93
96
  authenticationToken,
94
97
  syncs: groups.map((group) => this.conflictReplayPayload(group))
95
- })
98
+ }, {signal})
96
99
 
100
+ throwIfSyncAborted(signal)
97
101
  this.ensureSuccessfulResponse(response)
98
102
 
99
103
  const responsesById = new Map((response.syncs || []).map((result) => [String(result.id), result]))
100
104
 
101
105
  for (const group of groups) {
106
+ throwIfSyncAborted(signal)
102
107
  const result = responsesById.get(group[0].mutation.clientMutationId)
103
108
 
104
109
  if (!result) throw new Error(`Sync response missing result for mutation ${group[0].mutation.clientMutationId}`)
@@ -108,6 +113,7 @@ export default class SyncApiClient {
108
113
 
109
114
  for (const record of group) {
110
115
  await applySyncReplayResultToLocalMutationLog({mutationLog: conflictTracking.mutationLog, record, result: /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (result)})
116
+ throwIfSyncAborted(signal)
111
117
  }
112
118
 
113
119
  if (["successful", "duplicate"].includes(result.syncState) && result.serverVersion !== undefined) {
@@ -115,6 +121,7 @@ export default class SyncApiClient {
115
121
 
116
122
  if (remoteGeneration(identity) === generations.get(group[0].mutation.clientMutationId)) {
117
123
  await this.rebaseConflictSuccessor({conflictTracking, predecessor: group[group.length - 1], serverVersion: result.serverVersion})
124
+ throwIfSyncAborted(signal)
118
125
  }
119
126
  }
120
127
  }
@@ -261,6 +268,7 @@ export default class SyncApiClient {
261
268
  * @param {(payload: SyncChangesRequest) => Promise<SyncChangesResponse>} args.postChanges - Posts one changes request.
262
269
  * @param {Record<string, SyncResourceConfig>} args.resources - Resource policies.
263
270
  * @param {(progress: import("./sync-api-client-types.js").SyncPullProgress) => void} [args.onProgress] - Progress callback.
271
+ * @param {AbortSignal} [args.signal] - Lifecycle cancellation signal.
264
272
  * @returns {Promise<SyncChangesResult>} Pull result.
265
273
  */
266
274
  static async pullChangesWithCursor(args) {
@@ -271,7 +279,8 @@ export default class SyncApiClient {
271
279
  saveCursor: async (cursor) => await this.saveSyncCursor({cursor, cursorKey: args.cursorKey, cursorModel: args.cursorModel}),
272
280
  postChanges: args.postChanges,
273
281
  applySync: this.resourceApplier(args.resources),
274
- onProgress: args.onProgress
282
+ onProgress: args.onProgress,
283
+ signal: args.signal
275
284
  })
276
285
  }
277
286
 
@@ -309,12 +318,14 @@ export default class SyncApiClient {
309
318
  * @param {number} [args.batchSize] - Max syncs per request. Defaults to 100.
310
319
  * @param {() => Promise<SyncCursor | string | null | undefined>} args.loadCursor - Loads the persisted local cursor.
311
320
  * @param {(cursor: SyncCursor) => Promise<void>} args.saveCursor - Persists the final acknowledged cursor.
312
- * @param {(payload: SyncChangesRequest) => Promise<SyncChangesResponse>} args.postChanges - Posts one changes request.
321
+ * @param {(payload: SyncChangesRequest, options?: {signal?: AbortSignal}) => Promise<SyncChangesResponse>} args.postChanges - Posts one changes request.
313
322
  * @param {(sync: SyncChangeEnvelope) => Promise<SyncChangeApplyResult>} args.applySync - Applies one normalized sync row locally.
314
323
  * @param {(progress: import("./sync-api-client-types.js").SyncPullProgress) => void} [args.onProgress] - Progress callback invoked per applied page (and once for an empty pull) with the applied counts and the stable server total.
324
+ * @param {AbortSignal} [args.signal] - Lifecycle cancellation signal.
315
325
  * @returns {Promise<SyncChangesResult>} Pull result.
316
326
  */
317
327
  static async pullChanges(args) {
328
+ throwIfSyncAborted(args.signal)
318
329
  let afterCursor = this.syncCursorFromPayload(await args.loadCursor())
319
330
  let upToCursor = null
320
331
  let pages = 0
@@ -326,7 +337,9 @@ export default class SyncApiClient {
326
337
  const batchSize = this.normalizedBatchSize(args.batchSize)
327
338
 
328
339
  while (true) {
340
+ throwIfSyncAborted(args.signal)
329
341
  const changesResponse = await this.changesPage({...args, afterCursor, batchSize, upToCursor})
342
+ throwIfSyncAborted(args.signal)
330
343
  const syncs = changesResponse.syncs
331
344
 
332
345
  if (!upToCursor) upToCursor = changesResponse.upToCursor
@@ -341,6 +354,7 @@ export default class SyncApiClient {
341
354
  if (syncs.length === 0) {
342
355
  // Report the terminal progress once for an entirely empty pull so consumers observe
343
356
  // total 0; a pull that already applied pages reported its final counts on its last page.
357
+ throwIfSyncAborted(args.signal)
344
358
  if (pages === 0 && args.onProgress) args.onProgress({pages, syncedCount, total})
345
359
 
346
360
  break
@@ -354,7 +368,9 @@ export default class SyncApiClient {
354
368
  // waiting for the rest of the pull.
355
369
  await recordChanges.batch(async () => {
356
370
  for (const sync of syncs) {
371
+ throwIfSyncAborted(args.signal)
357
372
  const applyResult = await args.applySync(sync)
373
+ throwIfSyncAborted(args.signal)
358
374
  const resourceType = applyResult.resourceType ?? sync.resourceType()
359
375
 
360
376
  changed ||= applyResult.changed === true
@@ -369,11 +385,14 @@ export default class SyncApiClient {
369
385
 
370
386
  afterCursor = changesResponse.nextCursor
371
387
 
388
+ throwIfSyncAborted(args.signal)
372
389
  if (args.onProgress) args.onProgress({pages, syncedCount, total})
373
390
  if (syncs.length < batchSize) break
374
391
  }
375
392
 
393
+ throwIfSyncAborted(args.signal)
376
394
  if (afterCursor) await args.saveCursor(afterCursor)
395
+ throwIfSyncAborted(args.signal)
377
396
 
378
397
  return {changed, pages, resourceChanged, resourceCounts, syncedCount, total}
379
398
  }
@@ -384,18 +403,21 @@ export default class SyncApiClient {
384
403
  * @param {SyncCursor} args.afterCursor - Last acknowledged cursor.
385
404
  * @param {string} args.authenticationToken - Auth token.
386
405
  * @param {number} args.batchSize - Page size.
387
- * @param {(payload: SyncChangesRequest) => Promise<SyncChangesResponse>} args.postChanges - Changes poster.
406
+ * @param {(payload: SyncChangesRequest, options?: {signal?: AbortSignal}) => Promise<SyncChangesResponse>} args.postChanges - Changes poster.
407
+ * @param {AbortSignal} [args.signal] - Lifecycle cancellation signal.
388
408
  * @param {SyncCursor} args.upToCursor - Snapshot upper-bound cursor.
389
409
  * @returns {Promise<{nextCursor: SyncCursor, syncs: SyncChangeEnvelope[], total: number | null, upToCursor: SyncCursor}>} Normalized changes page.
390
410
  */
391
- static async changesPage({afterCursor, authenticationToken, batchSize, postChanges, upToCursor}) {
411
+ static async changesPage({afterCursor, authenticationToken, batchSize, postChanges, signal, upToCursor}) {
412
+ throwIfSyncAborted(signal)
392
413
  const response = await postChanges({
393
414
  authenticationToken,
394
415
  limit: batchSize,
395
416
  ...this.cursorPayload("after", afterCursor),
396
417
  ...this.cursorPayload("upTo", upToCursor)
397
- })
418
+ }, {signal})
398
419
 
420
+ throwIfSyncAborted(signal)
399
421
  this.ensureSuccessfulChangesResponse(response)
400
422
 
401
423
  const syncs = /** @type {unknown[]} */ (response.syncs)
@@ -578,7 +600,8 @@ export default class SyncApiClient {
578
600
  * @param {string} args.authenticationToken - Auth token to send with replay requests.
579
601
  * @param {number} [args.batchSize] - Max syncs per request.
580
602
  * @param {ReturnType<typeof JSON.parse>} args.syncModel - Local Sync model class.
581
- * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}) => Promise<SyncReplayResponse>} args.postReplay - Replay poster.
603
+ * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}, options?: {signal?: AbortSignal}) => Promise<SyncReplayResponse>} args.postReplay - Replay poster.
604
+ * @param {AbortSignal} [args.signal] - Lifecycle cancellation signal.
582
605
  * @returns {Promise<void>}
583
606
  */
584
607
  static async replayLocalSyncs(args) {
@@ -602,6 +625,7 @@ export default class SyncApiClient {
602
625
  },
603
626
  pendingSyncs: async () => await args.syncModel.preload({resource: true}).where({state: "pending"}).order("created_at").toArray(),
604
627
  postReplay: args.postReplay,
628
+ signal: args.signal,
605
629
  syncId: (sync) => (/** @type {{id: () => string | number | null | undefined}} */ (sync)).id(),
606
630
  syncPayload: (sync) => {
607
631
  postedSnapshotsBySyncId.set(String((/** @type {{id: () => string | number | null | undefined}} */ (sync)).id()), this.localSyncReplaySnapshot(sync))
@@ -810,15 +834,19 @@ export default class SyncApiClient {
810
834
  * @param {() => Promise<Array<unknown>>} args.pendingSyncs - Loads pending local sync rows in replay order.
811
835
  * @param {(sync: unknown) => string | number | null | undefined} args.syncId - Returns the local sync id.
812
836
  * @param {(sync: unknown) => Record<string, ReturnType<typeof JSON.parse>>} args.syncPayload - Builds the API sync envelope.
813
- * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}) => Promise<SyncReplayResponse>} args.postReplay - Posts one replay request.
837
+ * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}, options?: {signal?: AbortSignal}) => Promise<SyncReplayResponse>} args.postReplay - Posts one replay request.
814
838
  * @param {(sync: unknown, response: SyncReplayItem) => Promise<void>} args.markSuccessful - Marks one sync as successful locally.
839
+ * @param {AbortSignal} [args.signal] - Lifecycle cancellation signal.
815
840
  * @returns {Promise<void>} Resolves after all batches are replayed.
816
841
  */
817
842
  static async replayPending(args) {
843
+ throwIfSyncAborted(args.signal)
818
844
  const pendingSyncs = await args.pendingSyncs()
845
+ throwIfSyncAborted(args.signal)
819
846
  const batchSize = this.normalizedBatchSize(args.batchSize)
820
847
 
821
848
  for (let offset = 0; offset < pendingSyncs.length; offset += batchSize) {
849
+ throwIfSyncAborted(args.signal)
822
850
  await this.replayBatch({...args, pendingSyncs: pendingSyncs.slice(offset, offset + batchSize)})
823
851
  }
824
852
  }
@@ -830,14 +858,16 @@ export default class SyncApiClient {
830
858
  * @param {Array<unknown>} args.pendingSyncs - Batch syncs.
831
859
  * @param {(sync: unknown) => string | number | null | undefined} args.syncId - Sync id getter.
832
860
  * @param {(sync: unknown) => Record<string, ReturnType<typeof JSON.parse>>} args.syncPayload - Payload builder.
833
- * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}) => Promise<SyncReplayResponse>} args.postReplay - Replay poster.
861
+ * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}, options?: {signal?: AbortSignal}) => Promise<SyncReplayResponse>} args.postReplay - Replay poster.
834
862
  * @param {(sync: unknown, response: SyncReplayItem) => Promise<void>} args.markSuccessful - Success hook.
863
+ * @param {AbortSignal} [args.signal] - Lifecycle cancellation signal.
835
864
  * @returns {Promise<void>} Resolves after the batch is acknowledged.
836
865
  */
837
866
  static async replayBatch(args) {
838
- const {authenticationToken, markSuccessful, pendingSyncs, postReplay, syncId, syncPayload} = args
867
+ const {authenticationToken, markSuccessful, pendingSyncs, postReplay, signal, syncId, syncPayload} = args
839
868
 
840
869
  if (pendingSyncs.length === 0) return
870
+ throwIfSyncAborted(signal)
841
871
 
842
872
  const syncsById = new Map()
843
873
 
@@ -850,11 +880,13 @@ export default class SyncApiClient {
850
880
  const response = await postReplay({
851
881
  authenticationToken,
852
882
  syncs: pendingSyncs.map((sync) => syncPayload(sync))
853
- })
883
+ }, {signal})
854
884
 
885
+ throwIfSyncAborted(signal)
855
886
  this.ensureSuccessfulResponse(response)
856
887
 
857
888
  for (const syncResponse of response.syncs || []) {
889
+ throwIfSyncAborted(signal)
858
890
  const sync = syncsById.get(String(syncResponse.id))
859
891
 
860
892
  if (!sync) continue
@@ -863,6 +895,7 @@ export default class SyncApiClient {
863
895
  }
864
896
 
865
897
  await markSuccessful(sync, syncResponse)
898
+ throwIfSyncAborted(signal)
866
899
  }
867
900
  }
868
901
 
@@ -887,3 +920,15 @@ export default class SyncApiClient {
887
920
  return Math.floor(batchSize)
888
921
  }
889
922
  }
923
+
924
+ /**
925
+ * Throws the exact abort reason at lifecycle boundaries so callers can
926
+ * distinguish an expected cooperative stop from transport or apply failures.
927
+ * @param {AbortSignal | undefined} signal - Lifecycle signal.
928
+ * @returns {void}
929
+ */
930
+ function throwIfSyncAborted(signal) {
931
+ if (!signal?.aborted) return
932
+
933
+ throw signal.reason instanceof Error ? signal.reason : new Error("Sync operation aborted")
934
+ }
@@ -97,6 +97,26 @@
97
97
  * @property {import("../tenants/tenant-handle.js").default} [tenantHandle] - Immutable tenant handle binding every local sync state transition.
98
98
  */
99
99
 
100
+ /**
101
+ * Local cleanup hook that runs transactionally with selected scope resets.
102
+ * @typedef {(args: {connection: import("../database/drivers/base.js").default | null, scopes: SerializedSyncScope[]}) => Promise<void> | void} SyncClientScopeCleanup
103
+ */
104
+
105
+ /**
106
+ * Options for stopping and resetting a sync client lifecycle.
107
+ * @typedef {object} SyncClientStopOptions
108
+ * @property {SyncClientScopeCleanup} [cleanup] - App-owned local-row purge run atomically with resetScopes.
109
+ * @property {SerializedSyncScope[]} [resetScopes] - Selected scope rows to deactivate and restart from a null cursor.
110
+ */
111
+
112
+ /**
113
+ * Options for atomically replacing the external identity read by sync.client.
114
+ * @typedef {SyncClientStopOptions & {
115
+ * replace: () => Promise<void> | void,
116
+ * subscribeUserScope?: boolean
117
+ * }} SyncClientReplaceIdentityOptions
118
+ */
119
+
100
120
  /**
101
121
  * Internal derived sync client configuration built by the SyncClient
102
122
  * constructor — not an app-facing API.
@@ -107,8 +127,8 @@
107
127
  * @property {() => boolean | Promise<boolean>} [isOnline] - Connectivity gate for pulls and replays. Defaults to always online.
108
128
  * @property {(args: {scope: SerializedSyncScope}) => string | null | Promise<string | null>} [legacyCursor] - Seeds a newly declared scope's cursor (e.g. from a pre-scope cursor store) so devices don't re-pull everything.
109
129
  * @property {(error: Error) => void} [onError] - Reports background replay/pull failures. Defaults to rethrowing.
110
- * @property {(payload: import("./sync-api-client-types.js").SyncChangesRequest & {scope: SerializedSyncScope}) => Promise<import("./sync-api-client-types.js").SyncChangesResponse>} postChanges - Posts one changes request.
111
- * @property {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}) => Promise<import("./sync-api-client-types.js").SyncReplayResponse>} postReplay - Posts one replay request.
130
+ * @property {(payload: import("./sync-api-client-types.js").SyncChangesRequest & {scope: SerializedSyncScope}, options?: {signal?: AbortSignal}) => Promise<import("./sync-api-client-types.js").SyncChangesResponse>} postChanges - Posts one changes request.
131
+ * @property {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}, options?: {signal?: AbortSignal}) => Promise<import("./sync-api-client-types.js").SyncReplayResponse>} postReplay - Posts one replay request.
112
132
  * @property {import("../configuration-types.js").VelociousSyncClientRealtimeConfiguration} [realtime] - Realtime push configuration consumed by `subscribeRealtime(...)`.
113
133
  * @property {import("../remote-request-context.js").RemoteRequestContext} requestContext - Immutable scalar context captured for this client.
114
134
  * @property {Record<string, SyncClientResourceConfig>} resources - Derived resource policies keyed by resource/model name.