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.
- package/build/background-jobs/sync-replay-job.js +79 -0
- package/build/src/background-jobs/sync-replay-job.d.ts +45 -0
- package/build/src/background-jobs/sync-replay-job.d.ts.map +1 -0
- package/build/src/background-jobs/sync-replay-job.js +73 -0
- package/build/src/sync/sync-client-registry.d.ts +9 -0
- package/build/src/sync/sync-client-registry.d.ts.map +1 -1
- package/build/src/sync/sync-client-registry.js +12 -1
- package/build/sync/sync-client-registry.js +12 -0
- package/package.json +1 -1
- package/src/background-jobs/sync-replay-job.js +79 -0
- package/src/sync/sync-client-registry.js +12 -0
|
@@ -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
|
-
|
|
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
|
@@ -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
|
+
}
|