velocious 1.0.605 → 1.0.607
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/README.md +51 -1
- package/build/background-jobs/job-semantics.js +145 -0
- package/build/background-jobs/local-adapter.js +161 -0
- package/build/background-jobs/local-dispatcher.js +418 -0
- package/build/background-jobs/local-job-registry.js +59 -0
- package/build/background-jobs/local-store.js +1219 -0
- package/build/background-jobs/main.js +6 -1
- package/build/background-jobs/store.js +33 -97
- package/build/background-jobs/types.js +26 -1
- package/build/configuration-types.js +3 -1
- package/build/configuration.js +16 -1
- package/build/database/drivers/sqlite/index.js +46 -41
- package/build/environment-handlers/browser.js +10 -0
- package/build/src/background-jobs/job-semantics.d.ts +65 -0
- package/build/src/background-jobs/job-semantics.d.ts.map +1 -0
- package/build/src/background-jobs/job-semantics.js +125 -0
- package/build/src/background-jobs/local-adapter.d.ts +161 -0
- package/build/src/background-jobs/local-adapter.d.ts.map +1 -0
- package/build/src/background-jobs/local-adapter.js +141 -0
- package/build/src/background-jobs/local-dispatcher.d.ts +185 -0
- package/build/src/background-jobs/local-dispatcher.d.ts.map +1 -0
- package/build/src/background-jobs/local-dispatcher.js +383 -0
- package/build/src/background-jobs/local-job-registry.d.ts +26 -0
- package/build/src/background-jobs/local-job-registry.d.ts.map +1 -0
- package/build/src/background-jobs/local-job-registry.js +51 -0
- package/build/src/background-jobs/local-store.d.ts +364 -0
- package/build/src/background-jobs/local-store.d.ts.map +1 -0
- package/build/src/background-jobs/local-store.js +1078 -0
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +8 -2
- package/build/src/background-jobs/store.d.ts +0 -9
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +18 -88
- package/build/src/background-jobs/types.d.ts +97 -2
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +27 -2
- package/build/src/configuration-types.d.ts +9 -2
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +4 -2
- package/build/src/configuration.d.ts +5 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +15 -2
- package/build/src/database/drivers/sqlite/index.d.ts +8 -0
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +47 -41
- package/build/src/environment-handlers/browser.d.ts +9 -0
- package/build/src/environment-handlers/browser.d.ts.map +1 -1
- package/build/src/environment-handlers/browser.js +10 -1
- package/build/src/testing/test-runner.d.ts +42 -10
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +135 -35
- package/build/testing/test-runner.js +141 -33
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/test-browser.js +5 -1
- package/src/background-jobs/job-semantics.js +145 -0
- package/src/background-jobs/local-adapter.js +161 -0
- package/src/background-jobs/local-dispatcher.js +418 -0
- package/src/background-jobs/local-job-registry.js +59 -0
- package/src/background-jobs/local-store.js +1219 -0
- package/src/background-jobs/main.js +6 -1
- package/src/background-jobs/store.js +33 -97
- package/src/background-jobs/types.js +26 -1
- package/src/configuration-types.js +3 -1
- package/src/configuration.js +16 -1
- package/src/database/drivers/sqlite/index.js +46 -41
- package/src/environment-handlers/browser.js +10 -0
- package/src/testing/test-runner.js +141 -33
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import BackgroundJobRescheduleSignal from "./reschedule-signal.js"
|
|
4
|
+
import performBackgroundJob from "./perform-job.js"
|
|
5
|
+
|
|
6
|
+
/** @typedef {{type: "completed"} | {type: "failed", error: ReturnType<typeof JSON.parse>} | {type: "rescheduled", delayMs: number}} LocalBackgroundJobAcknowledgement */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} PendingLocalBackgroundJobAcknowledgement
|
|
10
|
+
* @property {LocalBackgroundJobAcknowledgement} acknowledgement - Durable transition still owned by this dispatcher.
|
|
11
|
+
* @property {import("./types.js").BackgroundJobHandoff} handoff - Fenced handoff being acknowledged.
|
|
12
|
+
* @property {import("./types.js").BackgroundJobRow} job - Claimed job snapshot.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const MAX_TIMER_MS = 2_147_483_647
|
|
16
|
+
const ERROR_RECOVERY_DELAY_MS = 1_000
|
|
17
|
+
|
|
18
|
+
/** Configuration-owned, event-driven in-process local dispatcher. */
|
|
19
|
+
export default class LocalBackgroundJobsDispatcher {
|
|
20
|
+
/**
|
|
21
|
+
* Creates a dispatcher owned by one configuration and local store.
|
|
22
|
+
* @param {object} args - Dispatcher options.
|
|
23
|
+
* @param {import("../configuration.js").default} args.configuration - Owning configuration.
|
|
24
|
+
* @param {import("./types.js").LocalBackgroundJobsClock} args.clock - Dispatcher clock.
|
|
25
|
+
* @param {import("./local-job-registry.js").default} args.registry - Static job registry.
|
|
26
|
+
* @param {import("./local-store.js").default} args.store - Durable local store.
|
|
27
|
+
*/
|
|
28
|
+
constructor({configuration, clock, registry, store}) {
|
|
29
|
+
this.clock = clock
|
|
30
|
+
this.configuration = configuration
|
|
31
|
+
this.registry = registry
|
|
32
|
+
this.store = store
|
|
33
|
+
this._accepting = false
|
|
34
|
+
this._started = false
|
|
35
|
+
/** @type {Promise<void> | null} */
|
|
36
|
+
this._startPromise = null
|
|
37
|
+
/** @type {Promise<void> | null} */
|
|
38
|
+
this._drainPromise = null
|
|
39
|
+
this._redrain = false
|
|
40
|
+
this._wakeQueued = false
|
|
41
|
+
/** @type {Set<Promise<void>>} */
|
|
42
|
+
this._inFlight = new Set()
|
|
43
|
+
/** @type {Map<string, PendingLocalBackgroundJobAcknowledgement>} */
|
|
44
|
+
this._pendingAcknowledgements = new Map()
|
|
45
|
+
/** @type {Set<() => void>} */
|
|
46
|
+
this._idleWaiters = new Set()
|
|
47
|
+
/** @type {ReturnType<typeof setTimeout> | number | undefined} */
|
|
48
|
+
this._scheduledTimer = undefined
|
|
49
|
+
/** @type {ReturnType<typeof setTimeout> | number | undefined} */
|
|
50
|
+
this._recoveryTimer = undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Starts, recovers, and catches up the local dispatcher.
|
|
55
|
+
* @returns {Promise<void>} - Resolves after admission starts.
|
|
56
|
+
*/
|
|
57
|
+
async start() {
|
|
58
|
+
if (this._started) return
|
|
59
|
+
if (this._startPromise) return await this._startPromise
|
|
60
|
+
|
|
61
|
+
this._startPromise = (async () => {
|
|
62
|
+
this.configuration.setCurrent()
|
|
63
|
+
this.registry.ensureReady()
|
|
64
|
+
await this.configuration.initialize({type: "local-background-jobs"})
|
|
65
|
+
await this.store.ensureReady()
|
|
66
|
+
await this.store.reconcileQueueConcurrency()
|
|
67
|
+
|
|
68
|
+
const recoveredJobs = await this.store.recoverHandedOffJobs()
|
|
69
|
+
|
|
70
|
+
for (const job of recoveredJobs) {
|
|
71
|
+
this._emitBackgroundJobFailed({
|
|
72
|
+
error: new Error(job.lastError || "Local background job recovered after an interrupted dispatcher"),
|
|
73
|
+
job
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._accepting = true
|
|
78
|
+
this._started = true
|
|
79
|
+
this.wake()
|
|
80
|
+
})()
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await this._startPromise
|
|
84
|
+
} catch (error) {
|
|
85
|
+
this._reportFrameworkError({error, stage: "local-background-jobs-start"})
|
|
86
|
+
throw error
|
|
87
|
+
} finally {
|
|
88
|
+
this._startPromise = null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Coalesces a dispatcher wake onto one tracked microtask.
|
|
94
|
+
* @returns {void} - No return value.
|
|
95
|
+
*/
|
|
96
|
+
wake() {
|
|
97
|
+
if (!this._accepting) return
|
|
98
|
+
|
|
99
|
+
if (this._drainPromise || this._wakeQueued) {
|
|
100
|
+
this._redrain = true
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this._wakeQueued = true
|
|
105
|
+
const drain = Promise.resolve().then(async () => {
|
|
106
|
+
this._wakeQueued = false
|
|
107
|
+
await this._drain()
|
|
108
|
+
})
|
|
109
|
+
const drainPromise = drain
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
this._reportFrameworkError({error, stage: "local-background-jobs-drain"})
|
|
112
|
+
this._armRecoveryTimer()
|
|
113
|
+
})
|
|
114
|
+
.finally(() => {
|
|
115
|
+
if (this._drainPromise === drainPromise) this._drainPromise = null
|
|
116
|
+
|
|
117
|
+
if (this._redrain && this._accepting) {
|
|
118
|
+
this._redrain = false
|
|
119
|
+
this.wake()
|
|
120
|
+
} else {
|
|
121
|
+
this._resolveIdleWaiters()
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
this._drainPromise = drainPromise
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Fills local capacity with short durable claims.
|
|
130
|
+
* @returns {Promise<void>} - Resolves after one stable drain pass.
|
|
131
|
+
*/
|
|
132
|
+
async _drain() {
|
|
133
|
+
await this._retryPendingAcknowledgements()
|
|
134
|
+
|
|
135
|
+
while (this._accepting && this._ownedPerformanceCount() < this._maxConcurrentJobs()) {
|
|
136
|
+
const job = await this.store.nextAvailableJob()
|
|
137
|
+
|
|
138
|
+
if (!job) break
|
|
139
|
+
|
|
140
|
+
const handoff = await this.store.markHandedOff({jobId: job.id})
|
|
141
|
+
|
|
142
|
+
if (!handoff) {
|
|
143
|
+
this._redrain = true
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this._startPerformance({handoff, job})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await this._armScheduledTimer()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Runs one claimed performance without retaining its claim transaction connection.
|
|
155
|
+
* @param {{handoff: import("./types.js").BackgroundJobHandoff, job: import("./types.js").BackgroundJobRow}} args - Claimed job.
|
|
156
|
+
* @returns {void} - No return value.
|
|
157
|
+
*/
|
|
158
|
+
_startPerformance({handoff, job}) {
|
|
159
|
+
const performance = this._perform({handoff, job})
|
|
160
|
+
|
|
161
|
+
this._inFlight.add(performance)
|
|
162
|
+
void performance
|
|
163
|
+
.catch((error) => this._reportFrameworkError({error, stage: "local-background-jobs-performance"}))
|
|
164
|
+
.finally(() => {
|
|
165
|
+
this._inFlight.delete(performance)
|
|
166
|
+
if (this._accepting) this.wake()
|
|
167
|
+
this._resolveIdleWaiters()
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Performs and acknowledges one durable handoff.
|
|
173
|
+
* @param {{handoff: import("./types.js").BackgroundJobHandoff, job: import("./types.js").BackgroundJobRow}} args - Claimed job.
|
|
174
|
+
* @returns {Promise<void>} - Resolves after acknowledgement.
|
|
175
|
+
*/
|
|
176
|
+
async _perform({handoff, job}) {
|
|
177
|
+
/** @type {LocalBackgroundJobAcknowledgement} */
|
|
178
|
+
let acknowledgement = {type: "completed"}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const JobClass = this.registry.resolve(job.jobName)
|
|
182
|
+
|
|
183
|
+
await performBackgroundJob({
|
|
184
|
+
configuration: this.configuration,
|
|
185
|
+
JobClass,
|
|
186
|
+
jobArgs: job.args,
|
|
187
|
+
name: `Local background job: ${job.jobName}`
|
|
188
|
+
})
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof BackgroundJobRescheduleSignal) {
|
|
191
|
+
acknowledgement = {delayMs: error.delayMs, type: "rescheduled"}
|
|
192
|
+
} else {
|
|
193
|
+
acknowledgement = {error, type: "failed"}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await this._acknowledge({acknowledgement, handoff, job})
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this._pendingAcknowledgements.set(job.id, {acknowledgement, handoff, job})
|
|
201
|
+
this._reportAcknowledgementError({acknowledgement, error, handoff, job})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Applies one fenced durable acknowledgement.
|
|
207
|
+
* @param {PendingLocalBackgroundJobAcknowledgement} args - Owned acknowledgement.
|
|
208
|
+
* @returns {Promise<void>} - Resolves after the durable transition is settled.
|
|
209
|
+
*/
|
|
210
|
+
async _acknowledge({acknowledgement, handoff, job}) {
|
|
211
|
+
if (acknowledgement.type === "rescheduled") {
|
|
212
|
+
await this.store.markRescheduled({delayMs: acknowledgement.delayMs, handoffId: handoff.handoffId, jobId: job.id})
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (acknowledgement.type === "failed") {
|
|
217
|
+
const updatedJob = await this.store.markFailed({error: acknowledgement.error, handoffId: handoff.handoffId, jobId: job.id})
|
|
218
|
+
|
|
219
|
+
if (updatedJob) this._emitBackgroundJobFailed({error: acknowledgement.error, job: updatedJob})
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await this.store.markCompleted({handoffId: handoff.handoffId, jobId: job.id})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Replays each retained acknowledgement once at an event-driven wake boundary.
|
|
228
|
+
* @param {{throwOnError?: boolean}} [args] - Recovery behavior.
|
|
229
|
+
* @returns {Promise<void>} - Resolves after one bounded recovery pass.
|
|
230
|
+
*/
|
|
231
|
+
async _retryPendingAcknowledgements({throwOnError = false} = {}) {
|
|
232
|
+
for (const [jobId, pendingAcknowledgement] of [...this._pendingAcknowledgements]) {
|
|
233
|
+
try {
|
|
234
|
+
await this._acknowledge(pendingAcknowledgement)
|
|
235
|
+
this._pendingAcknowledgements.delete(jobId)
|
|
236
|
+
} catch (error) {
|
|
237
|
+
this._reportAcknowledgementError({...pendingAcknowledgement, error})
|
|
238
|
+
if (throwOnError) throw error
|
|
239
|
+
this._armRecoveryTimer()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Arms the exact next future job timer, chunking platform-sized delays.
|
|
246
|
+
* @returns {Promise<void>} - Resolves after timer reconciliation.
|
|
247
|
+
*/
|
|
248
|
+
async _armScheduledTimer() {
|
|
249
|
+
if (this._scheduledTimer !== undefined) {
|
|
250
|
+
this.clock.clearTimeout(this._scheduledTimer)
|
|
251
|
+
this._scheduledTimer = undefined
|
|
252
|
+
}
|
|
253
|
+
if (!this._accepting) return
|
|
254
|
+
|
|
255
|
+
const nextJob = await this.store.nextScheduledJob()
|
|
256
|
+
|
|
257
|
+
if (!nextJob || nextJob.scheduledAtMs === null) return
|
|
258
|
+
|
|
259
|
+
const delayMs = Math.max(0, Math.min(nextJob.scheduledAtMs - this.clock.now(), MAX_TIMER_MS))
|
|
260
|
+
|
|
261
|
+
this._scheduledTimer = this.clock.setTimeout(() => {
|
|
262
|
+
this._scheduledTimer = undefined
|
|
263
|
+
this.wake()
|
|
264
|
+
}, delayMs)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Arms one bounded retry after an unexpected drain failure.
|
|
269
|
+
* @returns {void} - No return value.
|
|
270
|
+
*/
|
|
271
|
+
_armRecoveryTimer() {
|
|
272
|
+
if (!this._accepting || this._recoveryTimer !== undefined) return
|
|
273
|
+
|
|
274
|
+
this._recoveryTimer = this.clock.setTimeout(() => {
|
|
275
|
+
this._recoveryTimer = undefined
|
|
276
|
+
this.wake()
|
|
277
|
+
}, ERROR_RECOVERY_DELAY_MS)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Waits for admission and every in-flight acknowledgement without polling.
|
|
282
|
+
* @returns {Promise<void>} - Resolves when idle.
|
|
283
|
+
*/
|
|
284
|
+
async waitForIdle() {
|
|
285
|
+
if (this._isIdle()) return
|
|
286
|
+
|
|
287
|
+
await new Promise((resolve) => this._idleWaiters.add(() => resolve(undefined)))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Stops claims and waits for in-flight acknowledgement.
|
|
292
|
+
* @returns {Promise<void>} - Resolves after a graceful stop.
|
|
293
|
+
*/
|
|
294
|
+
async stop() {
|
|
295
|
+
this._accepting = false
|
|
296
|
+
this._redrain = false
|
|
297
|
+
|
|
298
|
+
if (this._scheduledTimer !== undefined) {
|
|
299
|
+
this.clock.clearTimeout(this._scheduledTimer)
|
|
300
|
+
this._scheduledTimer = undefined
|
|
301
|
+
}
|
|
302
|
+
if (this._recoveryTimer !== undefined) {
|
|
303
|
+
this.clock.clearTimeout(this._recoveryTimer)
|
|
304
|
+
this._recoveryTimer = undefined
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (this._drainPromise) await this._drainPromise
|
|
308
|
+
if (this._inFlight.size > 0) await Promise.all([...this._inFlight])
|
|
309
|
+
await this._retryPendingAcknowledgements({throwOnError: true})
|
|
310
|
+
|
|
311
|
+
this._wakeQueued = false
|
|
312
|
+
this._started = false
|
|
313
|
+
this._resolveIdleWaiters()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Reports whether dispatcher admission has started.
|
|
318
|
+
* @returns {boolean} - Whether dispatcher admission has started.
|
|
319
|
+
*/
|
|
320
|
+
isReady() { return this._started && this._accepting }
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Reads the configuration-owned in-process performance cap.
|
|
324
|
+
* @returns {number} - Configuration-owned in-process performance cap.
|
|
325
|
+
*/
|
|
326
|
+
_maxConcurrentJobs() { return this.configuration.getBackgroundJobsConfig().maxConcurrentInlineJobs }
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Counts performances whose durable acknowledgement is still owned locally.
|
|
330
|
+
* @returns {number} - Active or pending-acknowledgement performances.
|
|
331
|
+
*/
|
|
332
|
+
_ownedPerformanceCount() { return this._inFlight.size + this._pendingAcknowledgements.size }
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Reports whether no admission or acknowledgement work remains.
|
|
336
|
+
* @returns {boolean} - Whether the dispatcher is idle.
|
|
337
|
+
*/
|
|
338
|
+
_isIdle() {
|
|
339
|
+
return !this._wakeQueued && !this._drainPromise && this._inFlight.size === 0 && this._pendingAcknowledgements.size === 0
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Resolves event-based idle waiters at a stable idle boundary.
|
|
344
|
+
* @returns {void} - No return value.
|
|
345
|
+
*/
|
|
346
|
+
_resolveIdleWaiters() {
|
|
347
|
+
if (!this._isIdle()) return
|
|
348
|
+
|
|
349
|
+
const waiters = [...this._idleWaiters]
|
|
350
|
+
|
|
351
|
+
this._idleWaiters.clear()
|
|
352
|
+
for (const resolve of waiters) resolve()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Emits an expected job failure through the standard job/all-error channels.
|
|
357
|
+
* @param {{error: ReturnType<typeof JSON.parse>, job: import("./types.js").BackgroundJobRow}} args - Failure transition.
|
|
358
|
+
* @returns {void} - No return value.
|
|
359
|
+
*/
|
|
360
|
+
_emitBackgroundJobFailed({error, job}) {
|
|
361
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
362
|
+
const payload = {
|
|
363
|
+
context: {
|
|
364
|
+
attempts: job.attempts,
|
|
365
|
+
jobArgs: job.args,
|
|
366
|
+
jobId: job.id,
|
|
367
|
+
jobName: job.jobName,
|
|
368
|
+
maxRetries: job.maxRetries,
|
|
369
|
+
stage: "background-job-failed",
|
|
370
|
+
status: job.status,
|
|
371
|
+
terminal: job.status === "failed",
|
|
372
|
+
willRetry: job.status === "queued",
|
|
373
|
+
workerId: "local"
|
|
374
|
+
},
|
|
375
|
+
error: normalizedError
|
|
376
|
+
}
|
|
377
|
+
const errorEvents = this.configuration.getErrorEvents()
|
|
378
|
+
|
|
379
|
+
errorEvents.emit("background-job-failed", payload)
|
|
380
|
+
errorEvents.emit("all-error", {...payload, errorType: "background-job-failed"})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Reports one failed durable acknowledgement attempt with its fence context.
|
|
385
|
+
* @param {PendingLocalBackgroundJobAcknowledgement & {error: ReturnType<typeof JSON.parse>}} args - Failed acknowledgement.
|
|
386
|
+
* @returns {void} - No return value.
|
|
387
|
+
*/
|
|
388
|
+
_reportAcknowledgementError({acknowledgement, error, handoff, job}) {
|
|
389
|
+
this._reportFrameworkError({
|
|
390
|
+
context: {
|
|
391
|
+
acknowledgementType: acknowledgement.type,
|
|
392
|
+
handoffId: handoff.handoffId,
|
|
393
|
+
jobId: job.id,
|
|
394
|
+
jobName: job.jobName,
|
|
395
|
+
workerId: "local"
|
|
396
|
+
},
|
|
397
|
+
error,
|
|
398
|
+
stage: "local-background-jobs-acknowledgement"
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Reports an unexpected dispatcher failure through framework channels.
|
|
404
|
+
* @param {object} args - Unexpected failure.
|
|
405
|
+
* @param {Record<string, ReturnType<typeof JSON.parse>>} [args.context] - Additional failure context.
|
|
406
|
+
* @param {ReturnType<typeof JSON.parse>} args.error - Unexpected error.
|
|
407
|
+
* @param {string} args.stage - Dispatcher stage.
|
|
408
|
+
* @returns {void} - No return value.
|
|
409
|
+
*/
|
|
410
|
+
_reportFrameworkError({context = {}, error, stage}) {
|
|
411
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error))
|
|
412
|
+
const payload = {context: {...context, stage}, error: normalizedError}
|
|
413
|
+
const errorEvents = this.configuration.getErrorEvents()
|
|
414
|
+
|
|
415
|
+
errorEvents.emit("framework-error", payload)
|
|
416
|
+
errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
|
|
417
|
+
}
|
|
418
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import VelociousJob from "./platform-job.js"
|
|
4
|
+
|
|
5
|
+
/** Static, bundler-safe local background-job registry. */
|
|
6
|
+
export default class LocalBackgroundJobRegistry {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a registry from the configuration's statically imported job classes.
|
|
9
|
+
* @param {{jobClasses: Array<typeof VelociousJob>}} args - Registry options.
|
|
10
|
+
*/
|
|
11
|
+
constructor({jobClasses}) {
|
|
12
|
+
this.jobClasses = jobClasses
|
|
13
|
+
/** @type {Map<string, typeof VelociousJob> | undefined} */
|
|
14
|
+
this.jobsByName = undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validates and indexes the configured job classes.
|
|
19
|
+
* @returns {void} - No return value.
|
|
20
|
+
*/
|
|
21
|
+
ensureReady() {
|
|
22
|
+
if (this.jobsByName) return
|
|
23
|
+
if (!Array.isArray(this.jobClasses)) throw new TypeError("backgroundJobs.jobClasses must be an array")
|
|
24
|
+
|
|
25
|
+
const jobsByName = new Map()
|
|
26
|
+
|
|
27
|
+
for (const JobClass of this.jobClasses) {
|
|
28
|
+
if (typeof JobClass !== "function" || JobClass === VelociousJob || !(JobClass.prototype instanceof VelociousJob)) {
|
|
29
|
+
throw new TypeError("backgroundJobs.jobClasses must contain VelociousJob subclasses")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const jobName = JobClass.jobName()
|
|
33
|
+
|
|
34
|
+
if (typeof jobName !== "string" || jobName.trim().length === 0) {
|
|
35
|
+
throw new TypeError("backgroundJobs.jobClasses must declare non-empty job names")
|
|
36
|
+
}
|
|
37
|
+
if (jobsByName.has(jobName)) throw new Error(`Duplicate local background job name: ${jobName}`)
|
|
38
|
+
|
|
39
|
+
jobsByName.set(jobName, JobClass)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.jobsByName = jobsByName
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves a registered class.
|
|
47
|
+
* @param {string} jobName - Persisted job name.
|
|
48
|
+
* @returns {typeof VelociousJob} - Registered class.
|
|
49
|
+
*/
|
|
50
|
+
resolve(jobName) {
|
|
51
|
+
this.ensureReady()
|
|
52
|
+
|
|
53
|
+
const JobClass = this.jobsByName?.get(jobName)
|
|
54
|
+
|
|
55
|
+
if (!JobClass) throw new Error(`Local background job is not registered in backgroundJobs.jobClasses: ${jobName}`)
|
|
56
|
+
|
|
57
|
+
return JobClass
|
|
58
|
+
}
|
|
59
|
+
}
|