Skip to content

Commit ba545d8

Browse files
committed
feat: expose explicit job loading
1 parent 69172be commit ba545d8

3 files changed

Lines changed: 105 additions & 10 deletions

File tree

‎src/queue_manager.ts‎

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { QueueConfigResolver } from './queue_config_resolver.js'
77
import type { Adapter } from './contracts/adapter.js'
88
import type { AdapterFactory, JobFactory, QueueManagerConfig } from './types/main.js'
99

10-
const noopInternalOperationWrapper: NonNullable<QueueManagerConfig['internalOperationWrapper']> = async (fn) => fn()
10+
const noopInternalOperationWrapper: NonNullable<
11+
QueueManagerConfig['internalOperationWrapper']
12+
> = async (fn) => fn()
1113
const noopExecutionWrapper: NonNullable<QueueManagerConfig['executionWrapper']> = async (fn) => fn()
1214

1315
type QueueManagerFakeState = {
@@ -19,6 +21,7 @@ type QueueManagerFakeState = {
1921
internalOperationWrapper?: QueueManagerConfig['internalOperationWrapper']
2022
executionWrapper?: QueueManagerConfig['executionWrapper']
2123
configResolver: QueueConfigResolver
24+
locations: string[]
2225
fakeAdapter: FakeAdapter
2326
}
2427

@@ -63,6 +66,7 @@ class QueueManagerSingleton {
6366
#internalOperationWrapper?: QueueManagerConfig['internalOperationWrapper']
6467
#executionWrapper?: QueueManagerConfig['executionWrapper']
6568
#configResolver: QueueConfigResolver = new QueueConfigResolver({})
69+
#locations: string[] = []
6670
#fakeState?: QueueManagerFakeState
6771

6872
/**
@@ -103,23 +107,52 @@ class QueueManagerSingleton {
103107
this.#internalOperationWrapper = config.internalOperationWrapper
104108
this.#executionWrapper = config.executionWrapper
105109
this.#configResolver = QueueConfigResolver.from(config)
110+
this.#locations = config.locations ?? []
106111

107-
if (config.locations && config.locations.length > 0) {
108-
const registered = await Locator.registerFromGlob(config.locations)
109-
110-
if (registered === 0) {
111-
this.#logger.warn(
112-
`No jobs found for locations: ${config.locations.join(', ')}. ` +
113-
'Verify your glob patterns match your job files.'
114-
)
115-
}
112+
if (config.autoLoadJobs ?? true) {
113+
await this.loadJobs()
116114
}
117115

118116
this.#initialized = true
119117

120118
return this
121119
}
122120

121+
/**
122+
* Load and register job classes from configured or explicit locations.
123+
*
124+
* This low-level API is useful for framework integrations that need to
125+
* register jobs at a precise moment in their command lifecycle.
126+
*
127+
* @param locations - Optional glob patterns. Defaults to the configured locations.
128+
* @returns Number of jobs successfully registered.
129+
*
130+
* @example
131+
* ```typescript
132+
* await QueueManager.init(config)
133+
* await QueueManager.loadJobs()
134+
*
135+
* // Or with explicit locations
136+
* await QueueManager.loadJobs(['./app/jobs/**\/*.js'])
137+
* ```
138+
*/
139+
async loadJobs(locations: string[] = this.#locations): Promise<number> {
140+
if (locations.length === 0) {
141+
return 0
142+
}
143+
144+
const registered = await Locator.registerFromGlob(locations)
145+
146+
if (registered === 0) {
147+
this.#logger.warn(
148+
`No jobs found for locations: ${locations.join(', ')}. ` +
149+
'Verify your glob patterns match your job files.'
150+
)
151+
}
152+
153+
return registered
154+
}
155+
123156
/**
124157
* Destroy any materialized adapters from the current configuration before
125158
* replacing it with a new one.
@@ -254,6 +287,7 @@ class QueueManagerSingleton {
254287
internalOperationWrapper: this.#internalOperationWrapper,
255288
executionWrapper: this.#executionWrapper,
256289
configResolver: this.#configResolver,
290+
locations: this.#locations,
257291
fakeAdapter,
258292
}
259293

@@ -295,6 +329,7 @@ class QueueManagerSingleton {
295329
this.#internalOperationWrapper = state.internalOperationWrapper
296330
this.#executionWrapper = state.executionWrapper
297331
this.#configResolver = state.configResolver
332+
this.#locations = state.locations
298333
}
299334

300335
/**
@@ -399,6 +434,7 @@ class QueueManagerSingleton {
399434
this.#internalOperationWrapper = undefined
400435
this.#executionWrapper = undefined
401436
this.#configResolver = new QueueConfigResolver({})
437+
this.#locations = []
402438
this.#fakeState = undefined
403439
}
404440
}

‎src/types/main.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,16 @@ export interface QueueManagerConfig {
494494
queues?: Record<string, QueueConfig>
495495
worker?: WorkerConfig
496496
locations?: string[]
497+
498+
/**
499+
* Whether `init()` should immediately register jobs from configured locations.
500+
*
501+
* Framework integrations may disable this to defer job loading until a
502+
* command lifecycle is ready, then call `QueueManager.loadJobs()`.
503+
*
504+
* @default true
505+
*/
506+
autoLoadJobs?: boolean
497507
logger?: Logger
498508

499509
/**

‎tests/queue_manager.spec.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as errors from '../src/exceptions.js'
33
import { QueueManager } from '../src/queue_manager.js'
44
import { sync } from '../src/drivers/sync_adapter.js'
55
import { MemoryLogger } from './_mocks/memory_logger.js'
6+
import { Locator } from '../src/locator.js'
7+
import SendEmailJob from '../examples/jobs/send_email_job.js'
68
import type { Adapter } from '../src/contracts/adapter.js'
79

810
test.group('QueueManager', () => {
@@ -150,6 +152,53 @@ test.group('QueueManager', () => {
150152
assert.include(logger.logs[0].message, 'No jobs found for locations')
151153
})
152154

155+
test('should load jobs from configured locations', async ({ assert, cleanup }) => {
156+
cleanup(() => Locator.clear())
157+
158+
await QueueManager.init({
159+
default: 'sync',
160+
adapters: { sync: sync() },
161+
locations: ['./examples/jobs/*.ts'],
162+
})
163+
164+
Locator.clear()
165+
166+
const registered = await QueueManager.loadJobs()
167+
168+
assert.equal(registered, 3)
169+
assert.equal(Locator.get('SendEmailJob'), SendEmailJob)
170+
})
171+
172+
test('should keep configured locations when automatic job loading is disabled', async ({
173+
assert,
174+
cleanup,
175+
}) => {
176+
cleanup(() => Locator.clear())
177+
178+
await QueueManager.init({
179+
default: 'sync',
180+
adapters: { sync: sync() },
181+
locations: ['./examples/jobs/*.ts'],
182+
autoLoadJobs: false,
183+
})
184+
185+
assert.isUndefined(Locator.get('SendEmailJob'))
186+
187+
const registered = await QueueManager.loadJobs()
188+
189+
assert.equal(registered, 3)
190+
assert.equal(Locator.get('SendEmailJob'), SendEmailJob)
191+
})
192+
193+
test('should load jobs from explicit locations', async ({ assert, cleanup }) => {
194+
cleanup(() => Locator.clear())
195+
196+
const registered = await QueueManager.loadJobs(['./examples/jobs/*.ts'])
197+
198+
assert.equal(registered, 3)
199+
assert.equal(Locator.get('SendEmailJob'), SendEmailJob)
200+
})
201+
153202
test('should fake adapters and restore them', async ({ assert }) => {
154203
await QueueManager.init({
155204
default: 'sync',

0 commit comments

Comments
 (0)