velocious 1.0.606 → 1.0.608

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 (73) hide show
  1. package/README.md +45 -2
  2. package/build/background-jobs/job-semantics.js +145 -0
  3. package/build/background-jobs/local-adapter.js +161 -0
  4. package/build/background-jobs/local-dispatcher.js +418 -0
  5. package/build/background-jobs/local-job-registry.js +59 -0
  6. package/build/background-jobs/local-store.js +1220 -0
  7. package/build/background-jobs/main.js +2 -1
  8. package/build/background-jobs/store.js +97 -100
  9. package/build/background-jobs/types.js +28 -1
  10. package/build/background-jobs/worker.js +19 -14
  11. package/build/configuration-types.js +3 -1
  12. package/build/configuration.js +16 -1
  13. package/build/database/drivers/sqlite/index.js +46 -41
  14. package/build/environment-handlers/browser.js +10 -0
  15. package/build/src/background-jobs/job-semantics.d.ts +65 -0
  16. package/build/src/background-jobs/job-semantics.d.ts.map +1 -0
  17. package/build/src/background-jobs/job-semantics.js +125 -0
  18. package/build/src/background-jobs/local-adapter.d.ts +161 -0
  19. package/build/src/background-jobs/local-adapter.d.ts.map +1 -0
  20. package/build/src/background-jobs/local-adapter.js +141 -0
  21. package/build/src/background-jobs/local-dispatcher.d.ts +185 -0
  22. package/build/src/background-jobs/local-dispatcher.d.ts.map +1 -0
  23. package/build/src/background-jobs/local-dispatcher.js +383 -0
  24. package/build/src/background-jobs/local-job-registry.d.ts +26 -0
  25. package/build/src/background-jobs/local-job-registry.d.ts.map +1 -0
  26. package/build/src/background-jobs/local-job-registry.js +51 -0
  27. package/build/src/background-jobs/local-store.d.ts +364 -0
  28. package/build/src/background-jobs/local-store.d.ts.map +1 -0
  29. package/build/src/background-jobs/local-store.js +1079 -0
  30. package/build/src/background-jobs/main.d.ts.map +1 -1
  31. package/build/src/background-jobs/main.js +3 -2
  32. package/build/src/background-jobs/store.d.ts +17 -9
  33. package/build/src/background-jobs/store.d.ts.map +1 -1
  34. package/build/src/background-jobs/store.js +75 -91
  35. package/build/src/background-jobs/types.d.ts +107 -2
  36. package/build/src/background-jobs/types.d.ts.map +1 -1
  37. package/build/src/background-jobs/types.js +29 -2
  38. package/build/src/background-jobs/worker.d.ts +15 -7
  39. package/build/src/background-jobs/worker.d.ts.map +1 -1
  40. package/build/src/background-jobs/worker.js +20 -15
  41. package/build/src/configuration-types.d.ts +9 -2
  42. package/build/src/configuration-types.d.ts.map +1 -1
  43. package/build/src/configuration-types.js +4 -2
  44. package/build/src/configuration.d.ts +5 -0
  45. package/build/src/configuration.d.ts.map +1 -1
  46. package/build/src/configuration.js +15 -2
  47. package/build/src/database/drivers/sqlite/index.d.ts +8 -0
  48. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  49. package/build/src/database/drivers/sqlite/index.js +47 -41
  50. package/build/src/environment-handlers/browser.d.ts +9 -0
  51. package/build/src/environment-handlers/browser.d.ts.map +1 -1
  52. package/build/src/environment-handlers/browser.js +10 -1
  53. package/build/src/testing/test-runner.d.ts +42 -10
  54. package/build/src/testing/test-runner.d.ts.map +1 -1
  55. package/build/src/testing/test-runner.js +135 -35
  56. package/build/testing/test-runner.js +141 -33
  57. package/build/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +1 -1
  59. package/scripts/test-browser.js +5 -1
  60. package/src/background-jobs/job-semantics.js +145 -0
  61. package/src/background-jobs/local-adapter.js +161 -0
  62. package/src/background-jobs/local-dispatcher.js +418 -0
  63. package/src/background-jobs/local-job-registry.js +59 -0
  64. package/src/background-jobs/local-store.js +1220 -0
  65. package/src/background-jobs/main.js +2 -1
  66. package/src/background-jobs/store.js +97 -100
  67. package/src/background-jobs/types.js +28 -1
  68. package/src/background-jobs/worker.js +19 -14
  69. package/src/configuration-types.js +3 -1
  70. package/src/configuration.js +16 -1
  71. package/src/database/drivers/sqlite/index.js +46 -41
  72. package/src/environment-handlers/browser.js +10 -0
  73. package/src/testing/test-runner.js +141 -33
package/README.md CHANGED
@@ -34,7 +34,7 @@
34
34
  * Cross-process broadcast bus for `broadcastToChannel` via `velocious beacon`, including background job runner processes (see [docs/beacon.md](docs/beacon.md))
35
35
  * Configurable HTTP server worker handlers plus backpressured, descriptor-only file responses with completion callbacks (see [docs/http-server.md](docs/http-server.md))
36
36
  * Default-on buffered HTTP response compression with Brotli/gzip content negotiation, global and per-response opt-outs, and HEAD-correct representation headers (see [docs/http-server.md](docs/http-server.md#response-compression))
37
- * Background jobs with failure events for production reporting and authorized database-scoped dashboard count snapshots/deltas (see [docs/background-jobs.md](docs/background-jobs.md) and [docs/background-jobs-dashboard.md](docs/background-jobs-dashboard.md))
37
+ * Background jobs with Node SQL/TCP workers plus a Browser/Expo local SQLite store and in-process dispatcher, failure events, and authorized database-scoped dashboard count snapshots/deltas (see [docs/background-jobs.md](docs/background-jobs.md), [docs/local-background-jobs.md](docs/local-background-jobs.md), and [docs/background-jobs-dashboard.md](docs/background-jobs-dashboard.md))
38
38
  * Durable one-off background-job scheduling with exact epoch timestamps (see [docs/scheduled-background-job-enqueue.md](docs/scheduled-background-job-enqueue.md))
39
39
  * Rails-style request and database query logging (see [docs/logging.md](docs/logging.md))
40
40
  * EJS-backed mailers with delivery, queueing, and payload rendering support (see [docs/mailers.md](docs/mailers.md))
@@ -178,6 +178,17 @@ npx velocious test --include-tag fast,api
178
178
  npx velocious test --exclude-tag slow
179
179
  ```
180
180
 
181
+ `*.browser-spec.js` files remain eligible for both the Node database matrix and
182
+ the browser runner. Add the reserved `browser-only` tag only when a suite requires
183
+ the real browser environment; the Node runner discovers the file but does not
184
+ execute its tagged tests.
185
+
186
+ ```js
187
+ describe("Browser integration", {tags: ["browser-only"]}, () => {
188
+ it("uses browser runtime behavior", async () => {})
189
+ })
190
+ ```
191
+
181
192
  Target a test by line number or description.
182
193
 
183
194
  ```bash
@@ -2331,6 +2342,38 @@ synchronous factory. See [runtime modes and adapters](docs/background-jobs.md#ru
2331
2342
  for the contract, lifecycle, Node TCP/wake behavior, the explicit
2332
2343
  `platform-job.js` browser/Expo entry, and SQL-only compatibility boundaries.
2333
2344
 
2345
+ Browser and Expo configurations have a built-in local SQLite adapter in
2346
+ `"background"` mode. Register portable classes statically; no main process,
2347
+ worker, socket, or child process is started:
2348
+
2349
+ ```js
2350
+ import VelociousJob from "velocious/build/src/background-jobs/platform-job.js"
2351
+
2352
+ class UploadPendingChangesJob extends VelociousJob {
2353
+ async perform(projectId) {
2354
+ await uploadPendingChanges(projectId)
2355
+ }
2356
+ }
2357
+
2358
+ export default new Configuration({
2359
+ backgroundJobs: {
2360
+ databaseIdentifier: "default",
2361
+ jobClasses: [UploadPendingChangesJob],
2362
+ maxConcurrentInlineJobs: 4,
2363
+ queues: {uploads: {maxConcurrent: 2, priority: 10}}
2364
+ }
2365
+ })
2366
+ ```
2367
+
2368
+ Local enqueue participates in an existing application transaction and wakes the
2369
+ dispatcher only after commit. It supports queue caps/priorities, explicit
2370
+ concurrency, queued deduplication, one-off scheduling, retries/backoff,
2371
+ `rescheduleIn`, fenced acknowledgements, graceful close/reopen, interrupted-job
2372
+ recovery, and the existing SQL.js persistence backends. It supports one active
2373
+ configuration-owned adapter per app/database; OS background execution and
2374
+ multi-tab leadership are outside this backend. See
2375
+ [local background jobs](docs/local-background-jobs.md).
2376
+
2334
2377
  Or via env vars:
2335
2378
 
2336
2379
  ```
@@ -2362,7 +2405,7 @@ concurrency and per-job connection scopes are unchanged.
2362
2405
 
2363
2406
  `maxConcurrentForkedJobs` (default: `4`) caps how many out-of-process `executionMode: "forked"` or `executionMode: "spawned"` jobs one worker may keep in flight. Forked jobs use `child_process.fork()` with an attached IPC channel. After the main process acknowledges their durable status report, forked and spawned one-shot runners exit without waiting for graceful Beacon/database teardown; the OS closes their process-owned resources. A missing or rejected status acknowledgement makes the runner exit as failed instead of reporting clean success. Spawned jobs use the legacy `background-jobs-runner` CLI process via `child_process.spawn()` and are only for callers that intentionally want that spawned behavior.
2364
2407
 
2365
- `jobTimeoutMs` (or `VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS`, milliseconds; default: disabled) is a wall-clock backstop for `"forked"` and `"pooled"` jobs. A job still running after the timeout is terminated (`SIGTERM`, then `SIGKILL` after the reaping grace) and reported `failed`, so a genuinely-hung job can't pin a worker's capacity — and its whole-app boot and DB connections — indefinitely (notably a retired-release worker draining after a deploy). For a **pooled** job the whole child running it is killed, so its concurrent in-flight siblings on that child are also reported `failed` and requeued — a hung JS job can't be cancelled any other way — before a replacement child is spawned. It's a coarse safety net, not per-job tuning: it applies to every forked and pooled job, so set it well above the longest legitimate job. Omit it, or set `null`/`<= 0`, to disable. `"inline"` jobs are not covered — they share the worker's process and can't be killed without killing the worker. See [docs/background-jobs.md](docs/background-jobs.md#job-timeout-hung-runner-backstop).
2408
+ `jobTimeoutMs` (or `VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS`, milliseconds; default: disabled) is a wall-clock backstop for `"forked"` and `"pooled"` jobs. A job still running after the timeout is terminated (`SIGTERM`, then `SIGKILL` after the reaping grace) and reported `failed`, so a genuinely-hung job can't pin a worker's capacity — and its whole-app boot and DB connections — indefinitely (notably a retired-release worker draining after a deploy). For a **pooled** job the whole child running it is killed, so its concurrent in-flight siblings on that child are also reported `failed` and requeued — a hung JS job can't be cancelled any other way — before a replacement child is spawned. Set `options.timeoutMs` to a positive integer no greater than `2_147_483_647` for a per-job wall-clock timeout that overrides the worker default; a non-positive finite value disables the backstop for that job. Invalid types, non-finite values, fractions, and larger values are rejected before persistence. Omit it to retain the worker-level fallback. `"inline"` jobs are not covered — they share the worker's process and can't be killed without killing the worker. See [docs/background-jobs.md](docs/background-jobs.md#job-timeout-hung-runner-backstop).
2366
2409
 
2367
2410
  ### Dispatch strategy
2368
2411
 
@@ -0,0 +1,145 @@
1
+ // @ts-check
2
+
3
+ import VelociousError from "../velocious-error.js"
4
+
5
+ export const DEFAULT_BACKGROUND_JOB_EXECUTION_MODE = "pooled"
6
+ export const DEFAULT_BACKGROUND_JOB_MAX_RETRIES = 10
7
+ export const DEFAULT_BACKGROUND_JOB_QUEUE = "default"
8
+ export const QUEUE_CONCURRENCY_KEY_PREFIX = "queue:"
9
+ /** @type {import("./types.js").BackgroundJobExecutionMode[]} */
10
+ export const BACKGROUND_JOB_EXECUTION_MODES = ["inline", "forked", "pooled", "spawned"]
11
+
12
+ /**
13
+ * Normalizes a job queue.
14
+ * @param {import("./types.js").BackgroundJobOptions} [options] - Job options.
15
+ * @returns {string} - Queue name.
16
+ */
17
+ export function normalizeBackgroundJobQueue(options) {
18
+ const queue = options?.queue
19
+
20
+ if (typeof queue === "string" && queue.trim().length > 0) return queue.trim()
21
+
22
+ return DEFAULT_BACKGROUND_JOB_QUEUE
23
+ }
24
+
25
+ /**
26
+ * Normalizes an explicit execution mode while retaining the Node default.
27
+ * @param {Omit<import("./types.js").BackgroundJobOptions, "executionMode"> & {executionMode?: string}} options - Job options.
28
+ * @param {import("./types.js").BackgroundJobExecutionMode} [defaultExecutionMode] - Default mode.
29
+ * @param {import("./types.js").BackgroundJobExecutionMode[]} [supportedExecutionModes] - Modes accepted by the caller.
30
+ * @returns {import("./types.js").BackgroundJobExecutionMode} - Execution mode.
31
+ */
32
+ export function normalizeBackgroundJobExecutionMode(options = {}, defaultExecutionMode = DEFAULT_BACKGROUND_JOB_EXECUTION_MODE, supportedExecutionModes = BACKGROUND_JOB_EXECUTION_MODES) {
33
+ if ("forked" in options) {
34
+ throw new Error("The background job `forked` option was removed; pass `executionMode` (\"inline\", \"forked\", \"pooled\", or \"spawned\") instead")
35
+ }
36
+
37
+ const requestedExecutionMode = options.executionMode || defaultExecutionMode
38
+ /** @type {import("./types.js").BackgroundJobExecutionMode | undefined} */
39
+ let executionMode
40
+
41
+ for (const candidate of BACKGROUND_JOB_EXECUTION_MODES) {
42
+ if (candidate === requestedExecutionMode) executionMode = candidate
43
+ }
44
+
45
+ if (!executionMode) throw new Error(`Invalid background job executionMode: ${requestedExecutionMode}`)
46
+
47
+ if (!supportedExecutionModes.includes(executionMode)) {
48
+ throw new Error(`Background job executionMode "${executionMode}" is not supported by the local background-jobs adapter`)
49
+ }
50
+
51
+ return executionMode
52
+ }
53
+
54
+ /**
55
+ * Validates and normalizes a retry cap.
56
+ * @param {number | null | undefined} maxRetries - Requested retry cap.
57
+ * @returns {number} - Retry cap.
58
+ */
59
+ export function normalizeBackgroundJobMaxRetries(maxRetries) {
60
+ if (typeof maxRetries === "number" && Number.isFinite(maxRetries) && maxRetries >= 0) {
61
+ return Math.floor(maxRetries)
62
+ }
63
+
64
+ return DEFAULT_BACKGROUND_JOB_MAX_RETRIES
65
+ }
66
+
67
+ /**
68
+ * Validates an enqueue eligibility timestamp.
69
+ * @param {number | undefined} scheduledAtMs - Requested timestamp.
70
+ * @param {number} defaultScheduledAtMs - Default timestamp.
71
+ * @returns {number} - Eligibility timestamp.
72
+ */
73
+ export function normalizeBackgroundJobScheduledAtMs(scheduledAtMs, defaultScheduledAtMs) {
74
+ if (scheduledAtMs === undefined) return defaultScheduledAtMs
75
+ if (Number.isSafeInteger(scheduledAtMs) && scheduledAtMs >= 0) return scheduledAtMs
76
+
77
+ throw VelociousError.safe("background job scheduledAtMs must be a non-negative safe integer")
78
+ }
79
+
80
+ /**
81
+ * Validates a reschedule delay and resolves it at persistence time.
82
+ * @param {number} delayMs - Requested delay.
83
+ * @param {number} nowMs - Persistence timestamp.
84
+ * @returns {number} - New eligibility timestamp.
85
+ */
86
+ export function rescheduledBackgroundJobAtMs(delayMs, nowMs) {
87
+ if (!Number.isSafeInteger(delayMs) || delayMs < 0) {
88
+ throw VelociousError.safe("background job reschedule delayMs must be a non-negative safe integer")
89
+ }
90
+
91
+ const scheduledAtMs = nowMs + delayMs
92
+
93
+ if (!Number.isSafeInteger(scheduledAtMs)) {
94
+ throw VelociousError.safe("background job reschedule scheduledAtMs must be a safe integer")
95
+ }
96
+
97
+ return scheduledAtMs
98
+ }
99
+
100
+ /**
101
+ * Returns the shared failure backoff.
102
+ * @param {number} retryCount - One-based failed attempt count.
103
+ * @returns {number} - Backoff in milliseconds.
104
+ */
105
+ export function retryDelayMs(retryCount) {
106
+ const scheduleSeconds = [10, 60, 600, 3600]
107
+
108
+ if (retryCount <= scheduleSeconds.length) return scheduleSeconds[retryCount - 1] * 1000
109
+
110
+ return (retryCount - 3) * 60 * 60 * 1000
111
+ }
112
+
113
+ /**
114
+ * Resolves explicit or queue-derived concurrency.
115
+ * @param {object} args - Resolution arguments.
116
+ * @param {import("./types.js").BackgroundJobOptions} args.options - Job options.
117
+ * @param {string} args.queue - Normalized queue.
118
+ * @param {Record<string, {maxConcurrent?: number, priority?: number}>} args.queues - Queue configuration.
119
+ * @returns {import("./types.js").ResolvedBackgroundJobConcurrency | null} - Concurrency contract.
120
+ */
121
+ export function normalizeBackgroundJobConcurrency({options = {}, queue, queues}) {
122
+ const key = options.concurrencyKey
123
+ const cap = options.maxConcurrency
124
+
125
+ if (key !== undefined || cap !== undefined) {
126
+ if (typeof key !== "string" || key.length === 0 || !Number.isInteger(cap) || Number(cap) <= 0) {
127
+ throw new Error("background job concurrencyKey and maxConcurrency must be paired; concurrencyKey must be non-empty and maxConcurrency must be a positive integer")
128
+ }
129
+ if (key.startsWith(QUEUE_CONCURRENCY_KEY_PREFIX)) {
130
+ throw new Error(`background job concurrencyKey must not start with the reserved "${QUEUE_CONCURRENCY_KEY_PREFIX}" prefix, which is reserved for queue-derived concurrency caps`)
131
+ }
132
+
133
+ return {concurrencyKey: key, maxConcurrency: Number(cap), queueDerived: false}
134
+ }
135
+
136
+ const queueCap = queues[queue]?.maxConcurrent
137
+
138
+ if (!Number.isInteger(queueCap) || Number(queueCap) <= 0) return null
139
+
140
+ return {
141
+ concurrencyKey: `${QUEUE_CONCURRENCY_KEY_PREFIX}${queue}`,
142
+ maxConcurrency: Number(queueCap),
143
+ queueDerived: true
144
+ }
145
+ }
@@ -0,0 +1,161 @@
1
+ // @ts-check
2
+
3
+ import BackgroundJobsAdapter from "./adapter.js"
4
+ import LocalBackgroundJobsDispatcher from "./local-dispatcher.js"
5
+ import LocalBackgroundJobRegistry from "./local-job-registry.js"
6
+ import LocalBackgroundJobsStore, {localBackgroundJobsClock} from "./local-store.js"
7
+
8
+ /** Durable local SQLite adapter with an owned in-process dispatcher. */
9
+ export default class LocalBackgroundJobsAdapter extends BackgroundJobsAdapter {
10
+ /**
11
+ * Creates a local adapter for one configuration and database.
12
+ * @param {object} args - Adapter options.
13
+ * @param {import("../configuration.js").default} args.configuration - Owning configuration.
14
+ * @param {import("./types.js").LocalBackgroundJobsClock} [args.clock] - Injectable clock.
15
+ * @param {string} [args.databaseIdentifier] - Local database identifier.
16
+ */
17
+ constructor({configuration, clock = localBackgroundJobsClock(), databaseIdentifier}) {
18
+ super()
19
+ this.clock = clock
20
+ this.configuration = configuration
21
+ this.registry = new LocalBackgroundJobRegistry({jobClasses: configuration.getBackgroundJobClasses()})
22
+ this.store = new LocalBackgroundJobsStore({
23
+ clock,
24
+ configuration,
25
+ databaseIdentifier,
26
+ onCommittedEnqueue: () => this.dispatcher.wake()
27
+ })
28
+ this.dispatcher = new LocalBackgroundJobsDispatcher({clock, configuration, registry: this.registry, store: this.store})
29
+ }
30
+
31
+ /**
32
+ * Ensures that local persistence and dispatch are ready.
33
+ * @returns {Promise<void>} - Resolves when local dispatch is ready.
34
+ */
35
+ async ensureReady() { await this.dispatcher.start() }
36
+
37
+ /**
38
+ * Stops local dispatch gracefully.
39
+ * @returns {Promise<void>} - Resolves after graceful local shutdown.
40
+ */
41
+ async close() {
42
+ await this.dispatcher.stop()
43
+ this.store.resetReadiness()
44
+ }
45
+
46
+ /**
47
+ * Reports local dispatcher health.
48
+ * @returns {Promise<import("./types.js").BackgroundJobsHealth>} - Local adapter health.
49
+ */
50
+ async health() { return {ready: this.dispatcher.isReady()} }
51
+
52
+ /**
53
+ * Reconciles configuration-derived queue concurrency caps.
54
+ * @returns {Promise<void>} - Resolves after queue cap reconciliation.
55
+ */
56
+ async reconcileQueueConcurrency() { await this.store.reconcileQueueConcurrency() }
57
+
58
+ /**
59
+ * Enqueues one statically registered local job.
60
+ * @param {{jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} args - Enqueue request.
61
+ * @returns {Promise<string>} - Durable local job id.
62
+ */
63
+ async enqueue(args) {
64
+ await this.ensureReady()
65
+ this.registry.resolve(args.jobName)
66
+ return await this.store.enqueue(args)
67
+ }
68
+
69
+ /**
70
+ * Rejects stable-key cancellation, which is outside the local adapter contract.
71
+ * @param {string} _scheduleKey - Unsupported stable key.
72
+ * @returns {Promise<import("./types.js").BackgroundJobCancellationResult>} - Never resolves.
73
+ */
74
+ async cancelScheduled(_scheduleKey) { throw new Error("cancelScheduled is not supported by the local background-jobs adapter") }
75
+
76
+ /**
77
+ * Rejects stable-key replacement, which is outside the local adapter contract.
78
+ * @param {{scheduleKey: string, jobName: string, args: Array<ReturnType<typeof JSON.parse>>, options?: import("./types.js").BackgroundJobOptions}} _args - Unsupported request.
79
+ * @returns {Promise<import("./types.js").BackgroundJobReplacementResult>} - Never resolves.
80
+ */
81
+ async replaceScheduled(_args) { throw new Error("replaceScheduled is not supported by the local background-jobs adapter") }
82
+
83
+ /**
84
+ * Finds the next eligible local job.
85
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Next eligible job.
86
+ */
87
+ async nextAvailableJob() { return await this.store.nextAvailableJob() }
88
+
89
+ /**
90
+ * Finds the next future local job.
91
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Next future job.
92
+ */
93
+ async nextScheduledJob() { return await this.store.nextScheduledJob() }
94
+
95
+ /**
96
+ * Finds a local job by id.
97
+ * @param {string} jobId - Job id.
98
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Persisted job.
99
+ */
100
+ async getJob(jobId) { return await this.store.getJob(jobId) }
101
+
102
+ /**
103
+ * Lists local jobs in insertion order.
104
+ * @returns {Promise<import("./types.js").BackgroundJobRow[]>} - Local jobs.
105
+ */
106
+ async listJobs() { return await this.store.listJobs() }
107
+
108
+ /**
109
+ * Claims one queued local job.
110
+ * @param {{jobId: string, workerId?: string}} args - Claim request.
111
+ * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Handoff.
112
+ */
113
+ async markHandedOff(args) { return await this.store.markHandedOff(args) }
114
+
115
+ /**
116
+ * Finds active local handoffs owned by one worker.
117
+ * @param {{workerId: string}} args - Worker identity.
118
+ * @returns {Promise<Array<{jobId: string, handoffId: string}>>} - Active worker handoffs.
119
+ */
120
+ async handedOffJobsForWorker(args) { return await this.store.handedOffJobsForWorker(args) }
121
+
122
+ /**
123
+ * Returns an exact active local handoff to the queue.
124
+ * @param {{jobId: string, handoffId: string}} args - Handoff release.
125
+ * @returns {Promise<void>} - Resolves after the fenced release.
126
+ */
127
+ async markReturnedToQueue(args) { await this.store.markReturnedToQueue(args) }
128
+
129
+ /**
130
+ * Acknowledges successful local job completion.
131
+ * @param {{jobId: string, handoffId?: string}} args - Completion report.
132
+ * @returns {Promise<boolean>} - Whether accepted.
133
+ */
134
+ async markCompleted(args) { return await this.store.markCompleted(args) }
135
+
136
+ /**
137
+ * Acknowledges an explicit local reschedule.
138
+ * @param {{jobId: string, delayMs: number, handoffId?: string}} args - Reschedule report.
139
+ * @returns {Promise<boolean>} - Whether accepted.
140
+ */
141
+ async markRescheduled(args) { return await this.store.markRescheduled(args) }
142
+
143
+ /**
144
+ * Acknowledges a failed local performance.
145
+ * @param {{jobId: string, error: ReturnType<typeof JSON.parse>, handoffId?: string}} args - Failure report.
146
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Transition.
147
+ */
148
+ async markFailed(args) { return await this.store.markFailed(args) }
149
+
150
+ /**
151
+ * Coalesces a dispatcher wake.
152
+ * @returns {void} - No return value.
153
+ */
154
+ wake() { this.dispatcher.wake() }
155
+
156
+ /**
157
+ * Waits until current local work has been acknowledged.
158
+ * @returns {Promise<void>} - Resolves after all current work is acknowledged.
159
+ */
160
+ async waitForIdle() { await this.dispatcher.waitForIdle() }
161
+ }