velocious 1.0.512 → 1.0.514

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.
@@ -356,9 +356,11 @@ export default class SyncClient {
356
356
  /**
357
357
  * Declares (or re-activates) a sync scope from a model query and pulls it when online.
358
358
  * @param {import("../database/query/model-class-query.js").default<?>} query - Query declaring the sync scope.
359
+ * @param {object} [options] - Sync options.
360
+ * @param {(progress: import("./sync-api-client-types.js").SyncPullProgress) => void} [options.onProgress] - Called per applied page of the pull this declaration triggers, so the initial import of a newly declared scope can drive a "syncedCount of total" progress bar. See `pull()`.
359
361
  * @returns {Promise<{scope: import("./sync-client-types.js").SerializedSyncScope, pulled: import("./sync-api-client-types.js").SyncChangesResult | null}>} Declared scope and pull result (null while offline).
360
362
  */
361
- async sync(query) {
363
+ async sync(query, {onProgress} = {}) {
362
364
  const scope = serializedScopeFromQuery(query)
363
365
  const scopeStore = this.scopeStore()
364
366
  const scopeRow = await scopeStore.findOrCreateScope(scope)
@@ -370,7 +372,7 @@ export default class SyncClient {
370
372
  if (legacyCursor) await scopeStore.saveCursor(scopeRow, legacyCursor)
371
373
  }
372
374
 
373
- return {pulled: await this.pull(), scope}
375
+ return {pulled: await this.pull({onProgress}), scope}
374
376
  }
375
377
 
376
378
  /**
@@ -384,9 +386,11 @@ export default class SyncClient {
384
386
 
385
387
  /**
386
388
  * Pulls changes for every active scope with per-scope cursors (single-flighted, online-gated).
389
+ * @param {object} [options] - Pull options.
390
+ * @param {(progress: import("./sync-api-client-types.js").SyncPullProgress) => void} [options.onProgress] - Called per applied page with cumulative `{pages, syncedCount, total}` across the pulled scopes, for rendering a "syncedCount of total" progress bar (e.g. a full-import screen). Optional; omitting it keeps the existing behavior.
387
391
  * @returns {Promise<import("./sync-api-client-types.js").SyncChangesResult | null>} Combined pull result, or null while offline.
388
392
  */
389
- async pull() {
393
+ async pull({onProgress} = {}) {
390
394
  if (!(await this.isOnline())) return null
391
395
 
392
396
  /** @type {import("./sync-api-client-types.js").SyncChangesResult | null} */
@@ -401,15 +405,27 @@ export default class SyncClient {
401
405
  pages: 0,
402
406
  resourceChanged: /** @type {Record<string, boolean>} */ ({}),
403
407
  resourceCounts: /** @type {Record<string, number>} */ ({}),
404
- syncedCount: 0
408
+ syncedCount: 0,
409
+ total: 0
405
410
  }
406
411
 
407
412
  for (const scopeRow of await scopeStore.activeScopes()) {
413
+ // Cumulate scope progress onto the counts of the scopes already pulled so a single
414
+ // scope's per-page progress reads exactly its own counts (base 0), and multi-scope
415
+ // pulls report a running cumulative total across every scope.
416
+ const basePages = result.pages
417
+ const baseSyncedCount = result.syncedCount
418
+ const baseTotal = result.total
408
419
  const scopeResult = await SyncApiClient.pullChanges({
409
420
  applySync,
410
421
  authenticationToken,
411
422
  batchSize: this.config.batchSize,
412
423
  loadCursor: async () => await scopeStore.loadCursor(scopeRow),
424
+ onProgress: onProgress ? (progress) => onProgress({
425
+ pages: basePages + progress.pages,
426
+ syncedCount: baseSyncedCount + progress.syncedCount,
427
+ total: baseTotal + progress.total
428
+ }) : undefined,
413
429
  postChanges: async (payload) => await this.config.postChanges({
414
430
  ...payload,
415
431
  scope: {conditions: scopeRow.conditions, resourceType: scopeRow.resourceType}
@@ -420,6 +436,7 @@ export default class SyncClient {
420
436
  result.changed ||= scopeResult.changed
421
437
  result.pages += scopeResult.pages
422
438
  result.syncedCount += scopeResult.syncedCount
439
+ result.total += scopeResult.total
423
440
 
424
441
  for (const [resourceType, count] of Object.entries(scopeResult.resourceCounts)) {
425
442
  result.resourceCounts[resourceType] = (result.resourceCounts[resourceType] || 0) + count
@@ -32,15 +32,19 @@ export default class SyncModelChangeFeedService {
32
32
  }
33
33
 
34
34
  /**
35
- * Builds a stable change-feed page.
36
- * @returns {Promise<{status: string, nextCursor: {id: string, serverSequence: number, updatedAt: string} | null, syncs: Array<Record<string, unknown>>, upToCursor: {id: string, serverSequence: number, updatedAt: string} | null}>} Change-feed page result.
35
+ * Builds a stable change-feed page. The additive `total` is the scope's pending
36
+ * change count from the request cursor to the snapshot bound (a COUNT, not a
37
+ * materialized read), so a client can render a "synced of total" progress bar;
38
+ * older clients ignore it.
39
+ * @returns {Promise<{status: string, nextCursor: {id: string, serverSequence: number, updatedAt: string} | null, syncs: Array<Record<string, unknown>>, total: number, upToCursor: {id: string, serverSequence: number, updatedAt: string} | null}>} Change-feed page result.
37
40
  */
38
41
  async changes() {
39
42
  const limit = this.normalizedLimit(this.params.limit)
40
43
  const upToCursor = await this.resolveUpToCursor()
41
44
 
42
- if (!upToCursor) return {status: "success", nextCursor: null, syncs: [], upToCursor: null}
45
+ if (!upToCursor) return {status: "success", nextCursor: null, syncs: [], total: 0, upToCursor: null}
43
46
 
47
+ const total = await this.totalPendingChanges({upToCursor})
44
48
  const query = this.pageQuery({limit, upToCursor})
45
49
  const records = await query.toArray()
46
50
  const nextCursor = records.length > 0 ? this.cursorForRecord(records[records.length - 1]) : upToCursor
@@ -49,6 +53,7 @@ export default class SyncModelChangeFeedService {
49
53
  status: "success",
50
54
  nextCursor,
51
55
  syncs: records.map((record) => this.serializeRecord(record)),
56
+ total,
52
57
  upToCursor
53
58
  }
54
59
  }
@@ -124,6 +129,34 @@ export default class SyncModelChangeFeedService {
124
129
  * @returns {import("../database/query/model-class-query.js").default} Page query.
125
130
  */
126
131
  pageQuery({limit, upToCursor}) {
132
+ const query = this.cursorFilteredQuery({upToCursor})
133
+ const driver = query.driver
134
+ const serverSequenceColumn = `${driver.quoteTable(this.modelClass.tableName())}.${driver.quoteColumn("server_sequence")}`
135
+
136
+ return query
137
+ .order(`${serverSequenceColumn} ASC`)
138
+ .limit(limit)
139
+ }
140
+
141
+ /**
142
+ * Counts the scope's pending change rows from the request cursor to the snapshot
143
+ * bound without materializing them, so the client can render a stable "of Y"
144
+ * progress denominator.
145
+ * @param {{upToCursor: {id: string, serverSequence: number, updatedAt: string}}} args - Count args.
146
+ * @returns {Promise<number>} Pending change count from the request cursor.
147
+ */
148
+ async totalPendingChanges({upToCursor}) {
149
+ return await this.cursorFilteredQuery({upToCursor}).count()
150
+ }
151
+
152
+ /**
153
+ * Builds a scoped query with the snapshot upper bound and after-cursor filters
154
+ * applied, but without ordering or a page limit. Shared by the page read and the
155
+ * total-count so both count the same set of rows.
156
+ * @param {{upToCursor: {id: string, serverSequence: number, updatedAt: string}}} args - Filter args.
157
+ * @returns {import("../database/query/model-class-query.js").default} Cursor-filtered query.
158
+ */
159
+ cursorFilteredQuery({upToCursor}) {
127
160
  const query = this.scopedQuery()
128
161
  const driver = query.driver
129
162
  const table = driver.quoteTable(this.modelClass.tableName())
@@ -131,10 +164,7 @@ export default class SyncModelChangeFeedService {
131
164
  const updatedAtColumn = `${table}.${driver.quoteColumn("updated_at")}`
132
165
  const idColumn = `${table}.${driver.quoteColumn("id")}`
133
166
 
134
- query
135
- .where(`${serverSequenceColumn} <= ${driver.quote(upToCursor.serverSequence)}`)
136
- .order(`${serverSequenceColumn} ASC`)
137
- .limit(limit)
167
+ query.where(`${serverSequenceColumn} <= ${driver.quote(upToCursor.serverSequence)}`)
138
168
 
139
169
  const afterServerSequence = this.optionalPositiveIntegerParam(this.params.afterServerSequence, "afterServerSequence")
140
170