nmtjs 0.15.0-beta.20 → 0.15.0-beta.22
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 +2 -0
- 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/injectables.d.ts +5 -0
- package/dist/runtime/injectables.js +4 -0
- package/dist/runtime/injectables.js.map +1 -1
- package/dist/runtime/jobs/manager.d.ts +19 -16
- package/dist/runtime/jobs/manager.js +74 -18
- package/dist/runtime/jobs/manager.js.map +1 -1
- package/dist/runtime/jobs/router.d.ts +36 -10
- package/dist/runtime/jobs/router.js +64 -13
- package/dist/runtime/jobs/router.js.map +1 -1
- package/dist/runtime/jobs/runner.d.ts +27 -4
- package/dist/runtime/jobs/runner.js +74 -11
- package/dist/runtime/jobs/runner.js.map +1 -1
- package/dist/runtime/jobs/step.d.ts +1 -0
- package/dist/runtime/jobs/step.js.map +1 -1
- package/dist/runtime/jobs/types.d.ts +80 -0
- package/dist/runtime/jobs/types.js +5 -0
- package/dist/runtime/jobs/types.js.map +1 -0
- package/dist/runtime/server/jobs.js +19 -10
- package/dist/runtime/server/jobs.js.map +1 -1
- package/dist/runtime/workers/job.js.map +1 -1
- package/package.json +12 -12
- package/src/runtime/index.ts +1 -0
- package/src/runtime/injectables.ts +13 -0
- package/src/runtime/jobs/manager.ts +97 -37
- package/src/runtime/jobs/router.ts +117 -23
- package/src/runtime/jobs/runner.ts +120 -15
- package/src/runtime/jobs/step.ts +1 -0
- package/src/runtime/jobs/types.ts +110 -0
- package/src/runtime/server/jobs.ts +26 -10
- package/src/runtime/workers/job.ts +2 -7
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
QueueEventsListener,
|
|
9
9
|
RedisClient,
|
|
10
10
|
} from 'bullmq'
|
|
11
|
-
import { pick } from '@nmtjs/core'
|
|
12
11
|
import {
|
|
13
12
|
Queue,
|
|
14
13
|
QueueEvents,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
import type { ServerStoreConfig } from '../server/config.ts'
|
|
20
19
|
import type { Store } from '../types.ts'
|
|
21
20
|
import type { AnyJob, JobBackoffOptions } from './job.ts'
|
|
21
|
+
import type { JobDefinitionInfo, JobProgressCheckpoint } from './types.ts'
|
|
22
22
|
import { createStoreClient } from '../store/index.ts'
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -64,33 +64,29 @@ export class QueueJobResult<T extends AnyJob = AnyJob> {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export type
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| 'processedOn'
|
|
77
|
-
| 'finishedOn'
|
|
78
|
-
| 'failedReason'
|
|
79
|
-
| 'stacktrace'
|
|
80
|
-
> & { id: string; status: JobState | 'unknown' }
|
|
67
|
+
export type { JobDefinitionInfo, JobItem, JobStepInfo } from './types.ts'
|
|
68
|
+
|
|
69
|
+
import type { JobItem, JobStatus } from './types.ts'
|
|
70
|
+
|
|
71
|
+
/** Job item type with generic job typing for internal use */
|
|
72
|
+
export type JobItemOf<T extends AnyJob = AnyJob> = JobItem<
|
|
73
|
+
T['_']['input'],
|
|
74
|
+
T['_']['output']
|
|
75
|
+
>
|
|
81
76
|
|
|
82
77
|
export interface JobManagerInstance {
|
|
83
78
|
list<T extends AnyJob>(
|
|
84
79
|
job: T,
|
|
85
|
-
options?: { page?: number; limit?: number;
|
|
80
|
+
options?: { page?: number; limit?: number; status?: JobStatus[] },
|
|
86
81
|
): Promise<{
|
|
87
|
-
items:
|
|
82
|
+
items: JobItemOf<T>[]
|
|
88
83
|
page: number
|
|
89
84
|
limit: number
|
|
90
85
|
pages: number
|
|
91
86
|
total: number
|
|
92
87
|
}>
|
|
93
|
-
get<T extends AnyJob>(job: T, id: string): Promise<
|
|
88
|
+
get<T extends AnyJob>(job: T, id: string): Promise<JobItemOf<T> | null>
|
|
89
|
+
getInfo(job: AnyJob): JobDefinitionInfo
|
|
94
90
|
add<T extends AnyJob>(
|
|
95
91
|
job: T,
|
|
96
92
|
data: T['_']['input'],
|
|
@@ -133,6 +129,7 @@ export class JobManager {
|
|
|
133
129
|
list: this.list.bind(this),
|
|
134
130
|
add: this.add.bind(this),
|
|
135
131
|
get: this.get.bind(this),
|
|
132
|
+
getInfo: this.getInfo.bind(this),
|
|
136
133
|
retry: this.retry.bind(this),
|
|
137
134
|
remove: this.remove.bind(this),
|
|
138
135
|
cancel: this.cancel.bind(this),
|
|
@@ -188,20 +185,32 @@ export class JobManager {
|
|
|
188
185
|
return entry
|
|
189
186
|
}
|
|
190
187
|
|
|
188
|
+
getInfo(job: AnyJob): JobDefinitionInfo {
|
|
189
|
+
return {
|
|
190
|
+
name: job.options.name,
|
|
191
|
+
steps: job.steps.map((step, index) => ({
|
|
192
|
+
label: step.label,
|
|
193
|
+
conditional: job.conditions.has(index),
|
|
194
|
+
})),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
191
198
|
async list<T extends AnyJob>(
|
|
192
199
|
job: T,
|
|
193
200
|
{
|
|
194
201
|
limit = 20,
|
|
195
202
|
page = 1,
|
|
196
|
-
|
|
197
|
-
}: { page?: number; limit?: number;
|
|
203
|
+
status = [],
|
|
204
|
+
}: { page?: number; limit?: number; status?: JobStatus[] },
|
|
198
205
|
) {
|
|
199
206
|
const { queue } = this.getJobQueue(job)
|
|
200
|
-
|
|
207
|
+
// Convert vendor-agnostic status to BullMQ JobType for querying
|
|
208
|
+
const bullJobTypes = status.flatMap((s) => this._mapStatusToJobType(s))
|
|
209
|
+
const jobsCount = await queue.getJobCountByTypes(...bullJobTypes)
|
|
201
210
|
const totalPages = Math.ceil(jobsCount / limit)
|
|
202
211
|
if (page > totalPages) return []
|
|
203
212
|
const jobs = await queue.getJobs(
|
|
204
|
-
|
|
213
|
+
bullJobTypes,
|
|
205
214
|
(page - 1) * limit,
|
|
206
215
|
page * limit - 1,
|
|
207
216
|
)
|
|
@@ -307,26 +316,77 @@ export class JobManager {
|
|
|
307
316
|
return this.getJobQueue(job)
|
|
308
317
|
}
|
|
309
318
|
|
|
310
|
-
protected async _mapJob(bullJob: Job): Promise<
|
|
311
|
-
const
|
|
319
|
+
protected async _mapJob(bullJob: Job): Promise<JobItemOf> {
|
|
320
|
+
const bullState = await bullJob.getState()
|
|
312
321
|
const id = bullJob.id
|
|
313
322
|
assert(typeof id === 'string', 'Expected job id to be a string')
|
|
323
|
+
|
|
324
|
+
// Map BullMQ state to vendor-agnostic status
|
|
325
|
+
const status = this._mapStatus(bullState)
|
|
326
|
+
|
|
327
|
+
// Extract progress only if it's a valid checkpoint object
|
|
328
|
+
const progress =
|
|
329
|
+
typeof bullJob.progress === 'object' &&
|
|
330
|
+
bullJob.progress !== null &&
|
|
331
|
+
'stepIndex' in bullJob.progress
|
|
332
|
+
? (bullJob.progress as JobProgressCheckpoint)
|
|
333
|
+
: undefined
|
|
334
|
+
|
|
314
335
|
return {
|
|
315
|
-
...pick(bullJob, {
|
|
316
|
-
queueName: true,
|
|
317
|
-
priority: true,
|
|
318
|
-
progress: true,
|
|
319
|
-
name: true,
|
|
320
|
-
data: true,
|
|
321
|
-
returnvalue: true,
|
|
322
|
-
attemptsMade: true,
|
|
323
|
-
processedOn: true,
|
|
324
|
-
finishedOn: true,
|
|
325
|
-
failedReason: true,
|
|
326
|
-
stacktrace: true,
|
|
327
|
-
}),
|
|
328
336
|
id,
|
|
337
|
+
name: bullJob.name,
|
|
338
|
+
queue: bullJob.queueName,
|
|
339
|
+
data: bullJob.data,
|
|
340
|
+
output: bullJob.returnvalue,
|
|
329
341
|
status,
|
|
342
|
+
priority: bullJob.priority,
|
|
343
|
+
progress,
|
|
344
|
+
attempts: bullJob.attemptsMade,
|
|
345
|
+
startedAt: bullJob.processedOn,
|
|
346
|
+
completedAt: bullJob.finishedOn,
|
|
347
|
+
error: bullJob.failedReason,
|
|
348
|
+
stacktrace: bullJob.stacktrace,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Map BullMQ JobState to vendor-agnostic JobStatus */
|
|
353
|
+
protected _mapStatus(state: JobState | 'unknown'): JobStatus {
|
|
354
|
+
switch (state) {
|
|
355
|
+
case 'waiting':
|
|
356
|
+
case 'prioritized':
|
|
357
|
+
case 'waiting-children':
|
|
358
|
+
return 'pending'
|
|
359
|
+
case 'active':
|
|
360
|
+
return 'active'
|
|
361
|
+
case 'completed':
|
|
362
|
+
return 'completed'
|
|
363
|
+
case 'failed':
|
|
364
|
+
return 'failed'
|
|
365
|
+
case 'delayed':
|
|
366
|
+
return 'delayed'
|
|
367
|
+
default:
|
|
368
|
+
return 'unknown'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Map vendor-agnostic JobStatus to BullMQ JobType for queries */
|
|
373
|
+
protected _mapStatusToJobType(status: JobStatus): JobType[] {
|
|
374
|
+
switch (status) {
|
|
375
|
+
case 'pending':
|
|
376
|
+
return ['waiting', 'prioritized', 'waiting-children']
|
|
377
|
+
case 'active':
|
|
378
|
+
return ['active']
|
|
379
|
+
case 'completed':
|
|
380
|
+
return ['completed']
|
|
381
|
+
case 'failed':
|
|
382
|
+
return ['failed']
|
|
383
|
+
case 'delayed':
|
|
384
|
+
return ['delayed']
|
|
385
|
+
case 'cancelled':
|
|
386
|
+
return [] // BullMQ doesn't have a cancelled state
|
|
387
|
+
case 'unknown':
|
|
388
|
+
default:
|
|
389
|
+
return []
|
|
330
390
|
}
|
|
331
391
|
}
|
|
332
392
|
}
|
|
@@ -3,7 +3,6 @@ import type { TProcedureContract, TRouterContract } from '@nmtjs/contract'
|
|
|
3
3
|
import type { Dependencies, DependencyContext, Metadata } from '@nmtjs/core'
|
|
4
4
|
import type { NullableType, OptionalType } from '@nmtjs/type'
|
|
5
5
|
import type { NeverType } from '@nmtjs/type/never'
|
|
6
|
-
import type { JobState } from 'bullmq'
|
|
7
6
|
import { CoreInjectables } from '@nmtjs/core'
|
|
8
7
|
import { t } from '@nmtjs/type'
|
|
9
8
|
|
|
@@ -12,6 +11,7 @@ import type { AnyMiddleware } from '../application/api/middlewares.ts'
|
|
|
12
11
|
import type { AnyProcedure } from '../application/api/procedure.ts'
|
|
13
12
|
import type { AnyRouter, Router } from '../application/api/router.ts'
|
|
14
13
|
import type { AnyJob } from './job.ts'
|
|
14
|
+
import type { JobStatus } from './types.ts'
|
|
15
15
|
import { createProcedure } from '../application/api/procedure.ts'
|
|
16
16
|
import { createRouter } from '../application/api/router.ts'
|
|
17
17
|
import { jobManager } from '../injectables.ts'
|
|
@@ -37,6 +37,10 @@ export type ListOperationConfig<Deps extends Dependencies = {}> =
|
|
|
37
37
|
export type GetOperationConfig<Deps extends Dependencies = {}> =
|
|
38
38
|
BaseOperationConfig<Deps>
|
|
39
39
|
|
|
40
|
+
/** Info operation config (read-only, no hooks) */
|
|
41
|
+
export type InfoOperationConfig<Deps extends Dependencies = {}> =
|
|
42
|
+
BaseOperationConfig<Deps>
|
|
43
|
+
|
|
40
44
|
/** Add queue options */
|
|
41
45
|
export type AddQueueOptions = {
|
|
42
46
|
priority?: number
|
|
@@ -103,6 +107,7 @@ export type CancelOperationConfig<Deps extends Dependencies = {}> =
|
|
|
103
107
|
|
|
104
108
|
/** All operations for a job (false = disabled) */
|
|
105
109
|
export type JobOperations<T extends AnyJob = AnyJob> = {
|
|
110
|
+
info?: InfoOperationConfig<any> | false
|
|
106
111
|
list?: ListOperationConfig<any> | false
|
|
107
112
|
get?: GetOperationConfig<any> | false
|
|
108
113
|
add?: AddOperationConfig<T, any> | false
|
|
@@ -113,6 +118,7 @@ export type JobOperations<T extends AnyJob = AnyJob> = {
|
|
|
113
118
|
|
|
114
119
|
/** Default operations config */
|
|
115
120
|
export type DefaultOperations = {
|
|
121
|
+
info?: InfoOperationConfig<any> | false
|
|
116
122
|
list?: ListOperationConfig<any> | false
|
|
117
123
|
get?: GetOperationConfig<any> | false
|
|
118
124
|
add?: AddOperationConfig<AnyJob, any> | false
|
|
@@ -136,21 +142,35 @@ export type CreateJobsRouterOptions<Jobs extends Record<string, AnyJob>> = {
|
|
|
136
142
|
// Router Contract Types
|
|
137
143
|
// ============================================================================
|
|
138
144
|
|
|
145
|
+
/** Type-level schema for JobProgressCheckpoint - typed per job */
|
|
146
|
+
type JobProgressCheckpointSchemaType<T extends AnyJob> = t.ObjectType<{
|
|
147
|
+
stepIndex: t.NumberType
|
|
148
|
+
stepLabel: OptionalType<t.StringType>
|
|
149
|
+
result: t.RecordType<t.StringType, t.AnyType>
|
|
150
|
+
stepResults: t.ArrayType<
|
|
151
|
+
t.ObjectType<{
|
|
152
|
+
data: NullableType<t.RecordType<t.StringType, t.AnyType>>
|
|
153
|
+
duration: t.NumberType
|
|
154
|
+
}>
|
|
155
|
+
>
|
|
156
|
+
progress: T['progress']
|
|
157
|
+
}>
|
|
158
|
+
|
|
139
159
|
/** Type-level representation of createJobItemSchema output */
|
|
140
160
|
type JobItemSchemaType<T extends AnyJob> = t.ObjectType<{
|
|
141
161
|
id: t.StringType
|
|
142
|
-
queueName: t.StringType
|
|
143
|
-
priority: OptionalType<t.NumberType>
|
|
144
|
-
progress: t.AnyType
|
|
145
162
|
name: t.StringType
|
|
163
|
+
queue: t.StringType
|
|
146
164
|
data: T['input']
|
|
147
|
-
|
|
148
|
-
attemptsMade: t.NumberType
|
|
149
|
-
processedOn: OptionalType<t.NumberType>
|
|
150
|
-
finishedOn: OptionalType<t.NumberType>
|
|
151
|
-
failedReason: OptionalType<t.StringType>
|
|
152
|
-
stacktrace: OptionalType<t.ArrayType<t.StringType>>
|
|
165
|
+
output: OptionalType<NullableType<T['output']>>
|
|
153
166
|
status: t.StringType
|
|
167
|
+
priority: OptionalType<t.NumberType>
|
|
168
|
+
progress: OptionalType<JobProgressCheckpointSchemaType<T>>
|
|
169
|
+
attempts: t.NumberType
|
|
170
|
+
startedAt: OptionalType<t.NumberType>
|
|
171
|
+
completedAt: OptionalType<t.NumberType>
|
|
172
|
+
error: OptionalType<t.StringType>
|
|
173
|
+
stacktrace: OptionalType<t.ArrayType<t.StringType>>
|
|
154
174
|
}>
|
|
155
175
|
|
|
156
176
|
/** Type-level representation of createListOutputSchema output */
|
|
@@ -165,6 +185,9 @@ type ListOutputSchemaType<T extends AnyJob> = t.ObjectType<{
|
|
|
165
185
|
/** Type-level representation of createGetOutputSchema output */
|
|
166
186
|
type GetOutputSchemaType<T extends AnyJob> = NullableType<JobItemSchemaType<T>>
|
|
167
187
|
|
|
188
|
+
/** Type-level representation of infoOutputSchema */
|
|
189
|
+
type InfoOutputSchemaType = typeof infoOutputSchema
|
|
190
|
+
|
|
168
191
|
/** Type-level representation of createAddInputSchema output */
|
|
169
192
|
type AddInputSchemaType<T extends AnyJob> = t.ObjectType<{
|
|
170
193
|
data: T['input']
|
|
@@ -175,6 +198,7 @@ type AddInputSchemaType<T extends AnyJob> = t.ObjectType<{
|
|
|
175
198
|
|
|
176
199
|
/** Operations contract for a single job - now properly typed per job */
|
|
177
200
|
type JobOperationsProcedures<T extends AnyJob> = {
|
|
201
|
+
info: TProcedureContract<NeverType, InfoOutputSchemaType>
|
|
178
202
|
list: TProcedureContract<typeof listInputSchema, ListOutputSchemaType<T>>
|
|
179
203
|
get: TProcedureContract<typeof getInputSchema, GetOutputSchemaType<T>>
|
|
180
204
|
add: TProcedureContract<AddInputSchemaType<T>, typeof addOutputSchema>
|
|
@@ -206,7 +230,7 @@ export type JobsRouter<Jobs extends Record<string, AnyJob>> = Router<
|
|
|
206
230
|
const listInputSchema = t.object({
|
|
207
231
|
page: t.number().optional(),
|
|
208
232
|
limit: t.number().optional(),
|
|
209
|
-
|
|
233
|
+
status: t.array(t.string()).optional(),
|
|
210
234
|
})
|
|
211
235
|
|
|
212
236
|
/** Input schema for get operation */
|
|
@@ -224,22 +248,49 @@ const retryInputSchema = t.object({
|
|
|
224
248
|
/** Input schema for cancel/remove operations */
|
|
225
249
|
const idInputSchema = t.object({ id: t.string() })
|
|
226
250
|
|
|
251
|
+
/** Output schema for info operation */
|
|
252
|
+
const infoOutputSchema = t.object({
|
|
253
|
+
name: t.string(),
|
|
254
|
+
steps: t.array(
|
|
255
|
+
t.object({ label: t.string().optional(), conditional: t.boolean() }),
|
|
256
|
+
),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
/** Schema for step result entry */
|
|
260
|
+
const stepResultEntrySchema = t.object({
|
|
261
|
+
data: t.record(t.string(), t.any()).nullable(),
|
|
262
|
+
duration: t.number(),
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
/** Creates JobProgressCheckpoint schema for a specific job */
|
|
266
|
+
function createJobProgressCheckpointSchema<T extends AnyJob>(
|
|
267
|
+
job: T,
|
|
268
|
+
): JobProgressCheckpointSchemaType<T> {
|
|
269
|
+
return t.object({
|
|
270
|
+
stepIndex: t.number(),
|
|
271
|
+
stepLabel: t.string().optional(),
|
|
272
|
+
result: t.record(t.string(), t.any()),
|
|
273
|
+
stepResults: t.array(stepResultEntrySchema),
|
|
274
|
+
progress: job.progress,
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
227
278
|
/** JobItem schema for list/get responses - typed per job */
|
|
228
279
|
function createJobItemSchema<T extends AnyJob>(job: T): JobItemSchemaType<T> {
|
|
229
280
|
return t.object({
|
|
230
281
|
id: t.string(),
|
|
231
|
-
queueName: t.string(),
|
|
232
|
-
priority: t.number().optional(),
|
|
233
|
-
progress: t.any(),
|
|
234
282
|
name: t.string(),
|
|
283
|
+
queue: t.string(),
|
|
235
284
|
data: job.input,
|
|
236
|
-
|
|
237
|
-
attemptsMade: t.number(),
|
|
238
|
-
processedOn: t.number().optional(),
|
|
239
|
-
finishedOn: t.number().optional(),
|
|
240
|
-
failedReason: t.string().optional(),
|
|
241
|
-
stacktrace: t.array(t.string()).optional(),
|
|
285
|
+
output: job.output.nullish(),
|
|
242
286
|
status: t.string(),
|
|
287
|
+
priority: t.number().optional(),
|
|
288
|
+
progress: createJobProgressCheckpointSchema(job).optional(),
|
|
289
|
+
attempts: t.number(),
|
|
290
|
+
startedAt: t.number().optional(),
|
|
291
|
+
completedAt: t.number().optional(),
|
|
292
|
+
error: t.string().optional(),
|
|
293
|
+
stacktrace: t.array(t.string()).optional(),
|
|
243
294
|
})
|
|
244
295
|
}
|
|
245
296
|
|
|
@@ -352,6 +403,33 @@ type JobManagerDeps = {
|
|
|
352
403
|
logger: typeof CoreInjectables.logger
|
|
353
404
|
}
|
|
354
405
|
|
|
406
|
+
function createInfoProcedure(
|
|
407
|
+
job: AnyJob,
|
|
408
|
+
config: InfoOperationConfig<any> = {},
|
|
409
|
+
shared: { guards?: AnyGuard[]; middlewares?: AnyMiddleware[] },
|
|
410
|
+
): AnyProcedure {
|
|
411
|
+
const allGuards = [...(shared.guards ?? []), ...(config.guards ?? [])]
|
|
412
|
+
const allMiddlewares = [
|
|
413
|
+
...(shared.middlewares ?? []),
|
|
414
|
+
...(config.middlewares ?? []),
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
const deps: JobManagerDeps = { jobManager, logger: CoreInjectables.logger }
|
|
418
|
+
|
|
419
|
+
return createProcedure({
|
|
420
|
+
output: infoOutputSchema,
|
|
421
|
+
dependencies: { ...deps, ...(config.dependencies ?? {}) },
|
|
422
|
+
guards: allGuards,
|
|
423
|
+
middlewares: allMiddlewares,
|
|
424
|
+
metadata: config.metadata,
|
|
425
|
+
timeout: config.timeout,
|
|
426
|
+
handler: (ctx: DependencyContext<JobManagerDeps>) => {
|
|
427
|
+
ctx.logger.trace({ jobName: job.options.name }, 'Getting job info')
|
|
428
|
+
return ctx.jobManager.getInfo(job)
|
|
429
|
+
},
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
355
433
|
function createListProcedure(
|
|
356
434
|
job: AnyJob,
|
|
357
435
|
config: ListOperationConfig<any> = {},
|
|
@@ -379,14 +457,14 @@ function createListProcedure(
|
|
|
379
457
|
jobName: job.options.name,
|
|
380
458
|
page: input.page,
|
|
381
459
|
limit: input.limit,
|
|
382
|
-
|
|
460
|
+
status: input.status,
|
|
383
461
|
},
|
|
384
462
|
'Listing jobs',
|
|
385
463
|
)
|
|
386
464
|
const result = await ctx.jobManager.list(job, {
|
|
387
465
|
page: input.page,
|
|
388
466
|
limit: input.limit,
|
|
389
|
-
|
|
467
|
+
status: input.status as JobStatus[],
|
|
390
468
|
})
|
|
391
469
|
ctx.logger.debug(
|
|
392
470
|
{ jobName: job.options.name, total: result.total, pages: result.pages },
|
|
@@ -677,7 +755,15 @@ function mergeOperations(
|
|
|
677
755
|
): JobOperations {
|
|
678
756
|
const result: JobOperations = {}
|
|
679
757
|
|
|
680
|
-
const ops = [
|
|
758
|
+
const ops = [
|
|
759
|
+
'info',
|
|
760
|
+
'list',
|
|
761
|
+
'get',
|
|
762
|
+
'add',
|
|
763
|
+
'retry',
|
|
764
|
+
'cancel',
|
|
765
|
+
'remove',
|
|
766
|
+
] as const
|
|
681
767
|
|
|
682
768
|
for (const op of ops) {
|
|
683
769
|
const override = overrides[op]
|
|
@@ -778,6 +864,14 @@ export function createJobsRouter<const Jobs extends Record<string, AnyJob>>(
|
|
|
778
864
|
const jobRoutes: Record<string, AnyProcedure> = {}
|
|
779
865
|
|
|
780
866
|
// Generate each enabled operation
|
|
867
|
+
if (operations.info !== false) {
|
|
868
|
+
jobRoutes.info = createInfoProcedure(
|
|
869
|
+
job,
|
|
870
|
+
operations.info as InfoOperationConfig,
|
|
871
|
+
shared,
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
|
|
781
875
|
if (operations.list !== false) {
|
|
782
876
|
jobRoutes.list = createListProcedure(
|
|
783
877
|
job,
|
|
@@ -7,15 +7,21 @@ import { UnrecoverableError } from 'bullmq'
|
|
|
7
7
|
import type { LifecycleHooks } from '../core/hooks.ts'
|
|
8
8
|
import type { AnyJob } from './job.ts'
|
|
9
9
|
import type { AnyJobStep } from './step.ts'
|
|
10
|
+
import type {
|
|
11
|
+
JobExecutionContext,
|
|
12
|
+
JobProgressCheckpoint,
|
|
13
|
+
StepResultEntry,
|
|
14
|
+
} from './types.ts'
|
|
10
15
|
import { LifecycleHook } from '../enums.ts'
|
|
11
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
currentJobInfo,
|
|
18
|
+
jobAbortSignal,
|
|
19
|
+
saveJobProgress,
|
|
20
|
+
} from '../injectables.ts'
|
|
12
21
|
|
|
13
22
|
export type JobRunnerOptions = { logging?: LoggingOptions }
|
|
14
23
|
|
|
15
|
-
export
|
|
16
|
-
data: Record<string, unknown> | null
|
|
17
|
-
duration: number
|
|
18
|
-
}
|
|
24
|
+
export type { StepResultEntry } from './types.ts'
|
|
19
25
|
|
|
20
26
|
export interface JobRunnerRunOptions {
|
|
21
27
|
signal: AbortSignal
|
|
@@ -42,6 +48,18 @@ export interface JobRunnerRunAfterStepParams<
|
|
|
42
48
|
stepResult: StepResultEntry
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/** Context for saveProgress function - contains mutable state references */
|
|
52
|
+
export interface SaveProgressContext<
|
|
53
|
+
Options extends JobRunnerRunOptions = JobRunnerRunOptions,
|
|
54
|
+
> {
|
|
55
|
+
job: AnyJob
|
|
56
|
+
progress: Record<string, unknown>
|
|
57
|
+
result: Record<string, unknown>
|
|
58
|
+
stepResults: StepResultEntry[] | null
|
|
59
|
+
currentStepIndex: number
|
|
60
|
+
options: Options | null
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
export class JobRunner<
|
|
46
64
|
RunOptions extends JobRunnerRunOptions = JobRunnerRunOptions,
|
|
47
65
|
> {
|
|
@@ -61,6 +79,28 @@ export class JobRunner<
|
|
|
61
79
|
return this.runtime.container
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Creates a function that saves the current job progress state.
|
|
84
|
+
* Override in subclasses to implement actual persistence.
|
|
85
|
+
*/
|
|
86
|
+
protected createSaveProgressFn(
|
|
87
|
+
_context: SaveProgressContext<RunOptions>,
|
|
88
|
+
): () => Promise<void> {
|
|
89
|
+
// No-op in base runner - subclasses implement actual persistence
|
|
90
|
+
return async () => {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates the current job info object.
|
|
95
|
+
* Override in subclasses to include additional info (e.g., BullMQ job ID).
|
|
96
|
+
*/
|
|
97
|
+
protected createJobInfo(
|
|
98
|
+
job: AnyJob,
|
|
99
|
+
_options: Partial<RunOptions>,
|
|
100
|
+
): JobExecutionContext {
|
|
101
|
+
return { name: job.options.name }
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
async runJob<T extends AnyJob>(
|
|
65
105
|
job: T,
|
|
66
106
|
data: any,
|
|
@@ -91,6 +131,23 @@ export class JobRunner<
|
|
|
91
131
|
const signal = anyAbortSignal(runSignal, stopListener.signal)
|
|
92
132
|
await using container = this.container.fork(Scope.Global)
|
|
93
133
|
await container.provide(jobAbortSignal, signal)
|
|
134
|
+
await container.provide(currentJobInfo, this.createJobInfo(job, options))
|
|
135
|
+
|
|
136
|
+
// Create mutable state context for saveProgress
|
|
137
|
+
const progressContext = {
|
|
138
|
+
job,
|
|
139
|
+
progress,
|
|
140
|
+
result,
|
|
141
|
+
stepResults: null as StepResultEntry[] | null, // Will be set below
|
|
142
|
+
currentStepIndex,
|
|
143
|
+
options: null as RunOptions | null, // Will be set below
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Provide saveProgress injectable
|
|
147
|
+
await container.provide(
|
|
148
|
+
saveJobProgress,
|
|
149
|
+
this.createSaveProgressFn(progressContext),
|
|
150
|
+
)
|
|
94
151
|
|
|
95
152
|
const jobDependencyContext = await container.createContext(job.dependencies)
|
|
96
153
|
const jobData = job.options.data
|
|
@@ -116,6 +173,10 @@ export class JobRunner<
|
|
|
116
173
|
...rest,
|
|
117
174
|
} satisfies JobRunnerRunOptions
|
|
118
175
|
|
|
176
|
+
// Update mutable context references
|
|
177
|
+
progressContext.stepResults = stepResults
|
|
178
|
+
progressContext.options = runOptions
|
|
179
|
+
|
|
119
180
|
for (
|
|
120
181
|
let stepIndex = currentStepIndex;
|
|
121
182
|
stepIndex < steps.length;
|
|
@@ -279,6 +340,51 @@ export class ApplicationWorkerJobRunner extends JobRunner<
|
|
|
279
340
|
super(runtime)
|
|
280
341
|
}
|
|
281
342
|
|
|
343
|
+
protected createJobInfo(
|
|
344
|
+
job: AnyJob,
|
|
345
|
+
options: Partial<JobRunnerRunOptions & { queueJob: Job }>,
|
|
346
|
+
): JobExecutionContext {
|
|
347
|
+
const { queueJob } = options
|
|
348
|
+
return {
|
|
349
|
+
name: job.options.name,
|
|
350
|
+
id: queueJob?.id,
|
|
351
|
+
queue: queueJob?.queueName,
|
|
352
|
+
attempts: queueJob?.attemptsMade,
|
|
353
|
+
stepIndex: options.currentStepIndex,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected createSaveProgressFn(
|
|
358
|
+
context: SaveProgressContext<JobRunnerRunOptions & { queueJob: Job }>,
|
|
359
|
+
): () => Promise<void> {
|
|
360
|
+
return async () => {
|
|
361
|
+
const { job, progress, result, stepResults, options } = context
|
|
362
|
+
if (!options || !stepResults) return
|
|
363
|
+
|
|
364
|
+
const { queueJob } = options
|
|
365
|
+
|
|
366
|
+
// Find current step index based on completed steps
|
|
367
|
+
let currentStepIndex = 0
|
|
368
|
+
for (let i = 0; i < stepResults.length; i++) {
|
|
369
|
+
if (stepResults[i]) currentStepIndex = i + 1
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Encode progress before persisting if schema is defined
|
|
373
|
+
const encodedProgress = job.progress
|
|
374
|
+
? job.progress.encode(progress)
|
|
375
|
+
: progress
|
|
376
|
+
|
|
377
|
+
const checkpoint: JobProgressCheckpoint = {
|
|
378
|
+
stepIndex: currentStepIndex,
|
|
379
|
+
result,
|
|
380
|
+
stepResults,
|
|
381
|
+
progress: encodedProgress,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await queueJob.updateProgress(checkpoint)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
282
388
|
protected async afterStep(
|
|
283
389
|
params: JobRunnerRunAfterStepParams<
|
|
284
390
|
JobRunnerRunOptions & { queueJob: Job }
|
|
@@ -295,26 +401,25 @@ export class ApplicationWorkerJobRunner extends JobRunner<
|
|
|
295
401
|
options: { queueJob, progress },
|
|
296
402
|
} = params
|
|
297
403
|
const nextStepIndex = stepIndex + 1
|
|
298
|
-
const totalSteps = job.steps.length
|
|
299
|
-
const percentage = Math.round((nextStepIndex / totalSteps) * 100)
|
|
300
404
|
|
|
301
405
|
// Encode progress before persisting if schema is defined
|
|
302
406
|
const encodedProgress = job.progress
|
|
303
407
|
? job.progress.encode(progress)
|
|
304
408
|
: progress
|
|
305
409
|
|
|
410
|
+
const checkpoint: JobProgressCheckpoint = {
|
|
411
|
+
stepIndex: nextStepIndex,
|
|
412
|
+
stepLabel: step.label,
|
|
413
|
+
result,
|
|
414
|
+
stepResults,
|
|
415
|
+
progress: encodedProgress,
|
|
416
|
+
}
|
|
417
|
+
|
|
306
418
|
await Promise.all([
|
|
307
419
|
queueJob.log(
|
|
308
420
|
`Step ${step.label || nextStepIndex} completed in ${(stepResult.duration / 1000).toFixed(3)}s`,
|
|
309
421
|
),
|
|
310
|
-
queueJob.updateProgress(
|
|
311
|
-
stepIndex: nextStepIndex,
|
|
312
|
-
stepLabel: step.label,
|
|
313
|
-
result,
|
|
314
|
-
stepResults,
|
|
315
|
-
progress: encodedProgress,
|
|
316
|
-
percentage,
|
|
317
|
-
}),
|
|
422
|
+
queueJob.updateProgress(checkpoint),
|
|
318
423
|
])
|
|
319
424
|
}
|
|
320
425
|
}
|