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.
@@ -13,10 +13,10 @@ type DefaultObjectType = ObjectType<{}>
13
13
 
14
14
  type DefaultResultType = Record<string, any>
15
15
 
16
- export type AnyJobOptions = JobOptions<string, any, any, any, any>
16
+ export type AnyJobOptions = JobOptions<string, any, any, any, any, any>
17
17
 
18
18
  export interface AnyJob {
19
- _: { data: any; result: any; input: any; output: any }
19
+ _: { data: any; result: any; input: any; output: any; progress: any }
20
20
  [kJobKey]: true
21
21
  options: AnyJobOptions
22
22
  steps: readonly AnyJobStep[]
@@ -25,10 +25,11 @@ export interface AnyJob {
25
25
  dependencies: Dependencies
26
26
  input: AnyObjectLikeType
27
27
  output: AnyObjectLikeType
28
- afterEachHandler?: JobAfterEachHandler<any, any, any, any>
29
- beforeEachHandler?: JobBeforeEachHandler<any, any, any, any>
30
- onErrorHandler?: JobOnErrorHandler<any, any, any, any>
31
- returnHandler?: JobReturnHandler<any, any, any, any, any>
28
+ progress: AnyObjectLikeType
29
+ afterEachHandler?: JobAfterEachHandler<any, any, any, any, any>
30
+ beforeEachHandler?: JobBeforeEachHandler<any, any, any, any, any>
31
+ onErrorHandler?: JobOnErrorHandler<any, any, any, any, any>
32
+ returnHandler?: JobReturnHandler<any, any, any, any, any, any>
32
33
  }
33
34
 
34
35
  export type JobBackoffOptions = {
@@ -42,11 +43,13 @@ export type JobCondition<
42
43
  Result extends DefaultResultType = {},
43
44
  Data = any,
44
45
  Input extends AnyObjectLikeType = AnyObjectLikeType,
46
+ Progress extends AnyObjectLikeType = AnyObjectLikeType,
45
47
  > = (params: {
46
48
  context: DependencyContext<Deps>
47
49
  data: Data
48
50
  input: t.infer.decode.output<Input>
49
51
  result: Result
52
+ progress: t.infer.decode.output<Progress>
50
53
  }) => MaybePromise<boolean>
51
54
 
52
55
  export type JobReturnHandler<
@@ -55,20 +58,24 @@ export type JobReturnHandler<
55
58
  Input extends AnyObjectLikeType,
56
59
  Output extends AnyObjectLikeType,
57
60
  Data,
61
+ Progress extends AnyObjectLikeType = AnyObjectLikeType,
58
62
  > = (params: {
59
63
  context: DependencyContext<Deps>
60
64
  data: Data
61
65
  input: t.infer.decode.output<Input>
62
66
  result: Result
67
+ progress: t.infer.decode.output<Progress>
63
68
  }) => MaybePromise<t.infer.encode.input<Output>>
64
69
 
65
70
  export type JobDataHandler<
66
71
  Deps extends Dependencies,
67
72
  Input extends AnyObjectLikeType,
68
73
  Data,
74
+ Progress extends AnyObjectLikeType = AnyObjectLikeType,
69
75
  > = (
70
76
  ctx: DependencyContext<Deps>,
71
77
  input: t.infer.decode.output<Input>,
78
+ progress: t.infer.decode.output<Progress>,
72
79
  ) => MaybePromise<Data>
73
80
 
74
81
  export type JobAfterEachHandler<
@@ -76,11 +83,13 @@ export type JobAfterEachHandler<
76
83
  Result extends DefaultResultType,
77
84
  Input extends AnyObjectLikeType,
78
85
  Data,
86
+ Progress extends AnyObjectLikeType = AnyObjectLikeType,
79
87
  > = (params: {
80
88
  context: DependencyContext<Deps>
81
89
  data: Data
82
90
  input: t.infer.decode.output<Input>
83
91
  result: Result
92
+ progress: t.infer.decode.output<Progress>
84
93
  step: AnyJobStep
85
94
  stepIndex: number
86
95
  }) => MaybePromise<void>
@@ -90,11 +99,13 @@ export type JobBeforeEachHandler<
90
99
  Result extends DefaultResultType,
91
100
  Input extends AnyObjectLikeType,
92
101
  Data,
102
+ Progress extends AnyObjectLikeType = AnyObjectLikeType,
93
103
  > = (params: {
94
104
  context: DependencyContext<Deps>
95
105
  data: Data
96
106
  input: t.infer.decode.output<Input>
97
107
  result: Result
108
+ progress: t.infer.decode.output<Progress>
98
109
  step: AnyJobStep
99
110
  stepIndex: number
100
111
  }) => MaybePromise<void>
@@ -104,11 +115,13 @@ export type JobOnErrorHandler<
104
115
  Result extends DefaultResultType,
105
116
  Input extends AnyObjectLikeType,
106
117
  Data,
118
+ Progress extends AnyObjectLikeType = AnyObjectLikeType,
107
119
  > = (params: {
108
120
  context: DependencyContext<Deps>
109
121
  data: Data
110
122
  input: t.infer.decode.output<Input>
111
123
  result: Result
124
+ progress: t.infer.decode.output<Progress>
112
125
  step: AnyJobStep
113
126
  stepIndex: number
114
127
  error: unknown
@@ -122,6 +135,7 @@ export interface JobOptions<
122
135
  Name extends string = string,
123
136
  Input extends AnyObjectLikeType = AnyObjectLikeType,
124
137
  Output extends AnyObjectLikeType = AnyObjectLikeType,
138
+ Progress extends AnyObjectLikeType = DefaultObjectType,
125
139
  Deps extends Dependencies = {},
126
140
  Data = any,
127
141
  > {
@@ -129,10 +143,11 @@ export interface JobOptions<
129
143
  pool: JobWorkerPool
130
144
  input: Input
131
145
  output: Output
146
+ progress?: Progress
132
147
  concurrency?: number
133
148
  timeout?: number
134
149
  dependencies?: Deps
135
- data?: JobDataHandler<Deps, Input, Data>
150
+ data?: JobDataHandler<Deps, Input, Data, Progress>
136
151
  attempts?: number
137
152
  backoff?: JobBackoffOptions
138
153
  oneoff?: boolean
@@ -144,6 +159,7 @@ export class Job<
144
159
  in out Data = any,
145
160
  in out Input extends AnyObjectLikeType = DefaultObjectType,
146
161
  in out Output extends AnyObjectLikeType = DefaultObjectType,
162
+ in out Progress extends AnyObjectLikeType = DefaultObjectType,
147
163
  out Steps extends [...AnyJobStep[]] = [],
148
164
  in out Result extends DefaultResultType = {},
149
165
  out Return extends boolean = false,
@@ -154,28 +170,43 @@ export class Job<
154
170
  result: Result & t.infer.decode.output<Input>
155
171
  input: t.infer.decode.output<Input>
156
172
  output: t.infer.decode.output<Output>
173
+ progress: t.infer.decode.output<Progress>
157
174
  };
158
175
  [kJobKey] = true as const
159
176
  steps: Steps = [] as unknown as Steps
160
- conditions: Map<number, JobCondition<any, any, any, any>> = new Map()
177
+ conditions: Map<number, JobCondition<any, any, any, any, any>> = new Map()
161
178
  returnHandler?: JobReturnHandler<
162
179
  Deps,
163
180
  this['_']['result'],
164
181
  Input,
165
182
  Output,
166
- Data
183
+ Data,
184
+ Progress
185
+ >
186
+ afterEachHandler?: JobAfterEachHandler<
187
+ Deps,
188
+ this['_']['result'],
189
+ Input,
190
+ Data,
191
+ Progress
167
192
  >
168
- afterEachHandler?: JobAfterEachHandler<Deps, this['_']['result'], Input, Data>
169
193
  beforeEachHandler?: JobBeforeEachHandler<
170
194
  Deps,
171
195
  this['_']['result'],
172
196
  Input,
173
- Data
197
+ Data,
198
+ Progress
199
+ >
200
+ onErrorHandler?: JobOnErrorHandler<
201
+ Deps,
202
+ this['_']['result'],
203
+ Input,
204
+ Data,
205
+ Progress
174
206
  >
175
- onErrorHandler?: JobOnErrorHandler<Deps, this['_']['result'], Input, Data>
176
207
 
177
208
  constructor(
178
- public options: JobOptions<Name, Input, Output, Deps, Data>,
209
+ public options: JobOptions<Name, Input, Output, Progress, Deps, Data>,
179
210
  public stack?: string,
180
211
  ) {}
181
212
 
@@ -195,6 +226,10 @@ export class Job<
195
226
  return this.options.output as Output
196
227
  }
197
228
 
229
+ get progress(): Progress {
230
+ return this.options.progress as Progress
231
+ }
232
+
198
233
  step<
199
234
  StepInput extends AnyObjectLikeType,
200
235
  StepOutput extends AnyObjectLikeType,
@@ -233,6 +268,7 @@ export class Job<
233
268
  Data,
234
269
  Input,
235
270
  Output,
271
+ Progress,
236
272
  [...Steps, JobStep<StepInput, StepOutput, StepDeps, StepResult, any>],
237
273
  Result &
238
274
  (undefined extends Condition
@@ -250,8 +286,26 @@ export class Job<
250
286
  return(
251
287
  ...[handler]: Return extends false
252
288
  ? Result extends t.infer.encode.input<Output>
253
- ? [JobReturnHandler<Deps, this['_']['result'], Input, Output, Data>?]
254
- : [JobReturnHandler<Deps, this['_']['result'], Input, Output, Data>]
289
+ ? [
290
+ JobReturnHandler<
291
+ Deps,
292
+ this['_']['result'],
293
+ Input,
294
+ Output,
295
+ Data,
296
+ Progress
297
+ >?,
298
+ ]
299
+ : [
300
+ JobReturnHandler<
301
+ Deps,
302
+ this['_']['result'],
303
+ Input,
304
+ Output,
305
+ Data,
306
+ Progress
307
+ >,
308
+ ]
255
309
  : [TSError<'Job already has a return statement'>]
256
310
  ) {
257
311
  if (typeof handler === 'function') {
@@ -265,6 +319,7 @@ export class Job<
265
319
  Data,
266
320
  Input,
267
321
  Output,
322
+ Progress,
268
323
  Steps,
269
324
  Result,
270
325
  true
@@ -273,7 +328,7 @@ export class Job<
273
328
 
274
329
  afterEach(
275
330
  handler: Return extends true
276
- ? JobAfterEachHandler<Deps, this['_']['result'], Input, Data>
331
+ ? JobAfterEachHandler<Deps, this['_']['result'], Input, Data, Progress>
277
332
  : TSError<'Job must have a return statement to use afterEach'>,
278
333
  ) {
279
334
  this.afterEachHandler = handler as any
@@ -282,7 +337,7 @@ export class Job<
282
337
 
283
338
  beforeEach(
284
339
  handler: Return extends true
285
- ? JobBeforeEachHandler<Deps, this['_']['result'], Input, Data>
340
+ ? JobBeforeEachHandler<Deps, this['_']['result'], Input, Data, Progress>
286
341
  : TSError<'Job must have a return statement to use beforeEach'>,
287
342
  ) {
288
343
  this.beforeEachHandler = handler as any
@@ -291,7 +346,7 @@ export class Job<
291
346
 
292
347
  onError(
293
348
  handler: Return extends true
294
- ? JobOnErrorHandler<Deps, this['_']['result'], Input, Data>
349
+ ? JobOnErrorHandler<Deps, this['_']['result'], Input, Data, Progress>
295
350
  : TSError<'Job must have a return statement to use onError'>,
296
351
  ) {
297
352
  this.onErrorHandler = handler as any
@@ -305,9 +360,10 @@ export function createJob<
305
360
  Output extends AnyObjectLikeType,
306
361
  Deps extends Dependencies = {},
307
362
  Data = any,
308
- >(options: JobOptions<Name, Input, Output, Deps, Data>) {
363
+ Progress extends AnyObjectLikeType = DefaultObjectType,
364
+ >(options: JobOptions<Name, Input, Output, Progress, Deps, Data>) {
309
365
  const stack = tryCaptureStackTrace()
310
- return new Job<Name, Deps, Data, Input, Output>(
366
+ return new Job<Name, Deps, Data, Input, Output, Progress>(
311
367
  { dependencies: {} as Deps, ...options },
312
368
  stack,
313
369
  )
@@ -1,8 +1,14 @@
1
+ import assert from 'node:assert'
1
2
  import { randomUUID } from 'node:crypto'
2
3
 
3
- import type { Job, JobType, QueueEventsListener, RedisClient } from 'bullmq'
4
+ import type {
5
+ Job,
6
+ JobState,
7
+ JobType,
8
+ QueueEventsListener,
9
+ RedisClient,
10
+ } from 'bullmq'
4
11
  import { pick } from '@nmtjs/core'
5
- import { t } from '@nmtjs/type'
6
12
  import {
7
13
  Queue,
8
14
  QueueEvents,
@@ -13,10 +19,7 @@ import {
13
19
  import type { ServerStoreConfig } from '../server/config.ts'
14
20
  import type { Store } from '../types.ts'
15
21
  import type { AnyJob, JobBackoffOptions } from './job.ts'
16
- import { JobWorkerPool } from '../enums.ts'
17
22
  import { createStoreClient } from '../store/index.ts'
18
- import { createJob } from './job.ts'
19
- import { createStep } from './step.ts'
20
23
 
21
24
  /**
22
25
  * Get the dedicated BullMQ queue name for a job
@@ -61,9 +64,8 @@ export class QueueJobResult<T extends AnyJob = AnyJob> {
61
64
  }
62
65
  }
63
66
 
64
- export type JobListItem<T extends AnyJob = AnyJob> = Pick<
67
+ export type JobItem<T extends AnyJob = AnyJob> = Pick<
65
68
  Job<T['_']['input'], T['_']['output'], T['options']['name']>,
66
- | 'id'
67
69
  | 'queueName'
68
70
  | 'priority'
69
71
  | 'progress'
@@ -74,25 +76,33 @@ export type JobListItem<T extends AnyJob = AnyJob> = Pick<
74
76
  | 'processedOn'
75
77
  | 'finishedOn'
76
78
  | 'failedReason'
77
- >
79
+ | 'stacktrace'
80
+ > & { id: string; status: JobState | 'unknown' }
78
81
 
79
82
  export interface JobManagerInstance {
80
83
  list<T extends AnyJob>(
81
84
  job: T,
82
- options?: { page?: number; limit?: number; state?: JobType[] },
85
+ options?: { page?: number; limit?: number; state?: JobState[] },
83
86
  ): Promise<{
84
- items: JobListItem<T>[]
87
+ items: JobItem<T>[]
85
88
  page: number
86
89
  limit: number
87
90
  pages: number
88
91
  total: number
89
92
  }>
90
- get<T extends AnyJob>(job: T, id: string): Promise<JobListItem<T> | null>
93
+ get<T extends AnyJob>(job: T, id: string): Promise<JobItem<T> | null>
91
94
  add<T extends AnyJob>(
92
95
  job: T,
93
96
  data: T['_']['input'],
94
97
  options?: QueueJobAddOptions,
95
98
  ): Promise<QueueJobResult<T>>
99
+ retry(
100
+ job: AnyJob,
101
+ id: string,
102
+ options?: { clearState?: boolean },
103
+ ): Promise<void>
104
+ remove(job: AnyJob, id: string): Promise<void>
105
+ cancel(job: AnyJob, id: string): Promise<void>
96
106
  }
97
107
 
98
108
  export type CustomJobsEvents = QueueEventsListener & {
@@ -123,6 +133,9 @@ export class JobManager {
123
133
  list: this.list.bind(this),
124
134
  add: this.add.bind(this),
125
135
  get: this.get.bind(this),
136
+ retry: this.retry.bind(this),
137
+ remove: this.remove.bind(this),
138
+ cancel: this.cancel.bind(this),
126
139
  }
127
140
  }
128
141
 
@@ -192,16 +205,15 @@ export class JobManager {
192
205
  (page - 1) * limit,
193
206
  page * limit - 1,
194
207
  )
195
- const items = jobs.map((job) => this._mapJob(job))
208
+ const items = await Promise.all(jobs.map((job) => this._mapJob(job)))
196
209
  return { items, page, limit, pages: totalPages, total: jobsCount }
197
210
  }
198
211
 
199
212
  async get<T extends AnyJob>(job: T, id: string) {
200
213
  const { queue } = this.getJobQueue(job)
201
- job.steps
202
214
  const bullJob = await queue.getJob(id)
203
215
  if (!bullJob) return null
204
- return this._mapJob(bullJob)
216
+ return await this._mapJob(bullJob)
205
217
  }
206
218
 
207
219
  async add<T extends AnyJob>(
@@ -239,6 +251,30 @@ export class JobManager {
239
251
  return new QueueJobResult({ job, bullJob, events })
240
252
  }
241
253
 
254
+ async retry(job: AnyJob, id: string, options?: { clearState?: boolean }) {
255
+ const { queue } = this.getJobQueue(job)
256
+ const bullJob = await queue.getJob(id)
257
+ if (!bullJob) throw new Error(`Job with id [${id}] not found`)
258
+
259
+ const state = await bullJob.getState()
260
+ // For completed jobs, clear state by default so it reruns from scratch
261
+ // For failed jobs, keep state by default so it resumes from checkpoint
262
+ const shouldClearState = options?.clearState ?? state === 'completed'
263
+
264
+ if (shouldClearState) {
265
+ await bullJob.updateProgress({})
266
+ }
267
+
268
+ await bullJob.retry()
269
+ }
270
+
271
+ async remove(job: AnyJob, id: string) {
272
+ const { queue } = this.getJobQueue(job)
273
+ const bullJob = await queue.getJob(id)
274
+ if (!bullJob) throw new Error(`Job with id [${id}] not found`)
275
+ await bullJob.remove()
276
+ }
277
+
242
278
  async cancel(job: AnyJob, id: string) {
243
279
  const { custom, queue } = this.getJobQueue(job)
244
280
  const bullJob = await queue.getJob(id)
@@ -271,19 +307,26 @@ export class JobManager {
271
307
  return this.getJobQueue(job)
272
308
  }
273
309
 
274
- protected _mapJob(bullJob: Job) {
275
- return pick(bullJob, {
276
- id: true,
277
- queueName: true,
278
- priority: true,
279
- progress: true,
280
- name: true,
281
- data: true,
282
- returnvalue: true,
283
- attemptsMade: true,
284
- processedOn: true,
285
- finishedOn: true,
286
- failedReason: true,
287
- })
310
+ protected async _mapJob(bullJob: Job): Promise<JobItem> {
311
+ const status = await bullJob.getState()
312
+ const id = bullJob.id
313
+ assert(typeof id === 'string', 'Expected job id to be a string')
314
+ 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
+ id,
329
+ status,
330
+ }
288
331
  }
289
332
  }