@nexusts/queue 0.7.0 → 0.7.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/README.md CHANGED
@@ -15,7 +15,7 @@ This module is part of the NexusTS monorepo. Each module is published as its own
15
15
  Most apps start with just the core:
16
16
 
17
17
  ```bash
18
- bun add @nexusts/core reflect-metadata zod hono
18
+ bun add @nexusts/core
19
19
  ```
20
20
 
21
21
  Then add this module only if you need it:
@@ -27,10 +27,13 @@ bun add @nexusts/queue
27
27
  ## Peer dependencies
28
28
 
29
29
  ```bash
30
- bun add bullmq + ioredis
30
+ bun add bullmq ioredis
31
31
  ```
32
32
 
33
- Required by this module. Without them the module loads but its public methods throw a clear error pointing to this install command on first call.
33
+ - **`bullmq`** ^5.79.0 Required for the BullMQ backend. Skip if you only use the in-memory or Cloudflare backends.
34
+ - **`ioredis`** ^5.11.1 — Required for the BullMQ backend's Redis connection. Skip if you only use the in-memory or Cloudflare backends.
35
+
36
+ Without them the module loads but its public methods throw a clear error pointing to this install command on first call.
34
37
 
35
38
  ## Usage
36
39
 
@@ -0,0 +1,40 @@
1
+ /**
2
+ * BullMQ backend — Redis-backed queue for Bun / Node.
3
+ *
4
+ * Wraps `bullmq.Queue` (producer) and `bullmq.Worker` (consumer) with
5
+ * the common `QueueBackend` interface. We use the `Job` wrapper class
6
+ * (vs the lower-level `QueueBase`) so we can read job IDs back as
7
+ * strings without leaking BullMQ types to user code.
8
+ *
9
+ * Usage:
10
+ * const backend = new BullMQBackend({
11
+ * connection: 'redis://localhost:6379',
12
+ * prefix: 'nexusjs',
13
+ * });
14
+ * await backend.process('send-email', async (data) => {
15
+ * // ...
16
+ * });
17
+ * await backend.add('send-email', { to: 'a@b.c' });
18
+ */
19
+ import { type ConnectionOptions } from "bullmq";
20
+ import type { QueueBackend, JobHandler, WorkerHandle, WorkerOptions, AddedJob, AddOptions, QueueEventListener } from "../types.js";
21
+ export interface BullMQBackendOptions {
22
+ connection: string | ConnectionOptions;
23
+ prefix?: string;
24
+ defaultJobOptions?: AddOptions;
25
+ }
26
+ export declare class BullMQBackend implements QueueBackend {
27
+ #private;
28
+ readonly name: "bullmq";
29
+ constructor(options: BullMQBackendOptions);
30
+ add(name: string, data: unknown, options?: AddOptions): Promise<AddedJob>;
31
+ addBatch(jobs: Array<{
32
+ name: string;
33
+ data: unknown;
34
+ options?: AddOptions;
35
+ }>): Promise<AddedJob[]>;
36
+ process<T = unknown>(name: string, handler: JobHandler<T>, options?: WorkerOptions): Promise<WorkerHandle>;
37
+ drain(): Promise<void>;
38
+ stop(): Promise<void>;
39
+ on(listener: QueueEventListener): () => void;
40
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Cloudflare Queues backend — Workers-native, edge-friendly.
3
+ *
4
+ * Cloudflare Queues has a different shape from BullMQ:
5
+ * - The producer calls `queue.send(body)` / `queue.sendBatch(...)`.
6
+ * - The consumer is a Worker's `queue()` handler that receives a
7
+ * `MessageBatch`.
8
+ *
9
+ * We adapt the two halves to our common `QueueBackend` interface:
10
+ * - `add(name, data)` calls `queue.send({ name, data })` so the
11
+ * consumer knows which handler to route to.
12
+ * - `process(name, handler)` registers the handler in a local
13
+ * `Map`. When the consumer's `queue()` callback fires, we
14
+ * dispatch each message to the matching handler.
15
+ *
16
+ * Because Workers can't start a long-running process, `process()` is
17
+ * a no-op on the producer side. The actual `queue()` handler must be
18
+ * registered separately by the user (or by `QueueModule`'s
19
+ * `workerHandler(env, ctx, batch)` export).
20
+ */
21
+ import type { QueueBackend, JobHandler, WorkerHandle, WorkerOptions, AddedJob, AddOptions, QueueEventListener } from "../types.js";
22
+ interface CFQueue<Body = unknown> {
23
+ send(body: Body, options?: {
24
+ contentType?: string;
25
+ delaySeconds?: number;
26
+ }): Promise<unknown>;
27
+ sendBatch(messages: Array<{
28
+ body: unknown;
29
+ contentType?: string;
30
+ delaySeconds?: number;
31
+ }>, options?: {
32
+ delaySeconds?: number;
33
+ }): Promise<unknown>;
34
+ }
35
+ interface CFMessage<Body = unknown> {
36
+ readonly id: string;
37
+ readonly timestamp: Date;
38
+ readonly body: Body;
39
+ readonly attempts: number;
40
+ ack(): void;
41
+ retry(options?: {
42
+ delaySeconds?: number;
43
+ }): void;
44
+ }
45
+ interface CFMessageBatch<Body = unknown> {
46
+ readonly queue: string;
47
+ readonly messages: readonly CFMessage<Body>[];
48
+ ackAll(): void;
49
+ retryAll(options?: {
50
+ delaySeconds?: number;
51
+ }): void;
52
+ }
53
+ export interface CloudflareBackendOptions {
54
+ /** Resolver that pulls the Queue binding from the Worker's env. */
55
+ resolveBinding: (env: Record<string, unknown>) => CFQueue;
56
+ /** Queue name (for diagnostics + MessageBatch.queue matching). */
57
+ name?: string;
58
+ }
59
+ export declare class CloudflareQueueBackend implements QueueBackend {
60
+ #private;
61
+ readonly name: "cloudflare";
62
+ constructor(options: CloudflareBackendOptions);
63
+ /** Bind to the Worker's `env` once it's available. */
64
+ bind(env: Record<string, unknown>): void;
65
+ add(name: string, data: unknown, options?: AddOptions): Promise<AddedJob>;
66
+ addBatch(jobs: Array<{
67
+ name: string;
68
+ data: unknown;
69
+ options?: AddOptions;
70
+ }>): Promise<AddedJob[]>;
71
+ process<T = unknown>(name: string, handler: JobHandler<T>, options?: WorkerOptions): Promise<WorkerHandle>;
72
+ /**
73
+ * Build the Worker's `queue()` handler. Mount it as
74
+ * `export default { queue: backend.consumerHandler() }` in the
75
+ * Worker entry file.
76
+ */
77
+ consumerHandler(): (batch: CFMessageBatch<unknown>) => Promise<void>;
78
+ drain(): Promise<void>;
79
+ stop(): Promise<void>;
80
+ on(listener: QueueEventListener): () => void;
81
+ get queueName(): string;
82
+ }
83
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Re-exports for the queue backends.
3
+ */
4
+ export { MemoryQueueBackend } from "./memory.js";
5
+ export { BullMQBackend, type BullMQBackendOptions } from "./bullmq.js";
6
+ export { CloudflareQueueBackend, type CloudflareBackendOptions, } from "./cloudflare.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * In-memory queue backend — for tests, single-instance dev, and the
3
+ * `bunx nx dev` workflow when Redis isn't running yet.
4
+ *
5
+ * NOT for production. It does not persist across restarts, does not
6
+ * distribute across workers, and silently drops jobs on crash.
7
+ */
8
+ import type { QueueBackend, JobHandler, WorkerHandle, WorkerOptions, AddedJob, AddOptions, QueueEventListener } from "../types.js";
9
+ export declare class MemoryQueueBackend implements QueueBackend {
10
+ #private;
11
+ readonly name: "memory";
12
+ constructor();
13
+ add(name: string, data: unknown, options?: AddOptions): Promise<AddedJob>;
14
+ addBatch(jobs: Array<{
15
+ name: string;
16
+ data: unknown;
17
+ options?: AddOptions;
18
+ }>): Promise<AddedJob[]>;
19
+ process<T>(name: string, handler: JobHandler<T>, options?: WorkerOptions): Promise<WorkerHandle>;
20
+ drain(): Promise<void>;
21
+ stop(): Promise<void>;
22
+ on(listener: QueueEventListener): () => void;
23
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `@OnQueueReady()` — lifecycle hook that runs once when the
3
+ * application has booted and the queue service is ready.
4
+ *
5
+ * Use this to register workers without coupling to the `Application`
6
+ * lifecycle directly.
7
+ *
8
+ * Usage:
9
+ * class EmailWorker {
10
+ * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}
11
+ *
12
+ * @OnQueueReady()
13
+ * async register() {
14
+ * await this.queue.process('send-email', this.handle);
15
+ * }
16
+ *
17
+ * handle = async (data: { to: string }) => { ... };
18
+ * }
19
+ *
20
+ * @Module({
21
+ * providers: [EmailWorker],
22
+ * })
23
+ * class WorkerModule {}
24
+ */
25
+ import "reflect-metadata";
26
+ import type { QueueService } from "../queue.service.js";
27
+ /**
28
+ * Method decorator. The decorated method is invoked once with no
29
+ * arguments after the application has booted. Pair it with
30
+ * `QueueService.start()` — typically called by `Application.bootstrap`.
31
+ */
32
+ export declare function OnQueueReady(): MethodDecorator;
33
+ /**
34
+ * Get the queue-ready hooks declared on a class.
35
+ */
36
+ export declare function getQueueReadyHooks(target: unknown): Array<string | symbol>;
37
+ /**
38
+ * Helper — invoke all `@OnQueueReady` hooks on an instance.
39
+ * Pair this with `QueueService.start()` for a complete bootstrap.
40
+ */
41
+ export declare function invokeQueueReadyHooks(instance: object): Promise<void>;
42
+ export type { QueueService };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Public API for the NexusTS queue module.
3
+ *
4
+ * Two backends out of the box:
5
+ * - BullMQ → Redis-backed, for Bun / Node long-running servers
6
+ * - Cloudflare Queues → Workers-native, edge-friendly
7
+ * - In-memory → for tests and single-instance dev (no Redis required)
8
+ *
9
+ * Quick start:
10
+ *
11
+ * // src/app/app.module.ts
12
+ * import { Module } from 'nexusjs';
13
+ * import { QueueModule } from 'nexusjs/queue';
14
+ *
15
+ * @Module({
16
+ * imports: [
17
+ * QueueModule.forRoot({
18
+ * backend: 'bullmq',
19
+ * bullmq: { connection: process.env.REDIS_URL! },
20
+ * }),
21
+ * ],
22
+ * })
23
+ * export class AppModule {}
24
+ *
25
+ * // any controller or service
26
+ * import { QueueService } from 'nexusjs/queue';
27
+ *
28
+ * class SignupController {
29
+ * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}
30
+ *
31
+ * @Post('/')
32
+ * async signup(@Body() body: { email: string }) {
33
+ * await this.queue.add('send-welcome-email', { email: body.email });
34
+ * return { status: 'ok' };
35
+ * }
36
+ * }
37
+ *
38
+ * // any service — register a worker
39
+ * class EmailWorker {
40
+ * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}
41
+ * @OnQueueReady()
42
+ * async register() {
43
+ * await this.queue.process('send-welcome-email', this.handle);
44
+ * }
45
+ * handle = async (data: { email: string }) => {
46
+ * // ... send the email
47
+ * };
48
+ * }
49
+ */
50
+ export * from "./types.js";
51
+ export { MemoryQueueBackend, BullMQBackend, CloudflareQueueBackend, } from "./backends/index.js";
52
+ export type { BullMQBackendOptions } from "./backends/bullmq.js";
53
+ export type { CloudflareBackendOptions } from "./backends/cloudflare.js";
54
+ export { QueueService } from "./queue.service.js";
55
+ export { QueueModule } from "./queue.module.js";
56
+ export { OnQueueReady, getQueueReadyHooks, invokeQueueReadyHooks, } from "./decorators/on-queue-ready.js";
package/dist/index.js CHANGED
@@ -564,7 +564,7 @@ class CloudflareQueueBackend {
564
564
  }
565
565
  }
566
566
  // packages/queue/src/queue.service.ts
567
- import { Inject, Injectable } from "@nexusts/core/src/decorators/index.js";
567
+ import { Inject, Injectable } from "@nexusts/core";
568
568
  class QueueService {
569
569
  config;
570
570
  static TOKEN = Symbol.for("nexus:QueueService");
@@ -652,7 +652,7 @@ QueueService = __legacyDecorateClassTS([
652
652
  ], QueueService);
653
653
  // packages/queue/src/queue.module.ts
654
654
  import"reflect-metadata";
655
- import { Module } from "@nexusts/core/decorators/module.js";
655
+ import { Module } from "@nexusts/core";
656
656
  class QueueModule {
657
657
  static forRoot(config) {
658
658
  class ConfiguredQueueModule {
@@ -719,5 +719,5 @@ export {
719
719
  BullMQBackend
720
720
  };
721
721
 
722
- //# debugId=77CE421E465283D764756E2164756E21
722
+ //# debugId=0DEA3B2743EBA89564756E2164756E21
723
723
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -5,11 +5,11 @@
5
5
  "/**\n * In-memory queue backend — for tests, single-instance dev, and the\n * `bunx nx dev` workflow when Redis isn't running yet.\n *\n * NOT for production. It does not persist across restarts, does not\n * distribute across workers, and silently drops jobs on crash.\n */\n\nimport type {\n\tQueueBackend,\n\tJobHandler,\n\tWorkerHandle,\n\tWorkerOptions,\n\tAddedJob,\n\tAddOptions,\n\tQueueEvent,\n\tQueueEventListener,\n\tJobContext,\n} from \"../types.js\";\n\ninterface PendingJob {\n\tjobId: string;\n\tname: string;\n\tdata: unknown;\n\toptions: AddOptions;\n\tresolveAt: number; // ms epoch\n}\n\n/** Per-job worker handle. */\nclass MemoryWorkerHandle implements WorkerHandle {\n\t#running = true;\n\t#handler: JobHandler;\n\t#context: { jobId: string; name: string };\n\tconstructor(name: string, handler: JobHandler) {\n\t\tthis.#handler = handler;\n\t\tthis.#context = { jobId: \"\", name };\n\t}\n\tget name() {\n\t\treturn this.#context.name;\n\t}\n\tasync close() {\n\t\tthis.#running = false;\n\t}\n\tasync pause() {\n\t\tthis.#running = false;\n\t}\n\tasync resume() {\n\t\tthis.#running = true;\n\t}\n\tisRunning() {\n\t\treturn this.#running;\n\t}\n}\n\nexport class MemoryQueueBackend implements QueueBackend {\n\treadonly name = \"memory\" as const;\n\t#queue: PendingJob[] = [];\n\t#handlers = new Map<string, JobHandler>();\n\t#workerOptions = new Map<string, WorkerOptions>();\n\t#listeners = new Set<QueueEventListener>();\n\t#tickHandle: ReturnType<typeof setInterval> | null = null;\n\t#inFlight = 0;\n\n\tconstructor() {\n\t\t// Tick every 100 ms to dispatch due jobs.\n\t\tthis.#tickHandle = setInterval(() => this.#tick(), 100);\n\t\t// Allow Node to exit if only this timer is running.\n\t\tif (\n\t\t\ttypeof (this.#tickHandle as { unref?: () => void }).unref === \"function\"\n\t\t) {\n\t\t\t(this.#tickHandle as { unref: () => void }).unref();\n\t\t}\n\t}\n\n\t// ===========================================================================\n\t// Producer\n\t// ===========================================================================\n\n\tasync add(\n\t\tname: string,\n\t\tdata: unknown,\n\t\toptions: AddOptions = {},\n\t): Promise<AddedJob> {\n\t\tconst jobId = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n\t\tconst delayMs = (options.delaySeconds ?? 0) * 1000;\n\t\tthis.#queue.push({\n\t\t\tjobId,\n\t\t\tname,\n\t\t\tdata,\n\t\t\toptions,\n\t\t\tresolveAt: Date.now() + delayMs,\n\t\t});\n\t\tthis.#emit({ kind: \"job:added\", jobId, name });\n\t\treturn { jobId, name };\n\t}\n\n\tasync addBatch(\n\t\tjobs: Array<{ name: string; data: unknown; options?: AddOptions }>,\n\t): Promise<AddedJob[]> {\n\t\treturn Promise.all(jobs.map((j) => this.add(j.name, j.data, j.options)));\n\t}\n\n\t// ===========================================================================\n\t// Worker\n\t// ===========================================================================\n\n\tasync process<T>(\n\t\tname: string,\n\t\thandler: JobHandler<T>,\n\t\toptions: WorkerOptions = {},\n\t): Promise<WorkerHandle> {\n\t\tthis.#handlers.set(name, handler as JobHandler);\n\t\tthis.#workerOptions.set(name, options);\n\t\tconst handle = new MemoryWorkerHandle(name, handler as JobHandler);\n\t\tthis.#emit({\n\t\t\tkind: \"worker:started\",\n\t\t\tname,\n\t\t\tconcurrency: options.concurrency ?? 1,\n\t\t});\n\t\treturn handle;\n\t}\n\n\t// ===========================================================================\n\t// Lifecycle\n\t// ===========================================================================\n\n\tasync drain(): Promise<void> {\n\t\twhile (this.#inFlight > 0 || this.#queue.length > 0) {\n\t\t\tawait new Promise((r) => setTimeout(r, 50));\n\t\t}\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tfor (const name of this.#handlers.keys()) {\n\t\t\tthis.#emit({ kind: \"worker:stopped\", name });\n\t\t}\n\t\tif (this.#tickHandle) clearInterval(this.#tickHandle);\n\t\tthis.#tickHandle = null;\n\t}\n\n\t// ===========================================================================\n\t// Events\n\t// ===========================================================================\n\n\ton(listener: QueueEventListener): () => void {\n\t\tthis.#listeners.add(listener);\n\t\treturn () => this.#listeners.delete(listener);\n\t}\n\n\t// ===========================================================================\n\t// Internal\n\t// ===========================================================================\n\n\tasync #tick() {\n\t\tif (this.#queue.length === 0) return;\n\t\tconst now = Date.now();\n\t\tconst due = this.#queue.filter((j) => j.resolveAt <= now);\n\t\tfor (const job of due) {\n\t\t\tthis.#queue = this.#queue.filter((j) => j !== job);\n\t\t\tconst handler = this.#handlers.get(job.name);\n\t\t\tif (!handler) continue;\n\t\t\tconst options = this.#workerOptions.get(job.name) ?? {};\n\t\t\tconst concurrency = options.concurrency ?? 1;\n\t\t\tif (this.#inFlight >= concurrency) {\n\t\t\t\t// Re-queue at the front.\n\t\t\t\tthis.#queue.unshift(job);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tvoid this.#runJob(job, handler, options);\n\t\t}\n\t}\n\n\tasync #runJob(job: PendingJob, handler: JobHandler, options: WorkerOptions) {\n\t\tthis.#inFlight++;\n\t\tconst ctx: JobContext = {\n\t\t\tjobId: job.jobId,\n\t\t\tattempts: 1,\n\t\t\tjob: { name: job.name, data: job.data },\n\t\t\tprefix: `[queue:${job.name}]`,\n\t\t};\n\t\tthis.#emit({\n\t\t\tkind: \"job:active\",\n\t\t\tjobId: job.jobId,\n\t\t\tname: job.name,\n\t\t\tattempts: 1,\n\t\t});\n\t\ttry {\n\t\t\tconst result = await handler(job.data, ctx);\n\t\t\tif (result && typeof result === \"object\" && \"status\" in result) {\n\t\t\t\tconst r = result as {\n\t\t\t\t\tstatus: string;\n\t\t\t\t\treturnvalue?: unknown;\n\t\t\t\t\terror?: Error;\n\t\t\t\t};\n\t\t\t\tif (r.status === \"failed\") {\n\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\tkind: \"job:failed\",\n\t\t\t\t\t\tjobId: job.jobId,\n\t\t\t\t\t\tname: job.name,\n\t\t\t\t\t\terror: r.error ?? new Error(\"unknown\"),\n\t\t\t\t\t\twillRetry: false,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\tkind: \"job:completed\",\n\t\t\t\t\t\tjobId: job.jobId,\n\t\t\t\t\t\tname: job.name,\n\t\t\t\t\t\treturnvalue: r.returnvalue,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.#emit({\n\t\t\t\t\tkind: \"job:completed\",\n\t\t\t\t\tjobId: job.jobId,\n\t\t\t\t\tname: job.name,\n\t\t\t\t\treturnvalue: result,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst error = err instanceof Error ? err : new Error(String(err));\n\t\t\tconst willRetry = (job.options.attempts ?? 1) > 1;\n\t\t\tthis.#emit({\n\t\t\t\tkind: \"job:failed\",\n\t\t\t\tjobId: job.jobId,\n\t\t\t\tname: job.name,\n\t\t\t\terror,\n\t\t\t\twillRetry,\n\t\t\t});\n\t\t\tif (willRetry) {\n\t\t\t\tconst delayMs =\n\t\t\t\t\t(job.options.backoff?.delayMs ?? 1000) *\n\t\t\t\t\t(job.options.backoff?.type === \"exponential\" ? 2 ** ctx.attempts : 1);\n\t\t\t\tthis.#queue.push({\n\t\t\t\t\t...job,\n\t\t\t\t\tresolveAt: Date.now() + delayMs,\n\t\t\t\t});\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.#inFlight--;\n\t\t}\n\t\t// Touch options to silence \"unused\" warnings in strict configs.\n\t\tvoid options;\n\t}\n\n\t#emit(event: QueueEvent) {\n\t\tfor (const l of this.#listeners) {\n\t\t\tvoid Promise.resolve(l(event));\n\t\t}\n\t}\n}\n",
6
6
  "/**\n * BullMQ backend — Redis-backed queue for Bun / Node.\n *\n * Wraps `bullmq.Queue` (producer) and `bullmq.Worker` (consumer) with\n * the common `QueueBackend` interface. We use the `Job` wrapper class\n * (vs the lower-level `QueueBase`) so we can read job IDs back as\n * strings without leaking BullMQ types to user code.\n *\n * Usage:\n * const backend = new BullMQBackend({\n * connection: 'redis://localhost:6379',\n * prefix: 'nexusjs',\n * });\n * await backend.process('send-email', async (data) => {\n * // ...\n * });\n * await backend.add('send-email', { to: 'a@b.c' });\n */\n\nimport {\n\tQueue,\n\tWorker,\n\ttype ConnectionOptions,\n\ttype JobsOptions,\n} from \"bullmq\";\nimport IORedis from \"ioredis\";\nimport type {\n\tQueueBackend,\n\tJobHandler,\n\tWorkerHandle,\n\tWorkerOptions,\n\tAddedJob,\n\tAddOptions,\n\tQueueEvent,\n\tQueueEventListener,\n\tJobContext,\n} from \"../types.js\";\n\nexport interface BullMQBackendOptions {\n\tconnection: string | ConnectionOptions;\n\tprefix?: string;\n\tdefaultJobOptions?: AddOptions;\n}\n\nclass BullMQWorkerHandle implements WorkerHandle {\n\t#worker: Worker;\n\t#name: string;\n\tconstructor(name: string, worker: Worker) {\n\t\tthis.#name = name;\n\t\tthis.#worker = worker;\n\t}\n\tget name() {\n\t\treturn this.#name;\n\t}\n\tasync close() {\n\t\tawait this.#worker.close();\n\t}\n\tasync pause() {\n\t\tawait this.#worker.pause();\n\t}\n\tasync resume() {\n\t\tawait this.#worker.resume();\n\t}\n\tisRunning() {\n\t\treturn !this.#worker.closing;\n\t}\n}\n\nexport class BullMQBackend implements QueueBackend {\n\treadonly name = \"bullmq\" as const;\n\t#queue: Queue;\n\t#connection: ConnectionOptions;\n\t#prefix: string;\n\t#defaultJobOptions: AddOptions;\n\t#workers = new Map<string, Worker>();\n\t#listeners = new Set<QueueEventListener>();\n\t#closed = false;\n\n\tconstructor(options: BullMQBackendOptions) {\n\t\tthis.#connection =\n\t\t\ttypeof options.connection === \"string\"\n\t\t\t\t? (new IORedis(options.connection, {\n\t\t\t\t\t\tmaxRetriesPerRequest: null,\n\t\t\t\t\t}) as unknown as ConnectionOptions)\n\t\t\t\t: options.connection;\n\t\tthis.#prefix = options.prefix ?? \"nexusjs\";\n\t\tthis.#defaultJobOptions = options.defaultJobOptions ?? {};\n\n\t\tthis.#queue = new Queue(\"nexus-queue\", {\n\t\t\tconnection: this.#connection,\n\t\t\tprefix: this.#prefix,\n\t\t\tdefaultJobOptions: this.#toBullJobOptions(this.#defaultJobOptions),\n\t\t});\n\t}\n\n\t// ===========================================================================\n\t// Producer\n\t// ===========================================================================\n\n\tasync add(\n\t\tname: string,\n\t\tdata: unknown,\n\t\toptions: AddOptions = {},\n\t): Promise<AddedJob> {\n\t\tconst merged = { ...this.#defaultJobOptions, ...options };\n\t\tconst job = await this.#queue.add(\n\t\t\tname,\n\t\t\tdata,\n\t\t\tthis.#toBullJobOptions(merged),\n\t\t);\n\t\tthis.#emit({ kind: \"job:added\", jobId: String(job.id ?? \"\"), name });\n\t\treturn { jobId: String(job.id ?? \"\"), name, handle: job };\n\t}\n\n\tasync addBatch(\n\t\tjobs: Array<{ name: string; data: unknown; options?: AddOptions }>,\n\t): Promise<AddedJob[]> {\n\t\tconst bullJobs = jobs.map((j) => ({\n\t\t\tname: j.name,\n\t\t\tdata: j.data,\n\t\t\topts: this.#toBullJobOptions({\n\t\t\t\t...this.#defaultJobOptions,\n\t\t\t\t...j.options,\n\t\t\t}),\n\t\t}));\n\t\tconst added = await this.#queue.addBulk(bullJobs);\n\t\tfor (const job of added) {\n\t\t\tthis.#emit({\n\t\t\t\tkind: \"job:added\",\n\t\t\t\tjobId: String(job.id ?? \"\"),\n\t\t\t\tname: job.name,\n\t\t\t});\n\t\t}\n\t\treturn added.map((job) => ({\n\t\t\tjobId: String(job.id ?? \"\"),\n\t\t\tname: job.name,\n\t\t\thandle: job,\n\t\t}));\n\t}\n\n\t// ===========================================================================\n\t// Worker\n\t// ===========================================================================\n\n\tasync process<T = unknown>(\n\t\tname: string,\n\t\thandler: JobHandler<T>,\n\t\toptions: WorkerOptions = {},\n\t): Promise<WorkerHandle> {\n\t\tconst worker = new Worker(\n\t\t\t\"nexus-queue\",\n\t\t\tasync (job) => {\n\t\t\t\tconst ctx: JobContext = {\n\t\t\t\t\tjobId: String(job.id ?? \"\"),\n\t\t\t\t\tattempts: job.attemptsMade + 1,\n\t\t\t\t\tjob: { name: job.name, data: job.data },\n\t\t\t\t\tprefix: `[queue:${job.name}]`,\n\t\t\t\t};\n\t\t\t\tthis.#emit({\n\t\t\t\t\tkind: \"job:active\",\n\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\tname: job.name,\n\t\t\t\t\tattempts: ctx.attempts,\n\t\t\t\t});\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await handler(job.data as T, ctx);\n\t\t\t\t\tif (result && typeof result === \"object\" && \"status\" in result) {\n\t\t\t\t\t\tconst r = result as { status: string; returnvalue?: unknown };\n\t\t\t\t\t\tif (r.status === \"completed\") {\n\t\t\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\t\t\tkind: \"job:completed\",\n\t\t\t\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\t\t\t\tname: job.name,\n\t\t\t\t\t\t\t\treturnvalue: r.returnvalue,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\treturn r.returnvalue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (r.status === \"retry\") {\n\t\t\t\t\t\t\tconst r2 = result as { delaySeconds?: number; reason?: string };\n\t\t\t\t\t\t\tthrow new RetryError(\n\t\t\t\t\t\t\t\tr2.reason ?? \"retry requested\",\n\t\t\t\t\t\t\t\tr2.delaySeconds,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\tkind: \"job:completed\",\n\t\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\t\tname: job.name,\n\t\t\t\t\t\treturnvalue: result,\n\t\t\t\t\t});\n\t\t\t\t\treturn result;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tif (err instanceof RetryError) {\n\t\t\t\t\t\t// Force a retry with optional delay.\n\t\t\t\t\t\tawait job.moveToDelayed(\n\t\t\t\t\t\t\tDate.now() + (err.delaySeconds ?? 0) * 1000,\n\t\t\t\t\t\t\tjob.token!,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst error = err instanceof Error ? err : new Error(String(err));\n\t\t\t\t\tconst willRetry = (job.opts.attempts ?? 1) > job.attemptsMade;\n\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\tkind: \"job:failed\",\n\t\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\t\tname: job.name,\n\t\t\t\t\t\terror,\n\t\t\t\t\t\twillRetry,\n\t\t\t\t\t});\n\t\t\t\t\tthrow err; // let BullMQ handle the retry\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\tconnection: this.#connection,\n\t\t\t\tprefix: this.#prefix,\n\t\t\t\tconcurrency: options.concurrency ?? 1,\n\t\t\t\tlockDuration: options.lockDurationMs ?? 30000,\n\t\t\t\tlimiter: options.limiter\n\t\t\t\t\t? { max: options.limiter.max, duration: options.limiter.durationMs }\n\t\t\t\t\t: undefined,\n\t\t\t},\n\t\t);\n\n\t\tthis.#workers.set(name, worker);\n\t\tthis.#emit({\n\t\t\tkind: \"worker:started\",\n\t\t\tname,\n\t\t\tconcurrency: options.concurrency ?? 1,\n\t\t});\n\t\treturn new BullMQWorkerHandle(name, worker);\n\t}\n\n\t// ===========================================================================\n\t// Lifecycle\n\t// ===========================================================================\n\n\tasync drain(): Promise<void> {\n\t\t// Wait for active jobs on every worker to reach zero.\n\t\tconst workers = [...this.#workers.values()];\n\t\tawait Promise.all(workers.map((w) => w.waitUntilReady()));\n\t\twhile (\n\t\t\tworkers.some(\n\t\t\t\t(w) => (w as unknown as { _job?: unknown })._job !== undefined,\n\t\t\t)\n\t\t) {\n\t\t\tawait new Promise((r) => setTimeout(r, 50));\n\t\t}\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tfor (const [name, worker] of this.#workers) {\n\t\t\tawait worker.close();\n\t\t\tthis.#emit({ kind: \"worker:stopped\", name });\n\t\t}\n\t\tawait this.#queue.close();\n\t\t// If we own the connection, close it.\n\t\ttry {\n\t\t\tconst conn = this.#connection as { quit?: () => Promise<unknown> };\n\t\t\tif (typeof conn?.quit === \"function\") await conn.quit();\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\t// ===========================================================================\n\t// Events\n\t// ===========================================================================\n\n\ton(listener: QueueEventListener): () => void {\n\t\tthis.#listeners.add(listener);\n\t\treturn () => this.#listeners.delete(listener);\n\t}\n\n\t// ===========================================================================\n\t// Internal\n\t// ===========================================================================\n\n\t#toBullJobOptions(opts: AddOptions): JobsOptions {\n\t\tconst out: JobsOptions = {};\n\t\tif (opts.delaySeconds !== undefined) out.delay = opts.delaySeconds * 1000;\n\t\tif (opts.attempts !== undefined) out.attempts = opts.attempts;\n\t\tif (opts.backoff)\n\t\t\tout.backoff = { type: opts.backoff.type, delay: opts.backoff.delayMs };\n\t\tif (opts.priority !== undefined) out.priority = opts.priority;\n\t\tif (opts.jobId !== undefined) out.jobId = opts.jobId;\n\t\tif (opts.removeOnComplete !== undefined)\n\t\t\tout.removeOnComplete = opts.removeOnComplete;\n\t\tif (opts.removeOnFail !== undefined) out.removeOnFail = opts.removeOnFail;\n\t\treturn out;\n\t}\n\n\t#emit(event: QueueEvent) {\n\t\tfor (const l of this.#listeners) {\n\t\t\tvoid Promise.resolve(l(event));\n\t\t}\n\t}\n}\n\n/** Internal marker error to ask BullMQ to retry with optional delay. */\nclass RetryError extends Error {\n\treadonly __retry = true;\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly delaySeconds?: number,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"RetryError\";\n\t}\n}\n",
7
7
  "/**\n * Cloudflare Queues backend — Workers-native, edge-friendly.\n *\n * Cloudflare Queues has a different shape from BullMQ:\n * - The producer calls `queue.send(body)` / `queue.sendBatch(...)`.\n * - The consumer is a Worker's `queue()` handler that receives a\n * `MessageBatch`.\n *\n * We adapt the two halves to our common `QueueBackend` interface:\n * - `add(name, data)` calls `queue.send({ name, data })` so the\n * consumer knows which handler to route to.\n * - `process(name, handler)` registers the handler in a local\n * `Map`. When the consumer's `queue()` callback fires, we\n * dispatch each message to the matching handler.\n *\n * Because Workers can't start a long-running process, `process()` is\n * a no-op on the producer side. The actual `queue()` handler must be\n * registered separately by the user (or by `QueueModule`'s\n * `workerHandler(env, ctx, batch)` export).\n */\n\nimport type {\n\tQueueBackend,\n\tJobHandler,\n\tWorkerHandle,\n\tWorkerOptions,\n\tAddedJob,\n\tAddOptions,\n\tQueueEvent,\n\tQueueEventListener,\n\tJobContext,\n} from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Cloudflare type stubs (mirrored from @cloudflare/workers-types).\n// We don't import the package to keep the queue module light.\n// ---------------------------------------------------------------------------\n\ninterface CFQueue<Body = unknown> {\n\tsend(\n\t\tbody: Body,\n\t\toptions?: { contentType?: string; delaySeconds?: number },\n\t): Promise<unknown>;\n\tsendBatch(\n\t\tmessages: Array<{\n\t\t\tbody: unknown;\n\t\t\tcontentType?: string;\n\t\t\tdelaySeconds?: number;\n\t\t}>,\n\t\toptions?: { delaySeconds?: number },\n\t): Promise<unknown>;\n}\n\ninterface CFMessage<Body = unknown> {\n\treadonly id: string;\n\treadonly timestamp: Date;\n\treadonly body: Body;\n\treadonly attempts: number;\n\tack(): void;\n\tretry(options?: { delaySeconds?: number }): void;\n}\n\ninterface CFMessageBatch<Body = unknown> {\n\treadonly queue: string;\n\treadonly messages: readonly CFMessage<Body>[];\n\tackAll(): void;\n\tretryAll(options?: { delaySeconds?: number }): void;\n}\n\n// ---------------------------------------------------------------------------\n// Worker handle\n// ---------------------------------------------------------------------------\n\nclass CloudflareWorkerHandle implements WorkerHandle {\n\t#name: string;\n\t#running = true;\n\tconstructor(name: string) {\n\t\tthis.#name = name;\n\t}\n\tget name() {\n\t\treturn this.#name;\n\t}\n\tasync close() {\n\t\tthis.#running = false;\n\t}\n\tasync pause() {\n\t\tthis.#running = false;\n\t}\n\tasync resume() {\n\t\tthis.#running = true;\n\t}\n\tisRunning() {\n\t\treturn this.#running;\n\t}\n}\n\nexport interface CloudflareBackendOptions {\n\t/** Resolver that pulls the Queue binding from the Worker's env. */\n\tresolveBinding: (env: Record<string, unknown>) => CFQueue;\n\t/** Queue name (for diagnostics + MessageBatch.queue matching). */\n\tname?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Backend\n// ---------------------------------------------------------------------------\n\nexport class CloudflareQueueBackend implements QueueBackend {\n\treadonly name = \"cloudflare\" as const;\n\t#queue: CFQueue | null = null;\n\t#resolveBinding: (env: Record<string, unknown>) => CFQueue;\n\t#handlers = new Map<string, JobHandler>();\n\t#workerOptions = new Map<string, WorkerOptions>();\n\t#listeners = new Set<QueueEventListener>();\n\t#queueName: string;\n\n\tconstructor(options: CloudflareBackendOptions) {\n\t\tthis.#resolveBinding = options.resolveBinding;\n\t\tthis.#queueName = options.name ?? \"queue\";\n\t}\n\n\t/** Bind to the Worker's `env` once it's available. */\n\tbind(env: Record<string, unknown>): void {\n\t\tthis.#queue = this.#resolveBinding(env);\n\t}\n\n\t// ===========================================================================\n\t// Producer\n\t// ===========================================================================\n\n\tasync add(\n\t\tname: string,\n\t\tdata: unknown,\n\t\toptions: AddOptions = {},\n\t): Promise<AddedJob> {\n\t\tif (!this.#queue)\n\t\t\tthrow new Error(\"[queue/cloudflare] bind() must be called before add()\");\n\t\tconst id = `cf-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n\t\tawait this.#queue.send(\n\t\t\t{ name, data, jobId: id, options },\n\t\t\t{ delaySeconds: options.delaySeconds },\n\t\t);\n\t\tthis.#emit({ kind: \"job:added\", jobId: id, name });\n\t\treturn { jobId: id, name };\n\t}\n\n\tasync addBatch(\n\t\tjobs: Array<{ name: string; data: unknown; options?: AddOptions }>,\n\t): Promise<AddedJob[]> {\n\t\tif (!this.#queue)\n\t\t\tthrow new Error(\n\t\t\t\t\"[queue/cloudflare] bind() must be called before addBatch()\",\n\t\t\t);\n\t\tconst ids = jobs.map(\n\t\t\t() => `cf-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\n\t\t);\n\t\tawait this.#queue.sendBatch(\n\t\t\tjobs.map((j, i) => ({\n\t\t\t\tbody: { name: j.name, data: j.data, jobId: ids[i], options: j.options },\n\t\t\t\tdelaySeconds: j.options?.delaySeconds,\n\t\t\t})),\n\t\t);\n\t\tfor (let i = 0; i < jobs.length; i++) {\n\t\t\tconst j = jobs[i]!;\n\t\t\tthis.#emit({ kind: \"job:added\", jobId: ids[i]!, name: j.name });\n\t\t}\n\t\treturn jobs.map((j, i) => ({ jobId: ids[i]!, name: j.name }));\n\t}\n\n\t// ===========================================================================\n\t// Worker registration\n\t// ===========================================================================\n\n\tasync process<T = unknown>(\n\t\tname: string,\n\t\thandler: JobHandler<T>,\n\t\toptions: WorkerOptions = {},\n\t): Promise<WorkerHandle> {\n\t\tthis.#handlers.set(name, handler as JobHandler);\n\t\tthis.#workerOptions.set(name, options);\n\t\tthis.#emit({\n\t\t\tkind: \"worker:started\",\n\t\t\tname,\n\t\t\tconcurrency: options.concurrency ?? 1,\n\t\t});\n\t\treturn new CloudflareWorkerHandle(name);\n\t}\n\n\t// ===========================================================================\n\t// Cloudflare consumer entry point\n\t// ===========================================================================\n\n\t/**\n\t * Build the Worker's `queue()` handler. Mount it as\n\t * `export default { queue: backend.consumerHandler() }` in the\n\t * Worker entry file.\n\t */\n\tconsumerHandler(): (batch: CFMessageBatch<unknown>) => Promise<void> {\n\t\treturn async (batch: CFMessageBatch<unknown>) => {\n\t\t\tfor (const message of batch.messages) {\n\t\t\t\tconst body = (message.body ?? {}) as {\n\t\t\t\t\tname?: string;\n\t\t\t\t\tdata?: unknown;\n\t\t\t\t\tjobId?: string;\n\t\t\t\t\toptions?: AddOptions;\n\t\t\t\t};\n\t\t\t\tconst jobName = body.name ?? \"\";\n\t\t\t\tconst handler = this.#handlers.get(jobName);\n\t\t\t\tif (!handler) {\n\t\t\t\t\t// No handler registered — fail so the message is retried.\n\t\t\t\t\tmessage.retry();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst ctx: JobContext = {\n\t\t\t\t\tjobId: body.jobId ?? message.id,\n\t\t\t\t\tattempts: message.attempts,\n\t\t\t\t\tjob: { name: jobName, data: body.data },\n\t\t\t\t\tprefix: `[queue:${jobName}]`,\n\t\t\t\t};\n\t\t\t\tthis.#emit({\n\t\t\t\t\tkind: \"job:active\",\n\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\tname: jobName,\n\t\t\t\t\tattempts: ctx.attempts,\n\t\t\t\t});\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await handler(body.data, ctx);\n\t\t\t\t\tif (result && typeof result === \"object\" && \"status\" in result) {\n\t\t\t\t\t\tconst r = result as {\n\t\t\t\t\t\t\tstatus: string;\n\t\t\t\t\t\t\treturnvalue?: unknown;\n\t\t\t\t\t\t\terror?: Error;\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif (r.status === \"failed\") {\n\t\t\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\t\t\tkind: \"job:failed\",\n\t\t\t\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\t\t\t\tname: jobName,\n\t\t\t\t\t\t\t\terror: r.error ?? new Error(\"failed\"),\n\t\t\t\t\t\t\t\twillRetry: true,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tmessage.retry();\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (r.status === \"retry\") {\n\t\t\t\t\t\t\tconst r2 = result as { delaySeconds?: number };\n\t\t\t\t\t\t\tmessage.retry({ delaySeconds: r2.delaySeconds });\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\t\tkind: \"job:completed\",\n\t\t\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\t\t\tname: jobName,\n\t\t\t\t\t\t\treturnvalue: r.returnvalue,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\t\tkind: \"job:completed\",\n\t\t\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\t\t\tname: jobName,\n\t\t\t\t\t\t\treturnvalue: result,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst error = err instanceof Error ? err : new Error(String(err));\n\t\t\t\t\tconst willRetry = (body.options?.attempts ?? 1) > message.attempts;\n\t\t\t\t\tthis.#emit({\n\t\t\t\t\t\tkind: \"job:failed\",\n\t\t\t\t\t\tjobId: ctx.jobId,\n\t\t\t\t\t\tname: jobName,\n\t\t\t\t\t\terror,\n\t\t\t\t\t\twillRetry,\n\t\t\t\t\t});\n\t\t\t\t\tif (willRetry) message.retry();\n\t\t\t\t\telse message.ack();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\t// ===========================================================================\n\t// Lifecycle\n\t// ===========================================================================\n\n\tasync drain(): Promise<void> {\n\t\t// Cloudflare handles draining at the platform level (between\n\t\t// requests, the isolate is torn down). Nothing to do here.\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tfor (const name of this.#handlers.keys()) {\n\t\t\tthis.#emit({ kind: \"worker:stopped\", name });\n\t\t}\n\t\tthis.#handlers.clear();\n\t\tthis.#workerOptions.clear();\n\t}\n\n\t// ===========================================================================\n\t// Events\n\t// ===========================================================================\n\n\ton(listener: QueueEventListener): () => void {\n\t\tthis.#listeners.add(listener);\n\t\treturn () => this.#listeners.delete(listener);\n\t}\n\n\t// ===========================================================================\n\t// Internal\n\t// ===========================================================================\n\n\t#emit(event: QueueEvent) {\n\t\tfor (const l of this.#listeners) {\n\t\t\tvoid Promise.resolve(l(event));\n\t\t}\n\t}\n\n\tget queueName() {\n\t\treturn this.#queueName;\n\t}\n}\n",
8
- "/**\n * `QueueService` — DI-friendly facade over a `QueueBackend`.\n *\n * Controllers and other services inject this via `@Inject(QueueService.TOKEN)`\n * (or just `@Inject(QueueService)`) and call high-level methods without\n * caring whether the underlying backend is BullMQ, Cloudflare Queues,\n * or the in-memory test backend.\n *\n * Two layers:\n * - `QueueService.add(name, data, options?)` → schedule a job\n * - `QueueService.process(name, handler)` → register a worker\n *\n * The lifecycle is owned by `QueueService.start()` (called by\n * `QueueModule.forRoot` when the application boots) and\n * `QueueService.stop()` (called on shutdown).\n */\n\nimport { Inject, Injectable } from '@nexusts/core/src/decorators/index.js';\nimport type {\n\tQueueBackend,\n\tQueueConfig,\n\tJobHandler,\n\tWorkerHandle,\n\tAddedJob,\n\tAddOptions,\n\tQueueEvent,\n\tQueueEventListener,\n} from './types.js';\nimport {\n\tMemoryQueueBackend,\n\tBullMQBackend,\n\tCloudflareQueueBackend,\n} from './backends/index.js';\n\n@Injectable()\nexport class QueueService {\n\t/** DI token — use with `@Inject(QueueService.TOKEN)`. */\n\tstatic readonly TOKEN = Symbol.for('nexus:QueueService');\n\n\t/** The underlying backend. */\n\treadonly backend: QueueBackend;\n\t#workers = new Map<string, WorkerHandle>();\n\t#listeners = new Set<QueueEventListener>();\n\t#started = false;\n\n\tconstructor(@Inject('QUEUE_CONFIG') private readonly config: QueueConfig) {\n\t\tthis.backend = this.#createBackend(config);\n\t}\n\n\t// ===========================================================================\n\t// Producer API\n\t// ===========================================================================\n\n\t/**\n\t * Enqueue a job. Returns immediately (the backend may still be\n\t * persisting). On Cloudflare, returns once the message is on disk.\n\t */\n\tasync add<T = unknown>(\n\t\tname: string,\n\t\tdata: T,\n\t\toptions: AddOptions = {},\n\t): Promise<AddedJob> {\n\t\tconst merged = { ...this.config.defaults, ...options };\n\t\treturn this.backend.add(name, data, merged);\n\t}\n\n\t/** Enqueue many jobs at once (atomic on Cloudflare; batched on BullMQ). */\n\tasync addBatch<T = unknown>(\n\t\tjobs: Array<{ name: string; data: T; options?: AddOptions }>,\n\t): Promise<AddedJob[]> {\n\t\treturn this.backend.addBatch(jobs);\n\t}\n\n\t// ===========================================================================\n\t// Worker API\n\t// ===========================================================================\n\n\t/**\n\t * Register a worker for the given job name. Call this from\n\t * `Application.bootstrap` (or a feature module's `onInit`) — the\n\t * module wires it via DI so user code doesn't have to call this\n\t * manually.\n\t */\n\tasync process<T = unknown>(\n\t\tname: string,\n\t\thandler: JobHandler<T>,\n\t): Promise<WorkerHandle> {\n\t\tconst handle = await this.backend.process(name, handler);\n\t\tthis.#workers.set(name, handle);\n\t\treturn handle;\n\t}\n\n\t// ===========================================================================\n\t// Events\n\t// ===========================================================================\n\n\ton(listener: QueueEventListener): () => void {\n\t\tthis.#listeners.add(listener);\n\t\treturn () => this.#listeners.delete(listener);\n\t}\n\n\t// ===========================================================================\n\t// Lifecycle\n\t// ===========================================================================\n\n\t/** Called by `QueueModule.forRoot` when the app boots. */\n\tasync start(): Promise<void> {\n\t\tif (this.#started) return;\n\t\tthis.#started = true;\n\t\t// Bridge backend events through our listener set.\n\t\tthis.backend.on((event) => this.#broadcast(event));\n\t}\n\n\t/** Drain in-flight jobs and close all workers. */\n\tasync stop(): Promise<void> {\n\t\tif (!this.#started) return;\n\t\tthis.#started = false;\n\t\tawait this.backend.drain();\n\t\tfor (const handle of this.#workers.values()) {\n\t\t\tawait handle.close();\n\t\t}\n\t\tawait this.backend.stop();\n\t\tthis.#workers.clear();\n\t}\n\n\t// ===========================================================================\n\t// Cloudflare binding helper\n\t// ===========================================================================\n\n\t/**\n\t * For Cloudflare backends, call this once with the Worker's env so\n\t * the producer can find the Queue binding.\n\t *\n\t * const service = app.container.resolve(QueueService.TOKEN);\n\t * if (service.backend.name === 'cloudflare') {\n\t * (service.backend as CloudflareQueueBackend).bind(env);\n\t * }\n\t */\n\tgetCloudflareBackend(): CloudflareQueueBackend | null {\n\t\treturn this.backend instanceof CloudflareQueueBackend ? this.backend : null;\n\t}\n\n\t// ===========================================================================\n\t// Internal\n\t// ===========================================================================\n\n\t#createBackend(config: QueueConfig): QueueBackend {\n\t\tswitch (config.backend) {\n\t\t\tcase 'memory':\n\t\t\t\treturn new MemoryQueueBackend();\n\t\t\tcase 'bullmq': {\n\t\t\t\tif (!config.bullmq) {\n\t\t\t\t\tthrow new Error('[queue] backend=bullmq requires `bullmq.connection` in config.');\n\t\t\t\t}\n\t\t\t\treturn new BullMQBackend({\n\t\t\t\t\tconnection: config.bullmq.connection,\n\t\t\t\t\tprefix: config.bullmq.prefix,\n\t\t\t\t\tdefaultJobOptions: config.bullmq.defaultJobOptions,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcase 'cloudflare': {\n\t\t\t\tif (!config.cloudflare) {\n\t\t\t\t\tthrow new Error('[queue] backend=cloudflare requires `cloudflare.resolveBinding` in config.');\n\t\t\t\t}\n\t\t\t\treturn new CloudflareQueueBackend({\n\t\t\t\t\tresolveBinding: config.cloudflare.resolveBinding as never,\n\t\t\t\t\tname: config.cloudflare.name,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t#broadcast(event: QueueEvent) {\n\t\tfor (const l of this.#listeners) {\n\t\t\tvoid Promise.resolve(l(event));\n\t\t}\n\t}\n}",
9
- "/**\n * `QueueModule` — drop-in module for adding background jobs to a\n * NexusTS app.\n *\n * Usage:\n * // src/app/app.module.ts\n * @Module({\n * imports: [\n * QueueModule.forRoot({\n * backend: 'bullmq',\n * bullmq: { connection: 'redis://localhost:6379' },\n * }),\n * ],\n * })\n * export class AppModule {}\n *\n * // any controller or service\n * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}\n * await this.queue.add('send-email', { to: 'a@b.c' });\n *\n * // any service — register a worker\n * class EmailWorker {\n * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}\n * async onInit() {\n * await this.queue.process('send-email', async (data) => {\n * // ... send the email\n * return { status: 'completed' };\n * });\n * }\n * }\n */\n\nimport \"reflect-metadata\";\nimport { Module } from \"@nexusts/core/decorators/module.js\";\nimport { QueueService } from \"./queue.service.js\";\nimport type { QueueConfig } from \"./types.js\";\n\n@Module({\n\tproviders: [\n\t\tQueueService,\n\t\t{ provide: QueueService.TOKEN, useExisting: QueueService },\n\t],\n\texports: [QueueService, QueueService.TOKEN],\n})\nexport class QueueModule {\n\t/**\n\t * Build a configured `QueueModule` class with the given queue config.\n\t *\n\t * The returned class can be `imports`-ed by any other module and\n\t * will provide the `QueueService` (and a `QUEUE_CONFIG` value\n\t * provider) to its container.\n\t */\n\tstatic forRoot(config: QueueConfig) {\n\t\t@Module({\n\t\t\tproviders: [\n\t\t\t\tQueueService,\n\t\t\t\t{ provide: QueueService.TOKEN, useExisting: QueueService },\n\t\t\t\t{ provide: \"QUEUE_CONFIG\", useValue: config },\n\t\t\t],\n\t\t\texports: [QueueService, QueueService.TOKEN],\n\t\t})\n\t\tclass ConfiguredQueueModule {}\n\n\t\tObject.defineProperty(ConfiguredQueueModule, \"name\", {\n\t\t\tvalue: \"ConfiguredQueueModule\",\n\t\t});\n\n\t\treturn ConfiguredQueueModule;\n\t}\n}\n",
8
+ "/**\n * `QueueService` — DI-friendly facade over a `QueueBackend`.\n *\n * Controllers and other services inject this via `@Inject(QueueService.TOKEN)`\n * (or just `@Inject(QueueService)`) and call high-level methods without\n * caring whether the underlying backend is BullMQ, Cloudflare Queues,\n * or the in-memory test backend.\n *\n * Two layers:\n * - `QueueService.add(name, data, options?)` → schedule a job\n * - `QueueService.process(name, handler)` → register a worker\n *\n * The lifecycle is owned by `QueueService.start()` (called by\n * `QueueModule.forRoot` when the application boots) and\n * `QueueService.stop()` (called on shutdown).\n */\n\nimport { Inject, Injectable } from '@nexusts/core';\nimport type {\n\tQueueBackend,\n\tQueueConfig,\n\tJobHandler,\n\tWorkerHandle,\n\tAddedJob,\n\tAddOptions,\n\tQueueEvent,\n\tQueueEventListener,\n} from './types.js';\nimport {\n\tMemoryQueueBackend,\n\tBullMQBackend,\n\tCloudflareQueueBackend,\n} from './backends/index.js';\n\n@Injectable()\nexport class QueueService {\n\t/** DI token — use with `@Inject(QueueService.TOKEN)`. */\n\tstatic readonly TOKEN = Symbol.for('nexus:QueueService');\n\n\t/** The underlying backend. */\n\treadonly backend: QueueBackend;\n\t#workers = new Map<string, WorkerHandle>();\n\t#listeners = new Set<QueueEventListener>();\n\t#started = false;\n\n\tconstructor(@Inject('QUEUE_CONFIG') private readonly config: QueueConfig) {\n\t\tthis.backend = this.#createBackend(config);\n\t}\n\n\t// ===========================================================================\n\t// Producer API\n\t// ===========================================================================\n\n\t/**\n\t * Enqueue a job. Returns immediately (the backend may still be\n\t * persisting). On Cloudflare, returns once the message is on disk.\n\t */\n\tasync add<T = unknown>(\n\t\tname: string,\n\t\tdata: T,\n\t\toptions: AddOptions = {},\n\t): Promise<AddedJob> {\n\t\tconst merged = { ...this.config.defaults, ...options };\n\t\treturn this.backend.add(name, data, merged);\n\t}\n\n\t/** Enqueue many jobs at once (atomic on Cloudflare; batched on BullMQ). */\n\tasync addBatch<T = unknown>(\n\t\tjobs: Array<{ name: string; data: T; options?: AddOptions }>,\n\t): Promise<AddedJob[]> {\n\t\treturn this.backend.addBatch(jobs);\n\t}\n\n\t// ===========================================================================\n\t// Worker API\n\t// ===========================================================================\n\n\t/**\n\t * Register a worker for the given job name. Call this from\n\t * `Application.bootstrap` (or a feature module's `onInit`) — the\n\t * module wires it via DI so user code doesn't have to call this\n\t * manually.\n\t */\n\tasync process<T = unknown>(\n\t\tname: string,\n\t\thandler: JobHandler<T>,\n\t): Promise<WorkerHandle> {\n\t\tconst handle = await this.backend.process(name, handler);\n\t\tthis.#workers.set(name, handle);\n\t\treturn handle;\n\t}\n\n\t// ===========================================================================\n\t// Events\n\t// ===========================================================================\n\n\ton(listener: QueueEventListener): () => void {\n\t\tthis.#listeners.add(listener);\n\t\treturn () => this.#listeners.delete(listener);\n\t}\n\n\t// ===========================================================================\n\t// Lifecycle\n\t// ===========================================================================\n\n\t/** Called by `QueueModule.forRoot` when the app boots. */\n\tasync start(): Promise<void> {\n\t\tif (this.#started) return;\n\t\tthis.#started = true;\n\t\t// Bridge backend events through our listener set.\n\t\tthis.backend.on((event) => this.#broadcast(event));\n\t}\n\n\t/** Drain in-flight jobs and close all workers. */\n\tasync stop(): Promise<void> {\n\t\tif (!this.#started) return;\n\t\tthis.#started = false;\n\t\tawait this.backend.drain();\n\t\tfor (const handle of this.#workers.values()) {\n\t\t\tawait handle.close();\n\t\t}\n\t\tawait this.backend.stop();\n\t\tthis.#workers.clear();\n\t}\n\n\t// ===========================================================================\n\t// Cloudflare binding helper\n\t// ===========================================================================\n\n\t/**\n\t * For Cloudflare backends, call this once with the Worker's env so\n\t * the producer can find the Queue binding.\n\t *\n\t * const service = app.container.resolve(QueueService.TOKEN);\n\t * if (service.backend.name === 'cloudflare') {\n\t * (service.backend as CloudflareQueueBackend).bind(env);\n\t * }\n\t */\n\tgetCloudflareBackend(): CloudflareQueueBackend | null {\n\t\treturn this.backend instanceof CloudflareQueueBackend ? this.backend : null;\n\t}\n\n\t// ===========================================================================\n\t// Internal\n\t// ===========================================================================\n\n\t#createBackend(config: QueueConfig): QueueBackend {\n\t\tswitch (config.backend) {\n\t\t\tcase 'memory':\n\t\t\t\treturn new MemoryQueueBackend();\n\t\t\tcase 'bullmq': {\n\t\t\t\tif (!config.bullmq) {\n\t\t\t\t\tthrow new Error('[queue] backend=bullmq requires `bullmq.connection` in config.');\n\t\t\t\t}\n\t\t\t\treturn new BullMQBackend({\n\t\t\t\t\tconnection: config.bullmq.connection,\n\t\t\t\t\tprefix: config.bullmq.prefix,\n\t\t\t\t\tdefaultJobOptions: config.bullmq.defaultJobOptions,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcase 'cloudflare': {\n\t\t\t\tif (!config.cloudflare) {\n\t\t\t\t\tthrow new Error('[queue] backend=cloudflare requires `cloudflare.resolveBinding` in config.');\n\t\t\t\t}\n\t\t\t\treturn new CloudflareQueueBackend({\n\t\t\t\t\tresolveBinding: config.cloudflare.resolveBinding as never,\n\t\t\t\t\tname: config.cloudflare.name,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t#broadcast(event: QueueEvent) {\n\t\tfor (const l of this.#listeners) {\n\t\t\tvoid Promise.resolve(l(event));\n\t\t}\n\t}\n}",
9
+ "/**\n * `QueueModule` — drop-in module for adding background jobs to a\n * NexusTS app.\n *\n * Usage:\n * // src/app/app.module.ts\n * @Module({\n * imports: [\n * QueueModule.forRoot({\n * backend: 'bullmq',\n * bullmq: { connection: 'redis://localhost:6379' },\n * }),\n * ],\n * })\n * export class AppModule {}\n *\n * // any controller or service\n * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}\n * await this.queue.add('send-email', { to: 'a@b.c' });\n *\n * // any service — register a worker\n * class EmailWorker {\n * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}\n * async onInit() {\n * await this.queue.process('send-email', async (data) => {\n * // ... send the email\n * return { status: 'completed' };\n * });\n * }\n * }\n */\n\nimport \"reflect-metadata\";\nimport { Module } from \"@nexusts/core\";\nimport { QueueService } from \"./queue.service.js\";\nimport type { QueueConfig } from \"./types.js\";\n\n@Module({\n\tproviders: [\n\t\tQueueService,\n\t\t{ provide: QueueService.TOKEN, useExisting: QueueService },\n\t],\n\texports: [QueueService, QueueService.TOKEN],\n})\nexport class QueueModule {\n\t/**\n\t * Build a configured `QueueModule` class with the given queue config.\n\t *\n\t * The returned class can be `imports`-ed by any other module and\n\t * will provide the `QueueService` (and a `QUEUE_CONFIG` value\n\t * provider) to its container.\n\t */\n\tstatic forRoot(config: QueueConfig) {\n\t\t@Module({\n\t\t\tproviders: [\n\t\t\t\tQueueService,\n\t\t\t\t{ provide: QueueService.TOKEN, useExisting: QueueService },\n\t\t\t\t{ provide: \"QUEUE_CONFIG\", useValue: config },\n\t\t\t],\n\t\t\texports: [QueueService, QueueService.TOKEN],\n\t\t})\n\t\tclass ConfiguredQueueModule {}\n\n\t\tObject.defineProperty(ConfiguredQueueModule, \"name\", {\n\t\t\tvalue: \"ConfiguredQueueModule\",\n\t\t});\n\n\t\treturn ConfiguredQueueModule;\n\t}\n}\n",
10
10
  "/**\n * `@OnQueueReady()` — lifecycle hook that runs once when the\n * application has booted and the queue service is ready.\n *\n * Use this to register workers without coupling to the `Application`\n * lifecycle directly.\n *\n * Usage:\n * class EmailWorker {\n * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}\n *\n * @OnQueueReady()\n * async register() {\n * await this.queue.process('send-email', this.handle);\n * }\n *\n * handle = async (data: { to: string }) => { ... };\n * }\n *\n * @Module({\n * providers: [EmailWorker],\n * })\n * class WorkerModule {}\n */\n\nimport \"reflect-metadata\";\nimport type { QueueService } from \"../queue.service.js\";\n\n/**\n * Method decorator. The decorated method is invoked once with no\n * arguments after the application has booted. Pair it with\n * `QueueService.start()` — typically called by `Application.bootstrap`.\n */\nexport function OnQueueReady(): MethodDecorator {\n\treturn (target, propertyKey, descriptor) => {\n\t\tif (!descriptor || typeof descriptor.value !== \"function\") {\n\t\t\tthrow new Error(\"@OnQueueReady can only decorate methods.\");\n\t\t}\n\t\t// Register the hook in a per-class metadata slot. The bootstrap\n\t\t// code reads METADATA_KEY.QUEUE_READY_HOOKS and calls each.\n\t\tconst ctor = target.constructor as object;\n\t\tconst hooks: Array<string | symbol> =\n\t\t\t(Reflect.getMetadata(\"nexus:queue:ready-hooks\", ctor) as\n\t\t\t\t| Array<string | symbol>\n\t\t\t\t| undefined) ?? [];\n\t\thooks.push(propertyKey!);\n\t\tReflect.defineMetadata(\"nexus:queue:ready-hooks\", hooks, ctor);\n\t};\n}\n\n/**\n * Get the queue-ready hooks declared on a class.\n */\nexport function getQueueReadyHooks(target: unknown): Array<string | symbol> {\n\tconst ctor =\n\t\t(target as { constructor?: object }).constructor ?? (target as object);\n\treturn (\n\t\t(Reflect.getMetadata(\"nexus:queue:ready-hooks\", ctor) as\n\t\t\t| Array<string | symbol>\n\t\t\t| undefined) ?? []\n\t);\n}\n\n/**\n * Helper — invoke all `@OnQueueReady` hooks on an instance.\n * Pair this with `QueueService.start()` for a complete bootstrap.\n */\nexport async function invokeQueueReadyHooks(instance: object): Promise<void> {\n\tconst hooks = getQueueReadyHooks(instance);\n\tfor (const key of hooks) {\n\t\tconst fn = (instance as Record<string | symbol, unknown>)[key] as\n\t\t\t| ((...args: unknown[]) => Promise<void> | void)\n\t\t\t| undefined;\n\t\tif (typeof fn === \"function\") {\n\t\t\tawait fn.call(instance);\n\t\t}\n\t}\n}\n\n// Re-export for convenience.\nexport type { QueueService };\n"
11
11
  ],
12
12
  "mappings": "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",
13
- "debugId": "77CE421E465283D764756E2164756E21",
13
+ "debugId": "0DEA3B2743EBA89564756E2164756E21",
14
14
  "names": []
15
15
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `QueueModule` — drop-in module for adding background jobs to a
3
+ * NexusTS app.
4
+ *
5
+ * Usage:
6
+ * // src/app/app.module.ts
7
+ * @Module({
8
+ * imports: [
9
+ * QueueModule.forRoot({
10
+ * backend: 'bullmq',
11
+ * bullmq: { connection: 'redis://localhost:6379' },
12
+ * }),
13
+ * ],
14
+ * })
15
+ * export class AppModule {}
16
+ *
17
+ * // any controller or service
18
+ * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}
19
+ * await this.queue.add('send-email', { to: 'a@b.c' });
20
+ *
21
+ * // any service — register a worker
22
+ * class EmailWorker {
23
+ * constructor(@Inject(QueueService.TOKEN) private queue: QueueService) {}
24
+ * async onInit() {
25
+ * await this.queue.process('send-email', async (data) => {
26
+ * // ... send the email
27
+ * return { status: 'completed' };
28
+ * });
29
+ * }
30
+ * }
31
+ */
32
+ import "reflect-metadata";
33
+ import type { QueueConfig } from "./types.js";
34
+ export declare class QueueModule {
35
+ /**
36
+ * Build a configured `QueueModule` class with the given queue config.
37
+ *
38
+ * The returned class can be `imports`-ed by any other module and
39
+ * will provide the `QueueService` (and a `QUEUE_CONFIG` value
40
+ * provider) to its container.
41
+ */
42
+ static forRoot(config: QueueConfig): {
43
+ new (): {};
44
+ };
45
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `QueueService` — DI-friendly facade over a `QueueBackend`.
3
+ *
4
+ * Controllers and other services inject this via `@Inject(QueueService.TOKEN)`
5
+ * (or just `@Inject(QueueService)`) and call high-level methods without
6
+ * caring whether the underlying backend is BullMQ, Cloudflare Queues,
7
+ * or the in-memory test backend.
8
+ *
9
+ * Two layers:
10
+ * - `QueueService.add(name, data, options?)` → schedule a job
11
+ * - `QueueService.process(name, handler)` → register a worker
12
+ *
13
+ * The lifecycle is owned by `QueueService.start()` (called by
14
+ * `QueueModule.forRoot` when the application boots) and
15
+ * `QueueService.stop()` (called on shutdown).
16
+ */
17
+ import type { QueueBackend, QueueConfig, JobHandler, WorkerHandle, AddedJob, AddOptions, QueueEventListener } from './types.js';
18
+ import { CloudflareQueueBackend } from './backends/index.js';
19
+ export declare class QueueService {
20
+ #private;
21
+ private readonly config;
22
+ /** DI token — use with `@Inject(QueueService.TOKEN)`. */
23
+ static readonly TOKEN: unique symbol;
24
+ /** The underlying backend. */
25
+ readonly backend: QueueBackend;
26
+ constructor(config: QueueConfig);
27
+ /**
28
+ * Enqueue a job. Returns immediately (the backend may still be
29
+ * persisting). On Cloudflare, returns once the message is on disk.
30
+ */
31
+ add<T = unknown>(name: string, data: T, options?: AddOptions): Promise<AddedJob>;
32
+ /** Enqueue many jobs at once (atomic on Cloudflare; batched on BullMQ). */
33
+ addBatch<T = unknown>(jobs: Array<{
34
+ name: string;
35
+ data: T;
36
+ options?: AddOptions;
37
+ }>): Promise<AddedJob[]>;
38
+ /**
39
+ * Register a worker for the given job name. Call this from
40
+ * `Application.bootstrap` (or a feature module's `onInit`) — the
41
+ * module wires it via DI so user code doesn't have to call this
42
+ * manually.
43
+ */
44
+ process<T = unknown>(name: string, handler: JobHandler<T>): Promise<WorkerHandle>;
45
+ on(listener: QueueEventListener): () => void;
46
+ /** Called by `QueueModule.forRoot` when the app boots. */
47
+ start(): Promise<void>;
48
+ /** Drain in-flight jobs and close all workers. */
49
+ stop(): Promise<void>;
50
+ /**
51
+ * For Cloudflare backends, call this once with the Worker's env so
52
+ * the producer can find the Queue binding.
53
+ *
54
+ * const service = app.container.resolve(QueueService.TOKEN);
55
+ * if (service.backend.name === 'cloudflare') {
56
+ * (service.backend as CloudflareQueueBackend).bind(env);
57
+ * }
58
+ */
59
+ getCloudflareBackend(): CloudflareQueueBackend | null;
60
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Queue types — the public contract for `nexusjs/queue`.
3
+ *
4
+ * The queue module provides an abstraction over two backends:
5
+ * - BullMQ (Redis-backed, for Bun / Node / long-running servers)
6
+ * - Cloudflare Queues (Workers-native, edge-friendly)
7
+ *
8
+ * Both share a common `QueueBackend` interface so user code never
9
+ * knows which one is configured — just like `ViewAdapter` for views.
10
+ */
11
+ /**
12
+ * The payload sent to a worker. `name` selects the handler; `data` is
13
+ * the typed payload.
14
+ */
15
+ export interface Job<T = unknown> {
16
+ name: string;
17
+ data: T;
18
+ }
19
+ /**
20
+ * A job's outcome. `completed` is the normal path; `failed` triggers
21
+ * the configured retry policy; `retry` explicitly asks for one more
22
+ * attempt (BullMQ only — Cloudflare retries via `message.retry()`).
23
+ */
24
+ export type JobResult<T = unknown> = {
25
+ status: "completed";
26
+ returnvalue?: T;
27
+ } | {
28
+ status: "failed";
29
+ error: Error;
30
+ willRetry: boolean;
31
+ } | {
32
+ status: "retry";
33
+ reason?: string;
34
+ delaySeconds?: number;
35
+ };
36
+ /** A registered worker handler. */
37
+ export type JobHandler<T = unknown> = (data: T, ctx: JobContext) => Promise<JobResult<T> | void> | JobResult<T> | void;
38
+ /** Per-job execution context. */
39
+ export interface JobContext {
40
+ /** The unique BullMQ / Cloudflare job ID. */
41
+ jobId: string;
42
+ /** Number of attempts so far (1-indexed). */
43
+ attempts: number;
44
+ /** Original `Job` envelope (name + data). */
45
+ job: Job;
46
+ /** Logger-friendly prefix: `[queue:name]`. */
47
+ prefix: string;
48
+ }
49
+ /**
50
+ * Options for `add()`. Mirrors BullMQ's JobOpts plus our
51
+ * edge-friendly extensions.
52
+ */
53
+ export interface AddOptions {
54
+ /** Delay before the job is available. Seconds. */
55
+ delaySeconds?: number;
56
+ /** Max attempts before the job is dead-lettered. */
57
+ attempts?: number;
58
+ /** Backoff strategy between retries. */
59
+ backoff?: BackoffConfig;
60
+ /** Priority — lower runs first. Cloudflare ignores this. */
61
+ priority?: number;
62
+ /** Job ID for idempotency (BullMQ). */
63
+ jobId?: string;
64
+ /** Remove from the queue on completion. */
65
+ removeOnComplete?: boolean | number;
66
+ /** Remove from the queue on failure. */
67
+ removeOnFail?: boolean | number;
68
+ }
69
+ export interface BackoffConfig {
70
+ /** 'fixed' | 'exponential'. */
71
+ type: "fixed" | "exponential";
72
+ /** Base delay in milliseconds. */
73
+ delayMs: number;
74
+ }
75
+ /** Returned by `add()` — enough info to track or cancel. */
76
+ export interface AddedJob {
77
+ jobId: string;
78
+ name: string;
79
+ /** Backend-specific handle (BullMQ Job, Cloudflare Message, ...). */
80
+ handle?: unknown;
81
+ }
82
+ /** Options for `process()`. */
83
+ export interface WorkerOptions {
84
+ /** How many jobs this worker handles in parallel. Default: 1. */
85
+ concurrency?: number;
86
+ /** Rate limit — jobs per second. */
87
+ limiter?: {
88
+ max: number;
89
+ durationMs: number;
90
+ };
91
+ /** Lock duration in ms — how long a worker holds the job. Default: 30000. */
92
+ lockDurationMs?: number;
93
+ }
94
+ /**
95
+ * Backend contract. Each backend (BullMQ, Cloudflare) implements this.
96
+ *
97
+ * User code talks to `QueueService`, not to the backend directly —
98
+ * but advanced users can `@Inject(QueueService.BACKEND_TOKEN)` if they
99
+ * need a specific backend's native API.
100
+ */
101
+ export interface QueueBackend {
102
+ /** Backend name for diagnostics. */
103
+ readonly name: "bullmq" | "cloudflare" | "memory";
104
+ /** Add a job to the queue. */
105
+ add(name: string, data: unknown, options?: AddOptions): Promise<AddedJob>;
106
+ /** Add many jobs at once. */
107
+ addBatch(jobs: Array<{
108
+ name: string;
109
+ data: unknown;
110
+ options?: AddOptions;
111
+ }>): Promise<AddedJob[]>;
112
+ /** Register a worker. Returns a handle that can be closed. */
113
+ process<T = unknown>(name: string, handler: JobHandler<T>, options?: WorkerOptions): Promise<WorkerHandle>;
114
+ /** Drain — wait for in-flight jobs to finish. */
115
+ drain(): Promise<void>;
116
+ /** Stop all workers gracefully. */
117
+ stop(): Promise<void>;
118
+ /** Subscribe to queue events. Returns an unsubscribe function. */
119
+ on(listener: QueueEventListener): () => void;
120
+ }
121
+ /** Handle for a running worker. */
122
+ export interface WorkerHandle {
123
+ /** The job name this worker handles. */
124
+ name: string;
125
+ /** Stop the worker (wait for in-flight jobs). */
126
+ close(): Promise<void>;
127
+ /** Pause accepting new jobs. */
128
+ pause(): Promise<void>;
129
+ /** Resume accepting jobs. */
130
+ resume(): Promise<void>;
131
+ /** True if the worker is running. */
132
+ isRunning(): boolean;
133
+ }
134
+ export type QueueBackendKind = "bullmq" | "cloudflare" | "memory";
135
+ export interface QueueConfig {
136
+ /**
137
+ * Backend to use.
138
+ * - 'bullmq' → Redis-backed, works on Bun / Node
139
+ * - 'cloudflare' → Cloudflare Queues binding
140
+ * - 'memory' → in-process queue (tests, single-instance dev)
141
+ */
142
+ backend: QueueBackendKind;
143
+ /**
144
+ * BullMQ-specific configuration. Ignored by other backends.
145
+ */
146
+ bullmq?: BullMQConfig;
147
+ /**
148
+ * Cloudflare-specific configuration. Ignored by other backends.
149
+ */
150
+ cloudflare?: CloudflareQueueConfig;
151
+ /** Global default options for `add()`. */
152
+ defaults?: AddOptions;
153
+ }
154
+ export interface BullMQConfig {
155
+ /** Redis connection URL (e.g. redis://localhost:6379). */
156
+ connection: string | {
157
+ host: string;
158
+ port: number;
159
+ password?: string;
160
+ };
161
+ /** Prefix for all queue keys. Default: 'nexusjs'. */
162
+ prefix?: string;
163
+ /** Default job options. */
164
+ defaultJobOptions?: AddOptions;
165
+ }
166
+ export interface CloudflareQueueConfig {
167
+ /**
168
+ * Resolver for the Queue binding from the Worker's `env`.
169
+ * Called once at boot with the env object.
170
+ */
171
+ resolveBinding: (env: Record<string, unknown>) => unknown;
172
+ /** Queue name (used for diagnostics). */
173
+ name: string;
174
+ }
175
+ export type QueueEvent = {
176
+ kind: "job:added";
177
+ jobId: string;
178
+ name: string;
179
+ } | {
180
+ kind: "job:active";
181
+ jobId: string;
182
+ name: string;
183
+ attempts: number;
184
+ } | {
185
+ kind: "job:completed";
186
+ jobId: string;
187
+ name: string;
188
+ returnvalue?: unknown;
189
+ } | {
190
+ kind: "job:failed";
191
+ jobId: string;
192
+ name: string;
193
+ error: Error;
194
+ willRetry: boolean;
195
+ } | {
196
+ kind: "worker:started";
197
+ name: string;
198
+ concurrency: number;
199
+ } | {
200
+ kind: "worker:stopped";
201
+ name: string;
202
+ };
203
+ export type QueueEventListener = (event: QueueEvent) => void | Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexusts/queue",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Background jobs (BullMQ / Cloudflare / memory)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,32 +12,15 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
- "files": [
16
- "dist",
17
- "README.md"
18
- ],
15
+ "files": ["dist", "README.md"],
19
16
  "scripts": {
20
17
  "build": "bun run ../../build.ts"
21
18
  },
22
- "keywords": [
23
- "nexusts",
24
- "framework",
25
- "bun"
26
- ],
19
+ "keywords": ["nexusts", "framework", "bun"],
27
20
  "license": "MIT",
28
- "peerDependencies": {
29
- "bullmq": "^5.79.0",
30
- "ioredis": "^5.11.1"
31
- },
32
- "peerDependenciesMeta": {
33
- "bullmq": {
34
- "optional": true
35
- },
36
- "ioredis": {
37
- "optional": true
38
- }
39
- },
21
+ "peerDependencies": { "bullmq": "^5.79.0", "ioredis": "^5.11.1" },
22
+ "peerDependenciesMeta": { "bullmq": { "optional": true }, "ioredis": { "optional": true } },
40
23
  "dependencies": {
41
- "@nexusts/core": "^0.7.0"
24
+ "@nexusts/core": "file:../core"
42
25
  }
43
26
  }