├── .editorconfig ├── .gitignore ├── .npmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── commands ├── make_job.ts └── queue_listener.ts ├── compose.yml ├── configure.ts ├── index.ts ├── package.json ├── providers └── queue_provider.ts ├── services └── main.ts ├── src ├── define_config.ts ├── job.ts ├── queue.ts └── types │ ├── extended.ts │ └── main.ts ├── stubs ├── command │ └── main.stub ├── config │ └── queue.stub └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [{package.json,.eslintrc,tsconfig.json,*.yml,*.md,*.txt,*.stub}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | README.md 3 | docs 4 | *.md 5 | *.html 6 | config.json 7 | .eslintrc.json 8 | package.json 9 | *.txt 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2022 Romain Lanz, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | @rlanz/bull-queue 3 |

4 | 5 |

6 | Download 7 | Version 8 | License 9 |

10 | 11 | `@rlanz/bull-queue` is a queue system based on [BullMQ](https://github.com/taskforcesh/bullmq) 12 | for [AdonisJS](https://adonisjs.com/). 13 | 14 | > [!NOTE] 15 | > You must have a Redis server running on your machine. 16 | 17 | --- 18 | 19 | ## Getting Started 20 | 21 | This package is available in the npm registry. 22 | 23 | ```bash 24 | node ace add @rlanz/bull-queue 25 | ``` 26 | 27 | ## Usage 28 | 29 | The `queue` service gives you access to the `dispatch` method. 30 | It will dispatch the linked job to the queue with the given payload. 31 | 32 | ```ts 33 | import queue from '@rlanz/bull-queue/services/main'; 34 | 35 | queue.dispatch(RegisterStripeCustomer, {...}); 36 | 37 | // You can also specify options for a specific job 38 | queue.dispatch(RegisterStripeCustomer, {...}, { 39 | queueName: 'stripe', 40 | }); 41 | ``` 42 | 43 | You can create a job by running `node ace make:job {job}`. 44 | This will create the job within your `app/jobs` directory. 45 | 46 | The `handle` method is what gets called when the jobs is processed while 47 | the `rescue` method is called when the max attempts of the job has been reached. 48 | 49 | You can remove the `rescue` method if you want. 50 | 51 | Since the job instance is passed to the constructor, you can easily send notifications with the `rescue` method. See [this page](https://api.docs.bullmq.io/classes/Job.html) for full documentation on the job instance. 52 | 53 | **Example job file:** 54 | 55 | ```ts 56 | // app/jobs/register_stripe_customer.ts 57 | import { Job } from '@rlanz/bull-queue' 58 | 59 | interface RegisterStripeCustomerPayload { 60 | userId: string; 61 | }; 62 | 63 | export default class RegisterStripeCustomer extends Job { 64 | static get $$filepath() { 65 | return import.meta.url 66 | } 67 | 68 | public async handle(payload: RegisterStripeCustomerPayload) { 69 | // ... 70 | } 71 | 72 | /** 73 | * This is an optional method that gets called if it exists when the retries has exceeded and is marked failed. 74 | */ 75 | public async rescue(payload: RegisterStripeCustomerPayload, error: Error) {} 76 | } 77 | ``` 78 | 79 | #### Job Attempts 80 | 81 | By default, all jobs have a retry of 3 and this is set within your `config/queue.ts` under the `jobs` object. 82 | 83 | You can also set the attempts on a call basis by passing the override as shown below: 84 | 85 | ```ts 86 | queue.dispatch(SomeJob, {...}, { attempts: 3 }) 87 | ``` 88 | 89 | #### Delayed retries 90 | 91 | If you need to add delays between retries, you can either set it globally via by adding this to your `config/queue.ts`: 92 | 93 | ```ts 94 | // config/queue.ts 95 | import { defineConfig } from '@rlanz/bull-queue' 96 | 97 | export default defineConfig({ 98 | // ... 99 | 100 | jobs: { 101 | attempts: 3, 102 | backoff: { 103 | type: 'exponential', 104 | delay: 5000, 105 | } 106 | } 107 | }) 108 | ``` 109 | 110 | Or... you can also do it per job: 111 | 112 | ```ts 113 | queue.dispatch(Somejob, {...}, { 114 | attempts: 3, 115 | backoff: { type: 'exponential', delay: 5000 } 116 | }) 117 | ``` 118 | 119 | With that configuration above, BullMQ will first add a 5s delay before the first retry, 20s before the 2nd, and 40s for the 3rd. 120 | 121 | You can visit [this page](https://docs.bullmq.io/guide/retrying-failing-jobs) on further explanation / other retry options. 122 | 123 | #### Running the queue 124 | 125 | Run the queue worker with the following ace command: 126 | 127 | ```bash 128 | node ace queue:listen 129 | 130 | # or 131 | 132 | node ace queue:listen --queue=stripe 133 | 134 | # or 135 | 136 | node ace queue:listen --queue=stripe,cloudflare 137 | ``` 138 | 139 | Once done, you will see the message `Queue processing started`. 140 | -------------------------------------------------------------------------------- /commands/make_job.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import { BaseCommand, args } from '@adonisjs/core/ace' 9 | import { stubsRoot } from '../stubs/index.js' 10 | import type { CommandOptions } from '@adonisjs/core/types/ace' 11 | 12 | export default class MakeJob extends BaseCommand { 13 | static commandName = 'make:job' 14 | static description = 'Make a new dispatch-able job' 15 | static options: CommandOptions = { 16 | allowUnknownFlags: true, 17 | } 18 | 19 | @args.string({ description: 'Name of the job class' }) 20 | declare name: string 21 | 22 | async run() { 23 | const codemods = await this.createCodemods() 24 | 25 | await codemods.makeUsingStub(stubsRoot, 'command/main.stub', { 26 | entity: this.app.generators.createEntity(this.name), 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /commands/queue_listener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | import { BaseCommand, flags } from '@adonisjs/core/ace' 8 | import type { CommandOptions } from '@adonisjs/core/types/ace' 9 | import type { QueueConfig } from '../src/types/main.js' 10 | 11 | export default class QueueListener extends BaseCommand { 12 | static commandName = 'queue:listen' 13 | static description = 'Listen to one or multiple queues' 14 | 15 | @flags.array({ alias: 'q', description: 'The queue(s) to listen on' }) 16 | declare queue: string[] 17 | 18 | static options: CommandOptions = { 19 | startApp: true, 20 | staysAlive: true, 21 | } 22 | 23 | async run() { 24 | const config = this.app.config.get('queue') 25 | const queue = await this.app.container.make('rlanz/queue') 26 | const router = await this.app.container.make('router') 27 | router.commit() 28 | 29 | let shouldListenOn = this.parsed.flags.queue as string[] 30 | 31 | if (!shouldListenOn) { 32 | shouldListenOn = config.queueNames ?? ['default'] 33 | } 34 | 35 | await Promise.all( 36 | shouldListenOn.map((queueName) => 37 | queue.process({ 38 | queueName, 39 | }) 40 | ) 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:7 4 | ports: 5 | - 6379:6379 -------------------------------------------------------------------------------- /configure.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import { stubsRoot } from './stubs/index.js' 9 | import type Configure from '@adonisjs/core/commands/configure' 10 | 11 | export async function configure(command: Configure) { 12 | const codemods = await command.createCodemods() 13 | 14 | // Publish config file 15 | await codemods.makeUsingStub(stubsRoot, 'config/queue.stub', {}) 16 | 17 | // Add environment variables 18 | await codemods.defineEnvVariables({ 19 | QUEUE_REDIS_HOST: '127.0.0.1', 20 | QUEUE_REDIS_PORT: '6379', 21 | QUEUE_REDIS_PASSWORD: '', 22 | }) 23 | 24 | await codemods.defineEnvValidations({ 25 | variables: { 26 | QUEUE_REDIS_HOST: `Env.schema.string({ format: 'host' })`, 27 | QUEUE_REDIS_PORT: 'Env.schema.number()', 28 | QUEUE_REDIS_PASSWORD: 'Env.schema.string.optional()', 29 | }, 30 | leadingComment: 'Variables for @rlanz/bull-queue', 31 | }) 32 | 33 | // Add provider to rc file 34 | await codemods.updateRcFile((rcFile) => { 35 | rcFile.addProvider('@rlanz/bull-queue/queue_provider').addCommand('@rlanz/bull-queue/commands') 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import './src/types/extended.js' 9 | 10 | export { stubsRoot } from './stubs/index.js' 11 | export { configure } from './configure.js' 12 | export { defineConfig } from './src/define_config.js' 13 | export { Job } from './src/job.js' 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rlanz/bull-queue", 3 | "description": "Queue system based on BullMQ for AdonisJS", 4 | "version": "3.1.0", 5 | "engines": { 6 | "node": ">=18.16.0" 7 | }, 8 | "main": "./build/index.js", 9 | "type": "module", 10 | "files": [ 11 | "src", 12 | "build/commands", 13 | "build/providers", 14 | "build/services", 15 | "build/src", 16 | "build/stubs", 17 | "build/index.js", 18 | "build/index.d.ts", 19 | "build/configure.js", 20 | "build/configure.d.ts" 21 | ], 22 | "exports": { 23 | ".": "./build/index.js", 24 | "./commands": "./build/commands/main.js", 25 | "./commands/*": "./build/commands/*.js", 26 | "./services/*": "./build/services/*.js", 27 | "./queue_provider": "./build/providers/queue_provider.js" 28 | }, 29 | "scripts": { 30 | "index:commands": "adonis-kit index build/commands", 31 | "build": "npm run lint && npm run clean && npm run build-only && npm run copyfiles && npm run index:commands", 32 | "build-only": "tsc", 33 | "clean": "del-cli build", 34 | "copyfiles": "copyfiles \"stubs/**/**/*.stub\" build", 35 | "format": "prettier --write .", 36 | "lint": "eslint .", 37 | "release": "npx release-it", 38 | "prepublishOnly": "npm run build" 39 | }, 40 | "devDependencies": { 41 | "@adonisjs/assembler": "^7.7.0", 42 | "@adonisjs/core": "^6.9.1", 43 | "@adonisjs/eslint-config": "^1.3.0", 44 | "@adonisjs/prettier-config": "^1.3.0", 45 | "@adonisjs/tsconfig": "^1.3.0", 46 | "@swc/core": "^1.5.24", 47 | "copyfiles": "^2.4.1", 48 | "del-cli": "^5.1.0", 49 | "eslint": "^8.57.0", 50 | "prettier": "^3.3.0", 51 | "release-it": "^17.3.0", 52 | "ts-node": "^10.9.2", 53 | "typescript": "^5.4.5" 54 | }, 55 | "dependencies": { 56 | "@poppinss/utils": "^6.7.3", 57 | "@sindresorhus/is": "^6.3.1", 58 | "bullmq": "^5.7.14" 59 | }, 60 | "peerDependencies": { 61 | "@adonisjs/core": "^6.5.0" 62 | }, 63 | "author": "Romain Lanz ", 64 | "license": "MIT", 65 | "homepage": "https://github.com/romainlanz/adonis-bull-queue#readme", 66 | "repository": { 67 | "type": "git", 68 | "url": "git+https://github.com/romainlanz/adonis-bull-queue.git" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/romainlanz/adonis-bull-queue/issues" 72 | }, 73 | "keywords": [ 74 | "adonisjs", 75 | "bullmq", 76 | "queue" 77 | ], 78 | "eslintConfig": { 79 | "extends": "@adonisjs/eslint-config/package" 80 | }, 81 | "release-it": { 82 | "git": { 83 | "commitMessage": "chore(release): ${version}", 84 | "tagAnnotation": "v${version}", 85 | "tagName": "v${version}" 86 | }, 87 | "github": { 88 | "release": true, 89 | "releaseName": "v${version}", 90 | "web": true 91 | } 92 | }, 93 | "prettier": "@adonisjs/prettier-config", 94 | "publishConfig": { 95 | "tag": "latest", 96 | "access": "public" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /providers/queue_provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import type { ApplicationService } from '@adonisjs/core/types' 9 | import type { QueueConfig } from '../src/types/main.js' 10 | import type { QueueManager } from '../src/queue.js' 11 | 12 | export default class QueueProvider { 13 | #queue: QueueManager | null = null 14 | 15 | constructor(protected app: ApplicationService) {} 16 | 17 | register() { 18 | this.app.container.singleton('rlanz/queue', async () => { 19 | const { QueueManager } = await import('../src/queue.js') 20 | 21 | const config = this.app.config.get('queue') 22 | const logger = await this.app.container.make('logger') 23 | 24 | this.#queue = new QueueManager(config, logger, this.app) 25 | 26 | return this.#queue 27 | }) 28 | } 29 | 30 | async shutdown() { 31 | if (this.#queue) { 32 | await this.#queue.closeAll() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /services/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import app from '@adonisjs/core/services/app' 9 | import { QueueManager } from '../src/queue.js' 10 | 11 | let queue: QueueManager 12 | 13 | await app.booted(async () => { 14 | queue = await app.container.make('rlanz/queue') 15 | }) 16 | 17 | export { queue as default } 18 | -------------------------------------------------------------------------------- /src/define_config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import { InvalidArgumentsException } from '@poppinss/utils' 9 | import type { QueueConfig } from './types/main.js' 10 | 11 | export function defineConfig(config: T): T { 12 | if (!config) { 13 | throw new InvalidArgumentsException('Invalid config. It must be a valid object') 14 | } 15 | 16 | if (!config.defaultConnection) { 17 | throw new InvalidArgumentsException( 18 | 'Invalid config. Missing property "defaultConnection" inside it' 19 | ) 20 | } 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /src/job.ts: -------------------------------------------------------------------------------- 1 | import { Job as BullMQJob } from 'bullmq' 2 | import type { LoggerService } from '@adonisjs/core/types' 3 | 4 | interface InternalToInject { 5 | job: BullMQJob 6 | logger: LoggerService 7 | } 8 | 9 | export abstract class Job { 10 | declare logger: LoggerService 11 | #bullMqJob!: BullMQJob 12 | #injected = false 13 | 14 | $injectInternal(internals: InternalToInject) { 15 | if (this.#injected) { 16 | return 17 | } 18 | 19 | this.#bullMqJob = internals.job 20 | this.logger = internals.logger 21 | 22 | this.#injected = true 23 | } 24 | 25 | getJob(): BullMQJob { 26 | return this.#bullMqJob 27 | } 28 | 29 | getId(): string | undefined { 30 | return this.#bullMqJob.id 31 | } 32 | 33 | getDelay(): number | undefined { 34 | return this.#bullMqJob.delay 35 | } 36 | 37 | getAttempts(): number { 38 | return this.#bullMqJob.attemptsMade 39 | } 40 | 41 | getFailedReason(): string | undefined { 42 | return this.#bullMqJob.failedReason 43 | } 44 | 45 | abstract handle(payload: unknown): Promise 46 | abstract rescue(payload: unknown, error: Error): Promise 47 | } 48 | -------------------------------------------------------------------------------- /src/queue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import { isClass } from '@sindresorhus/is' 9 | import { Queue, QueueOptions, Worker, WorkerOptions } from 'bullmq' 10 | import { RuntimeException } from '@poppinss/utils' 11 | import { Job } from './job.js' 12 | import type { Job as BullMQJob, JobsOptions } from 'bullmq' 13 | import type { ApplicationService, LoggerService } from '@adonisjs/core/types' 14 | import type { 15 | AllowedJobTypes, 16 | InferJobPayload, 17 | JobHandlerConstructor, 18 | QueueConfig, 19 | } from './types/main.js' 20 | 21 | export class QueueManager { 22 | #app: ApplicationService 23 | #logger: LoggerService 24 | #options: QueueConfig 25 | 26 | #queues: Map = new Map() 27 | 28 | constructor(options: QueueConfig, logger: LoggerService, app: ApplicationService) { 29 | this.#options = options 30 | this.#logger = logger 31 | this.#app = app 32 | 33 | const computedConfig = { 34 | ...this.#options.queue, 35 | } 36 | 37 | if (typeof computedConfig.connection === 'undefined') { 38 | computedConfig.connection = this.#options.defaultConnection 39 | } 40 | 41 | // Define the default queue 42 | this.#queues.set('default', new Queue('default', computedConfig as QueueOptions)) 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | async #resolveJob(job: AllowedJobTypes): Promise { 49 | if (isClass(job)) { 50 | return job 51 | } 52 | 53 | const jobClass = await job() 54 | return jobClass['default'] 55 | } 56 | 57 | #getJobPath(job: JobHandlerConstructor): string { 58 | if (!job['$$filepath'] || typeof job['$$filepath'] !== 'string') { 59 | throw new RuntimeException('Job handler is missing the $$filepath property') 60 | } 61 | 62 | return job['$$filepath'] 63 | } 64 | 65 | #maybeAddQueue(queueName = 'default') { 66 | const computedConfig = { 67 | ...this.#options.queue, 68 | } 69 | 70 | if (typeof computedConfig.connection === 'undefined') { 71 | computedConfig.connection = this.#options.defaultConnection 72 | } 73 | 74 | if (!this.#queues.has(queueName)) { 75 | this.#queues.set(queueName, new Queue(queueName, computedConfig as QueueOptions)) 76 | } 77 | 78 | return this.#queues.get(queueName)! 79 | } 80 | 81 | async #instantiateJob(job: BullMQJob) { 82 | const { default: jobClass } = await import(job.name) 83 | const jobClassInstance = await this.#app.container.make(jobClass) 84 | jobClassInstance.$injectInternal({ job, logger: this.#logger }) 85 | 86 | return jobClassInstance 87 | } 88 | 89 | async dispatch( 90 | job: Job, 91 | payload: Job extends JobHandlerConstructor 92 | ? InferJobPayload 93 | : Job extends Promise 94 | ? A extends { default: JobHandlerConstructor } 95 | ? InferJobPayload 96 | : never 97 | : never, 98 | options: JobsOptions & { queueName?: string } = {} 99 | ) { 100 | const queueName = options.queueName || 'default' 101 | const queue = this.#maybeAddQueue(queueName) 102 | 103 | const jobClass = await this.#resolveJob(job) 104 | const jobPath = this.#getJobPath(jobClass) 105 | 106 | return queue.add(jobPath, payload, { 107 | ...this.#options.jobs, 108 | ...options, 109 | }) 110 | } 111 | 112 | process({ queueName }: { queueName?: string }) { 113 | this.#logger.info(`Queue [${queueName || 'default'}] processing started...`) 114 | 115 | const computedConfig = { 116 | ...this.#options.worker, 117 | } 118 | 119 | if (typeof computedConfig.connection === 'undefined') { 120 | computedConfig.connection = this.#options.defaultConnection 121 | } 122 | 123 | let worker = new Worker( 124 | queueName || 'default', 125 | async (job) => { 126 | let jobClassInstance: Job 127 | 128 | try { 129 | jobClassInstance = await this.#instantiateJob(job) 130 | } catch (e) { 131 | this.#logger.error(`Job ${job.name} was not able to be created`) 132 | this.#logger.error(e) 133 | return 134 | } 135 | 136 | this.#logger.info(`Job ${job.name} started`) 137 | await this.#app.container.call(jobClassInstance, 'handle', [job.data]) 138 | this.#logger.info(`Job ${job.name} finished`) 139 | }, 140 | computedConfig as WorkerOptions 141 | ) 142 | 143 | worker.on('failed', async (job, error) => { 144 | this.#logger.error(error.message, []) 145 | 146 | // If removeOnFail is set to true in the job options, job instance may be undefined. 147 | // This can occur if worker maxStalledCount has been reached and the removeOnFail is set to true. 148 | if (job && (job.attemptsMade === job.opts.attempts || job.finishedOn)) { 149 | // Call the failed method of the handler class if there is one 150 | const jobClassInstance = await this.#instantiateJob(job) 151 | 152 | await this.#app.container.call(jobClassInstance, 'rescue', [job.data, error]) 153 | } 154 | }) 155 | 156 | return this 157 | } 158 | 159 | get(queueName = 'default') { 160 | return this.#queues.get(queueName) 161 | } 162 | 163 | getOrSet(queueName = 'default') { 164 | return this.#maybeAddQueue(queueName) 165 | } 166 | 167 | async clear(queueName = 'default') { 168 | const queue = this.#queues.get(queueName) 169 | 170 | if (!queue) { 171 | return this.#logger.error(`Queue [${queueName}] not found`) 172 | } 173 | 174 | await queue.obliterate() 175 | 176 | return this.#logger.info(`Queue [${queueName}] cleared`) 177 | } 178 | 179 | async closeAll() { 180 | for (const [queueName, queue] of this.#queues.entries()) { 181 | await queue.close() 182 | this.#queues.delete(queueName) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/types/extended.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import { QueueManager } from '../queue.js' 9 | 10 | declare module '@adonisjs/core/types' { 11 | export interface ContainerBindings { 12 | 'rlanz/queue': QueueManager 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import { Job } from '../job.js' 9 | import type { ConnectionOptions, WorkerOptions, QueueOptions, JobsOptions } from 'bullmq' 10 | 11 | export type AllowedJobTypes = 12 | | JobHandlerConstructor 13 | | (() => Promise<{ default: JobHandlerConstructor }>) 14 | 15 | export type QueueConfig = { 16 | queueNames?: string[] 17 | defaultConnection: ConnectionOptions 18 | queue: Omit & { connection?: ConnectionOptions } 19 | worker: Omit & { connection?: ConnectionOptions } 20 | jobs: JobsOptions 21 | } 22 | 23 | export type InferJobPayload = Parameters< 24 | InstanceType['handle'] 25 | >[0] 26 | 27 | export interface JobHandlerConstructor { 28 | $$filepath: string 29 | new (...args: any[]): Job 30 | } 31 | -------------------------------------------------------------------------------- /stubs/command/main.stub: -------------------------------------------------------------------------------- 1 | {{#var jobName = string(entity.name).removeSuffix('job').suffix('Job').pascalCase().toString()}} 2 | {{#var jobFileName = string(entity.name).removeSuffix('job').suffix('Job').snakeCase().removeExtension().ext('.ts').toString()}} 3 | {{{ 4 | exports({ to: app.makePath('app/jobs', entity.path, jobFileName) }) 5 | }}} 6 | import { Job } from '@rlanz/bull-queue' 7 | 8 | interface {{ jobName }}Payload {} 9 | 10 | export default class {{ jobName }} extends Job { 11 | // This is the path to the file that is used to create the job 12 | static get $$filepath() { 13 | return import.meta.url 14 | } 15 | 16 | /** 17 | * Base Entry point 18 | */ 19 | async handle(payload: {{ jobName }}Payload) {} 20 | 21 | /** 22 | * This is an optional method that gets called when the retries has exceeded and is marked failed. 23 | */ 24 | async rescue(payload: {{ jobName }}Payload) {} 25 | } 26 | -------------------------------------------------------------------------------- /stubs/config/queue.stub: -------------------------------------------------------------------------------- 1 | {{{ 2 | exports({ to: app.configPath('queue.ts') }) 3 | }}} 4 | import env from '#start/env' 5 | import { defineConfig } from '@rlanz/bull-queue' 6 | 7 | export default defineConfig({ 8 | defaultConnection: { 9 | host: env.get('QUEUE_REDIS_HOST'), 10 | port: env.get('QUEUE_REDIS_PORT'), 11 | password: env.get('QUEUE_REDIS_PASSWORD'), 12 | }, 13 | 14 | queue: {}, 15 | 16 | worker: {}, 17 | 18 | jobs: { 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Default Job Attempts 22 | |-------------------------------------------------------------------------- 23 | | 24 | | The default number of attempts after which the job will be marked as 25 | | failed. You can also set the number of attempts on individual jobs 26 | | as well. 27 | | 28 | | @see https://docs.bullmq.io/guide/retrying-failing-jobs 29 | | 30 | */ 31 | attempts: 3, 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Auto-Removal of Jobs 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Numbers of jobs to keep in the completed and failed queues before they 39 | | are removed. This is important to keep the size of these queues in 40 | | control. Set the value to false to disable auto-removal. 41 | | 42 | | @see https://docs.bullmq.io/guide/queues/auto-removal-of-jobs 43 | | 44 | */ 45 | removeOnComplete: 100, 46 | removeOnFail: 100, 47 | }, 48 | }) 49 | -------------------------------------------------------------------------------- /stubs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @rlanz/bull-queue 3 | * 4 | * @license MIT 5 | * @copyright Romain Lanz 6 | */ 7 | 8 | import { getDirname } from '@poppinss/utils' 9 | 10 | export const stubsRoot = getDirname(import.meta.url) 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@adonisjs/tsconfig/tsconfig.package.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build" 6 | } 7 | } --------------------------------------------------------------------------------