nmtjs 0.15.0-beta.19 → 0.15.0-beta.20
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/dist/index.d.ts +4 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/jobs/job.d.ts +41 -27
- package/dist/runtime/jobs/job.js +3 -0
- package/dist/runtime/jobs/job.js.map +1 -1
- package/dist/runtime/jobs/manager.d.ts +23 -8
- package/dist/runtime/jobs/manager.js +48 -21
- package/dist/runtime/jobs/manager.js.map +1 -1
- package/dist/runtime/jobs/router.d.ts +251 -0
- package/dist/runtime/jobs/router.js +396 -0
- package/dist/runtime/jobs/router.js.map +1 -0
- package/dist/runtime/jobs/runner.d.ts +10 -5
- package/dist/runtime/jobs/runner.js +80 -57
- package/dist/runtime/jobs/runner.js.map +1 -1
- package/dist/runtime/jobs/ui.js +3 -5
- package/dist/runtime/jobs/ui.js.map +1 -1
- package/dist/runtime/server/jobs.js +1 -0
- package/dist/runtime/server/jobs.js.map +1 -1
- package/dist/runtime/workers/job.d.ts +1 -1
- package/dist/runtime/workers/job.js +8 -5
- package/dist/runtime/workers/job.js.map +1 -1
- package/package.json +12 -12
- package/src/index.ts +3 -0
- package/src/runtime/index.ts +1 -0
- package/src/runtime/jobs/job.ts +76 -20
- package/src/runtime/jobs/manager.ts +71 -28
- package/src/runtime/jobs/router.ts +835 -0
- package/src/runtime/jobs/runner.ts +119 -80
- package/src/runtime/jobs/ui.ts +3 -5
- package/src/runtime/server/jobs.ts +1 -0
- package/src/runtime/workers/job.ts +11 -6
|
@@ -12,11 +12,17 @@ import { jobAbortSignal } from '../injectables.ts'
|
|
|
12
12
|
|
|
13
13
|
export type JobRunnerOptions = { logging?: LoggingOptions }
|
|
14
14
|
|
|
15
|
+
export interface StepResultEntry {
|
|
16
|
+
data: Record<string, unknown> | null
|
|
17
|
+
duration: number
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
export interface JobRunnerRunOptions {
|
|
16
21
|
signal: AbortSignal
|
|
17
22
|
result: Record<string, unknown>
|
|
18
|
-
stepResults:
|
|
23
|
+
stepResults: StepResultEntry[]
|
|
19
24
|
currentStepIndex: number
|
|
25
|
+
progress: Record<string, unknown>
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface JobRunnerRunBeforeStepParams<
|
|
@@ -25,15 +31,15 @@ export interface JobRunnerRunBeforeStepParams<
|
|
|
25
31
|
job: AnyJob
|
|
26
32
|
step: AnyJobStep
|
|
27
33
|
stepIndex: number
|
|
28
|
-
result:
|
|
29
|
-
stepResults:
|
|
34
|
+
result: Record<string, unknown>
|
|
35
|
+
stepResults: StepResultEntry[]
|
|
30
36
|
options: Options
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export interface JobRunnerRunAfterStepParams<
|
|
34
40
|
Options extends JobRunnerRunOptions,
|
|
35
41
|
> extends JobRunnerRunBeforeStepParams<Options> {
|
|
36
|
-
stepResult:
|
|
42
|
+
stepResult: StepResultEntry
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export class JobRunner<
|
|
@@ -58,117 +64,132 @@ export class JobRunner<
|
|
|
58
64
|
async runJob<T extends AnyJob>(
|
|
59
65
|
job: T,
|
|
60
66
|
data: any,
|
|
61
|
-
|
|
67
|
+
options: Partial<RunOptions> = {},
|
|
62
68
|
): Promise<T['_']['output']> {
|
|
63
69
|
const {
|
|
64
|
-
signal:
|
|
65
|
-
result:
|
|
66
|
-
stepResults:
|
|
70
|
+
signal: runSignal,
|
|
71
|
+
result: runResult = {},
|
|
72
|
+
stepResults: runStepResults = [] as RunOptions['stepResults'],
|
|
73
|
+
progress: runProgress = {},
|
|
67
74
|
currentStepIndex = 0,
|
|
68
|
-
|
|
75
|
+
...rest
|
|
76
|
+
} = options
|
|
69
77
|
|
|
70
78
|
const { input, output, steps } = job
|
|
71
79
|
|
|
72
|
-
const result: Record<string, unknown> = { ...
|
|
80
|
+
const result: Record<string, unknown> = { ...runResult }
|
|
73
81
|
const decodedInput = input.decode(data)
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
// Initialize progress: decode from checkpoint or start fresh
|
|
84
|
+
const progress: Record<string, unknown> = job.progress
|
|
85
|
+
? job.progress.decode(runProgress)
|
|
86
|
+
: { ...runProgress }
|
|
87
|
+
|
|
88
|
+
using stopListener = this.runtime.lifecycleHooks.once(
|
|
89
|
+
LifecycleHook.BeforeDispose,
|
|
79
90
|
)
|
|
91
|
+
const signal = anyAbortSignal(runSignal, stopListener.signal)
|
|
80
92
|
await using container = this.container.fork(Scope.Global)
|
|
81
93
|
await container.provide(jobAbortSignal, signal)
|
|
82
94
|
|
|
83
95
|
const jobDependencyContext = await container.createContext(job.dependencies)
|
|
84
96
|
const jobData = job.options.data
|
|
85
|
-
? await job.options.data(jobDependencyContext, decodedInput)
|
|
97
|
+
? await job.options.data(jobDependencyContext, decodedInput, progress)
|
|
86
98
|
: undefined
|
|
87
99
|
|
|
88
|
-
const stepResults = Array.from({ length: steps.length })
|
|
100
|
+
const stepResults: StepResultEntry[] = Array.from({ length: steps.length })
|
|
101
|
+
|
|
102
|
+
// Restore previous step results and reconstruct accumulated result
|
|
103
|
+
for (let stepIndex = 0; stepIndex < runStepResults.length; stepIndex++) {
|
|
104
|
+
const entry = runStepResults[stepIndex]
|
|
105
|
+
stepResults[stepIndex] = entry
|
|
106
|
+
if (entry?.data) Object.assign(result, entry.data)
|
|
107
|
+
}
|
|
89
108
|
|
|
90
|
-
|
|
91
|
-
const
|
|
109
|
+
// @ts-expect-error
|
|
110
|
+
const runOptions: RunOptions = {
|
|
92
111
|
signal,
|
|
93
112
|
result,
|
|
94
113
|
stepResults,
|
|
95
114
|
currentStepIndex: currentStepIndex,
|
|
115
|
+
progress,
|
|
116
|
+
...rest,
|
|
96
117
|
} satisfies JobRunnerRunOptions
|
|
97
118
|
|
|
98
|
-
for (let stepIndex = 0; stepIndex < _stepResults.length; stepIndex++) {
|
|
99
|
-
stepResults[stepIndex] = _stepResults[stepIndex]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
119
|
for (
|
|
103
120
|
let stepIndex = currentStepIndex;
|
|
104
121
|
stepIndex < steps.length;
|
|
105
122
|
stepIndex++
|
|
106
123
|
) {
|
|
107
|
-
if (signal.aborted) {
|
|
108
|
-
const reason = (signal as unknown as { reason?: unknown }).reason
|
|
109
|
-
if (reason instanceof UnrecoverableError) throw reason
|
|
110
|
-
throw new UnrecoverableError('Job cancelled')
|
|
111
|
-
}
|
|
112
|
-
|
|
113
124
|
const step = steps[stepIndex]
|
|
114
|
-
const resultSnapshot = Object.freeze(
|
|
125
|
+
const resultSnapshot = Object.freeze(
|
|
126
|
+
Object.assign({}, decodedInput, result),
|
|
127
|
+
)
|
|
115
128
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
input: decodedInput as any,
|
|
122
|
-
result: resultSnapshot as any,
|
|
123
|
-
})
|
|
124
|
-
if (!shouldRun) {
|
|
125
|
-
stepResults[stepIndex] = null
|
|
126
|
-
continue
|
|
129
|
+
try {
|
|
130
|
+
if (signal.aborted) {
|
|
131
|
+
const { reason } = signal
|
|
132
|
+
if (reason instanceof UnrecoverableError) throw reason
|
|
133
|
+
throw new UnrecoverableError('Job cancelled')
|
|
127
134
|
}
|
|
128
|
-
}
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
const condition = job.conditions.get(stepIndex)
|
|
137
|
+
if (condition) {
|
|
138
|
+
const shouldRun = await condition({
|
|
139
|
+
context: jobDependencyContext,
|
|
140
|
+
data: jobData,
|
|
141
|
+
input: decodedInput,
|
|
142
|
+
result: resultSnapshot,
|
|
143
|
+
progress,
|
|
144
|
+
})
|
|
145
|
+
if (!shouldRun) {
|
|
146
|
+
stepResults[stepIndex] = { data: null, duration: 0 }
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
}
|
|
134
150
|
|
|
135
|
-
|
|
151
|
+
const stepStartTime = Date.now()
|
|
136
152
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
await this.beforeStep({
|
|
154
|
+
job,
|
|
155
|
+
step,
|
|
156
|
+
stepIndex,
|
|
157
|
+
result,
|
|
158
|
+
options: runOptions,
|
|
159
|
+
stepResults,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const stepContext = await container.createContext(step.dependencies)
|
|
163
|
+
const stepInput = step.input.decode(resultSnapshot)
|
|
145
164
|
|
|
146
|
-
try {
|
|
147
165
|
await job.beforeEachHandler?.({
|
|
148
|
-
context: jobDependencyContext
|
|
166
|
+
context: jobDependencyContext,
|
|
149
167
|
data: jobData,
|
|
150
|
-
input: decodedInput
|
|
151
|
-
result: resultSnapshot
|
|
168
|
+
input: decodedInput,
|
|
169
|
+
result: resultSnapshot,
|
|
170
|
+
progress,
|
|
152
171
|
step,
|
|
153
172
|
stepIndex,
|
|
154
173
|
})
|
|
155
174
|
|
|
156
175
|
const handlerReturn = await step.handler(
|
|
157
|
-
stepContext
|
|
158
|
-
stepInput
|
|
176
|
+
stepContext,
|
|
177
|
+
stepInput,
|
|
159
178
|
jobData,
|
|
160
179
|
)
|
|
161
180
|
|
|
162
|
-
const produced = step.output.encode(
|
|
181
|
+
const produced = step.output.encode(handlerReturn ?? {})
|
|
182
|
+
const duration = Date.now() - stepStartTime
|
|
163
183
|
|
|
164
|
-
stepResults[stepIndex] = produced
|
|
184
|
+
stepResults[stepIndex] = { data: produced, duration }
|
|
165
185
|
Object.assign(result, produced)
|
|
166
186
|
|
|
167
187
|
await job.afterEachHandler?.({
|
|
168
|
-
context: jobDependencyContext
|
|
188
|
+
context: jobDependencyContext,
|
|
169
189
|
data: jobData,
|
|
170
|
-
input: decodedInput
|
|
171
|
-
result
|
|
190
|
+
input: decodedInput,
|
|
191
|
+
result,
|
|
192
|
+
progress,
|
|
172
193
|
step,
|
|
173
194
|
stepIndex,
|
|
174
195
|
})
|
|
@@ -178,16 +199,22 @@ export class JobRunner<
|
|
|
178
199
|
step,
|
|
179
200
|
stepIndex,
|
|
180
201
|
result,
|
|
181
|
-
stepResult:
|
|
202
|
+
stepResult: stepResults[stepIndex],
|
|
182
203
|
stepResults,
|
|
183
|
-
options,
|
|
204
|
+
options: runOptions,
|
|
184
205
|
})
|
|
185
206
|
} catch (error) {
|
|
207
|
+
const wrapped = new Error(`Error during step [${stepIndex}]`, {
|
|
208
|
+
cause: error,
|
|
209
|
+
})
|
|
210
|
+
this.logger.error(wrapped)
|
|
211
|
+
|
|
186
212
|
const allowRetry = await job.onErrorHandler?.({
|
|
187
|
-
context: jobDependencyContext
|
|
213
|
+
context: jobDependencyContext,
|
|
188
214
|
data: jobData,
|
|
189
|
-
input: decodedInput
|
|
190
|
-
result:
|
|
215
|
+
input: decodedInput,
|
|
216
|
+
result: result,
|
|
217
|
+
progress,
|
|
191
218
|
step,
|
|
192
219
|
stepIndex,
|
|
193
220
|
error,
|
|
@@ -197,22 +224,19 @@ export class JobRunner<
|
|
|
197
224
|
throw new UnrecoverableError('Job failed (unrecoverable)')
|
|
198
225
|
}
|
|
199
226
|
|
|
200
|
-
const wrapped = new Error(`Error during step [${stepIndex}]`, {
|
|
201
|
-
cause: error,
|
|
202
|
-
})
|
|
203
|
-
this.logger.error(wrapped)
|
|
204
227
|
throw wrapped
|
|
205
228
|
}
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
const finalPayload = await job.returnHandler!({
|
|
209
|
-
context: jobDependencyContext
|
|
232
|
+
context: jobDependencyContext,
|
|
210
233
|
data: jobData,
|
|
211
|
-
input: decodedInput
|
|
212
|
-
result
|
|
234
|
+
input: decodedInput,
|
|
235
|
+
result,
|
|
236
|
+
progress,
|
|
213
237
|
})
|
|
214
238
|
|
|
215
|
-
return output.encode(finalPayload
|
|
239
|
+
return output.encode(finalPayload)
|
|
216
240
|
}
|
|
217
241
|
|
|
218
242
|
protected async beforeStep(
|
|
@@ -260,21 +284,36 @@ export class ApplicationWorkerJobRunner extends JobRunner<
|
|
|
260
284
|
JobRunnerRunOptions & { queueJob: Job }
|
|
261
285
|
>,
|
|
262
286
|
): Promise<void> {
|
|
287
|
+
await super.afterStep(params)
|
|
263
288
|
const {
|
|
289
|
+
job,
|
|
264
290
|
step,
|
|
265
291
|
result,
|
|
292
|
+
stepResult,
|
|
266
293
|
stepResults,
|
|
267
294
|
stepIndex,
|
|
268
|
-
options: { queueJob },
|
|
295
|
+
options: { queueJob, progress },
|
|
269
296
|
} = params
|
|
270
297
|
const nextStepIndex = stepIndex + 1
|
|
298
|
+
const totalSteps = job.steps.length
|
|
299
|
+
const percentage = Math.round((nextStepIndex / totalSteps) * 100)
|
|
300
|
+
|
|
301
|
+
// Encode progress before persisting if schema is defined
|
|
302
|
+
const encodedProgress = job.progress
|
|
303
|
+
? job.progress.encode(progress)
|
|
304
|
+
: progress
|
|
305
|
+
|
|
271
306
|
await Promise.all([
|
|
272
|
-
queueJob.log(
|
|
307
|
+
queueJob.log(
|
|
308
|
+
`Step ${step.label || nextStepIndex} completed in ${(stepResult.duration / 1000).toFixed(3)}s`,
|
|
309
|
+
),
|
|
273
310
|
queueJob.updateProgress({
|
|
274
311
|
stepIndex: nextStepIndex,
|
|
275
312
|
stepLabel: step.label,
|
|
276
313
|
result,
|
|
277
314
|
stepResults,
|
|
315
|
+
progress: encodedProgress,
|
|
316
|
+
percentage,
|
|
278
317
|
}),
|
|
279
318
|
])
|
|
280
319
|
}
|
package/src/runtime/jobs/ui.ts
CHANGED
|
@@ -4,19 +4,17 @@ import type { Queue } from 'bullmq'
|
|
|
4
4
|
import { createBullBoard } from '@bull-board/api'
|
|
5
5
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'
|
|
6
6
|
import { H3Adapter } from '@bull-board/h3'
|
|
7
|
-
import { createApp,
|
|
7
|
+
import { createApp, toNodeListener } from 'h3'
|
|
8
8
|
|
|
9
9
|
export function createJobsUI(queues: Queue[]) {
|
|
10
10
|
const app = createApp()
|
|
11
|
-
const router = createRouter()
|
|
12
11
|
const serverAdapter = new H3Adapter()
|
|
13
|
-
serverAdapter.setBasePath('/')
|
|
14
12
|
createBullBoard({
|
|
15
|
-
queues: queues.map((q) => new BullMQAdapter(q)),
|
|
13
|
+
queues: queues.map((q) => new BullMQAdapter(q, { readOnlyMode: true })),
|
|
16
14
|
serverAdapter,
|
|
17
15
|
})
|
|
16
|
+
const router = serverAdapter.registerHandlers()
|
|
18
17
|
app.use(router)
|
|
19
|
-
app.use(serverAdapter.registerHandlers())
|
|
20
18
|
return createServer(toNodeListener(app))
|
|
21
19
|
}
|
|
22
20
|
|
|
@@ -184,6 +184,7 @@ export class ApplicationServerJobs {
|
|
|
184
184
|
case 'queue_job_not_found':
|
|
185
185
|
throw new UnrecoverableError(result.type)
|
|
186
186
|
case 'error':
|
|
187
|
+
console.error(result.error)
|
|
187
188
|
throw result.error
|
|
188
189
|
default:
|
|
189
190
|
throw new UnrecoverableError('Unknown job task result')
|
|
@@ -3,6 +3,7 @@ import type { MessagePort } from 'node:worker_threads'
|
|
|
3
3
|
import { UnrecoverableError } from 'bullmq'
|
|
4
4
|
|
|
5
5
|
import type { JobWorkerPool } from '../enums.ts'
|
|
6
|
+
import type { StepResultEntry } from '../jobs/runner.ts'
|
|
6
7
|
import type { ServerConfig } from '../server/config.ts'
|
|
7
8
|
import type { ServerPortMessage, ThreadPortMessage } from '../types.ts'
|
|
8
9
|
import { LifecycleHook, WorkerType } from '../enums.ts'
|
|
@@ -93,20 +94,22 @@ export class JobWorkerRuntime extends BaseWorkerRuntime {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
// Load checkpoint from BullMQ progress for resume support
|
|
96
|
-
const
|
|
97
|
+
const checkpoint = bullJob.progress as
|
|
97
98
|
| {
|
|
98
99
|
stepIndex: number
|
|
99
100
|
result: Record<string, unknown>
|
|
100
|
-
stepResults:
|
|
101
|
+
stepResults: StepResultEntry[]
|
|
102
|
+
progress: Record<string, unknown>
|
|
101
103
|
}
|
|
102
104
|
| undefined
|
|
103
105
|
|
|
104
106
|
const result = await this.jobRunner.runJob(job, task.data, {
|
|
105
107
|
signal: cancellationSignal,
|
|
106
108
|
queueJob: bullJob,
|
|
107
|
-
result:
|
|
108
|
-
stepResults:
|
|
109
|
-
currentStepIndex:
|
|
109
|
+
result: checkpoint?.result,
|
|
110
|
+
stepResults: checkpoint?.stepResults,
|
|
111
|
+
currentStepIndex: checkpoint?.stepIndex ?? 0,
|
|
112
|
+
progress: checkpoint?.progress,
|
|
110
113
|
})
|
|
111
114
|
this.runtimeOptions.port.postMessage({
|
|
112
115
|
type: 'task',
|
|
@@ -141,7 +144,9 @@ export class JobWorkerRuntime extends BaseWorkerRuntime {
|
|
|
141
144
|
if (this.config.jobs) {
|
|
142
145
|
for (const job of this.config.jobs.jobs.values()) {
|
|
143
146
|
yield job
|
|
144
|
-
yield
|
|
147
|
+
// we explicitly DO NOT WANT to yield steps here, so per-job container
|
|
148
|
+
// resolves them independently each time — creates more isolation
|
|
149
|
+
// yield* job.steps
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
}
|