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.
@@ -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 JobItem<T extends AnyJob = AnyJob> = Pick<
68
- Job<T['_']['input'], T['_']['output'], T['options']['name']>,
69
- | 'queueName'
70
- | 'priority'
71
- | 'progress'
72
- | 'name'
73
- | 'data'
74
- | 'returnvalue'
75
- | 'attemptsMade'
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; state?: JobState[] },
80
+ options?: { page?: number; limit?: number; status?: JobStatus[] },
86
81
  ): Promise<{
87
- items: JobItem<T>[]
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<JobItem<T> | null>
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
- state = [],
197
- }: { page?: number; limit?: number; state?: JobType[] },
203
+ status = [],
204
+ }: { page?: number; limit?: number; status?: JobStatus[] },
198
205
  ) {
199
206
  const { queue } = this.getJobQueue(job)
200
- const jobsCount = await queue.getJobCountByTypes(...state)
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
- state,
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<JobItem> {
311
- const status = await bullJob.getState()
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
- returnvalue: OptionalType<T['output']>
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
- state: t.array(t.string()).optional(),
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
- returnvalue: job.output.optional(),
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
- state: input.state,
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
- state: input.state as JobState[],
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 = ['list', 'get', 'add', 'retry', 'cancel', 'remove'] as const
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 { jobAbortSignal } from '../injectables.ts'
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 interface StepResultEntry {
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
  }
@@ -28,6 +28,7 @@ export interface JobStep<
28
28
  Data = any,
29
29
  > extends Dependant {
30
30
  [kJobStepKey]: any
31
+ label?: string
31
32
  input: Input
32
33
  output: Output
33
34
  dependencies: Deps