@tulipes/bullmq 0.1.0-rc.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TulipesJS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ <div align="center">
2
+
3
+ <img src="https://cdn.jsdelivr.net/npm/@tulipes/core@0.7.1/assets/tulipesjs-logo.png" alt="TulipesJs" width="340">
4
+
5
+ </div>
6
+
7
+ # @tulipes/bullmq
8
+
9
+ The BullMQ **queues provider** for [Tulipes](https://www.npmjs.com/package/@tulipes/core)
10
+ apps. Every module's `queues/*.queues.ts` runs in every process: producers exist
11
+ everywhere, processors are registered everywhere, and the worker process turns
12
+ them into consumers. Core itself no longer installs BullMQ or ioredis; an app
13
+ without background work declares no provider and never pays for one.
14
+
15
+ Requires Node 24.x and a core that ships the provider contract with `serve`/`drain`
16
+ hooks. `bullmq` and `ioredis` are this package's own dependencies — the app never
17
+ imports BullMQ, and the provider's connections are never shared with the app's
18
+ own Redis clients.
19
+
20
+ ## Select it
21
+
22
+ ```jsonc
23
+ { "tulipes": { "providers": { "queues": "@tulipes/bullmq" } } }
24
+ ```
25
+
26
+ plus `REDIS_URL` declared by the app's sys `core` module and set. A `queues/`
27
+ directory requires the provider automatically; a module that only produces
28
+ declares `"tulipes": { "requires": ["queues"] }`. Installing this package
29
+ without the declaration selects nothing; a `REDIS_URL` on its own starts nothing.
30
+
31
+ ## Use it
32
+
33
+ ```ts
34
+ // modules/billing/queues/invoices.queues.ts
35
+ import type { Ctx } from "@tulipes/core/boot";
36
+ import type { QueueRegistry } from "@tulipes/bullmq";
37
+
38
+ export default function invoiceQueues(ctx: Ctx, queues: QueueRegistry): void {
39
+ queues.define("billing.send-invoice");
40
+ queues.process("billing.send-invoice", async (job) => ({ sent: true }));
41
+ }
42
+
43
+ // anywhere after acquisition
44
+ await requireQueues(ctx).add("billing.send-invoice", "send", { invoiceId });
45
+ ```
46
+
47
+ ## Behavior
48
+
49
+ - One ioredis connection per queue and one blocking connection per worker,
50
+ `maxRetriesPerRequest: null`; owned by core before the first queue file runs.
51
+ - Worker mode: `serve()` starts one `Worker` per registered processor; core
52
+ refuses a worker that serves nothing.
53
+ - Shutdown: `drain()` closes workers (finishing active jobs) concurrently with
54
+ the HTTP drain and module `onDrain` hooks while producers stay usable — a
55
+ module's `onShutdown` may still enqueue; then queues and connections close in
56
+ reverse provider order, with forced `disconnect()` under the shared deadline.
57
+ - Queue names use `.`; duplicates and processors for undefined queues are
58
+ aggregate boot errors naming the modules.
59
+
60
+ ## Versions
61
+
62
+ `0.1.0-rc.1` requires core `^0.10.0-rc.2`, the candidate that added the `serve`/`drain`
63
+ hooks; install both with `@next`. See core's
64
+ [MIGRATING.md](https://github.com/kemora13conf/bp-backend-express/blob/v0.10.0-rc.2/packages/core/MIGRATING.md#0100-rc2--declare-the-queues-provider).
@@ -0,0 +1,4 @@
1
+ export { QueueManager, QueueRegistry } from "./queue-manager.js";
2
+ export { initQueues, type QueuesFn } from "./load-queues.js";
3
+ export { provider } from "./provider.js";
4
+ export { provider as default } from "./provider.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { QueueManager, QueueRegistry } from "./queue-manager.js";
2
+ export { initQueues } from "./load-queues.js";
3
+ export { provider } from "./provider.js";
4
+ export { provider as default } from "./provider.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAiB,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,QAAQ,IAAI,OAAO,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type Ctx } from "@tulipes/core/boot";
2
+ import type { BootReport } from "@tulipes/core/errors";
3
+ import type { ModuleFiles } from "@tulipes/core/modules";
4
+ import { QueueManager, type QueueRegistry } from "./queue-manager.js";
5
+ /** Default export of `queues/*.queues.ts`. */
6
+ export type QueuesFn = (ctx: Ctx, queues: QueueRegistry) => void | Promise<void>;
7
+ /**
8
+ * Phase 8, run by the provider: run every `*.queues.ts` in load order so
9
+ * producers exist in every process and processors are registered for the
10
+ * worker to start. Reached only when the app declared the provider, so a
11
+ * missing connection string is a contradiction the boot must refuse.
12
+ */
13
+ export declare function initQueues(explored: readonly ModuleFiles[], ctx: Ctx, report: BootReport, onManager?: (manager: QueueManager) => void): Promise<QueueManager | undefined>;
@@ -0,0 +1,53 @@
1
+ import { importFile } from "@tulipes/core/boot";
2
+ import { QueueManager } from "./queue-manager.js";
3
+ /** Well-known infra variable, declared by the app's sys core module. */
4
+ const REDIS_URL = "REDIS_URL";
5
+ /**
6
+ * Phase 8, run by the provider: run every `*.queues.ts` in load order so
7
+ * producers exist in every process and processors are registered for the
8
+ * worker to start. Reached only when the app declared the provider, so a
9
+ * missing connection string is a contradiction the boot must refuse.
10
+ */
11
+ export async function initQueues(explored, ctx, report, onManager) {
12
+ const url = ctx.Environment.store.has(REDIS_URL)
13
+ ? String(ctx.Environment.get(REDIS_URL))
14
+ : undefined;
15
+ if (!url) {
16
+ report.add({
17
+ scope: "providers",
18
+ message: `provider "bullmq" is selected but "${REDIS_URL}" is not declared/set — declare it in a sys module's meta.variables.json and set it`,
19
+ });
20
+ return undefined;
21
+ }
22
+ const manager = new QueueManager(url);
23
+ // Owned before any queue file runs: a define() opens a connection.
24
+ onManager?.(manager);
25
+ ctx.queues = manager;
26
+ for (const { module, queueFiles } of explored) {
27
+ for (const file of queueFiles) {
28
+ const exported = await importFile(file, "boot", module.name, report);
29
+ if (!exported)
30
+ continue;
31
+ if (typeof exported.default !== "function") {
32
+ report.add({
33
+ scope: "boot",
34
+ module: module.name,
35
+ message: `${file} must default-export a queues function (ctx, queues) => void`,
36
+ });
37
+ continue;
38
+ }
39
+ try {
40
+ await exported.default(ctx, manager.forModule(module.name, report));
41
+ }
42
+ catch (error) {
43
+ report.add({
44
+ scope: "boot",
45
+ module: module.name,
46
+ message: `queues function threw: ${error.message}`,
47
+ });
48
+ }
49
+ }
50
+ }
51
+ return manager;
52
+ }
53
+ //# sourceMappingURL=load-queues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-queues.js","sourceRoot":"","sources":["../src/load-queues.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAY,MAAM,oBAAoB,CAAC;AAI1D,OAAO,EAAE,YAAY,EAAsB,MAAM,oBAAoB,CAAC;AAKtE,wEAAwE;AACxE,MAAM,SAAS,GAAG,WAAW,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,QAAgC,EAChC,GAAQ,EACR,MAAkB,EAClB,SAA2C;IAE3C,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;QAC9C,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACxC,CAAC,CAAC,SAAS,CAAC;IAEd,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,CAAC,GAAG,CAAC;YACT,KAAK,EAAE,WAAW;YAClB,OAAO,EAAE,sCAAsC,SAAS,qFAAqF;SAC9I,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC;IACtC,mEAAmE;IACnE,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;IACrB,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC;IAErB,KAAK,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC9C,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,MAAM,QAAQ,GAAG,MAAM,UAAU,CAC/B,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAClC,CAAC;YACF,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAExB,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;gBAC3C,MAAM,CAAC,GAAG,CAAC;oBACT,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,OAAO,EAAE,GAAG,IAAI,8DAA8D;iBAC/E,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,IAAI,CAAC;gBACH,MAAO,QAAQ,CAAC,OAAoB,CAClC,GAAG,EACH,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CACvC,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,GAAG,CAAC;oBACT,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,OAAO,EAAE,0BAA2B,KAAe,CAAC,OAAO,EAAE;iBAC9D,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Provider } from "@tulipes/core/boot";
2
+ /**
3
+ * The "queues" provider an app selects with
4
+ * `"tulipes": { "providers": { "queues": "@tulipes/bullmq" } }`.
5
+ *
6
+ * Core owns the phase order, the worker refusal and the shutdown deadline;
7
+ * this package owns the Redis connections BullMQ needs — one per queue and
8
+ * one blocking connection per worker, never shared with the application's
9
+ * own cache or rate-limiter clients — and their drain/close/force order.
10
+ */
11
+ export declare const provider: Provider<"queues">;
@@ -0,0 +1,36 @@
1
+ import { initQueues } from "./load-queues.js";
2
+ /**
3
+ * The "queues" provider an app selects with
4
+ * `"tulipes": { "providers": { "queues": "@tulipes/bullmq" } }`.
5
+ *
6
+ * Core owns the phase order, the worker refusal and the shutdown deadline;
7
+ * this package owns the Redis connections BullMQ needs — one per queue and
8
+ * one blocking connection per worker, never shared with the application's
9
+ * own cache or rate-limiter clients — and their drain/close/force order.
10
+ */
11
+ export const provider = {
12
+ contract: 1,
13
+ capability: "queues",
14
+ name: "bullmq",
15
+ async acquire({ ctx, files }, report, own) {
16
+ let manager;
17
+ const acquired = await initQueues(files, ctx, report, (created) => {
18
+ manager = created;
19
+ own({
20
+ // Consumers stop while producers stay usable by draining requests,
21
+ // jobs and module onShutdown hooks; those close at dispose.
22
+ drain: () => created.drain(),
23
+ dispose: () => created.close(),
24
+ forceDispose: () => created.disconnect(),
25
+ serve: (workerReport) => created.startWorkers(workerReport),
26
+ status: () => ({
27
+ label: `redis (${created.names().length} queue(s))`,
28
+ ready: true,
29
+ detail: `${created.processorCount} processor(s) registered${created.workerCount ? `, ${created.workerCount} consuming` : ""}`,
30
+ }),
31
+ });
32
+ });
33
+ return acquired && manager ? manager : undefined;
34
+ },
35
+ };
36
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAuB;IAC1C,QAAQ,EAAE,CAAC;IACX,UAAU,EAAE,QAAQ;IACpB,IAAI,EAAE,QAAQ;IACd,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG;QACvC,IAAI,OAAiC,CAAC;QACtC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;YAChE,OAAO,GAAG,OAAO,CAAC;YAClB,GAAG,CAAC;gBACF,mEAAmE;gBACnE,4DAA4D;gBAC5D,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;gBAC5B,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;gBAC9B,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE;gBACxC,KAAK,EAAE,CAAC,YAAY,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC;gBAC3D,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;oBACb,KAAK,EAAE,UAAU,OAAO,CAAC,KAAK,EAAE,CAAC,MAAM,YAAY;oBACnD,KAAK,EAAE,IAAI;oBACX,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,2BAA2B,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,WAAW,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE;iBAC9H,CAAC;aACH,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,OAAO,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACnD,CAAC;CACF,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { Queue, type JobsOptions, type Processor, type QueueOptions, type WorkerOptions } from "bullmq";
2
+ import type { QueuesCapability } from "@tulipes/core/boot";
3
+ import type { BootReport } from "@tulipes/core/errors";
4
+ /**
5
+ * What `ctx.queues` is once this provider is in the compilation. Core's
6
+ * QueuesCapability is empty; an app that imports anything from this package
7
+ * (its queue files do, for QueueRegistry) sees these members typed.
8
+ */
9
+ declare module "@tulipes/core/boot" {
10
+ interface QueuesCapability {
11
+ /** Produce a job. The queue must have been defined by some module. */
12
+ add(queueName: string, jobName: string, data: unknown, options?: JobsOptions): Promise<void>;
13
+ queue(name: string): Queue;
14
+ names(): string[];
15
+ readonly processorCount: number;
16
+ readonly workerCount: number;
17
+ }
18
+ }
19
+ /**
20
+ * One manager owns every queue in the process. The same `*.queues.ts` files
21
+ * run in BOTH modes — definitions and processors are always registered — and
22
+ * the mode decides what happens with them:
23
+ *
24
+ * backend → queues produce; processors are registered but never started
25
+ * worker → `startWorkers()` turns every registered processor into a
26
+ * consuming BullMQ Worker
27
+ *
28
+ * That symmetry is what lets a module describe its background work once.
29
+ */
30
+ export declare class QueueManager implements QueuesCapability {
31
+ #private;
32
+ constructor(redisUrl: string);
33
+ /** @internal Module-scoped facade for the loader. */
34
+ forModule(moduleName: string, report: BootReport): QueueRegistry;
35
+ /** @internal QueueRegistry only. */
36
+ define_(name: string, owner: string, options?: Partial<QueueOptions>): string | undefined;
37
+ /** @internal QueueRegistry only. */
38
+ process_(name: string, handler: Processor, owner: string, options?: Partial<WorkerOptions>): string | undefined;
39
+ /** Introspection for the startup banner and tooling. */
40
+ names(): string[];
41
+ get processorCount(): number;
42
+ get workerCount(): number;
43
+ /** Produce a job. The queue must have been defined by some module. */
44
+ add(queueName: string, jobName: string, data: unknown, options?: JobsOptions): Promise<void>;
45
+ queue(name: string): Queue;
46
+ /** Worker mode only — called by the provider's serve() hook, never by modules. */
47
+ startWorkers(report: BootReport): number;
48
+ /** Stop consumers and finish active jobs while producers remain available. */
49
+ drain(): Promise<void>;
50
+ /** Emergency connection teardown after the graceful shutdown deadline. */
51
+ disconnect(): void;
52
+ /** Drain order: stop consuming, then producers, then raw connections. */
53
+ close(): Promise<void>;
54
+ }
55
+ /**
56
+ * Module-scoped facade handed to `*.queues.ts` — mirrors AclBuilder: every
57
+ * registration records its module so conflicts crash with both names.
58
+ */
59
+ export declare class QueueRegistry {
60
+ #private;
61
+ constructor(manager: QueueManager, moduleName: string, report: BootReport);
62
+ define(name: string, options?: Partial<QueueOptions>): this;
63
+ process(name: string, handler: Processor, options?: Partial<WorkerOptions>): this;
64
+ }
@@ -0,0 +1,183 @@
1
+ import { Queue, Worker, } from "bullmq";
2
+ import { Redis } from "ioredis";
3
+ /**
4
+ * One manager owns every queue in the process. The same `*.queues.ts` files
5
+ * run in BOTH modes — definitions and processors are always registered — and
6
+ * the mode decides what happens with them:
7
+ *
8
+ * backend → queues produce; processors are registered but never started
9
+ * worker → `startWorkers()` turns every registered processor into a
10
+ * consuming BullMQ Worker
11
+ *
12
+ * That symmetry is what lets a module describe its background work once.
13
+ */
14
+ export class QueueManager {
15
+ #url;
16
+ #queues = new Map();
17
+ #processors = new Map();
18
+ #workers = [];
19
+ #connections = [];
20
+ #closing = false;
21
+ constructor(redisUrl) {
22
+ this.#url = redisUrl;
23
+ }
24
+ /**
25
+ * BullMQ re-emits every driver error on its Queue and Worker instances and
26
+ * Node turns an unhandled "error" event into a crash — including the reply
27
+ * that a QUIT cuts short during shutdown. Producers and consumers therefore
28
+ * always carry a listener: during shutdown the error is expected and dropped,
29
+ * otherwise it is surfaced on stderr, since a lost job is a job BullMQ will
30
+ * retry and a dead process would lose the whole queue.
31
+ */
32
+ #observe(name, emitter) {
33
+ emitter.on("error", (error) => {
34
+ if (this.#closing)
35
+ return;
36
+ console.error(`[tulipes/bullmq] queue "${name}": ${error.message}`);
37
+ });
38
+ }
39
+ /**
40
+ * Every BullMQ client gets its own connection: workers need dedicated
41
+ * blocking connections anyway, and never sharing keeps close() reasoning
42
+ * trivial. maxRetriesPerRequest: null is a BullMQ requirement.
43
+ */
44
+ #connect() {
45
+ const connection = new Redis(this.#url, { maxRetriesPerRequest: null });
46
+ this.#connections.push(connection);
47
+ return connection;
48
+ }
49
+ /** @internal Module-scoped facade for the loader. */
50
+ forModule(moduleName, report) {
51
+ return new QueueRegistry(this, moduleName, report);
52
+ }
53
+ /** @internal QueueRegistry only. */
54
+ define_(name, owner, options) {
55
+ const existing = this.#queues.get(name);
56
+ if (existing)
57
+ return existing.owner;
58
+ const queue = new Queue(name, { ...options, connection: this.#connect() });
59
+ this.#observe(name, queue);
60
+ this.#queues.set(name, { queue, owner });
61
+ return undefined;
62
+ }
63
+ /** @internal QueueRegistry only. */
64
+ process_(name, handler, owner, options) {
65
+ const existing = this.#processors.get(name);
66
+ if (existing)
67
+ return existing.owner;
68
+ this.#processors.set(name, { handler, owner, options });
69
+ return undefined;
70
+ }
71
+ /** Introspection for the startup banner and tooling. */
72
+ names() {
73
+ return [...this.#queues.keys()];
74
+ }
75
+ get processorCount() {
76
+ return this.#processors.size;
77
+ }
78
+ get workerCount() {
79
+ return this.#workers.length;
80
+ }
81
+ /** Produce a job. The queue must have been defined by some module. */
82
+ async add(queueName, jobName, data, options) {
83
+ await this.queue(queueName).add(jobName, data, options);
84
+ }
85
+ queue(name) {
86
+ const stored = this.#queues.get(name);
87
+ if (!stored) {
88
+ throw new Error(`Queue "${name}" was never defined by any module (known: ${[...this.#queues.keys()].join(", ") || "none"})`);
89
+ }
90
+ return stored.queue;
91
+ }
92
+ /** Worker mode only — called by the provider's serve() hook, never by modules. */
93
+ startWorkers(report) {
94
+ for (const [name, { handler, owner, options }] of this.#processors) {
95
+ if (!this.#queues.has(name)) {
96
+ report.add({
97
+ scope: "boot",
98
+ module: owner,
99
+ message: `processor registered for undefined queue "${name}" — call define() first`,
100
+ });
101
+ continue;
102
+ }
103
+ const worker = new Worker(name, handler, { ...options, connection: this.#connect() });
104
+ this.#observe(name, worker);
105
+ this.#workers.push(worker);
106
+ }
107
+ return this.#workers.length;
108
+ }
109
+ /** Stop consumers and finish active jobs while producers remain available. */
110
+ async drain() {
111
+ // The first shutdown signal this manager receives; errors are expected from here on.
112
+ this.#closing = true;
113
+ const results = await Promise.allSettled(this.#workers.map((worker) => worker.close()));
114
+ throwFailures(results);
115
+ }
116
+ /** Emergency connection teardown after the graceful shutdown deadline. */
117
+ disconnect() {
118
+ this.#closing = true;
119
+ for (const worker of this.#workers)
120
+ void worker.disconnect().catch(() => { });
121
+ for (const connection of this.#connections)
122
+ connection.disconnect();
123
+ }
124
+ /** Drain order: stop consuming, then producers, then raw connections. */
125
+ async close() {
126
+ this.#closing = true;
127
+ const results = await Promise.allSettled([this.drain()]);
128
+ // A queue still initializing (its meta write is issued once the client is
129
+ // ready) must finish before its connection is quit, or that reply is lost.
130
+ results.push(...await Promise.allSettled([...this.#queues.values()].map(async ({ queue }) => { await queue.waitUntilReady().catch(() => { }); await queue.close(); })));
131
+ results.push(...await Promise.allSettled(this.#connections.map(async (connection) => {
132
+ try {
133
+ await connection.quit();
134
+ }
135
+ finally {
136
+ connection.disconnect();
137
+ }
138
+ })));
139
+ throwFailures(results);
140
+ }
141
+ }
142
+ // BullMQ reserves ":" (its redis key separator), so module namespacing in
143
+ // queue names uses "." instead: "users.welcome".
144
+ const QUEUE_NAME = /^[a-z0-9-]+(\.[a-z0-9-]+)?$/;
145
+ /**
146
+ * Module-scoped facade handed to `*.queues.ts` — mirrors AclBuilder: every
147
+ * registration records its module so conflicts crash with both names.
148
+ */
149
+ export class QueueRegistry {
150
+ #manager;
151
+ #module;
152
+ #report;
153
+ constructor(manager, moduleName, report) {
154
+ this.#manager = manager;
155
+ this.#module = moduleName;
156
+ this.#report = report;
157
+ }
158
+ define(name, options) {
159
+ if (!QUEUE_NAME.test(name)) {
160
+ this.#issue(`queue name "${name}" is invalid — lowercase kebab-case, optional "." namespace`);
161
+ return this;
162
+ }
163
+ const owner = this.#manager.define_(name, this.#module, options);
164
+ if (owner)
165
+ this.#issue(`queue "${name}" is already defined by module "${owner}"`);
166
+ return this;
167
+ }
168
+ process(name, handler, options) {
169
+ const owner = this.#manager.process_(name, handler, this.#module, options);
170
+ if (owner)
171
+ this.#issue(`queue "${name}" already has a processor from module "${owner}"`);
172
+ return this;
173
+ }
174
+ #issue(message) {
175
+ this.#report.add({ scope: "boot", module: this.#module, message });
176
+ }
177
+ }
178
+ function throwFailures(results) {
179
+ const errors = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
180
+ if (errors.length)
181
+ throw new AggregateError(errors, `Queue cleanup failed — ${errors.map(error => error instanceof Error ? error.message : String(error)).join("; ")}`);
182
+ }
183
+ //# sourceMappingURL=queue-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-manager.js","sourceRoot":"","sources":["../src/queue-manager.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@tulipes/bullmq",
3
+ "version": "0.1.0-rc.1",
4
+ "description": "BullMQ queues provider for Tulipes apps: producers in every process, consumers in the worker, behind core's queues capability",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit",
16
+ "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
17
+ "prepack": "yarn build"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "exports": {
22
+ ".": "./dist/index.js"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "bullmq": "^5",
27
+ "ioredis": "^5"
28
+ },
29
+ "peerDependencies": {
30
+ "@tulipes/core": "^0.10.0-rc.2"
31
+ },
32
+ "devDependencies": {
33
+ "@tulipes/core": "0.10.0-rc.2",
34
+ "@types/node": "^24.2.0",
35
+ "typescript": "^7.0.2"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/kemora13conf/bp-backend-express.git",
40
+ "directory": "packages/bullmq"
41
+ },
42
+ "keywords": [
43
+ "tulipes",
44
+ "bullmq",
45
+ "redis",
46
+ "queues",
47
+ "express"
48
+ ],
49
+ "license": "MIT",
50
+ "engines": {
51
+ "node": ">=24.0.0 <25"
52
+ },
53
+ "homepage": "https://github.com/kemora13conf/bp-backend-express/tree/master/packages/bullmq#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/kemora13conf/bp-backend-express/issues"
56
+ }
57
+ }