@nest-batch/bullmq 0.2.0

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/src/adapters/bullmq.adapter.d.ts +157 -0
  4. package/dist/src/adapters/bullmq.adapter.d.ts.map +1 -0
  5. package/dist/src/adapters/bullmq.adapter.js +252 -0
  6. package/dist/src/adapters/bullmq.adapter.js.map +1 -0
  7. package/dist/src/adapters/index.d.ts +12 -0
  8. package/dist/src/adapters/index.d.ts.map +1 -0
  9. package/dist/src/adapters/index.js +29 -0
  10. package/dist/src/adapters/index.js.map +1 -0
  11. package/dist/src/bullmq-execution-strategy.d.ts +59 -0
  12. package/dist/src/bullmq-execution-strategy.d.ts.map +1 -0
  13. package/dist/src/bullmq-execution-strategy.js +60 -0
  14. package/dist/src/bullmq-execution-strategy.js.map +1 -0
  15. package/dist/src/bullmq-runtime.service.d.ts +237 -0
  16. package/dist/src/bullmq-runtime.service.d.ts.map +1 -0
  17. package/dist/src/bullmq-runtime.service.js +441 -0
  18. package/dist/src/bullmq-runtime.service.js.map +1 -0
  19. package/dist/src/bullmq-schedule.service.d.ts +121 -0
  20. package/dist/src/bullmq-schedule.service.d.ts.map +1 -0
  21. package/dist/src/bullmq-schedule.service.js +232 -0
  22. package/dist/src/bullmq-schedule.service.js.map +1 -0
  23. package/dist/src/connection.d.ts +83 -0
  24. package/dist/src/connection.d.ts.map +1 -0
  25. package/dist/src/connection.js +72 -0
  26. package/dist/src/connection.js.map +1 -0
  27. package/dist/src/index.d.ts +29 -0
  28. package/dist/src/index.d.ts.map +1 -0
  29. package/dist/src/index.js +46 -0
  30. package/dist/src/index.js.map +1 -0
  31. package/dist/src/module-options.d.ts +68 -0
  32. package/dist/src/module-options.d.ts.map +1 -0
  33. package/dist/src/module-options.js +13 -0
  34. package/dist/src/module-options.js.map +1 -0
  35. package/package.json +71 -0
  36. package/src/adapters/bullmq.adapter.ts +346 -0
  37. package/src/adapters/index.ts +11 -0
  38. package/src/bullmq-execution-strategy.ts +81 -0
  39. package/src/bullmq-runtime.service.ts +540 -0
  40. package/src/bullmq-schedule.service.ts +271 -0
  41. package/src/connection.ts +97 -0
  42. package/src/index.ts +28 -0
  43. package/src/module-options.ts +74 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bullmq-runtime.service.ts"],"sourcesContent":["import {\n Inject,\n Injectable,\n Logger,\n OnApplicationBootstrap,\n OnApplicationShutdown,\n Optional,\n} from '@nestjs/common';\nimport { Queue, QueueEvents, Worker, type JobsOptions } from 'bullmq';\n\nimport {\n type IExecutionStrategy,\n type JobDefinition,\n type BatchObserver,\n type JsonValue,\n type JobRepository,\n JOB_REPOSITORY_TOKEN,\n enforcePartitionIndex,\n validatePartitions,\n} from '@nest-batch/core';\nimport { JobExecutor, JobRegistry, NoopBatchObserver, BATCH_EVENT } from '@nest-batch/core';\n\nimport {\n BULLMQ_MODULE_OPTIONS,\n type ResolvedBullMqModuleOptions,\n} from './module-options';\n\n/**\n * Payload shape stored in a BullMQ job's `data` field.\n *\n * The strategy enqueues one BullMQ job per step (or per partition,\n * in a future enhancement). The worker reconstructs the\n * `JobExecution` from the repository via `executionId` and the\n * `JobDefinition` from the registry via `jobId`.\n *\n * Why not store the full `JobDefinition` in the payload?\n * - IR is mutable across the host process (decorators / builders\n * may swap providers in tests, hot-reload, etc.). The\n * repository + registry are the canonical sources; the\n * payload carries only the keys needed to look them up.\n * - Storage size — IRs can be large (listeners, resolvers).\n * Redis is transport, not cache; small payloads are cheaper.\n */\nexport interface BullmqJobPayload {\n /** JobExecution id, used to load the canonical execution row. */\n readonly executionId: string;\n /** Mirrors `executionId` today; kept distinct for forward compat. */\n readonly jobExecutionId: string;\n /** JobDefinition id, used to look up the IR from the registry. */\n readonly jobId: string;\n /** Step id (the `name` field of the BullMQ job). */\n readonly stepId: string;\n /**\n * Partition index. Reserved for a future enhancement where a\n * chunk step is split into N partitions and enqueued as N\n * BullMQ jobs. Today the strategy always enqueues one job\n * per step (regardless of chunk size), so the field is\n * `undefined`. Kept in the payload shape so the worker\n * can distinguish \"this is a step\" from \"this is a partition\"\n * without a separate discriminator.\n */\n readonly partitionIndex?: number;\n}\n\n/**\n * The single BullMQ queue name used by the strategy + worker +\n * queue-events. We deliberately do not fan out into per-step\n * queues — that would force the host to pre-declare every step\n * name at compile time, which is at odds with the decorator /\n * builder APIs that discover steps at runtime. A single queue\n * keyed by the step's `name` field is the standard BullMQ pattern\n * (the `name` field discriminates the work).\n *\n * BullMQ 5 rejects queue names that contain a colon (`:`) because\n * it is the path separator in the Redis key layout. We use a\n * hyphen-separated name accordingly.\n */\nexport const BULLMQ_QUEUE_NAME = 'nest-batch-work';\n\n/**\n * Name of the BullMQ strategy. Logged by the bridge for diagnostic\n * purposes and asserted by tests that need to distinguish the\n * real implementation from the T17 stub.\n */\nexport const BULLMQ_STRATEGY_NAME = 'bullmq';\n\n/**\n * Bridge between the BullMQ `Queue` / `Worker` / `QueueEvents` and\n * the `@nest-batch/core` execution pipeline.\n *\n * Responsibilities (T18 contract):\n * 1. Own the producer / worker connection clients with the\n * role-specific tuning (fail-fast producer, blocking worker).\n * 2. Implement the `IExecutionStrategy` contract: `launch()`\n * enqueues a single BullMQ job per step, returns\n * `{ kind: 'enqueued', queueJobId }`. The launch is\n * fire-and-forget — the strategy does NOT block on the\n * worker.\n * 3. Drive the worker lifecycle (`OnApplicationBootstrap` /\n * `OnApplicationShutdown`).\n * 4. Bridge `QueueEvents` `completed` / `failed` / `stalled`\n * into the `BatchObserver` (defaulting to `NoopBatchObserver`).\n * 5. Hand off to `JobExecutor.execute(execution, jobDef)` from\n * inside the worker — Batch Core remains the source of truth\n * for state transitions, skip/retry, checkpoint, restart.\n *\n * Why a single class (not separate `Queue` / `Worker` providers)?\n * - The producer and worker share a `connection` record but\n * carry *different* `ConnectionOptions` (different\n * `maxRetriesPerRequest`, `enableReadyCheck`, ...). Splitting\n * them across providers would force the connection-tuning\n * logic into two places and risk the worker accidentally\n * inheriting the producer's fail-fast config (or vice versa).\n * - Lifecycle is a unit: open producer + worker + events\n * together, close them together in the documented order\n * (workers first, then events, then queues). Centralising\n * this in one class makes the close-order a single source\n * of truth and a single method (`close()`).\n */\n@Injectable()\nexport class BullmqRuntimeService\n implements IExecutionStrategy, OnApplicationBootstrap, OnApplicationShutdown\n{\n /**\n * Strategy name. Distinct from the T17 stub's `'bullmq-stub'`\n * so log lines and boundary reports can tell them apart.\n */\n readonly name = BULLMQ_STRATEGY_NAME;\n\n private readonly logger = new Logger(BullmqRuntimeService.name);\n\n /** BullMQ queue (producer side). */\n private queue: Queue | null = null;\n /** BullMQ worker (consumer side). */\n private worker: Worker<BullmqJobPayload> | null = null;\n /** BullMQ QueueEvents stream listener. */\n private queueEvents: QueueEvents | null = null;\n /**\n * Promise-chain lock for the close path. We capture the first\n * `close()` invocation and short-circuit subsequent ones so a\n * stray double-shutdown (Nest calls `OnApplicationShutdown`\n * once, but tests sometimes do their own) does not race the\n * in-flight close.\n */\n private closePromise: Promise<void> | null = null;\n\n constructor(\n @Inject(BULLMQ_MODULE_OPTIONS)\n private readonly options: ResolvedBullMqModuleOptions,\n @Inject(JOB_REPOSITORY_TOKEN)\n private readonly repository: JobRepository,\n private readonly registry: JobRegistry,\n private readonly jobExecutor: JobExecutor,\n @Optional()\n private readonly observer: BatchObserver = new NoopBatchObserver() as BatchObserver,\n ) {}\n\n /**\n * Nest lifecycle: spin up the queue, worker, and queue-events\n * after the DI container is fully wired. We do this in\n * `onApplicationBootstrap` (not `onModuleInit`) so every other\n * provider — including user-supplied `JobRepository` overrides —\n * is already instantiated and injectable.\n *\n * Worker startup is gated on `options.autoStartWorker`. The\n * flag exists for launcher-only deployments (e.g. an API\n * service that only enqueues) and for tests that want to\n * exercise the producer side in isolation. When the flag is\n * `false` the queue is still created (so `launch()` can\n * enqueue), but the worker is not started (no consumer means\n * the jobs sit in the queue indefinitely).\n */\n onApplicationBootstrap(): void {\n this.queue = this.buildQueue();\n this.queueEvents = this.buildQueueEvents();\n this.attachQueueEventsBridge();\n\n if (this.options.autoStartWorker) {\n this.worker = this.buildWorker();\n this.logger.log(\n `BullmqRuntimeService started: queue=\"${BULLMQ_QUEUE_NAME}\" ` +\n `worker=auto, keyPrefix=\"${this.options.connection.keyPrefix}\"`,\n );\n } else {\n this.logger.log(\n `BullmqRuntimeService started: queue=\"${BULLMQ_QUEUE_NAME}\" ` +\n `worker=manual (autoStartWorker=false)`,\n );\n }\n }\n\n /**\n * Nest lifecycle: close every BullMQ resource in the documented\n * order — workers first (let in-flight jobs finish or be\n * returned to the queue), then events (no new events can\n * arrive once the worker is closed), then queues (the producer\n * is closed last so any pending `add()` calls had a chance to\n * land).\n *\n * Idempotent: a second call to `onApplicationShutdown` (which\n * can happen in tests) short-circuits to the first close's\n * promise rather than racing.\n */\n async onApplicationShutdown(): Promise<void> {\n if (this.closePromise !== null) {\n return this.closePromise;\n }\n this.closePromise = this.close();\n return this.closePromise;\n }\n\n // -----------------------------------------------------------------------\n // IExecutionStrategy\n // -----------------------------------------------------------------------\n\n /**\n * Enqueue a single BullMQ job per step. Returns\n * `{ kind: 'enqueued', queueJobId }` after the producer has\n * acknowledged the enqueue. The execution is fire-and-forget:\n * the launcher resolves the latest persisted `JobExecution`\n * (which is still in `STARTING`/`STARTED` because the executor\n * has not run yet).\n *\n * The canonical `JobExecution` row is created by the launcher\n * via `repository.createExecutionAtomic` BEFORE this method is\n * called (the `executionId` in `ctx` is the result). This\n * strategy does NOT re-create it; doing so would race the\n * launcher's atomic create and break the `SELECT ... FOR\n * UPDATE SKIP LOCKED` invariant.\n *\n * Throws if the producer cannot enqueue (Redis down, key\n * collision, etc.). The launcher re-throws the error to its\n * caller; the `JobExecution` row remains in `STARTING` —\n * the host's recovery path (or a manual cleanup) is\n * responsible for transitioning it.\n */\n async launch(\n job: JobDefinition,\n _params: Record<string, unknown>,\n ctx: { executionId: string; jobExecutionId: string },\n ): Promise<{ kind: 'enqueued'; queueJobId: string }> {\n if (this.queue === null) {\n throw new Error(\n `[BullmqRuntimeService] launch() called before onApplicationBootstrap — ` +\n 'module is not initialized. Did you forget to import BullmqBatchModule?',\n );\n }\n // T8 (partition orchestration): when the start step declares\n // `partitions.count >= 2`, the strategy enqueues one BullMQ job\n // per partition (each carrying a distinct `partitionIndex`).\n // Otherwise (default, `count === 1`, or absent) it preserves\n // the 0.1.0 \"one job per step\" behaviour. The validate call\n // surfaces a misconfiguration (e.g. `count <= 0`) at the\n // launcher's boundary so the host's caller sees the failure\n // before the worker is ever asked to process the job.\n const stepId = job.startStepId;\n const startStep = job.steps[stepId];\n const partitions = startStep?.kind === 'chunk' ? startStep.partitions : undefined;\n validatePartitions(partitions);\n const partitionCount = partitions?.count ?? 1;\n const partitionOrdinals: Array<number | undefined> =\n partitionCount >= 2 ? Array.from({ length: partitionCount }, (_, i) => i) : [undefined];\n\n const jobOpts: JobsOptions = {\n attempts: 3,\n backoff: { type: 'exponential', delay: 100, jitter: 0.5 },\n removeOnComplete: { count: 100, age: 3600 },\n removeOnFail: { count: 1000 },\n };\n\n let lastQueueJobId: string | null = null;\n for (const partitionIndex of partitionOrdinals) {\n const payload: BullmqJobPayload = {\n executionId: ctx.executionId,\n jobExecutionId: ctx.jobExecutionId,\n jobId: job.id,\n stepId,\n ...(partitionIndex !== undefined ? { partitionIndex } : {}),\n };\n const enqueued = await this.queue.add(stepId, payload, jobOpts);\n if (enqueued.id === undefined) {\n // BullMQ returns a job with `id` undefined only when the\n // producer cannot reach Redis and the in-memory buffer\n // (which is disabled by `enableOfflineQueue: false`) is\n // not available. Surface this as a hard error so the\n // launcher propagates the failure.\n throw new Error(\n `[BullmqRuntimeService] enqueue returned undefined job id (Redis down?)`,\n );\n }\n const qid = String(enqueued.id);\n lastQueueJobId = qid;\n this.logger.debug(\n `Enqueued step \"${stepId}\" for execution ${ctx.executionId}` +\n (partitionIndex !== undefined ? ` (partition ${partitionIndex}/${partitionCount})` : '') +\n ` as BullMQ job ${qid}`,\n );\n }\n if (lastQueueJobId === null) {\n // Defensive: the loop above always runs at least once\n // (partitionOrdinals has length >= 1), so this branch is\n // unreachable in practice. Keep the explicit throw so a\n // future refactor cannot quietly enqueue zero jobs.\n throw new Error(`[BullmqRuntimeService] enqueued zero jobs for execution ${ctx.executionId}`);\n }\n return { kind: 'enqueued', queueJobId: lastQueueJobId };\n }\n\n // -----------------------------------------------------------------------\n // Construction\n // -----------------------------------------------------------------------\n\n private buildQueue(): Queue {\n return new Queue(BULLMQ_QUEUE_NAME, {\n connection: this.producerConnectionOptions(),\n // `defaultJobOptions` is a defence-in-depth measure. The\n // strategy already passes per-call `JobsOptions` (with\n // the T18 retry / remove policy) so this is the fallback\n // for any code path that calls `queue.add` without\n // explicit options. Today the only caller is the strategy.\n defaultJobOptions: {\n attempts: 3,\n backoff: { type: 'exponential', delay: 100, jitter: 0.5 },\n removeOnComplete: { count: 100, age: 3600 },\n removeOnFail: { count: 1000 },\n },\n prefix: this.options.connection.keyPrefix,\n // Skip waiting for the producer connection to become ready\n // before returning from `add`. The fail-fast producer\n // options (see `producerConnectionOptions`) make a dead\n // Redis surface as a synchronous error on the first `add`,\n // which is exactly what the \"Redis-down\" test asserts.\n skipWaitingForReady: true,\n // BullMQ 5 calls `client.info()` to discover the server\n // version + database type at `Queue` construction time. With\n // `enableOfflineQueue: false` and the ioredis client not\n // yet ready, the call throws `Stream isn't writeable`.\n // `skipVersionCheck: true` short-circuits that probe — the\n // strategy never depends on the version, and a dead Redis\n // still surfaces synchronously on the first `add()` (per\n // the fail-fast contract above).\n skipVersionCheck: true,\n });\n }\n\n private buildWorker(): Worker<BullmqJobPayload> {\n return new Worker<BullmqJobPayload>(\n BULLMQ_QUEUE_NAME,\n async (job) => this.processJob(job.data),\n {\n connection: this.workerConnectionOptions(),\n prefix: this.options.connection.keyPrefix,\n concurrency: 1,\n },\n );\n }\n\n private buildQueueEvents(): QueueEvents {\n return new QueueEvents(BULLMQ_QUEUE_NAME, {\n connection: this.workerConnectionOptions(),\n prefix: this.options.connection.keyPrefix,\n });\n }\n\n /**\n * Wire the `QueueEvents` listeners to the configured\n * `BatchObserver`. Each listener swallows observer errors so\n * a slow / failing observer cannot poison the BullMQ event\n * stream.\n */\n private attachQueueEventsBridge(): void {\n if (this.queueEvents === null) return;\n this.queueEvents.on('completed', ({ jobId }) => {\n void this.bridgeEvent(BATCH_EVENT.JOB_COMPLETED, { queueJobId: jobId, kind: 'completed' });\n });\n this.queueEvents.on('failed', ({ jobId, failedReason }) => {\n void this.bridgeEvent(BATCH_EVENT.JOB_FAILED, {\n queueJobId: jobId,\n kind: 'failed',\n reason: failedReason,\n });\n });\n this.queueEvents.on('stalled', ({ jobId }) => {\n void this.bridgeEvent(BATCH_EVENT.JOB_FAILED, { queueJobId: jobId, kind: 'stalled' });\n });\n }\n\n private async bridgeEvent(\n type: (typeof BATCH_EVENT)[keyof typeof BATCH_EVENT],\n data: Record<string, unknown>,\n ): Promise<void> {\n try {\n await this.observer.onEvent({\n type,\n timestamp: new Date(),\n jobExecutionId: String(data['queueJobId'] ?? '<unknown>'),\n data: data as unknown as JsonValue,\n });\n } catch (err) {\n this.logger.warn(\n `BatchObserver threw on event ${type}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // -----------------------------------------------------------------------\n // Worker processor — delegated to JobExecutor\n // -----------------------------------------------------------------------\n\n /**\n * Worker entry point. Loads the canonical `JobExecution` from\n * the repository and the `JobDefinition` from the registry, then\n * hands the work to `JobExecutor.execute`. All batch semantics\n * (step dispatch, chunk loop, skip/retry, checkpoint) live in\n * the executor — this method is a thin bridge.\n */\n private async processJob(payload: BullmqJobPayload): Promise<void> {\n const execution = await this.repository.getJobExecution(payload.executionId);\n if (execution === null) {\n // The DB row is gone. The launcher pre-created it via\n // `createExecutionAtomic`; if it's missing now, the host\n // either deleted it or restored a DB without the row.\n // Surface as a BullMQ-level failure so the technical\n // retry / dead-letter path handles it.\n throw new Error(\n `[BullmqRuntimeService] JobExecution ${payload.executionId} not found in repository`,\n );\n }\n const jobDef = this.registry.get(payload.jobId);\n // `JobRegistry.get` throws `JobNotFoundError` if the\n // definition is missing. We let it propagate so BullMQ\n // records the failure and the dead-letter queue catches\n // it (a missing job definition is a misconfiguration that\n // should be loud, not silent).\n await this.jobExecutor.execute(execution, jobDef);\n }\n\n // -----------------------------------------------------------------------\n // Connection options\n // -----------------------------------------------------------------------\n\n /**\n * Producer-side connection tuning. The two flags below are\n * the contract the T18 \"Redis-down\" test depends on:\n *\n * - `enableOfflineQueue: false` — a `Queue.add()` against a\n * dead Redis MUST throw synchronously rather than buffer\n * the command. Without this, BullMQ keeps the command in\n * memory and `add()` returns success, breaking the\n * \"fail fast\" guarantee.\n * - `maxRetriesPerRequest: 1` — keep the first `add`\n * fast; subsequent reconnects are handled by ioredis\n * itself (we do not want BullMQ to block on retries\n * during the launcher call).\n *\n * BullMQ specifically warns against `maxRetriesPerRequest: null`\n * on the producer, because the producer does not use blocking\n * commands. We use `1` for the same reason.\n */\n private producerConnectionOptions(): Record<string, unknown> {\n return {\n host: this.options.connection.host,\n port: this.options.connection.port,\n password: this.options.connection.password,\n username: this.options.connection.username,\n db: this.options.connection.db,\n ...(this.options.connection.tls ? { tls: true } : {}),\n enableOfflineQueue: false,\n maxRetriesPerRequest: 1,\n };\n }\n\n /**\n * Worker-side connection tuning. Two flags that BullMQ\n * *requires* for blocking workers (per the BullMQ docs):\n *\n * - `maxRetriesPerRequest: null` — the worker's\n * `BLPOP` / `BRPOPLPUSH` / `XREADGROUP` commands MUST NOT\n * retry per request. A stalled worker surfaces as a\n * stall, not a connection error.\n * - `enableReadyCheck: false` — the worker should not\n * refuse to start when Redis is in the middle of a\n * failover; ioredis reconnects on its own.\n */\n private workerConnectionOptions(): Record<string, unknown> {\n return {\n host: this.options.connection.host,\n port: this.options.connection.port,\n password: this.options.connection.password,\n username: this.options.connection.username,\n db: this.options.connection.db,\n ...(this.options.connection.tls ? { tls: true } : {}),\n maxRetriesPerRequest: null,\n enableReadyCheck: false,\n };\n }\n\n // -----------------------------------------------------------------------\n // Close\n // -----------------------------------------------------------------------\n\n /**\n * Close all BullMQ resources in the documented order:\n * worker → events → queue. Each step is best-effort: a close\n * error on one resource does not prevent the others from\n * being closed.\n */\n private async close(): Promise<void> {\n if (this.worker !== null) {\n try {\n await this.worker.close();\n } catch (err) {\n this.logger.warn(\n `Worker close failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n this.worker = null;\n }\n if (this.queueEvents !== null) {\n try {\n await this.queueEvents.close();\n } catch (err) {\n this.logger.warn(\n `QueueEvents close failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n this.queueEvents = null;\n }\n if (this.queue !== null) {\n try {\n await this.queue.close();\n } catch (err) {\n this.logger.warn(\n `Queue close failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n this.queue = null;\n }\n }\n}\n"],"names":["BULLMQ_QUEUE_NAME","BULLMQ_STRATEGY_NAME","BullmqRuntimeService","name","logger","Logger","queue","worker","queueEvents","closePromise","options","repository","registry","jobExecutor","observer","NoopBatchObserver","onApplicationBootstrap","buildQueue","buildQueueEvents","attachQueueEventsBridge","autoStartWorker","buildWorker","log","connection","keyPrefix","onApplicationShutdown","close","launch","job","_params","ctx","Error","stepId","startStepId","startStep","steps","partitions","kind","undefined","validatePartitions","partitionCount","count","partitionOrdinals","Array","from","length","_","i","jobOpts","attempts","backoff","type","delay","jitter","removeOnComplete","age","removeOnFail","lastQueueJobId","partitionIndex","payload","executionId","jobExecutionId","jobId","id","enqueued","add","qid","String","debug","queueJobId","Queue","producerConnectionOptions","defaultJobOptions","prefix","skipWaitingForReady","skipVersionCheck","Worker","processJob","data","workerConnectionOptions","concurrency","QueueEvents","on","bridgeEvent","BATCH_EVENT","JOB_COMPLETED","failedReason","JOB_FAILED","reason","onEvent","timestamp","Date","err","warn","message","execution","getJobExecution","jobDef","get","execute","host","port","password","username","db","tls","enableOfflineQueue","maxRetriesPerRequest","enableReadyCheck"],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ import { OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
2
+ import { BatchScheduleRegistry } from '@nest-batch/core';
3
+ import { type ResolvedBullMqModuleOptions } from './module-options';
4
+ /**
5
+ * The single BullMQ queue name used by the schedule service. We
6
+ * intentionally use a DIFFERENT queue from the runtime service's
7
+ * `BULLMQ_QUEUE_NAME` so cron-triggered jobs and ad-hoc
8
+ * `launch()`-triggered jobs are inspectable in isolation (and so
9
+ * the schedule-removal path on shutdown can tear them down
10
+ * without touching the runtime work queue).
11
+ *
12
+ * BullMQ 5 rejects queue names that contain a colon (`:`) because
13
+ * it is the path separator in the key layout. We use a hyphen
14
+ * (`-`) instead, matching the existing `BULLMQ_QUEUE_NAME`
15
+ * convention (`'nest-batch-work'`).
16
+ */
17
+ export declare const BULLMQ_SCHEDULE_QUEUE_NAME = "nest-batch-schedule";
18
+ /**
19
+ * `BullmqScheduleService` — the runtime scheduler for
20
+ * `@BatchScheduled` entries.
21
+ *
22
+ * Lifecycle:
23
+ * 1. `OnApplicationBootstrap` walks the `BatchScheduleRegistry`
24
+ * and, for every entry with `inert: false`, registers a
25
+ * BullMQ repeating job via `queue.upsertJobScheduler(...)`.
26
+ * Entries with `inert: true` are logged and skipped — that
27
+ * is the only place the inert flag is consulted.
28
+ * 2. BullMQ's `upsertJobScheduler` internally fires the
29
+ * schedule at the configured cron time. Each fire enqueues a
30
+ * job into the schedule queue (named after the schedule
31
+ * entry's method). A separate `Worker` (the one owned by
32
+ * `BullmqRuntimeService` if `autoStartWorker` is `true`)
33
+ * processes the jobs.
34
+ * 3. `OnApplicationShutdown` removes every installed scheduler
35
+ * (via `queue.removeJobScheduler`) and closes the queue.
36
+ * Removal is best-effort: a partial failure logs a warning
37
+ * but does not block the rest of the shutdown.
38
+ *
39
+ * Why a dedicated service (not a method on `BullmqRuntimeService`)?
40
+ * - The runtime service is `IExecutionStrategy`-facing; it
41
+ * knows about `JobExecution`, the in-process launch contract,
42
+ * and the worker bridge. Mixing scheduler concerns in would
43
+ * bloat its surface and couple two lifecycles that happen to
44
+ * share a Redis client but are otherwise independent.
45
+ * - The scheduler does NOT need a `Worker`; the runtime service
46
+ * does. A separate service can run with `autoStartWorker:
47
+ * false` cleanly (a launcher-only deployment that still wants
48
+ * cron schedules to fire).
49
+ * - The schedule service owns its own `Queue` (the schedule
50
+ * queue) so cron jobs are not interleaved with manually-launched
51
+ * jobs. They share the same `keyPrefix` so the host's Redis
52
+ * namespace policy still applies.
53
+ */
54
+ export declare class BullmqScheduleService implements OnApplicationBootstrap, OnApplicationShutdown {
55
+ private readonly scheduleRegistry;
56
+ private readonly options;
57
+ private readonly logger;
58
+ /** BullMQ queue for the scheduler (producer side only). */
59
+ private scheduleQueue;
60
+ /**
61
+ * Every schedule key installed during `onApplicationBootstrap`.
62
+ * Tracked so the shutdown path can `removeJobScheduler` for
63
+ * each one deterministically. A `Set` keeps the test assertions
64
+ * order-independent.
65
+ */
66
+ private readonly installedKeys;
67
+ /** Promise-chain lock for the close path. Mirrors the runtime service. */
68
+ private closePromise;
69
+ constructor(scheduleRegistry: BatchScheduleRegistry, options: ResolvedBullMqModuleOptions);
70
+ /**
71
+ * Walk the registry and install every non-inert entry as a
72
+ * BullMQ repeating job. Runs AFTER the `BatchBootstrapper` has
73
+ * populated the registry (both hooks are on
74
+ * `OnApplicationBootstrap`, but Nest calls them in
75
+ * provider-registration order; the bootstrapper is registered
76
+ * before this service by `BullmqBatchModule.forRoot()`).
77
+ *
78
+ * Each entry is wrapped in a per-entry `try` so a single bad
79
+ * schedule does not abort the rest of the installation. Bad
80
+ * schedules are logged and skipped — the runtime keeps running
81
+ * for the valid ones.
82
+ */
83
+ onApplicationBootstrap(): void;
84
+ /**
85
+ * Tear down every installed scheduler and close the schedule
86
+ * queue. Idempotent: a second `onApplicationShutdown` short-
87
+ * circuits to the first close's promise.
88
+ */
89
+ onApplicationShutdown(): Promise<void>;
90
+ /**
91
+ * Installed scheduler keys, in insertion order. Exposed for
92
+ * tests and diagnostics. Read-only: callers MUST NOT mutate
93
+ * the returned array.
94
+ */
95
+ installedSchedulerKeys(): readonly string[];
96
+ /**
97
+ * Install a single entry as a BullMQ repeating job. Skips
98
+ * inert entries (the runtime honours the inert flag by NOT
99
+ * calling `upsertJobScheduler` for them). Throws on
100
+ * installation failure so the caller can log + continue.
101
+ */
102
+ private installSchedule;
103
+ /**
104
+ * Build the producer-side BullMQ queue for the scheduler. The
105
+ * connection tuning mirrors the runtime service's producer
106
+ * options: fail-fast on Redis-down (`enableOfflineQueue:
107
+ * false`) and a tight per-request retry budget
108
+ * (`maxRetriesPerRequest: 1`).
109
+ */
110
+ private buildScheduleQueue;
111
+ private producerConnectionOptions;
112
+ /**
113
+ * Close the schedule queue. `removeJobScheduler` is called
114
+ * first for every installed key so the next run of the host
115
+ * app does not inherit leftover schedulers. Each removal is
116
+ * best-effort: a failure on one key does not prevent the
117
+ * others from being removed.
118
+ */
119
+ private close;
120
+ }
121
+ //# sourceMappingURL=bullmq-schedule.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-schedule.service.d.ts","sourceRoot":"","sources":["../../src/bullmq-schedule.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAA2B,qBAAqB,EAA2B,MAAM,kBAAkB,CAAC;AAE3G,OAAO,EAAyB,KAAK,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAE3F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,0BAA0B,wBAAwB,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBACa,qBAAsB,YAAW,sBAAsB,EAAE,qBAAqB;IAkBvF,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAEjC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAnB1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IAEjE,2DAA2D;IAC3D,OAAO,CAAC,aAAa,CAAsB;IAE3C;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IAEnD,0EAA0E;IAC1E,OAAO,CAAC,YAAY,CAA8B;gBAG/B,gBAAgB,EAAE,qBAAqB,EAEvC,OAAO,EAAE,2BAA2B;IAGvD;;;;;;;;;;;;OAYG;IACH,sBAAsB,IAAI,IAAI;IAoB9B;;;;OAIG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5C;;;;OAIG;IACH,sBAAsB,IAAI,SAAS,MAAM,EAAE;IAQ3C;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA6CvB;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,yBAAyB;IAiBjC;;;;;;OAMG;YACW,KAAK;CAuBpB"}
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get BULLMQ_SCHEDULE_QUEUE_NAME () {
13
+ return BULLMQ_SCHEDULE_QUEUE_NAME;
14
+ },
15
+ get BullmqScheduleService () {
16
+ return BullmqScheduleService;
17
+ }
18
+ });
19
+ const _common = require("@nestjs/common");
20
+ const _bullmq = require("bullmq");
21
+ const _core = require("@nest-batch/core");
22
+ const _moduleoptions = require("./module-options");
23
+ function _ts_decorate(decorators, target, key, desc) {
24
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
25
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
26
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
27
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
28
+ }
29
+ function _ts_metadata(k, v) {
30
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
31
+ }
32
+ function _ts_param(paramIndex, decorator) {
33
+ return function(target, key) {
34
+ decorator(target, key, paramIndex);
35
+ };
36
+ }
37
+ const BULLMQ_SCHEDULE_QUEUE_NAME = 'nest-batch-schedule';
38
+ let BullmqScheduleService = class BullmqScheduleService {
39
+ scheduleRegistry;
40
+ options;
41
+ logger = new _common.Logger(BullmqScheduleService.name);
42
+ /** BullMQ queue for the scheduler (producer side only). */ scheduleQueue = null;
43
+ /**
44
+ * Every schedule key installed during `onApplicationBootstrap`.
45
+ * Tracked so the shutdown path can `removeJobScheduler` for
46
+ * each one deterministically. A `Set` keeps the test assertions
47
+ * order-independent.
48
+ */ installedKeys = new Set();
49
+ /** Promise-chain lock for the close path. Mirrors the runtime service. */ closePromise = null;
50
+ constructor(scheduleRegistry, options){
51
+ this.scheduleRegistry = scheduleRegistry;
52
+ this.options = options;
53
+ }
54
+ /**
55
+ * Walk the registry and install every non-inert entry as a
56
+ * BullMQ repeating job. Runs AFTER the `BatchBootstrapper` has
57
+ * populated the registry (both hooks are on
58
+ * `OnApplicationBootstrap`, but Nest calls them in
59
+ * provider-registration order; the bootstrapper is registered
60
+ * before this service by `BullmqBatchModule.forRoot()`).
61
+ *
62
+ * Each entry is wrapped in a per-entry `try` so a single bad
63
+ * schedule does not abort the rest of the installation. Bad
64
+ * schedules are logged and skipped — the runtime keeps running
65
+ * for the valid ones.
66
+ */ onApplicationBootstrap() {
67
+ this.scheduleQueue = this.buildScheduleQueue();
68
+ const entries = this.scheduleRegistry.getAll();
69
+ for (const entry of entries){
70
+ try {
71
+ this.installSchedule(entry);
72
+ } catch (err) {
73
+ this.logger.warn(`Failed to install schedule for "${entry.jobId}::${entry.methodName}": ` + `${err instanceof Error ? err.message : String(err)}`);
74
+ }
75
+ }
76
+ this.logger.log(`BullmqScheduleService started: queue="${BULLMQ_SCHEDULE_QUEUE_NAME}" ` + `schedules=${this.installedKeys.size}/${entries.length} ` + `(skipped=${entries.length - this.installedKeys.size} inert)`);
77
+ }
78
+ /**
79
+ * Tear down every installed scheduler and close the schedule
80
+ * queue. Idempotent: a second `onApplicationShutdown` short-
81
+ * circuits to the first close's promise.
82
+ */ async onApplicationShutdown() {
83
+ if (this.closePromise !== null) {
84
+ return this.closePromise;
85
+ }
86
+ this.closePromise = this.close();
87
+ return this.closePromise;
88
+ }
89
+ /**
90
+ * Installed scheduler keys, in insertion order. Exposed for
91
+ * tests and diagnostics. Read-only: callers MUST NOT mutate
92
+ * the returned array.
93
+ */ installedSchedulerKeys() {
94
+ return Array.from(this.installedKeys);
95
+ }
96
+ // -------------------------------------------------------------------------
97
+ // Installation
98
+ // -------------------------------------------------------------------------
99
+ /**
100
+ * Install a single entry as a BullMQ repeating job. Skips
101
+ * inert entries (the runtime honours the inert flag by NOT
102
+ * calling `upsertJobScheduler` for them). Throws on
103
+ * installation failure so the caller can log + continue.
104
+ */ installSchedule(entry) {
105
+ if (entry.inert) {
106
+ this.logger.log(`Skipping inert schedule: ${entry.jobId}::${entry.methodName} ` + `(cron="${entry.cron}", tz="${entry.timezone}")`);
107
+ return;
108
+ }
109
+ if (this.scheduleQueue === null) {
110
+ // Defensive: should never happen because `onApplicationBootstrap`
111
+ // builds the queue before iterating entries, but a future
112
+ // refactor that calls `installSchedule` from elsewhere
113
+ // should fail loudly.
114
+ throw new Error('[BullmqScheduleService] scheduleQueue is null');
115
+ }
116
+ const schedulerKey = `${entry.jobId}::${entry.methodName}`;
117
+ const template = {
118
+ name: entry.methodName,
119
+ data: {
120
+ jobId: entry.jobId,
121
+ methodName: entry.methodName
122
+ },
123
+ opts: {
124
+ attempts: 3,
125
+ backoff: {
126
+ type: 'exponential',
127
+ delay: 100,
128
+ jitter: 0.5
129
+ },
130
+ removeOnComplete: {
131
+ count: 100,
132
+ age: 3600
133
+ },
134
+ removeOnFail: {
135
+ count: 1000
136
+ }
137
+ }
138
+ };
139
+ void this.scheduleQueue.upsertJobScheduler(schedulerKey, {
140
+ pattern: entry.cron,
141
+ tz: entry.timezone
142
+ }, template);
143
+ this.installedKeys.add(schedulerKey);
144
+ this.logger.log(`Installed schedule: ${schedulerKey} (cron="${entry.cron}", tz="${entry.timezone}")`);
145
+ }
146
+ // -------------------------------------------------------------------------
147
+ // Queue construction
148
+ // -------------------------------------------------------------------------
149
+ /**
150
+ * Build the producer-side BullMQ queue for the scheduler. The
151
+ * connection tuning mirrors the runtime service's producer
152
+ * options: fail-fast on Redis-down (`enableOfflineQueue:
153
+ * false`) and a tight per-request retry budget
154
+ * (`maxRetriesPerRequest: 1`).
155
+ */ buildScheduleQueue() {
156
+ return new _bullmq.Queue(BULLMQ_SCHEDULE_QUEUE_NAME, {
157
+ connection: this.producerConnectionOptions(),
158
+ defaultJobOptions: {
159
+ attempts: 3,
160
+ backoff: {
161
+ type: 'exponential',
162
+ delay: 100,
163
+ jitter: 0.5
164
+ },
165
+ removeOnComplete: {
166
+ count: 100,
167
+ age: 3600
168
+ },
169
+ removeOnFail: {
170
+ count: 1000
171
+ }
172
+ },
173
+ prefix: this.options.connection.keyPrefix,
174
+ skipWaitingForReady: true,
175
+ // Mirrors the runtime service: skip the constructor-time
176
+ // version probe so the queue does not throw on a Redis
177
+ // client that is not yet ready.
178
+ skipVersionCheck: true
179
+ });
180
+ }
181
+ producerConnectionOptions() {
182
+ return {
183
+ host: this.options.connection.host,
184
+ port: this.options.connection.port,
185
+ password: this.options.connection.password,
186
+ username: this.options.connection.username,
187
+ db: this.options.connection.db,
188
+ ...this.options.connection.tls ? {
189
+ tls: true
190
+ } : {},
191
+ enableOfflineQueue: false,
192
+ maxRetriesPerRequest: 1
193
+ };
194
+ }
195
+ // -------------------------------------------------------------------------
196
+ // Close
197
+ // -------------------------------------------------------------------------
198
+ /**
199
+ * Close the schedule queue. `removeJobScheduler` is called
200
+ * first for every installed key so the next run of the host
201
+ * app does not inherit leftover schedulers. Each removal is
202
+ * best-effort: a failure on one key does not prevent the
203
+ * others from being removed.
204
+ */ async close() {
205
+ if (this.scheduleQueue !== null) {
206
+ for (const key of this.installedKeys){
207
+ try {
208
+ await this.scheduleQueue.removeJobScheduler(key);
209
+ } catch (err) {
210
+ this.logger.warn(`removeJobScheduler("${key}") failed: ${err instanceof Error ? err.message : String(err)}`);
211
+ }
212
+ }
213
+ try {
214
+ await this.scheduleQueue.close();
215
+ } catch (err) {
216
+ this.logger.warn(`Schedule queue close failed: ${err instanceof Error ? err.message : String(err)}`);
217
+ }
218
+ this.scheduleQueue = null;
219
+ }
220
+ }
221
+ };
222
+ BullmqScheduleService = _ts_decorate([
223
+ (0, _common.Injectable)(),
224
+ _ts_param(1, (0, _common.Inject)(_moduleoptions.BULLMQ_MODULE_OPTIONS)),
225
+ _ts_metadata("design:type", Function),
226
+ _ts_metadata("design:paramtypes", [
227
+ typeof _core.BatchScheduleRegistry === "undefined" ? Object : _core.BatchScheduleRegistry,
228
+ typeof ResolvedBullMqModuleOptions === "undefined" ? Object : ResolvedBullMqModuleOptions
229
+ ])
230
+ ], BullmqScheduleService);
231
+
232
+ //# sourceMappingURL=bullmq-schedule.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bullmq-schedule.service.ts"],"sourcesContent":["import {\n Inject,\n Injectable,\n Logger,\n OnApplicationBootstrap,\n OnApplicationShutdown,\n} from '@nestjs/common';\nimport { Queue, type JobsOptions } from 'bullmq';\n\nimport { BATCH_SCHEDULE_REGISTRY, BatchScheduleRegistry, type BatchScheduleEntry } from '@nest-batch/core';\n\nimport { BULLMQ_MODULE_OPTIONS, type ResolvedBullMqModuleOptions } from './module-options';\n\n/**\n * The single BullMQ queue name used by the schedule service. We\n * intentionally use a DIFFERENT queue from the runtime service's\n * `BULLMQ_QUEUE_NAME` so cron-triggered jobs and ad-hoc\n * `launch()`-triggered jobs are inspectable in isolation (and so\n * the schedule-removal path on shutdown can tear them down\n * without touching the runtime work queue).\n *\n * BullMQ 5 rejects queue names that contain a colon (`:`) because\n * it is the path separator in the key layout. We use a hyphen\n * (`-`) instead, matching the existing `BULLMQ_QUEUE_NAME`\n * convention (`'nest-batch-work'`).\n */\nexport const BULLMQ_SCHEDULE_QUEUE_NAME = 'nest-batch-schedule';\n\n/**\n * `BullmqScheduleService` — the runtime scheduler for\n * `@BatchScheduled` entries.\n *\n * Lifecycle:\n * 1. `OnApplicationBootstrap` walks the `BatchScheduleRegistry`\n * and, for every entry with `inert: false`, registers a\n * BullMQ repeating job via `queue.upsertJobScheduler(...)`.\n * Entries with `inert: true` are logged and skipped — that\n * is the only place the inert flag is consulted.\n * 2. BullMQ's `upsertJobScheduler` internally fires the\n * schedule at the configured cron time. Each fire enqueues a\n * job into the schedule queue (named after the schedule\n * entry's method). A separate `Worker` (the one owned by\n * `BullmqRuntimeService` if `autoStartWorker` is `true`)\n * processes the jobs.\n * 3. `OnApplicationShutdown` removes every installed scheduler\n * (via `queue.removeJobScheduler`) and closes the queue.\n * Removal is best-effort: a partial failure logs a warning\n * but does not block the rest of the shutdown.\n *\n * Why a dedicated service (not a method on `BullmqRuntimeService`)?\n * - The runtime service is `IExecutionStrategy`-facing; it\n * knows about `JobExecution`, the in-process launch contract,\n * and the worker bridge. Mixing scheduler concerns in would\n * bloat its surface and couple two lifecycles that happen to\n * share a Redis client but are otherwise independent.\n * - The scheduler does NOT need a `Worker`; the runtime service\n * does. A separate service can run with `autoStartWorker:\n * false` cleanly (a launcher-only deployment that still wants\n * cron schedules to fire).\n * - The schedule service owns its own `Queue` (the schedule\n * queue) so cron jobs are not interleaved with manually-launched\n * jobs. They share the same `keyPrefix` so the host's Redis\n * namespace policy still applies.\n */\n@Injectable()\nexport class BullmqScheduleService implements OnApplicationBootstrap, OnApplicationShutdown {\n private readonly logger = new Logger(BullmqScheduleService.name);\n\n /** BullMQ queue for the scheduler (producer side only). */\n private scheduleQueue: Queue | null = null;\n\n /**\n * Every schedule key installed during `onApplicationBootstrap`.\n * Tracked so the shutdown path can `removeJobScheduler` for\n * each one deterministically. A `Set` keeps the test assertions\n * order-independent.\n */\n private readonly installedKeys = new Set<string>();\n\n /** Promise-chain lock for the close path. Mirrors the runtime service. */\n private closePromise: Promise<void> | null = null;\n\n constructor(\n private readonly scheduleRegistry: BatchScheduleRegistry,\n @Inject(BULLMQ_MODULE_OPTIONS)\n private readonly options: ResolvedBullMqModuleOptions,\n ) {}\n\n /**\n * Walk the registry and install every non-inert entry as a\n * BullMQ repeating job. Runs AFTER the `BatchBootstrapper` has\n * populated the registry (both hooks are on\n * `OnApplicationBootstrap`, but Nest calls them in\n * provider-registration order; the bootstrapper is registered\n * before this service by `BullmqBatchModule.forRoot()`).\n *\n * Each entry is wrapped in a per-entry `try` so a single bad\n * schedule does not abort the rest of the installation. Bad\n * schedules are logged and skipped — the runtime keeps running\n * for the valid ones.\n */\n onApplicationBootstrap(): void {\n this.scheduleQueue = this.buildScheduleQueue();\n const entries = this.scheduleRegistry.getAll();\n for (const entry of entries) {\n try {\n this.installSchedule(entry);\n } catch (err) {\n this.logger.warn(\n `Failed to install schedule for \"${entry.jobId}::${entry.methodName}\": ` +\n `${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n this.logger.log(\n `BullmqScheduleService started: queue=\"${BULLMQ_SCHEDULE_QUEUE_NAME}\" ` +\n `schedules=${this.installedKeys.size}/${entries.length} ` +\n `(skipped=${entries.length - this.installedKeys.size} inert)`,\n );\n }\n\n /**\n * Tear down every installed scheduler and close the schedule\n * queue. Idempotent: a second `onApplicationShutdown` short-\n * circuits to the first close's promise.\n */\n async onApplicationShutdown(): Promise<void> {\n if (this.closePromise !== null) {\n return this.closePromise;\n }\n this.closePromise = this.close();\n return this.closePromise;\n }\n\n /**\n * Installed scheduler keys, in insertion order. Exposed for\n * tests and diagnostics. Read-only: callers MUST NOT mutate\n * the returned array.\n */\n installedSchedulerKeys(): readonly string[] {\n return Array.from(this.installedKeys);\n }\n\n // -------------------------------------------------------------------------\n // Installation\n // -------------------------------------------------------------------------\n\n /**\n * Install a single entry as a BullMQ repeating job. Skips\n * inert entries (the runtime honours the inert flag by NOT\n * calling `upsertJobScheduler` for them). Throws on\n * installation failure so the caller can log + continue.\n */\n private installSchedule(entry: BatchScheduleEntry): void {\n if (entry.inert) {\n this.logger.log(\n `Skipping inert schedule: ${entry.jobId}::${entry.methodName} ` +\n `(cron=\"${entry.cron}\", tz=\"${entry.timezone}\")`,\n );\n return;\n }\n if (this.scheduleQueue === null) {\n // Defensive: should never happen because `onApplicationBootstrap`\n // builds the queue before iterating entries, but a future\n // refactor that calls `installSchedule` from elsewhere\n // should fail loudly.\n throw new Error('[BullmqScheduleService] scheduleQueue is null');\n }\n const schedulerKey = `${entry.jobId}::${entry.methodName}`;\n const template: {\n name: string;\n data: Record<string, unknown>;\n opts: JobsOptions;\n } = {\n name: entry.methodName,\n data: { jobId: entry.jobId, methodName: entry.methodName },\n opts: {\n attempts: 3,\n backoff: { type: 'exponential', delay: 100, jitter: 0.5 },\n removeOnComplete: { count: 100, age: 3600 },\n removeOnFail: { count: 1000 },\n },\n };\n void this.scheduleQueue.upsertJobScheduler(\n schedulerKey,\n { pattern: entry.cron, tz: entry.timezone },\n template,\n );\n this.installedKeys.add(schedulerKey);\n this.logger.log(\n `Installed schedule: ${schedulerKey} (cron=\"${entry.cron}\", tz=\"${entry.timezone}\")`,\n );\n }\n\n // -------------------------------------------------------------------------\n // Queue construction\n // -------------------------------------------------------------------------\n\n /**\n * Build the producer-side BullMQ queue for the scheduler. The\n * connection tuning mirrors the runtime service's producer\n * options: fail-fast on Redis-down (`enableOfflineQueue:\n * false`) and a tight per-request retry budget\n * (`maxRetriesPerRequest: 1`).\n */\n private buildScheduleQueue(): Queue {\n return new Queue(BULLMQ_SCHEDULE_QUEUE_NAME, {\n connection: this.producerConnectionOptions(),\n defaultJobOptions: {\n attempts: 3,\n backoff: { type: 'exponential', delay: 100, jitter: 0.5 },\n removeOnComplete: { count: 100, age: 3600 },\n removeOnFail: { count: 1000 },\n },\n prefix: this.options.connection.keyPrefix,\n skipWaitingForReady: true,\n // Mirrors the runtime service: skip the constructor-time\n // version probe so the queue does not throw on a Redis\n // client that is not yet ready.\n skipVersionCheck: true,\n });\n }\n\n private producerConnectionOptions(): Record<string, unknown> {\n return {\n host: this.options.connection.host,\n port: this.options.connection.port,\n password: this.options.connection.password,\n username: this.options.connection.username,\n db: this.options.connection.db,\n ...(this.options.connection.tls ? { tls: true } : {}),\n enableOfflineQueue: false,\n maxRetriesPerRequest: 1,\n };\n }\n\n // -------------------------------------------------------------------------\n // Close\n // -------------------------------------------------------------------------\n\n /**\n * Close the schedule queue. `removeJobScheduler` is called\n * first for every installed key so the next run of the host\n * app does not inherit leftover schedulers. Each removal is\n * best-effort: a failure on one key does not prevent the\n * others from being removed.\n */\n private async close(): Promise<void> {\n if (this.scheduleQueue !== null) {\n for (const key of this.installedKeys) {\n try {\n await this.scheduleQueue.removeJobScheduler(key);\n } catch (err) {\n this.logger.warn(\n `removeJobScheduler(\"${key}\") failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n try {\n await this.scheduleQueue.close();\n } catch (err) {\n this.logger.warn(\n `Schedule queue close failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n this.scheduleQueue = null;\n }\n }\n}\n"],"names":["BULLMQ_SCHEDULE_QUEUE_NAME","BullmqScheduleService","logger","Logger","name","scheduleQueue","installedKeys","Set","closePromise","scheduleRegistry","options","onApplicationBootstrap","buildScheduleQueue","entries","getAll","entry","installSchedule","err","warn","jobId","methodName","Error","message","String","log","size","length","onApplicationShutdown","close","installedSchedulerKeys","Array","from","inert","cron","timezone","schedulerKey","template","data","opts","attempts","backoff","type","delay","jitter","removeOnComplete","count","age","removeOnFail","upsertJobScheduler","pattern","tz","add","Queue","connection","producerConnectionOptions","defaultJobOptions","prefix","keyPrefix","skipWaitingForReady","skipVersionCheck","host","port","password","username","db","tls","enableOfflineQueue","maxRetriesPerRequest","key","removeJobScheduler"],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * BullMQ Redis connection options accepted by `BullmqBatchModule`.
3
+ *
4
+ * `BullMQ` is opinionated about Redis client behavior: workers and
5
+ * producers must opt into different connection options so that a
6
+ * Redis outage is observed correctly in each role:
7
+ *
8
+ * - Workers MUST set `maxRetriesPerRequest: null` and
9
+ * `enableReadyCheck: false`. BullMQ's internal blocking commands
10
+ * (`BLPOP`, `BRPOPLPUSH`, `XREADGROUP`) MUST NOT retry
11
+ * per-request — a stalled worker will not surface as a connection
12
+ * error. The Redis client is expected to keep retrying
13
+ * `reconnectOnError` until the operator intervenes.
14
+ *
15
+ * - Producers (the `Queue` used to enqueue work) MUST set
16
+ * `enableOfflineQueue: false` so that a Redis-down condition
17
+ * raises an error *synchronously* on the enqueue call rather than
18
+ * buffering the command and returning success. The `JobLauncher`
19
+ * propagates the failure to the caller, so the call site can
20
+ * mark the `JobExecution` as `FAILED` and surface the error
21
+ * to its caller (HTTP, RPC, cron trigger, ...).
22
+ *
23
+ * Both roles share `host` / `port` / `password` / `username` /
24
+ * `db` / `keyPrefix` / `tls` for connection-target configuration. The
25
+ * role-specific tuning lives on `BullMqConnectionOptions` so callers
26
+ * declare the split explicitly. The default keyPrefix is
27
+ * `nest-batch:` — every key the package writes is namespaced under
28
+ * it, and a key-collision in a shared Redis is impossible.
29
+ *
30
+ * The interface is intentionally `Partial<>`-friendly: a host that
31
+ * only needs a local single-node Redis can pass `{ host: '127.0.0.1' }`
32
+ * and accept all defaults.
33
+ */
34
+ export interface BullMqConnectionOptions {
35
+ /** Redis host (default: `'127.0.0.1'`). */
36
+ host?: string;
37
+ /** Redis port (default: `6379`). */
38
+ port?: number;
39
+ /** AUTH password, if any. */
40
+ password?: string;
41
+ /** ACL username, if any (Redis 6+ ACL). */
42
+ username?: string;
43
+ /** Logical database index (default: `0`). */
44
+ db?: number;
45
+ /**
46
+ * Key prefix. Every BullMQ key the package writes is prefixed with
47
+ * this string (BullMQ appends its own `bull:` after the prefix).
48
+ * Default: `'nest-batch:'`.
49
+ */
50
+ keyPrefix?: string;
51
+ /** Enable TLS for the connection. */
52
+ tls?: boolean;
53
+ }
54
+ /**
55
+ * Resolved Redis connection settings, with all defaults filled in.
56
+ *
57
+ * `BullmqBatchModule.forRoot()` returns a frozen copy of this object
58
+ * under its module-options token; `BullMqExecutionStrategy` reads it
59
+ * to build the `ConnectionOptions` passed into BullMQ's `Queue` /
60
+ * `Worker` / `QueueEvents` constructors.
61
+ */
62
+ export interface BullMqResolvedConnection {
63
+ readonly host: string;
64
+ readonly port: number;
65
+ readonly password: string | undefined;
66
+ readonly username: string | undefined;
67
+ readonly db: number;
68
+ readonly keyPrefix: string;
69
+ readonly tls: boolean;
70
+ }
71
+ export declare const BULLMQ_DEFAULT_HOST = "127.0.0.1";
72
+ export declare const BULLMQ_DEFAULT_PORT = 6379;
73
+ export declare const BULLMQ_DEFAULT_KEY_PREFIX = "nest-batch:";
74
+ /**
75
+ * Fill in defaults for a `BullMqConnectionOptions` bag and return a
76
+ * frozen, fully-resolved connection descriptor.
77
+ *
78
+ * Splitting this out from the module factory keeps the module file
79
+ * focused on DI plumbing and lets the strategy (and tests) construct
80
+ * a resolved connection without re-implementing the defaults.
81
+ */
82
+ export declare function resolveBullMqConnection(options: BullMqConnectionOptions | undefined): BullMqResolvedConnection;
83
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/connection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,WAAW,uBAAuB;IACtC,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,mBAAmB,cAAc,CAAC;AAC/C,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,yBAAyB,gBAAgB,CAAC;AAEvD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,uBAAuB,GAAG,SAAS,GAC3C,wBAAwB,CAU1B"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * BullMQ Redis connection options accepted by `BullmqBatchModule`.
3
+ *
4
+ * `BullMQ` is opinionated about Redis client behavior: workers and
5
+ * producers must opt into different connection options so that a
6
+ * Redis outage is observed correctly in each role:
7
+ *
8
+ * - Workers MUST set `maxRetriesPerRequest: null` and
9
+ * `enableReadyCheck: false`. BullMQ's internal blocking commands
10
+ * (`BLPOP`, `BRPOPLPUSH`, `XREADGROUP`) MUST NOT retry
11
+ * per-request — a stalled worker will not surface as a connection
12
+ * error. The Redis client is expected to keep retrying
13
+ * `reconnectOnError` until the operator intervenes.
14
+ *
15
+ * - Producers (the `Queue` used to enqueue work) MUST set
16
+ * `enableOfflineQueue: false` so that a Redis-down condition
17
+ * raises an error *synchronously* on the enqueue call rather than
18
+ * buffering the command and returning success. The `JobLauncher`
19
+ * propagates the failure to the caller, so the call site can
20
+ * mark the `JobExecution` as `FAILED` and surface the error
21
+ * to its caller (HTTP, RPC, cron trigger, ...).
22
+ *
23
+ * Both roles share `host` / `port` / `password` / `username` /
24
+ * `db` / `keyPrefix` / `tls` for connection-target configuration. The
25
+ * role-specific tuning lives on `BullMqConnectionOptions` so callers
26
+ * declare the split explicitly. The default keyPrefix is
27
+ * `nest-batch:` — every key the package writes is namespaced under
28
+ * it, and a key-collision in a shared Redis is impossible.
29
+ *
30
+ * The interface is intentionally `Partial<>`-friendly: a host that
31
+ * only needs a local single-node Redis can pass `{ host: '127.0.0.1' }`
32
+ * and accept all defaults.
33
+ */ "use strict";
34
+ Object.defineProperty(exports, "__esModule", {
35
+ value: true
36
+ });
37
+ function _export(target, all) {
38
+ for(var name in all)Object.defineProperty(target, name, {
39
+ enumerable: true,
40
+ get: Object.getOwnPropertyDescriptor(all, name).get
41
+ });
42
+ }
43
+ _export(exports, {
44
+ get BULLMQ_DEFAULT_HOST () {
45
+ return BULLMQ_DEFAULT_HOST;
46
+ },
47
+ get BULLMQ_DEFAULT_KEY_PREFIX () {
48
+ return BULLMQ_DEFAULT_KEY_PREFIX;
49
+ },
50
+ get BULLMQ_DEFAULT_PORT () {
51
+ return BULLMQ_DEFAULT_PORT;
52
+ },
53
+ get resolveBullMqConnection () {
54
+ return resolveBullMqConnection;
55
+ }
56
+ });
57
+ const BULLMQ_DEFAULT_HOST = '127.0.0.1';
58
+ const BULLMQ_DEFAULT_PORT = 6379;
59
+ const BULLMQ_DEFAULT_KEY_PREFIX = 'nest-batch:';
60
+ function resolveBullMqConnection(options) {
61
+ return Object.freeze({
62
+ host: options?.host ?? BULLMQ_DEFAULT_HOST,
63
+ port: options?.port ?? BULLMQ_DEFAULT_PORT,
64
+ password: options?.password,
65
+ username: options?.username,
66
+ db: options?.db ?? 0,
67
+ keyPrefix: options?.keyPrefix ?? BULLMQ_DEFAULT_KEY_PREFIX,
68
+ tls: options?.tls ?? false
69
+ });
70
+ }
71
+
72
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/connection.ts"],"sourcesContent":["/**\n * BullMQ Redis connection options accepted by `BullmqBatchModule`.\n *\n * `BullMQ` is opinionated about Redis client behavior: workers and\n * producers must opt into different connection options so that a\n * Redis outage is observed correctly in each role:\n *\n * - Workers MUST set `maxRetriesPerRequest: null` and\n * `enableReadyCheck: false`. BullMQ's internal blocking commands\n * (`BLPOP`, `BRPOPLPUSH`, `XREADGROUP`) MUST NOT retry\n * per-request — a stalled worker will not surface as a connection\n * error. The Redis client is expected to keep retrying\n * `reconnectOnError` until the operator intervenes.\n *\n * - Producers (the `Queue` used to enqueue work) MUST set\n * `enableOfflineQueue: false` so that a Redis-down condition\n * raises an error *synchronously* on the enqueue call rather than\n * buffering the command and returning success. The `JobLauncher`\n * propagates the failure to the caller, so the call site can\n * mark the `JobExecution` as `FAILED` and surface the error\n * to its caller (HTTP, RPC, cron trigger, ...).\n *\n * Both roles share `host` / `port` / `password` / `username` /\n * `db` / `keyPrefix` / `tls` for connection-target configuration. The\n * role-specific tuning lives on `BullMqConnectionOptions` so callers\n * declare the split explicitly. The default keyPrefix is\n * `nest-batch:` — every key the package writes is namespaced under\n * it, and a key-collision in a shared Redis is impossible.\n *\n * The interface is intentionally `Partial<>`-friendly: a host that\n * only needs a local single-node Redis can pass `{ host: '127.0.0.1' }`\n * and accept all defaults.\n */\nexport interface BullMqConnectionOptions {\n /** Redis host (default: `'127.0.0.1'`). */\n host?: string;\n /** Redis port (default: `6379`). */\n port?: number;\n /** AUTH password, if any. */\n password?: string;\n /** ACL username, if any (Redis 6+ ACL). */\n username?: string;\n /** Logical database index (default: `0`). */\n db?: number;\n /**\n * Key prefix. Every BullMQ key the package writes is prefixed with\n * this string (BullMQ appends its own `bull:` after the prefix).\n * Default: `'nest-batch:'`.\n */\n keyPrefix?: string;\n /** Enable TLS for the connection. */\n tls?: boolean;\n}\n\n/**\n * Resolved Redis connection settings, with all defaults filled in.\n *\n * `BullmqBatchModule.forRoot()` returns a frozen copy of this object\n * under its module-options token; `BullMqExecutionStrategy` reads it\n * to build the `ConnectionOptions` passed into BullMQ's `Queue` /\n * `Worker` / `QueueEvents` constructors.\n */\nexport interface BullMqResolvedConnection {\n readonly host: string;\n readonly port: number;\n readonly password: string | undefined;\n readonly username: string | undefined;\n readonly db: number;\n readonly keyPrefix: string;\n readonly tls: boolean;\n}\n\nexport const BULLMQ_DEFAULT_HOST = '127.0.0.1';\nexport const BULLMQ_DEFAULT_PORT = 6379;\nexport const BULLMQ_DEFAULT_KEY_PREFIX = 'nest-batch:';\n\n/**\n * Fill in defaults for a `BullMqConnectionOptions` bag and return a\n * frozen, fully-resolved connection descriptor.\n *\n * Splitting this out from the module factory keeps the module file\n * focused on DI plumbing and lets the strategy (and tests) construct\n * a resolved connection without re-implementing the defaults.\n */\nexport function resolveBullMqConnection(\n options: BullMqConnectionOptions | undefined,\n): BullMqResolvedConnection {\n return Object.freeze({\n host: options?.host ?? BULLMQ_DEFAULT_HOST,\n port: options?.port ?? BULLMQ_DEFAULT_PORT,\n password: options?.password,\n username: options?.username,\n db: options?.db ?? 0,\n keyPrefix: options?.keyPrefix ?? BULLMQ_DEFAULT_KEY_PREFIX,\n tls: options?.tls ?? false,\n });\n}\n"],"names":["BULLMQ_DEFAULT_HOST","BULLMQ_DEFAULT_KEY_PREFIX","BULLMQ_DEFAULT_PORT","resolveBullMqConnection","options","Object","freeze","host","port","password","username","db","keyPrefix","tls"],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC;;;;;;;;;;;QAwCYA;eAAAA;;QAEAC;eAAAA;;QADAC;eAAAA;;QAWGC;eAAAA;;;AAZT,MAAMH,sBAAsB;AAC5B,MAAME,sBAAsB;AAC5B,MAAMD,4BAA4B;AAUlC,SAASE,wBACdC,OAA4C;IAE5C,OAAOC,OAAOC,MAAM,CAAC;QACnBC,MAAMH,SAASG,QAAQP;QACvBQ,MAAMJ,SAASI,QAAQN;QACvBO,UAAUL,SAASK;QACnBC,UAAUN,SAASM;QACnBC,IAAIP,SAASO,MAAM;QACnBC,WAAWR,SAASQ,aAAaX;QACjCY,KAAKT,SAASS,OAAO;IACvB;AACF"}