├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
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 | }
--------------------------------------------------------------------------------