velocious 1.0.715 → 1.0.716

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.
@@ -0,0 +1,79 @@
1
+ // @ts-check
2
+
3
+ import Logger from "../logger.js"
4
+ import VelociousJob from "./platform-job.js"
5
+ import {currentSyncClientOrNull} from "../sync/sync-client-registry.js"
6
+
7
+ const logger = new Logger("SyncReplayJob")
8
+
9
+ /**
10
+ * Durable, deduplicated replay of the device's pending sync queue.
11
+ *
12
+ * Offline-capable apps write local mutations with `syncClient.queue()`, which
13
+ * schedules an in-process replay attempt. That attempt lives only as long as the
14
+ * JavaScript runtime, so a scan (or any other mutation) that happens while the app
15
+ * is closing, backgrounding, or briefly online can leave rows pending. Enqueueing
16
+ * this job after such a mutation makes the replay durable: the local background-jobs
17
+ * adapter stores the trigger in SQLite, runs it in process as soon as the app is
18
+ * active, and collapses overlapping triggers onto one queued job.
19
+ *
20
+ * `perform()` is deliberately best-effort. Pending rows are only deleted after the
21
+ * backend acknowledges them, so a failed or offline replay leaves the queue intact
22
+ * for the next enqueue (next mutation, focus, or foreground trigger) instead of
23
+ * consuming the job's retries on a device that may stay offline for hours.
24
+ *
25
+ * Apps register it next to their other jobs and enqueue it after local mutations:
26
+ *
27
+ * ```js
28
+ * backgroundJobs: {mode: "background", jobClasses: [SyncReplayJob]}
29
+ *
30
+ * await SyncReplayJob.enqueue()
31
+ * ```
32
+ * @augments {VelociousJob}
33
+ */
34
+ export default class SyncReplayJob extends VelociousJob {
35
+ /**
36
+ * Returns the stable job name used by the persisted queue.
37
+ * @returns {string} - Job name.
38
+ */
39
+ static jobName() { return "syncReplay" }
40
+
41
+ /**
42
+ * Enqueues one deduplicated replay. Resolves after the durable enqueue, not after
43
+ * the replay finishes, so callers can stay on the synchronous local path.
44
+ * @returns {Promise<string>} - Queued job id.
45
+ */
46
+ static async enqueue() {
47
+ return await this.performLaterWithOptions({
48
+ args: [],
49
+ options: {
50
+ concurrencyKey: "sync-replay",
51
+ deduplicateWhileQueued: true,
52
+ maxConcurrency: 1
53
+ }
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Drains the pending sync queue through the app's current sync client.
59
+ * @returns {Promise<void>}
60
+ */
61
+ async perform() {
62
+ const syncClient = currentSyncClientOrNull()
63
+
64
+ if (!syncClient) {
65
+ void logger.debug("sync replay skipped because no sync client is registered")
66
+
67
+ return
68
+ }
69
+
70
+ try {
71
+ await syncClient.replayPending()
72
+ } catch (error) {
73
+ // Pending rows survive a failed or offline replay, so the next enqueue retries
74
+ // them. Re-throwing would make the adapter retry a device that may be offline
75
+ // for hours and mark healthy offline periods as job failures.
76
+ void logger.error(() => ["sync replay failed (pending rows stay queued)", error])
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,45 @@
1
+ import VelociousJob from "./platform-job.js";
2
+ /**
3
+ * Durable, deduplicated replay of the device's pending sync queue.
4
+ *
5
+ * Offline-capable apps write local mutations with `syncClient.queue()`, which
6
+ * schedules an in-process replay attempt. That attempt lives only as long as the
7
+ * JavaScript runtime, so a scan (or any other mutation) that happens while the app
8
+ * is closing, backgrounding, or briefly online can leave rows pending. Enqueueing
9
+ * this job after such a mutation makes the replay durable: the local background-jobs
10
+ * adapter stores the trigger in SQLite, runs it in process as soon as the app is
11
+ * active, and collapses overlapping triggers onto one queued job.
12
+ *
13
+ * `perform()` is deliberately best-effort. Pending rows are only deleted after the
14
+ * backend acknowledges them, so a failed or offline replay leaves the queue intact
15
+ * for the next enqueue (next mutation, focus, or foreground trigger) instead of
16
+ * consuming the job's retries on a device that may stay offline for hours.
17
+ *
18
+ * Apps register it next to their other jobs and enqueue it after local mutations:
19
+ *
20
+ * ```js
21
+ * backgroundJobs: {mode: "background", jobClasses: [SyncReplayJob]}
22
+ *
23
+ * await SyncReplayJob.enqueue()
24
+ * ```
25
+ * @augments {VelociousJob}
26
+ */
27
+ export default class SyncReplayJob extends VelociousJob {
28
+ /**
29
+ * Returns the stable job name used by the persisted queue.
30
+ * @returns {string} - Job name.
31
+ */
32
+ static jobName(): string;
33
+ /**
34
+ * Enqueues one deduplicated replay. Resolves after the durable enqueue, not after
35
+ * the replay finishes, so callers can stay on the synchronous local path.
36
+ * @returns {Promise<string>} - Queued job id.
37
+ */
38
+ static enqueue(): Promise<string>;
39
+ /**
40
+ * Drains the pending sync queue through the app's current sync client.
41
+ * @returns {Promise<void>}
42
+ */
43
+ perform(): Promise<void>;
44
+ }
45
+ //# sourceMappingURL=sync-replay-job.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-replay-job.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/sync-replay-job.js"],"names":[],"mappings":"AAGA,OAAO,YAAY,MAAM,mBAAmB,CAAA;AAK5C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,YAAY;IACrD;;;OAGG;IACH,MAAM,CAAC,OAAO,IAFD,MAAM,CAEqB;IAExC;;;;OAIG;IACH,OAAa,OAAO,IAFP,OAAO,CAAC,MAAM,CAAC,CAW3B;IAED;;;OAGG;IACG,OAAO,IAFA,OAAO,CAAC,IAAI,CAAC,CAmBzB;CACF"}
@@ -0,0 +1,73 @@
1
+ // @ts-check
2
+ import Logger from "../logger.js";
3
+ import VelociousJob from "./platform-job.js";
4
+ import { currentSyncClientOrNull } from "../sync/sync-client-registry.js";
5
+ const logger = new Logger("SyncReplayJob");
6
+ /**
7
+ * Durable, deduplicated replay of the device's pending sync queue.
8
+ *
9
+ * Offline-capable apps write local mutations with `syncClient.queue()`, which
10
+ * schedules an in-process replay attempt. That attempt lives only as long as the
11
+ * JavaScript runtime, so a scan (or any other mutation) that happens while the app
12
+ * is closing, backgrounding, or briefly online can leave rows pending. Enqueueing
13
+ * this job after such a mutation makes the replay durable: the local background-jobs
14
+ * adapter stores the trigger in SQLite, runs it in process as soon as the app is
15
+ * active, and collapses overlapping triggers onto one queued job.
16
+ *
17
+ * `perform()` is deliberately best-effort. Pending rows are only deleted after the
18
+ * backend acknowledges them, so a failed or offline replay leaves the queue intact
19
+ * for the next enqueue (next mutation, focus, or foreground trigger) instead of
20
+ * consuming the job's retries on a device that may stay offline for hours.
21
+ *
22
+ * Apps register it next to their other jobs and enqueue it after local mutations:
23
+ *
24
+ * ```js
25
+ * backgroundJobs: {mode: "background", jobClasses: [SyncReplayJob]}
26
+ *
27
+ * await SyncReplayJob.enqueue()
28
+ * ```
29
+ * @augments {VelociousJob}
30
+ */
31
+ export default class SyncReplayJob extends VelociousJob {
32
+ /**
33
+ * Returns the stable job name used by the persisted queue.
34
+ * @returns {string} - Job name.
35
+ */
36
+ static jobName() { return "syncReplay"; }
37
+ /**
38
+ * Enqueues one deduplicated replay. Resolves after the durable enqueue, not after
39
+ * the replay finishes, so callers can stay on the synchronous local path.
40
+ * @returns {Promise<string>} - Queued job id.
41
+ */
42
+ static async enqueue() {
43
+ return await this.performLaterWithOptions({
44
+ args: [],
45
+ options: {
46
+ concurrencyKey: "sync-replay",
47
+ deduplicateWhileQueued: true,
48
+ maxConcurrency: 1
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * Drains the pending sync queue through the app's current sync client.
54
+ * @returns {Promise<void>}
55
+ */
56
+ async perform() {
57
+ const syncClient = currentSyncClientOrNull();
58
+ if (!syncClient) {
59
+ void logger.debug("sync replay skipped because no sync client is registered");
60
+ return;
61
+ }
62
+ try {
63
+ await syncClient.replayPending();
64
+ }
65
+ catch (error) {
66
+ // Pending rows survive a failed or offline replay, so the next enqueue retries
67
+ // them. Re-throwing would make the adapter retry a device that may be offline
68
+ // for hours and mark healthy offline periods as job failures.
69
+ void logger.error(() => ["sync replay failed (pending rows stay queued)", error]);
70
+ }
71
+ }
72
+ }
73
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1yZXBsYXktam9iLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2JhY2tncm91bmQtam9icy9zeW5jLXJlcGxheS1qb2IuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWTtBQUVaLE9BQU8sTUFBTSxNQUFNLGNBQWMsQ0FBQTtBQUNqQyxPQUFPLFlBQVksTUFBTSxtQkFBbUIsQ0FBQTtBQUM1QyxPQUFPLEVBQUMsdUJBQXVCLEVBQUMsTUFBTSxpQ0FBaUMsQ0FBQTtBQUV2RSxNQUFNLE1BQU0sR0FBRyxJQUFJLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQTtBQUUxQzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBd0JHO0FBQ0gsTUFBTSxDQUFDLE9BQU8sT0FBTyxhQUFjLFNBQVEsWUFBWTtJQUNyRDs7O09BR0c7SUFDSCxNQUFNLENBQUMsT0FBTyxLQUFLLE9BQU8sWUFBWSxDQUFBLENBQUMsQ0FBQztJQUV4Qzs7OztPQUlHO0lBQ0gsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFPO1FBQ2xCLE9BQU8sTUFBTSxJQUFJLENBQUMsdUJBQXVCLENBQUM7WUFDeEMsSUFBSSxFQUFFLEVBQUU7WUFDUixPQUFPLEVBQUU7Z0JBQ1AsY0FBYyxFQUFFLGFBQWE7Z0JBQzdCLHNCQUFzQixFQUFFLElBQUk7Z0JBQzVCLGNBQWMsRUFBRSxDQUFDO2FBQ2xCO1NBQ0YsQ0FBQyxDQUFBO0lBQ0osQ0FBQztJQUVEOzs7T0FHRztJQUNILEtBQUssQ0FBQyxPQUFPO1FBQ1gsTUFBTSxVQUFVLEdBQUcsdUJBQXVCLEVBQUUsQ0FBQTtRQUU1QyxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDaEIsS0FBSyxNQUFNLENBQUMsS0FBSyxDQUFDLDBEQUEwRCxDQUFDLENBQUE7WUFFN0UsT0FBTTtRQUNSLENBQUM7UUFFRCxJQUFJLENBQUM7WUFDSCxNQUFNLFVBQVUsQ0FBQyxhQUFhLEVBQUUsQ0FBQTtRQUNsQyxDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLCtFQUErRTtZQUMvRSw4RUFBOEU7WUFDOUUsOERBQThEO1lBQzlELEtBQUssTUFBTSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsQ0FBQyxDQUFDLCtDQUErQyxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUE7UUFDbkYsQ0FBQztJQUNILENBQUM7Q0FDRiIsInNvdXJjZXNDb250ZW50IjpbIi8vIEB0cy1jaGVja1xuXG5pbXBvcnQgTG9nZ2VyIGZyb20gXCIuLi9sb2dnZXIuanNcIlxuaW1wb3J0IFZlbG9jaW91c0pvYiBmcm9tIFwiLi9wbGF0Zm9ybS1qb2IuanNcIlxuaW1wb3J0IHtjdXJyZW50U3luY0NsaWVudE9yTnVsbH0gZnJvbSBcIi4uL3N5bmMvc3luYy1jbGllbnQtcmVnaXN0cnkuanNcIlxuXG5jb25zdCBsb2dnZXIgPSBuZXcgTG9nZ2VyKFwiU3luY1JlcGxheUpvYlwiKVxuXG4vKipcbiAqIER1cmFibGUsIGRlZHVwbGljYXRlZCByZXBsYXkgb2YgdGhlIGRldmljZSdzIHBlbmRpbmcgc3luYyBxdWV1ZS5cbiAqXG4gKiBPZmZsaW5lLWNhcGFibGUgYXBwcyB3cml0ZSBsb2NhbCBtdXRhdGlvbnMgd2l0aCBgc3luY0NsaWVudC5xdWV1ZSgpYCwgd2hpY2hcbiAqIHNjaGVkdWxlcyBhbiBpbi1wcm9jZXNzIHJlcGxheSBhdHRlbXB0LiBUaGF0IGF0dGVtcHQgbGl2ZXMgb25seSBhcyBsb25nIGFzIHRoZVxuICogSmF2YVNjcmlwdCBydW50aW1lLCBzbyBhIHNjYW4gKG9yIGFueSBvdGhlciBtdXRhdGlvbikgdGhhdCBoYXBwZW5zIHdoaWxlIHRoZSBhcHBcbiAqIGlzIGNsb3NpbmcsIGJhY2tncm91bmRpbmcsIG9yIGJyaWVmbHkgb25saW5lIGNhbiBsZWF2ZSByb3dzIHBlbmRpbmcuIEVucXVldWVpbmdcbiAqIHRoaXMgam9iIGFmdGVyIHN1Y2ggYSBtdXRhdGlvbiBtYWtlcyB0aGUgcmVwbGF5IGR1cmFibGU6IHRoZSBsb2NhbCBiYWNrZ3JvdW5kLWpvYnNcbiAqIGFkYXB0ZXIgc3RvcmVzIHRoZSB0cmlnZ2VyIGluIFNRTGl0ZSwgcnVucyBpdCBpbiBwcm9jZXNzIGFzIHNvb24gYXMgdGhlIGFwcCBpc1xuICogYWN0aXZlLCBhbmQgY29sbGFwc2VzIG92ZXJsYXBwaW5nIHRyaWdnZXJzIG9udG8gb25lIHF1ZXVlZCBqb2IuXG4gKlxuICogYHBlcmZvcm0oKWAgaXMgZGVsaWJlcmF0ZWx5IGJlc3QtZWZmb3J0LiBQZW5kaW5nIHJvd3MgYXJlIG9ubHkgZGVsZXRlZCBhZnRlciB0aGVcbiAqIGJhY2tlbmQgYWNrbm93bGVkZ2VzIHRoZW0sIHNvIGEgZmFpbGVkIG9yIG9mZmxpbmUgcmVwbGF5IGxlYXZlcyB0aGUgcXVldWUgaW50YWN0XG4gKiBmb3IgdGhlIG5leHQgZW5xdWV1ZSAobmV4dCBtdXRhdGlvbiwgZm9jdXMsIG9yIGZvcmVncm91bmQgdHJpZ2dlcikgaW5zdGVhZCBvZlxuICogY29uc3VtaW5nIHRoZSBqb2IncyByZXRyaWVzIG9uIGEgZGV2aWNlIHRoYXQgbWF5IHN0YXkgb2ZmbGluZSBmb3IgaG91cnMuXG4gKlxuICogQXBwcyByZWdpc3RlciBpdCBuZXh0IHRvIHRoZWlyIG90aGVyIGpvYnMgYW5kIGVucXVldWUgaXQgYWZ0ZXIgbG9jYWwgbXV0YXRpb25zOlxuICpcbiAqIGBgYGpzXG4gKiBiYWNrZ3JvdW5kSm9iczoge21vZGU6IFwiYmFja2dyb3VuZFwiLCBqb2JDbGFzc2VzOiBbU3luY1JlcGxheUpvYl19XG4gKlxuICogYXdhaXQgU3luY1JlcGxheUpvYi5lbnF1ZXVlKClcbiAqIGBgYFxuICogQGF1Z21lbnRzIHtWZWxvY2lvdXNKb2J9XG4gKi9cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFN5bmNSZXBsYXlKb2IgZXh0ZW5kcyBWZWxvY2lvdXNKb2Ige1xuICAvKipcbiAgICogUmV0dXJucyB0aGUgc3RhYmxlIGpvYiBuYW1lIHVzZWQgYnkgdGhlIHBlcnNpc3RlZCBxdWV1ZS5cbiAgICogQHJldHVybnMge3N0cmluZ30gLSBKb2IgbmFtZS5cbiAgICovXG4gIHN0YXRpYyBqb2JOYW1lKCkgeyByZXR1cm4gXCJzeW5jUmVwbGF5XCIgfVxuXG4gIC8qKlxuICAgKiBFbnF1ZXVlcyBvbmUgZGVkdXBsaWNhdGVkIHJlcGxheS4gUmVzb2x2ZXMgYWZ0ZXIgdGhlIGR1cmFibGUgZW5xdWV1ZSwgbm90IGFmdGVyXG4gICAqIHRoZSByZXBsYXkgZmluaXNoZXMsIHNvIGNhbGxlcnMgY2FuIHN0YXkgb24gdGhlIHN5bmNocm9ub3VzIGxvY2FsIHBhdGguXG4gICAqIEByZXR1cm5zIHtQcm9taXNlPHN0cmluZz59IC0gUXVldWVkIGpvYiBpZC5cbiAgICovXG4gIHN0YXRpYyBhc3luYyBlbnF1ZXVlKCkge1xuICAgIHJldHVybiBhd2FpdCB0aGlzLnBlcmZvcm1MYXRlcldpdGhPcHRpb25zKHtcbiAgICAgIGFyZ3M6IFtdLFxuICAgICAgb3B0aW9uczoge1xuICAgICAgICBjb25jdXJyZW5jeUtleTogXCJzeW5jLXJlcGxheVwiLFxuICAgICAgICBkZWR1cGxpY2F0ZVdoaWxlUXVldWVkOiB0cnVlLFxuICAgICAgICBtYXhDb25jdXJyZW5jeTogMVxuICAgICAgfVxuICAgIH0pXG4gIH1cblxuICAvKipcbiAgICogRHJhaW5zIHRoZSBwZW5kaW5nIHN5bmMgcXVldWUgdGhyb3VnaCB0aGUgYXBwJ3MgY3VycmVudCBzeW5jIGNsaWVudC5cbiAgICogQHJldHVybnMge1Byb21pc2U8dm9pZD59XG4gICAqL1xuICBhc3luYyBwZXJmb3JtKCkge1xuICAgIGNvbnN0IHN5bmNDbGllbnQgPSBjdXJyZW50U3luY0NsaWVudE9yTnVsbCgpXG5cbiAgICBpZiAoIXN5bmNDbGllbnQpIHtcbiAgICAgIHZvaWQgbG9nZ2VyLmRlYnVnKFwic3luYyByZXBsYXkgc2tpcHBlZCBiZWNhdXNlIG5vIHN5bmMgY2xpZW50IGlzIHJlZ2lzdGVyZWRcIilcblxuICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IHN5bmNDbGllbnQucmVwbGF5UGVuZGluZygpXG4gICAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAgIC8vIFBlbmRpbmcgcm93cyBzdXJ2aXZlIGEgZmFpbGVkIG9yIG9mZmxpbmUgcmVwbGF5LCBzbyB0aGUgbmV4dCBlbnF1ZXVlIHJldHJpZXNcbiAgICAgIC8vIHRoZW0uIFJlLXRocm93aW5nIHdvdWxkIG1ha2UgdGhlIGFkYXB0ZXIgcmV0cnkgYSBkZXZpY2UgdGhhdCBtYXkgYmUgb2ZmbGluZVxuICAgICAgLy8gZm9yIGhvdXJzIGFuZCBtYXJrIGhlYWx0aHkgb2ZmbGluZSBwZXJpb2RzIGFzIGpvYiBmYWlsdXJlcy5cbiAgICAgIHZvaWQgbG9nZ2VyLmVycm9yKCgpID0+IFtcInN5bmMgcmVwbGF5IGZhaWxlZCAocGVuZGluZyByb3dzIHN0YXkgcXVldWVkKVwiLCBlcnJvcl0pXG4gICAgfVxuICB9XG59XG4iXX0=
@@ -9,4 +9,13 @@ export declare function setCurrentSyncClient(client: ReturnType<typeof JSON.pars
9
9
  * @returns {ReturnType<typeof JSON.parse>} Current sync client.
10
10
  */
11
11
  export declare function currentSyncClient(): ReturnType<typeof JSON.parse>;
12
+ /**
13
+ * Returns the current sync client when one is registered.
14
+ *
15
+ * Background jobs that replay the sync queue can legitimately run before an app has
16
+ * registered its client, or after it cleared it on sign-out. They need a non-throwing
17
+ * lookup so that state is a no-op instead of a failed job.
18
+ * @returns {ReturnType<typeof JSON.parse> | null} Current sync client, or null.
19
+ */
20
+ export declare function currentSyncClientOrNull(): ReturnType<typeof JSON.parse> | null;
12
21
  //# sourceMappingURL=sync-client-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync-client-registry.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-client-registry.js"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAHhC,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,CAGI,GAF9B,IAAI,CAIhB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAFpB,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,CAMzC"}
1
+ {"version":3,"file":"sync-client-registry.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-client-registry.js"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAHhC,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,CAGI,GAF9B,IAAI,CAIhB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAFpB,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,CAMzC;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,IAF1B,UAAU,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAIhD"}
@@ -24,4 +24,15 @@ export function currentSyncClient() {
24
24
  throw new Error("No sync client configured - create a SyncClient and call setCurrent() on it first");
25
25
  return currentClient;
26
26
  }
27
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1jbGllbnQtcmVnaXN0cnkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvc3luYy9zeW5jLWNsaWVudC1yZWdpc3RyeS5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVo7Ozs7O0dBS0c7QUFFSCw0Q0FBNEM7QUFDNUMsSUFBSSxhQUFhLEdBQUcsSUFBSSxDQUFBO0FBRXhCOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQUMsTUFBTTtJQUN6QyxhQUFhLEdBQUcsTUFBTSxDQUFBO0FBQ3hCLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsaUJBQWlCO0lBQy9CLElBQUksQ0FBQyxhQUFhO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxtRkFBbUYsQ0FBQyxDQUFBO0lBRXhILE9BQU8sYUFBYSxDQUFBO0FBQ3RCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuLyoqXG4gKiBSZWdpc3RyeSBmb3IgdGhlIGFwcCdzIGN1cnJlbnQgc3luYyBjbGllbnQuXG4gKlxuICogS2VwdCBkZXBlbmRlbmN5LWZyZWUgc28gY2xpZW50LWJ1bmRsZWQgbW9kdWxlcyAobGlrZSB0aGUgcXVlcnkgbGF5ZXIpIGNhblxuICogZGVsZWdhdGUgdG8gYSBjb25maWd1cmVkIHN5bmMgY2xpZW50IHdpdGhvdXQgaW1wb3J0aW5nIHRoZSBzeW5jIHN0YWNrLlxuICovXG5cbi8qKiBAdHlwZSB7UmV0dXJuVHlwZTx0eXBlb2YgSlNPTi5wYXJzZT59ICovXG5sZXQgY3VycmVudENsaWVudCA9IG51bGxcblxuLyoqXG4gKiBSZWdpc3RlcnMgdGhlIGN1cnJlbnQgc3luYyBjbGllbnQuXG4gKiBAcGFyYW0ge1JldHVyblR5cGU8dHlwZW9mIEpTT04ucGFyc2U+fSBjbGllbnQgLSBDb25maWd1cmVkIHN5bmMgY2xpZW50IChvciBudWxsIHRvIGNsZWFyKS5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5leHBvcnQgZnVuY3Rpb24gc2V0Q3VycmVudFN5bmNDbGllbnQoY2xpZW50KSB7XG4gIGN1cnJlbnRDbGllbnQgPSBjbGllbnRcbn1cblxuLyoqXG4gKiBSZXR1cm5zIHRoZSBjdXJyZW50IHN5bmMgY2xpZW50LlxuICogQHJldHVybnMge1JldHVyblR5cGU8dHlwZW9mIEpTT04ucGFyc2U+fSBDdXJyZW50IHN5bmMgY2xpZW50LlxuICovXG5leHBvcnQgZnVuY3Rpb24gY3VycmVudFN5bmNDbGllbnQoKSB7XG4gIGlmICghY3VycmVudENsaWVudCkgdGhyb3cgbmV3IEVycm9yKFwiTm8gc3luYyBjbGllbnQgY29uZmlndXJlZCAtIGNyZWF0ZSBhIFN5bmNDbGllbnQgYW5kIGNhbGwgc2V0Q3VycmVudCgpIG9uIGl0IGZpcnN0XCIpXG5cbiAgcmV0dXJuIGN1cnJlbnRDbGllbnRcbn1cbiJdfQ==
27
+ /**
28
+ * Returns the current sync client when one is registered.
29
+ *
30
+ * Background jobs that replay the sync queue can legitimately run before an app has
31
+ * registered its client, or after it cleared it on sign-out. They need a non-throwing
32
+ * lookup so that state is a no-op instead of a failed job.
33
+ * @returns {ReturnType<typeof JSON.parse> | null} Current sync client, or null.
34
+ */
35
+ export function currentSyncClientOrNull() {
36
+ return currentClient;
37
+ }
38
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1jbGllbnQtcmVnaXN0cnkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvc3luYy9zeW5jLWNsaWVudC1yZWdpc3RyeS5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVo7Ozs7O0dBS0c7QUFFSCw0Q0FBNEM7QUFDNUMsSUFBSSxhQUFhLEdBQUcsSUFBSSxDQUFBO0FBRXhCOzs7O0dBSUc7QUFDSCxNQUFNLFVBQVUsb0JBQW9CLENBQUMsTUFBTTtJQUN6QyxhQUFhLEdBQUcsTUFBTSxDQUFBO0FBQ3hCLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsaUJBQWlCO0lBQy9CLElBQUksQ0FBQyxhQUFhO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxtRkFBbUYsQ0FBQyxDQUFBO0lBRXhILE9BQU8sYUFBYSxDQUFBO0FBQ3RCLENBQUM7QUFFRDs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxVQUFVLHVCQUF1QjtJQUNyQyxPQUFPLGFBQWEsQ0FBQTtBQUN0QixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbi8qKlxuICogUmVnaXN0cnkgZm9yIHRoZSBhcHAncyBjdXJyZW50IHN5bmMgY2xpZW50LlxuICpcbiAqIEtlcHQgZGVwZW5kZW5jeS1mcmVlIHNvIGNsaWVudC1idW5kbGVkIG1vZHVsZXMgKGxpa2UgdGhlIHF1ZXJ5IGxheWVyKSBjYW5cbiAqIGRlbGVnYXRlIHRvIGEgY29uZmlndXJlZCBzeW5jIGNsaWVudCB3aXRob3V0IGltcG9ydGluZyB0aGUgc3luYyBzdGFjay5cbiAqL1xuXG4vKiogQHR5cGUge1JldHVyblR5cGU8dHlwZW9mIEpTT04ucGFyc2U+fSAqL1xubGV0IGN1cnJlbnRDbGllbnQgPSBudWxsXG5cbi8qKlxuICogUmVnaXN0ZXJzIHRoZSBjdXJyZW50IHN5bmMgY2xpZW50LlxuICogQHBhcmFtIHtSZXR1cm5UeXBlPHR5cGVvZiBKU09OLnBhcnNlPn0gY2xpZW50IC0gQ29uZmlndXJlZCBzeW5jIGNsaWVudCAob3IgbnVsbCB0byBjbGVhcikuXG4gKiBAcmV0dXJucyB7dm9pZH1cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHNldEN1cnJlbnRTeW5jQ2xpZW50KGNsaWVudCkge1xuICBjdXJyZW50Q2xpZW50ID0gY2xpZW50XG59XG5cbi8qKlxuICogUmV0dXJucyB0aGUgY3VycmVudCBzeW5jIGNsaWVudC5cbiAqIEByZXR1cm5zIHtSZXR1cm5UeXBlPHR5cGVvZiBKU09OLnBhcnNlPn0gQ3VycmVudCBzeW5jIGNsaWVudC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGN1cnJlbnRTeW5jQ2xpZW50KCkge1xuICBpZiAoIWN1cnJlbnRDbGllbnQpIHRocm93IG5ldyBFcnJvcihcIk5vIHN5bmMgY2xpZW50IGNvbmZpZ3VyZWQgLSBjcmVhdGUgYSBTeW5jQ2xpZW50IGFuZCBjYWxsIHNldEN1cnJlbnQoKSBvbiBpdCBmaXJzdFwiKVxuXG4gIHJldHVybiBjdXJyZW50Q2xpZW50XG59XG5cbi8qKlxuICogUmV0dXJucyB0aGUgY3VycmVudCBzeW5jIGNsaWVudCB3aGVuIG9uZSBpcyByZWdpc3RlcmVkLlxuICpcbiAqIEJhY2tncm91bmQgam9icyB0aGF0IHJlcGxheSB0aGUgc3luYyBxdWV1ZSBjYW4gbGVnaXRpbWF0ZWx5IHJ1biBiZWZvcmUgYW4gYXBwIGhhc1xuICogcmVnaXN0ZXJlZCBpdHMgY2xpZW50LCBvciBhZnRlciBpdCBjbGVhcmVkIGl0IG9uIHNpZ24tb3V0LiBUaGV5IG5lZWQgYSBub24tdGhyb3dpbmdcbiAqIGxvb2t1cCBzbyB0aGF0IHN0YXRlIGlzIGEgbm8tb3AgaW5zdGVhZCBvZiBhIGZhaWxlZCBqb2IuXG4gKiBAcmV0dXJucyB7UmV0dXJuVHlwZTx0eXBlb2YgSlNPTi5wYXJzZT4gfCBudWxsfSBDdXJyZW50IHN5bmMgY2xpZW50LCBvciBudWxsLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY3VycmVudFN5bmNDbGllbnRPck51bGwoKSB7XG4gIHJldHVybiBjdXJyZW50Q2xpZW50XG59XG4iXX0=
@@ -28,3 +28,15 @@ export function currentSyncClient() {
28
28
 
29
29
  return currentClient
30
30
  }
31
+
32
+ /**
33
+ * Returns the current sync client when one is registered.
34
+ *
35
+ * Background jobs that replay the sync queue can legitimately run before an app has
36
+ * registered its client, or after it cleared it on sign-out. They need a non-throwing
37
+ * lookup so that state is a no-op instead of a failed job.
38
+ * @returns {ReturnType<typeof JSON.parse> | null} Current sync client, or null.
39
+ */
40
+ export function currentSyncClientOrNull() {
41
+ return currentClient
42
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.715",
6
+ "version": "1.0.716",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -0,0 +1,79 @@
1
+ // @ts-check
2
+
3
+ import Logger from "../logger.js"
4
+ import VelociousJob from "./platform-job.js"
5
+ import {currentSyncClientOrNull} from "../sync/sync-client-registry.js"
6
+
7
+ const logger = new Logger("SyncReplayJob")
8
+
9
+ /**
10
+ * Durable, deduplicated replay of the device's pending sync queue.
11
+ *
12
+ * Offline-capable apps write local mutations with `syncClient.queue()`, which
13
+ * schedules an in-process replay attempt. That attempt lives only as long as the
14
+ * JavaScript runtime, so a scan (or any other mutation) that happens while the app
15
+ * is closing, backgrounding, or briefly online can leave rows pending. Enqueueing
16
+ * this job after such a mutation makes the replay durable: the local background-jobs
17
+ * adapter stores the trigger in SQLite, runs it in process as soon as the app is
18
+ * active, and collapses overlapping triggers onto one queued job.
19
+ *
20
+ * `perform()` is deliberately best-effort. Pending rows are only deleted after the
21
+ * backend acknowledges them, so a failed or offline replay leaves the queue intact
22
+ * for the next enqueue (next mutation, focus, or foreground trigger) instead of
23
+ * consuming the job's retries on a device that may stay offline for hours.
24
+ *
25
+ * Apps register it next to their other jobs and enqueue it after local mutations:
26
+ *
27
+ * ```js
28
+ * backgroundJobs: {mode: "background", jobClasses: [SyncReplayJob]}
29
+ *
30
+ * await SyncReplayJob.enqueue()
31
+ * ```
32
+ * @augments {VelociousJob}
33
+ */
34
+ export default class SyncReplayJob extends VelociousJob {
35
+ /**
36
+ * Returns the stable job name used by the persisted queue.
37
+ * @returns {string} - Job name.
38
+ */
39
+ static jobName() { return "syncReplay" }
40
+
41
+ /**
42
+ * Enqueues one deduplicated replay. Resolves after the durable enqueue, not after
43
+ * the replay finishes, so callers can stay on the synchronous local path.
44
+ * @returns {Promise<string>} - Queued job id.
45
+ */
46
+ static async enqueue() {
47
+ return await this.performLaterWithOptions({
48
+ args: [],
49
+ options: {
50
+ concurrencyKey: "sync-replay",
51
+ deduplicateWhileQueued: true,
52
+ maxConcurrency: 1
53
+ }
54
+ })
55
+ }
56
+
57
+ /**
58
+ * Drains the pending sync queue through the app's current sync client.
59
+ * @returns {Promise<void>}
60
+ */
61
+ async perform() {
62
+ const syncClient = currentSyncClientOrNull()
63
+
64
+ if (!syncClient) {
65
+ void logger.debug("sync replay skipped because no sync client is registered")
66
+
67
+ return
68
+ }
69
+
70
+ try {
71
+ await syncClient.replayPending()
72
+ } catch (error) {
73
+ // Pending rows survive a failed or offline replay, so the next enqueue retries
74
+ // them. Re-throwing would make the adapter retry a device that may be offline
75
+ // for hours and mark healthy offline periods as job failures.
76
+ void logger.error(() => ["sync replay failed (pending rows stay queued)", error])
77
+ }
78
+ }
79
+ }
@@ -28,3 +28,15 @@ export function currentSyncClient() {
28
28
 
29
29
  return currentClient
30
30
  }
31
+
32
+ /**
33
+ * Returns the current sync client when one is registered.
34
+ *
35
+ * Background jobs that replay the sync queue can legitimately run before an app has
36
+ * registered its client, or after it cleared it on sign-out. They need a non-throwing
37
+ * lookup so that state is a no-op instead of a failed job.
38
+ * @returns {ReturnType<typeof JSON.parse> | null} Current sync client, or null.
39
+ */
40
+ export function currentSyncClientOrNull() {
41
+ return currentClient
42
+ }