Kysely, Adapter-Aware Workers, and Hot Reloading
What's Changed
New Features
Kysely Adapter
@boringnode/queue now includes a Kysely adapter for PostgreSQL, MySQL, and SQLite.
The adapter uses an application-owned Kysely instance and supports the same queue and schedule primitives as the other persistent adapters, including delayed jobs, priorities, retries, retention, deduplication, and scheduled jobs.
import {
kysely,
KyselyQueueSchemaService,
type QueueDatabase,
} from '@boringnode/queue/drivers/kysely_adapter'
interface Database extends QueueDatabase {
orders: OrderTable
}
const adapter = kysely<Database>(db, { dialect: 'postgres' })KyselyQueueSchemaService can create and remove the required tables:
const schema = new KyselyQueueSchemaService(db, { dialect: 'postgres' })
await schema.createJobsTable()
await schema.createSchedulesTable()The adapter never destroys the supplied Kysely instance. Custom job and schedule table names can be configured with tableName and schedulesTableName.
Adapter-Aware Workers and Schedules
Workers can now listen on a specific registered Adapter with worker.adapter. When omitted, the worker continues to use the queue manager's default Adapter.
const config = {
default: 'redis',
adapters: {
redis: redis(redisConfig),
database: knex(databaseConfig),
},
worker: {
adapter: 'database',
concurrency: 5,
},
}
const worker = new Worker(config)
await worker.start(['default', 'emails'])This makes it possible to run separate workers for queues stored by different Adapters.
Schedules can also select their owning Adapter with .with():
await CleanupJob.schedule({ days: 30 })
.id('daily-cleanup')
.with('redis')
.cron('0 0 * * *')Schedule.find() and Schedule.list() accept an Adapter selector when accessing schedules outside the default Adapter:
const schedule = await Schedule.find('daily-cleanup', { adapter: 'redis' })
const schedules = await Schedule.list({ status: 'active' }, { adapter: 'redis' })A returned Schedule retains the selected Adapter for subsequent pause(), resume(), delete(), and trigger() calls. Jobs dispatched by a schedule stay on the Adapter that owns that schedule.
Scheduled jobs now also include their originating schedule ID in JobData.scheduleId. Jobs can access it through this.context.scheduleId:
async execute() {
console.log(this.context.scheduleId)
}The value is undefined for jobs that were not dispatched by a schedule.
Hot Reloading Jobs
Workers can now execute the latest saved version of a job without restarting during development.
Enable hotReload when initializing the queue manager:
await QueueManager.init({
default: 'redis',
adapters: {
redis: redis({ host: 'localhost', port: 6379 }),
},
locations: ['./app/jobs/**/*.ts'],
hotReload: process.env.NODE_ENV === 'development',
})Hot reload integrates with Hot Hook. The queue provides the dynamic import boundary, while the application remains responsible for installing and initializing Hot Hook.
AdonisJS applications can use node ace serve --hmr. Standalone worker processes must initialize Hot Hook themselves.
Locator.registerFromGlob() also accepts the option directly:
await Locator.registerFromGlob(['./app/jobs/**/*.ts'], { hotReload: true })Runtime Improvements
Consistent Job Dispatch and Execution
All job dispatch paths now apply the same routing and job options. This includes dispatch(), dispatchMany(), manual schedule triggers, and schedules claimed by workers.
Routing is now resolved consistently in the following order:
- Fluent overrides such as
.toQueue()and.with() - Static
Job.options - The Adapter configured for the selected queue
- The queue manager's default Adapter
Queue, Adapter, priority, custom job name, creation timestamp, and schedule provenance are therefore preserved consistently regardless of how a job is dispatched.
Static job options are resolved when the fluent builder runs, so changes made between builder creation and execution are applied.
The Sync adapter and Worker execution paths now also share the same job lifecycle behavior, including:
- context construction;
- dependency injection through
jobFactory; - execution wrappers;
- timeouts;
- retries;
- failed hooks;
- tracing.
Bug Fixes
Scheduled Jobs Now Honor Static Queue Options
Fixed an issue where Job.schedule() ignored Job.options.queue, causing scheduled jobs to land on the default queue while regular dispatches correctly used the configured queue.
Scheduled jobs now use the same queue and Adapter resolution rules as every other dispatch path.
See #21.
Redis Connections from Duplicate ioredis Copies
Fixed Redis connection detection when the application and @boringnode/queue resolve different copies of the ioredis package.
The adapter previously relied on instanceof Redis. With package managers such as pnpm, a valid application-owned connection could fail that check and be mistaken for a configuration object. Its connection settings were then silently discarded, causing the queue to connect to the default localhost:6379 instance instead.
Redis connections are now detected by their interface, allowing the supplied connection to be reused across package boundaries.
See #20.
Upgrade Notes
Workers and Adapter Routing
Start a Worker for every Adapter that owns queues or schedules. A Worker only claims schedules and jobs from its configured Adapter.
When a schedule does not call .with(), its Adapter is resolved from:
- the job's
adapteroption; - the Adapter configured for the job's queue;
- the queue manager's default Adapter.
An explicit .with() always takes precedence.
A job routed to a queue with queues.<name>.adapter now uses that Adapter when neither .with() nor Job.options.adapter selects another one. Previously, some dispatch paths could incorrectly fall back to the queue manager's default Adapter.
Verify that a Worker is running for every Adapter referenced by queue configuration.
Hot Reload
Hot reload is disabled by default and should only be enabled in development.
Only jobs discovered from locations or registered with Locator.registerFromGlob() can be reloaded. Jobs registered manually with Locator.register() do not have a module path to reload.
Changes to the set of registered jobs still require a restart. This includes adding, deleting, moving, or renaming a job, as well as changing its configured name.
A job that is already running keeps its current implementation. The next execution receives the updated version.
Avoid import-time side effects in hot-reloaded job modules, since their module code can execute again after an invalidation.
Commits Since v0.6.0
fe122cdfeat: support hot reloading jobsb88f4c9refactor: deepen job execution runtimed437182refactor: concentrate job dispatch paths757055adocs: update changelog for latest runtime changes207d641feat: add Kysely queue adapter591dafdci: add MySQL service to checksf86e54efix(redis): support connections from duplicate ioredis copies
Full Changelog: v0.6.0...v0.7.0