@warlock.js/queue 5.13.0 → 5.15.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 (46) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +43 -7
  3. package/cjs/index.cjs +525 -29
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/dashboard-boot.d.mts +20 -0
  6. package/esm/dashboard-boot.d.mts.map +1 -0
  7. package/esm/dashboard-boot.mjs +38 -0
  8. package/esm/dashboard-boot.mjs.map +1 -0
  9. package/esm/dashboard-guard-plugin.mjs +28 -0
  10. package/esm/dashboard-guard-plugin.mjs.map +1 -0
  11. package/esm/dashboard-middleware-adapter.mjs +39 -0
  12. package/esm/dashboard-middleware-adapter.mjs.map +1 -0
  13. package/esm/dashboard.d.mts +8 -0
  14. package/esm/dashboard.d.mts.map +1 -1
  15. package/esm/dashboard.mjs +3 -1
  16. package/esm/dashboard.mjs.map +1 -1
  17. package/esm/define-job.mjs +13 -6
  18. package/esm/define-job.mjs.map +1 -1
  19. package/esm/index.d.mts +4 -2
  20. package/esm/index.mjs +3 -1
  21. package/esm/queue-connector.d.mts.map +1 -1
  22. package/esm/queue-connector.mjs +17 -1
  23. package/esm/queue-connector.mjs.map +1 -1
  24. package/esm/queue-dashboard-unguarded.error.d.mts +13 -0
  25. package/esm/queue-dashboard-unguarded.error.d.mts.map +1 -0
  26. package/esm/queue-dashboard-unguarded.error.mjs +17 -0
  27. package/esm/queue-dashboard-unguarded.error.mjs.map +1 -0
  28. package/esm/types.d.mts +19 -2
  29. package/esm/types.d.mts.map +1 -1
  30. package/llms-full.txt +45 -9
  31. package/llms.txt +2 -2
  32. package/package.json +3 -17
  33. package/skills/configure-queue/SKILL.md +4 -0
  34. package/skills/manage-failed-jobs/SKILL.md +33 -4
  35. package/skills/overview/SKILL.md +1 -1
  36. package/skills/queue-notifications/SKILL.md +6 -4
  37. package/cjs/define-job-DideGKQK.cjs +0 -468
  38. package/cjs/define-job-DideGKQK.cjs.map +0 -1
  39. package/cjs/notifications/index.cjs +0 -68
  40. package/cjs/notifications/index.cjs.map +0 -1
  41. package/esm/notifications/index.d.mts +0 -2
  42. package/esm/notifications/index.mjs +0 -3
  43. package/esm/notifications/queue-notification-dispatcher.d.mts +0 -34
  44. package/esm/notifications/queue-notification-dispatcher.d.mts.map +0 -1
  45. package/esm/notifications/queue-notification-dispatcher.mjs +0 -67
  46. package/esm/notifications/queue-notification-dispatcher.mjs.map +0 -1
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["defaultQueueName","registeredJobs","queueOf","getQueue","QueueDashboardDependencyError","getQueue","defaultQueueName","FailedJobNotFoundError","startWorkers","closeQueue"],"sources":["../../../../../../queue/src/dashboard.ts","../../../../../../queue/src/failed-jobs.ts","../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["import { defaultQueueName } from \"./config\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n await server.register(serverAdapter.registerPlugin() as never, { prefix: basePath });\n}\n\n/**\n * Load the optional bull-board packages. Exported for tests of the missing\n * dependency path; `importer` defaults to a real dynamic import.\n */\nexport async function loadBullBoard(\n importer: (specifier: string) => Promise<Record<string, unknown>> = (specifier) => import(specifier),\n): Promise<BullBoardModules> {\n const api = await importOptional(importer, \"@bull-board/api\");\n const adapter = await importOptional(importer, \"@bull-board/api/bullMQAdapter\");\n const fastify = await importOptional(importer, \"@bull-board/fastify\");\n\n return {\n createBullBoard: api.createBullBoard as BullBoardModules[\"createBullBoard\"],\n BullMQAdapter: adapter.BullMQAdapter as BullBoardModules[\"BullMQAdapter\"],\n FastifyAdapter: fastify.FastifyAdapter as BullBoardModules[\"FastifyAdapter\"],\n };\n}\n\nasync function importOptional(\n importer: (specifier: string) => Promise<Record<string, unknown>>,\n specifier: string,\n): Promise<Record<string, unknown>> {\n try {\n return await importer(specifier);\n } catch (error) {\n if (isModuleNotFound(error)) {\n throw new QueueDashboardDependencyError(specifier.split(\"/\").slice(0, 2).join(\"/\"));\n }\n\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n","import type { Job } from \"bullmq\";\nimport { defaultQueueName } from \"./config\";\nimport { FailedJobNotFoundError } from \"./errors\";\nimport { getQueue } from \"./queue-manager\";\nimport type { FailedJob } from \"./types\";\n\nexport type FailedJobsOptions = {\n /** Queue to read. Default: the default queue. */\n queue?: string;\n /** First index (newest first). Default `0`. */\n start?: number;\n /** Last index, inclusive. Default `99`. */\n end?: number;\n};\n\n/**\n * List failed jobs, newest first — jobs that used up every attempt, or\n * failed unrecoverably. Each entry can be retried.\n */\nexport async function failedJobs(options: FailedJobsOptions = {}): Promise<FailedJob[]> {\n const queueName = options.queue ?? defaultQueueName();\n const jobs = await getQueue(queueName).getFailed(options.start ?? 0, options.end ?? 99);\n\n return jobs.map((job) => toFailedJob(job));\n}\n\n/**\n * Retry one failed job by id: it goes back to waiting with its attempts reset.\n * Throws {@link FailedJobNotFoundError} when no FAILED job has that id.\n */\nexport async function retryFailedJob(id: string, options: { queue?: string } = {}): Promise<void> {\n const queueName = options.queue ?? defaultQueueName();\n const job = await getQueue(queueName).getJob(id);\n\n if (!job || !(await job.isFailed())) {\n throw new FailedJobNotFoundError(id, queueName);\n }\n\n await job.retry(\"failed\");\n}\n\nfunction toFailedJob(job: Job): FailedJob {\n return {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n payload: job.data,\n attemptsMade: job.attemptsMade,\n failedReason: job.failedReason,\n stacktrace: job.stacktrace ?? [],\n failedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\n retry: () => job.retry(\"failed\"),\n };\n}\n","/**\n * The queue's connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\n * at config-load time. Core is imported lazily inside `start()`, where the\n * app has already loaded it.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\nimport { closeQueue, startWorkers } from \"./queue-manager\";\nimport type { QueueConfig } from \"./types\";\n\n/**\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\n * the logger is up and anything a job handler needs is already connected;\n * shuts down before them for the same reason.\n */\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\n\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\n\nexport type QueueConnectorOptions = {\n /**\n * Supply the configuration directly instead of reading the `queue` config\n * key (`src/config/queue.ts`).\n */\n config?: QueueConfig;\n};\n\n/**\n * Construct the queue connector.\n *\n * Runs in the `late` lifecycle phase — after app code is imported — so every\n * `defineJob` in the app has registered before workers start. At start it\n * reads the `queue` config and starts in-process workers unless\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\n *\n * @example\n * // warlock.config.ts\n * import { queueConnector } from \"@warlock.js/queue\";\n *\n * export default defineConfig({ connectors: [queueConnector()] });\n */\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\n let active = false;\n\n const connector: Connector = {\n name: \"queue\",\n priority: QUEUE_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\n // module stays free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n boot: () => undefined,\n async start() {\n const queueConfig = options.config ?? (await readQueueConfig());\n\n if (!queueConfig) {\n log.warn(\n \"queue\",\n \"configured\",\n \"queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started\",\n );\n return;\n }\n\n setQueueConfig(queueConfig);\n const started = await startWorkers();\n active = true;\n\n log.info(\n \"queue\",\n \"configured\",\n started.length > 0\n ? `Queue workers running for: ${started.join(\", \")}`\n : \"Queue configured (no in-process workers)\",\n );\n },\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n if (!active) {\n return;\n }\n\n await closeQueue();\n resetQueueConfig();\n active = false;\n },\n shouldRestart(changedFiles) {\n return changedFiles.some((file) => {\n const normalized = file.replace(/\\\\/g, \"/\");\n\n return WATCHED_FILES.some((watched) => normalized === watched || normalized.endsWith(`/${watched}`));\n });\n },\n };\n\n return connector;\n}\n\nasync function readQueueConfig(): Promise<QueueConfig | undefined> {\n const { config } = await import(\"@warlock.js/core\");\n\n return config.get<QueueConfig | undefined>(\"queue\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["UnrecoverableError","Queue","Worker"],"sources":["../../../../../../queue/src/errors.ts","../../../../../../queue/src/config.ts","../../../../../../queue/src/dashboard-middleware-adapter.ts","../../../../../../queue/src/dashboard-guard-plugin.ts","../../../../../../queue/src/job-registry.ts","../../../../../../queue/src/process-job.ts","../../../../../../queue/src/queue-manager.ts","../../../../../../queue/src/dashboard.ts","../../../../../../queue/src/queue-dashboard-unguarded.error.ts","../../../../../../queue/src/dashboard-boot.ts","../../../../../../queue/src/duration.ts","../../../../../../queue/src/define-job.ts","../../../../../../queue/src/failed-jobs.ts","../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["/**\n * Thrown when the queue is used before `setQueueConfig` (or the queue\n * connector) supplied a configuration.\n */\nexport class QueueNotConfiguredError extends Error {\n public constructor() {\n super(\n \"@warlock.js/queue is not configured. Add src/config/queue.ts exporting a QueueConfig \" +\n \"and register queueConnector() in warlock.config.ts > connectors, \" +\n \"or call setQueueConfig() yourself.\",\n );\n this.name = \"QueueNotConfiguredError\";\n }\n}\n\n/**\n * Thrown for a malformed duration such as `\"10 minutes\"`.\n */\nexport class InvalidDurationError extends Error {\n public constructor(value: unknown) {\n super(\n `Invalid duration ${JSON.stringify(value)}: expected milliseconds as a non-negative number ` +\n `or a string like \"500ms\", \"30s\", \"10m\", \"2h\", \"1d\".`,\n );\n this.name = \"InvalidDurationError\";\n }\n}\n\n/**\n * Thrown by `defineJob` for an invalid definition.\n */\nexport class InvalidJobDefinitionError extends Error {\n public constructor(message: string) {\n super(message);\n this.name = \"InvalidJobDefinitionError\";\n }\n}\n\n/**\n * Thrown by `retryFailedJob` when no failed job has the given id.\n */\nexport class FailedJobNotFoundError extends Error {\n public constructor(id: string, queue: string) {\n super(`No failed job with id \"${id}\" on queue \"${queue}\".`);\n this.name = \"FailedJobNotFoundError\";\n }\n}\n\n/**\n * Thrown by `queueDashboard` when an optional bull-board package is not\n * installed.\n */\nexport class QueueDashboardDependencyError extends Error {\n public constructor(missing: string) {\n super(\n `The queue dashboard needs the optional package \"${missing}\", which is not installed.\\n` +\n \"Install both bull-board packages:\\n\\n\" +\n \" npm install @bull-board/api @bull-board/fastify\\n\",\n );\n this.name = \"QueueDashboardDependencyError\";\n }\n}\n","import { QueueNotConfiguredError } from \"./errors\";\nimport type { QueueConfig } from \"./types\";\n\nlet activeConfig: QueueConfig | undefined;\n\n/**\n * Set the active queue configuration. In a Warlock app the queue connector\n * calls this at boot with `src/config/queue.ts`; scripts and tests may call\n * it directly. Replaces (does not merge) any previous configuration.\n */\nexport function setQueueConfig(config: QueueConfig): void {\n activeConfig = config;\n}\n\n/**\n * The active queue configuration. Throws {@link QueueNotConfiguredError}\n * when none was set.\n */\nexport function getQueueConfig(): QueueConfig {\n if (!activeConfig) {\n throw new QueueNotConfiguredError();\n }\n\n return activeConfig;\n}\n\n/** Forget the active configuration. */\nexport function resetQueueConfig(): void {\n activeConfig = undefined;\n}\n\n/** The queue a job runs on when it names none. */\nexport function defaultQueueName(): string {\n return activeConfig?.defaultQueue ?? \"default\";\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\n\n/**\n * Run a Warlock middleware list against a raw Fastify request/reply pair.\n *\n * Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a\n * Warlock route — there is no `Route`, no validation pipeline, and none of\n * `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`\n * stores). Reusing that full pipeline here would pull the whole request\n * machinery into a place it was never meant to run. Instead this builds the\n * minimal `Request`/`Response` pair — enough for guard-style middleware\n * (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the\n * request and short-circuit with a response, which covers every one of\n * bull-board's routes because they all sit behind the same hook.\n *\n * `@warlock.js/core` is imported dynamically so this module never drags\n * core's runtime graph into a process that never mounts the dashboard.\n *\n * @returns `true` when a middleware sent a response and the caller must not\n * continue (bull-board's handler must not run); `false` to continue.\n */\nexport async function runDashboardMiddleware(\n middlewareList: Middleware[],\n fastifyRequest: FastifyRequest,\n fastifyReply: FastifyReply,\n): Promise<boolean> {\n if (middlewareList.length === 0) {\n return false;\n }\n\n const { Request, Response } = await import(\"@warlock.js/core\");\n\n const request = new Request();\n const response = new Response();\n\n response.setResponse(fastifyReply);\n request.response = response;\n response.request = request;\n request.setRequest(fastifyRequest);\n\n for (const middlewareFunction of middlewareList) {\n const result = await middlewareFunction({ request, response });\n\n if (result) {\n return true;\n }\n }\n\n return false;\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyInstance, FastifyPluginCallback } from \"fastify\";\nimport { runDashboardMiddleware } from \"./dashboard-middleware-adapter\";\n\n/**\n * Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`\n * on every request before bull-board's own routes see it.\n *\n * Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify\n * plugin with no hook for Warlock middleware to run through, and Fastify\n * only lets an `onRequest` hook be added to a plugin scope — never spliced\n * into a plugin someone else wrote. So this builds ONE plugin that adds the\n * hook to its own scope and then registers bull-board's plugin as a child of\n * that scope; Fastify's encapsulation runs the hook for every route the\n * child registers, which is every dashboard route.\n */\nexport function buildDashboardGuardPlugin(\n middlewareList: Middleware[],\n bullBoardPlugin: unknown,\n): FastifyPluginCallback {\n return function dashboardGuardPlugin(instance: FastifyInstance, _options, done) {\n if (middlewareList.length > 0) {\n instance.addHook(\"onRequest\", async (request, reply) => {\n const handled = await runDashboardMiddleware(middlewareList, request, reply);\n\n if (handled) {\n // Returning the reply is Fastify's own way for an `onRequest` hook to\n // END the lifecycle, so bull-board's handler never runs. Relying\n // instead on Fastify noticing the reply was already sent leaves the\n // outcome to write ORDERING: a guard answering asynchronously can lose\n // that race and let the dashboard render to an unauthenticated caller.\n // The adapter returns this boolean precisely so the decision is\n // explicit rather than emergent.\n return reply;\n }\n });\n }\n\n instance.register(bullBoardPlugin as never);\n done();\n };\n}\n","import { defaultQueueName } from \"./config\";\nimport type { JobDefinition } from \"./types\";\n\n/** A registered definition, payload/result erased for storage. */\nexport type RegisteredJob = JobDefinition<unknown, unknown>;\n\ntype RegistryListener = (job: RegisteredJob) => void;\n\nconst jobs = new Map<string, RegisteredJob>();\nconst listeners = new Set<RegistryListener>();\n\n/**\n * Register a definition under its name.\n *\n * Re-registering a name REPLACES the previous definition: in development a\n * job module is re-evaluated on every reload, and refusing the second\n * evaluation would break the reload. Job names must therefore be unique\n * across the app — two different modules using one name leave only the\n * later handler active.\n */\nexport function registerJob(job: RegisteredJob): void {\n jobs.set(job.name, job);\n\n for (const listener of listeners) {\n listener(job);\n }\n}\n\n/** The definition registered under `name`, if any. */\nexport function findRegisteredJob(name: string): RegisteredJob | undefined {\n return jobs.get(name);\n}\n\n/** Every registered definition. */\nexport function registeredJobs(): RegisteredJob[] {\n return [...jobs.values()];\n}\n\n/** The queue a definition runs on, resolved against the active config. */\nexport function queueOf(job: Pick<RegisteredJob, \"queue\">): string {\n return job.queue ?? defaultQueueName();\n}\n\n/** Be told whenever a job is registered. Returns an unsubscribe function. */\nexport function onJobRegistered(listener: RegistryListener): () => void {\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n}\n","import { type Job, UnrecoverableError } from \"bullmq\";\nimport { findRegisteredJob } from \"./job-registry\";\nimport type { JobContext } from \"./types\";\n\n/**\n * The single BullMQ processor every worker runs: route the job to the\n * handler registered under its name.\n *\n * A name with no handler in this process fails with `UnrecoverableError` —\n * retrying cannot make a missing definition appear, so it must not burn\n * through its attempts.\n */\nexport async function processJob(job: Job): Promise<unknown> {\n const definition = findRegisteredJob(job.name);\n\n if (!definition) {\n throw new UnrecoverableError(\n `No job named \"${job.name}\" is defined in this process. ` +\n \"Make sure the module that calls defineJob() is imported by the worker process.\",\n );\n }\n\n const context: JobContext = {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n attempt: job.attemptsMade + 1,\n maxAttempts: job.opts.attempts ?? 1,\n progress: (value) => job.updateProgress(value),\n log: async (line) => {\n await job.log(line);\n },\n };\n\n return definition.handle(job.data, context);\n}\n","import { log } from \"@warlock.js/logger\";\nimport { Queue, Worker } from \"bullmq\";\nimport { getQueueConfig } from \"./config\";\nimport { onJobRegistered, queueOf, registeredJobs } from \"./job-registry\";\nimport { processJob } from \"./process-job\";\n\nconst DEFAULT_SHUTDOWN_TIMEOUT = 30_000;\n\nconst queues = new Map<string, Queue>();\nconst workers = new Map<string, Worker>();\nlet stopListening: (() => void) | undefined;\n\n/**\n * The BullMQ queue for `name`, created on first use with the configured\n * connection and prefix.\n */\nexport function getQueue(name: string): Queue {\n let queue = queues.get(name);\n\n if (!queue) {\n const config = getQueueConfig();\n\n queue = new Queue(name, {\n connection: config.connection,\n prefix: config.prefix ?? \"warlock\",\n });\n\n queue.on(\"error\", (error) => {\n log.error(\"queue\", \"connection\", error);\n });\n\n queues.set(name, queue);\n }\n\n return queue;\n}\n\n/**\n * Start one worker per queue that has a registered job, and keep starting\n * workers for queues whose first job is defined later.\n *\n * A no-op returning `[]` when `workers.enabled` is `false`. Calling it again\n * while workers run starts only the missing ones.\n *\n * @returns the queue names that now have a worker in this process.\n */\nexport async function startWorkers(): Promise<string[]> {\n const config = getQueueConfig();\n\n if (config.workers?.enabled === false) {\n return [];\n }\n\n for (const job of registeredJobs()) {\n ensureWorker(queueOf(job));\n }\n\n stopListening ??= onJobRegistered((job) => {\n ensureWorker(queueOf(job));\n });\n\n await Promise.all([...workers.values()].map((worker) => worker.waitUntilReady()));\n\n return [...workers.keys()];\n}\n\n/** The queue names with a running worker in this process. */\nexport function runningWorkers(): string[] {\n return [...workers.keys()];\n}\n\nfunction ensureWorker(queueName: string): void {\n if (workers.has(queueName)) {\n return;\n }\n\n const config = getQueueConfig();\n\n const worker = new Worker(queueName, processJob, {\n connection: config.connection,\n prefix: config.prefix ?? \"warlock\",\n concurrency: config.workers?.concurrency ?? 1,\n });\n\n worker.on(\"error\", (error) => {\n log.error(\"queue\", \"worker\", error);\n });\n\n worker.on(\"failed\", (job, error) => {\n log.error(\"queue\", \"job.failed\", `${job?.name ?? \"unknown\"} (${job?.id ?? \"?\"}): ${error.message}`);\n });\n\n workers.set(queueName, worker);\n}\n\nexport type CloseQueueOptions = {\n /**\n * How long to wait for active jobs before force-closing workers, in\n * milliseconds. Default: `workers.shutdownTimeout`, else `30000`.\n */\n timeout?: number;\n};\n\n/**\n * Graceful shutdown: stop workers taking new jobs and wait for active ones\n * (bounded by `timeout`, then force-close), then close every queue\n * connection. Safe to call when nothing was started, and more than once.\n */\nexport async function closeQueue(options: CloseQueueOptions = {}): Promise<void> {\n stopListening?.();\n stopListening = undefined;\n\n const timeout = options.timeout ?? configuredShutdownTimeout();\n const closingWorkers = [...workers.values()];\n workers.clear();\n\n await Promise.all(closingWorkers.map((worker) => closeWorker(worker, timeout)));\n\n const closingQueues = [...queues.values()];\n queues.clear();\n\n await Promise.all(closingQueues.map((queue) => queue.close()));\n}\n\nfunction configuredShutdownTimeout(): number {\n try {\n return getQueueConfig().workers?.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;\n } catch {\n return DEFAULT_SHUTDOWN_TIMEOUT;\n }\n}\n\nasync function closeWorker(worker: Worker, timeout: number): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), timeout);\n });\n\n // A close already in progress cannot be upgraded to a forced one (BullMQ\n // returns the pending promise), so the timeout path drops the connections\n // instead and lets the graceful close settle in the background.\n const closing = worker.close().then(() => \"closed\" as const);\n closing.catch(() => undefined);\n\n const outcome = await Promise.race([closing, timedOut]);\n\n clearTimeout(timer);\n\n if (outcome === \"timeout\") {\n log.warn(\n \"queue\",\n \"shutdown\",\n `Worker for \"${worker.name}\" still had active jobs after ${timeout}ms; disconnecting. ` +\n \"Those jobs are retried once their lock expires.\",\n );\n\n await worker.disconnect();\n }\n}\n","import type { Middleware } from \"@warlock.js/core\";\nimport { defaultQueueName } from \"./config\";\nimport { buildDashboardGuardPlugin } from \"./dashboard-guard-plugin\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n /**\n * Run before every dashboard route. Applied via a wrapping Fastify plugin\n * scope, since bull-board's own plugin has no hook to splice Warlock\n * middleware into — see `dashboard-guard-plugin.ts`.\n */\n middleware?: Middleware[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());\n\n await server.register(guardedPlugin as never, { prefix: basePath });\n}\n\n/**\n * Load the optional bull-board packages. Exported for tests of the missing\n * dependency path; `importer` defaults to a real dynamic import.\n */\nexport async function loadBullBoard(\n importer: (specifier: string) => Promise<Record<string, unknown>> = (specifier) => import(specifier),\n): Promise<BullBoardModules> {\n const api = await importOptional(importer, \"@bull-board/api\");\n const adapter = await importOptional(importer, \"@bull-board/api/bullMQAdapter\");\n const fastify = await importOptional(importer, \"@bull-board/fastify\");\n\n return {\n createBullBoard: api.createBullBoard as BullBoardModules[\"createBullBoard\"],\n BullMQAdapter: adapter.BullMQAdapter as BullBoardModules[\"BullMQAdapter\"],\n FastifyAdapter: fastify.FastifyAdapter as BullBoardModules[\"FastifyAdapter\"],\n };\n}\n\nasync function importOptional(\n importer: (specifier: string) => Promise<Record<string, unknown>>,\n specifier: string,\n): Promise<Record<string, unknown>> {\n try {\n return await importer(specifier);\n } catch (error) {\n if (isModuleNotFound(error)) {\n throw new QueueDashboardDependencyError(specifier.split(\"/\").slice(0, 2).join(\"/\"));\n }\n\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n","/**\n * Thrown at boot when `queue.dashboard.enabled` is `true` in production with\n * no guard middleware. The dashboard can retry and delete jobs; mounting it\n * on the open internet without a guard is a production incident waiting to\n * happen, so this fails the boot instead of shipping the hole.\n */\nexport class QueueDashboardUnguardedError extends Error {\n public constructor() {\n super(\n \"queue.dashboard.enabled is true in production with no middleware. The dashboard can \" +\n \"retry and delete jobs, so it must be guarded before it is exposed.\\n\\n\" +\n \"Add a guard middleware:\\n\\n\" +\n \" import { middleware } from \\\"@warlock.js/core\\\";\\n\" +\n \" import { authMiddleware } from \\\"@warlock.js/auth\\\";\\n\\n\" +\n \" const queueConfig: QueueConfig = {\\n\" +\n \" // ...\\n\" +\n \" dashboard: {\\n\" +\n \" enabled: true,\\n\" +\n \" middleware: [authMiddleware(\\\"admin\\\")],\\n\" +\n \" },\\n\" +\n \" };\\n\",\n );\n this.name = \"QueueDashboardUnguardedError\";\n }\n}\n","import { log } from \"@warlock.js/logger\";\nimport { type DashboardServer, queueDashboard } from \"./dashboard\";\nimport { QueueDashboardUnguardedError } from \"./queue-dashboard-unguarded.error\";\nimport type { QueueConfig } from \"./types\";\n\n/** Default path the dashboard mounts on when `dashboard.path` is not set. */\nexport const DEFAULT_DASHBOARD_PATH = \"/admin/queues\";\n\n/**\n * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying\n * the safety rule: `enabled` in production with no middleware throws\n * {@link QueueDashboardUnguardedError} instead of booting exposed; outside\n * production with no middleware it logs one warning and mounts anyway.\n *\n * Called by `queueConnector()` at boot, once the HTTP server exists but\n * before it starts listening — see `queue-connector.ts`. Exported so it can\n * be unit-tested without going through the whole connector lifecycle.\n */\nexport async function mountQueueDashboard(\n server: DashboardServer | undefined,\n config: QueueConfig,\n): Promise<void> {\n const dashboard = config.dashboard;\n\n if (!dashboard?.enabled) {\n return;\n }\n\n const middlewareList = dashboard.middleware ?? [];\n\n if (middlewareList.length === 0) {\n if (process.env.NODE_ENV === \"production\") {\n throw new QueueDashboardUnguardedError();\n }\n\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete \" +\n \"jobs — add a guard middleware before this ships to production.\",\n );\n }\n\n if (!server) {\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.\",\n );\n\n return;\n }\n\n await queueDashboard(server, {\n basePath: dashboard.path ?? DEFAULT_DASHBOARD_PATH,\n middleware: middlewareList,\n });\n}\n","import { InvalidDurationError } from \"./errors\";\nimport type { Duration } from \"./types\";\n\nconst UNIT_MILLISECONDS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\nconst DURATION_PATTERN = /^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$/;\n\n/**\n * Convert a {@link Duration} to milliseconds. Numbers are already\n * milliseconds. Anything else is rejected loudly rather than guessed at.\n */\nexport function toMilliseconds(value: Duration | string): number {\n if (typeof value === \"number\") {\n if (!Number.isFinite(value) || value < 0) {\n throw new InvalidDurationError(value);\n }\n\n return Math.round(value);\n }\n\n const match = typeof value === \"string\" ? DURATION_PATTERN.exec(value.trim()) : null;\n\n if (!match) {\n throw new InvalidDurationError(value);\n }\n\n return Math.round(Number(match[1]) * UNIT_MILLISECONDS[match[2]!]!);\n}\n","import type { Job, JobsOptions } from \"bullmq\";\r\nimport { getQueueConfig } from \"./config\";\r\nimport { toMilliseconds } from \"./duration\";\r\nimport { InvalidJobDefinitionError } from \"./errors\";\r\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\r\nimport { getQueue } from \"./queue-manager\";\r\nimport type {\r\n DispatchOptions,\r\n JobBackoff,\r\n JobDefinition,\r\n JobOptions,\r\n JobSnapshot,\r\n JobState,\r\n QueueJob,\r\n} from \"./types\";\r\n\r\n/**\r\n * Define a background job.\r\n *\r\n * The definition is registered by name so any worker in the process can run\r\n * it; the returned object dispatches it with a typed payload.\r\n *\r\n * @example\r\n * export const sendInvoice = defineJob({\r\n * name: \"invoices.send\",\r\n * attempts: 5,\r\n * backoff: { type: \"exponential\", delay: 2000 },\r\n * async handle(payload: { invoiceId: string }, ctx) {\r\n * await ctx.progress(50);\r\n * },\r\n * });\r\n *\r\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\r\n */\r\nexport function defineJob<TPayload, TResult = unknown>(\r\n definition: JobDefinition<TPayload, TResult>,\r\n): QueueJob<TPayload, TResult> {\r\n assertValidDefinition(definition);\r\n registerJob(definition as RegisteredJob);\r\n\r\n return {\r\n name: definition.name,\r\n get queue() {\r\n return queueOf(definition);\r\n },\r\n async dispatch(payload, options = {}) {\r\n const queueName = queueOf(definition);\r\n const job = await getQueue(queueName).add(\r\n definition.name,\r\n payload,\r\n toBullJobOptions(definition, options),\r\n );\r\n\r\n return { id: String(job.id), name: definition.name, queue: queueName };\r\n },\r\n async find(id) {\r\n const queue = getQueue(queueOf(definition));\r\n const initialJob = await queue.getJob(id);\r\n\r\n if (!initialJob || initialJob.name !== definition.name) {\r\n return undefined;\r\n }\r\n\r\n // Read the state first, then (re)fetch the job. BullMQ writes a job's\r\n // result/attemptsMade/finishedOn fields *before* it becomes visible\r\n // under a new state, so re-reading the job after the state is known\r\n // guarantees those fields are consistent with the reported state\r\n // (rather than reflecting a moment before the job finished).\r\n const state = (await initialJob.getState()) as JobState;\r\n const job = (await queue.getJob(id)) ?? initialJob;\r\n\r\n return toSnapshot<TPayload, TResult>(job, state);\r\n },\r\n };\r\n}\r\n\r\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\r\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\r\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\r\n }\r\n\r\n if (typeof definition.handle !== \"function\") {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\r\n );\r\n }\r\n\r\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\r\n */\r\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\r\n const defaults = getQueueConfig().defaultJobOptions ?? {};\r\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\r\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\r\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\r\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\r\n\r\n const bullOptions: JobsOptions = {};\r\n\r\n if (attempts !== undefined) bullOptions.attempts = attempts;\r\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\r\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\r\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\r\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\r\n if (options.priority !== undefined) bullOptions.priority = options.priority;\r\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\r\n\r\n return bullOptions;\r\n}\r\n\r\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\r\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\r\n}\r\n\r\n/**\r\n * A plain view of a BullMQ job.\r\n *\r\n * @param job The job to read fields from.\r\n * @param state The job's state; pass a state read *before* `job` was\r\n * fetched (or re-fetched) so the returned snapshot's fields are consistent\r\n * with it. If omitted, the state is read from `job` directly.\r\n */\r\nexport async function toSnapshot<TPayload, TResult>(\r\n job: Job,\r\n state?: JobState,\r\n): Promise<JobSnapshot<TPayload, TResult>> {\r\n const resolvedState = state ?? ((await job.getState()) as JobState);\r\n\r\n return {\r\n id: String(job.id),\r\n name: job.name,\r\n queue: job.queueName,\r\n state: resolvedState,\r\n payload: job.data as TPayload,\r\n progress: job.progress as JobSnapshot[\"progress\"],\r\n attemptsMade: job.attemptsMade,\r\n result: job.returnvalue as TResult | undefined,\r\n failedReason: job.failedReason || undefined,\r\n createdAt: new Date(job.timestamp),\r\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\r\n };\r\n}\r\n","import type { Job } from \"bullmq\";\nimport { defaultQueueName } from \"./config\";\nimport { FailedJobNotFoundError } from \"./errors\";\nimport { getQueue } from \"./queue-manager\";\nimport type { FailedJob } from \"./types\";\n\nexport type FailedJobsOptions = {\n /** Queue to read. Default: the default queue. */\n queue?: string;\n /** First index (newest first). Default `0`. */\n start?: number;\n /** Last index, inclusive. Default `99`. */\n end?: number;\n};\n\n/**\n * List failed jobs, newest first — jobs that used up every attempt, or\n * failed unrecoverably. Each entry can be retried.\n */\nexport async function failedJobs(options: FailedJobsOptions = {}): Promise<FailedJob[]> {\n const queueName = options.queue ?? defaultQueueName();\n const jobs = await getQueue(queueName).getFailed(options.start ?? 0, options.end ?? 99);\n\n return jobs.map((job) => toFailedJob(job));\n}\n\n/**\n * Retry one failed job by id: it goes back to waiting with its attempts reset.\n * Throws {@link FailedJobNotFoundError} when no FAILED job has that id.\n */\nexport async function retryFailedJob(id: string, options: { queue?: string } = {}): Promise<void> {\n const queueName = options.queue ?? defaultQueueName();\n const job = await getQueue(queueName).getJob(id);\n\n if (!job || !(await job.isFailed())) {\n throw new FailedJobNotFoundError(id, queueName);\n }\n\n await job.retry(\"failed\");\n}\n\nfunction toFailedJob(job: Job): FailedJob {\n return {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n payload: job.data,\n attemptsMade: job.attemptsMade,\n failedReason: job.failedReason,\n stacktrace: job.stacktrace ?? [],\n failedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\n retry: () => job.retry(\"failed\"),\n };\n}\n","/**\r\n * The queue's connector for `warlock.config.ts > connectors`.\r\n *\r\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\r\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\r\n * at config-load time. Core is imported lazily inside `start()`, where the\r\n * app has already loaded it.\r\n */\r\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\r\nimport { mountQueueDashboard } from \"./dashboard-boot\";\r\nimport { closeQueue, startWorkers } from \"./queue-manager\";\r\nimport type { QueueConfig } from \"./types\";\r\n\r\n/**\r\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\r\n * the logger is up and anything a job handler needs is already connected;\r\n * shuts down before them for the same reason.\r\n */\r\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\r\n\r\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\r\n\r\nexport type QueueConnectorOptions = {\r\n /**\r\n * Supply the configuration directly instead of reading the `queue` config\r\n * key (`src/config/queue.ts`).\r\n */\r\n config?: QueueConfig;\r\n};\r\n\r\n/**\r\n * Construct the queue connector.\r\n *\r\n * Runs in the `late` lifecycle phase — after app code is imported — so every\r\n * `defineJob` in the app has registered before workers start. At start it\r\n * reads the `queue` config and starts in-process workers unless\r\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\r\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\r\n *\r\n * @example\r\n * // warlock.config.ts\r\n * import { queueConnector } from \"@warlock.js/queue\";\r\n *\r\n * export default defineConfig({ connectors: [queueConnector()] });\r\n */\r\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\r\n let active = false;\r\n\r\n const connector: Connector = {\r\n name: \"queue\",\r\n priority: QUEUE_CONNECTOR_PRIORITY,\r\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\r\n // module stays free of a runtime import of core.\r\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\r\n isActive: () => active,\r\n /**\r\n * Mounts the dashboard, when configured, here rather than in `start()`:\r\n * `boot()` runs for every late-phase connector, in priority order, before\r\n * any of them `start()`s — so by the time this runs, the HTTP connector\r\n * (priority 5, before queue's 11) has already built its Fastify instance\r\n * and registered its own plugins, but has not yet called `listen()`.\r\n * Fastify refuses new plugin registrations after `listen()`, so this is\r\n * the only point in the boot sequence where mounting is possible.\r\n */\r\n async boot() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig?.dashboard?.enabled) {\r\n return;\r\n }\r\n\r\n // The dashboard resolves queues through the active config, so it has to\r\n // be registered here rather than only in `start()`, which runs after\r\n // every late connector has booted. `start()` sets it again; the setter\r\n // is idempotent for the same object.\r\n setQueueConfig(queueConfig);\r\n\r\n const { getHttpServer } = await import(\"@warlock.js/core\");\r\n\r\n await mountQueueDashboard(getHttpServer(), queueConfig);\r\n },\r\n async start() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig) {\r\n log.warn(\r\n \"queue\",\r\n \"configured\",\r\n \"queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started\",\r\n );\r\n return;\r\n }\r\n\r\n setQueueConfig(queueConfig);\r\n const started = await startWorkers();\r\n active = true;\r\n\r\n log.info(\r\n \"queue\",\r\n \"configured\",\r\n started.length > 0\r\n ? `Queue workers running for: ${started.join(\", \")}`\r\n : \"Queue configured (no in-process workers)\",\r\n );\r\n },\r\n async restart() {\r\n await connector.shutdown();\r\n await connector.start();\r\n },\r\n async shutdown() {\r\n if (!active) {\r\n return;\r\n }\r\n\r\n await closeQueue();\r\n resetQueueConfig();\r\n active = false;\r\n },\r\n shouldRestart(changedFiles) {\r\n return changedFiles.some((file) => {\r\n const normalized = file.replace(/\\\\/g, \"/\");\r\n\r\n return WATCHED_FILES.some((watched) => normalized === watched || normalized.endsWith(`/${watched}`));\r\n });\r\n },\r\n };\r\n\r\n return connector;\r\n}\r\n\r\nasync function readQueueConfig(): Promise<QueueConfig | undefined> {\r\n const { config } = await import(\"@warlock.js/core\");\r\n\r\n return config.get<QueueConfig | undefined>(\"queue\");\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { QueueConfig } from "./types.mjs";
2
+ import { DashboardServer } from "./dashboard.mjs";
3
+
4
+ //#region ../queue/src/dashboard-boot.d.ts
5
+ /** Default path the dashboard mounts on when `dashboard.path` is not set. */
6
+ declare const DEFAULT_DASHBOARD_PATH = "/admin/queues";
7
+ /**
8
+ * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying
9
+ * the safety rule: `enabled` in production with no middleware throws
10
+ * {@link QueueDashboardUnguardedError} instead of booting exposed; outside
11
+ * production with no middleware it logs one warning and mounts anyway.
12
+ *
13
+ * Called by `queueConnector()` at boot, once the HTTP server exists but
14
+ * before it starts listening — see `queue-connector.ts`. Exported so it can
15
+ * be unit-tested without going through the whole connector lifecycle.
16
+ */
17
+ declare function mountQueueDashboard(server: DashboardServer | undefined, config: QueueConfig): Promise<void>;
18
+ //#endregion
19
+ export { DEFAULT_DASHBOARD_PATH, mountQueueDashboard };
20
+ //# sourceMappingURL=dashboard-boot.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-boot.d.mts","names":[],"sources":["../../../../../../queue/src/dashboard-boot.ts"],"mappings":";;;;;cAMa,sBAAA;AAAb;;;;AAAmC;AAYnC;;;;;AAZA,iBAYsB,mBAAA,CACpB,MAAA,EAAQ,eAAA,cACR,MAAA,EAAQ,WAAA,GACP,OAAA"}
@@ -0,0 +1,38 @@
1
+ import { queueDashboard } from "./dashboard.mjs";
2
+ import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
3
+ import { log } from "@warlock.js/logger";
4
+
5
+ //#region ../queue/src/dashboard-boot.ts
6
+ /** Default path the dashboard mounts on when `dashboard.path` is not set. */
7
+ const DEFAULT_DASHBOARD_PATH = "/admin/queues";
8
+ /**
9
+ * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying
10
+ * the safety rule: `enabled` in production with no middleware throws
11
+ * {@link QueueDashboardUnguardedError} instead of booting exposed; outside
12
+ * production with no middleware it logs one warning and mounts anyway.
13
+ *
14
+ * Called by `queueConnector()` at boot, once the HTTP server exists but
15
+ * before it starts listening — see `queue-connector.ts`. Exported so it can
16
+ * be unit-tested without going through the whole connector lifecycle.
17
+ */
18
+ async function mountQueueDashboard(server, config) {
19
+ const dashboard = config.dashboard;
20
+ if (!dashboard?.enabled) return;
21
+ const middlewareList = dashboard.middleware ?? [];
22
+ if (middlewareList.length === 0) {
23
+ if (process.env.NODE_ENV === "production") throw new QueueDashboardUnguardedError();
24
+ log.warn("queue", "dashboard", "queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete jobs — add a guard middleware before this ships to production.");
25
+ }
26
+ if (!server) {
27
+ log.warn("queue", "dashboard", "queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.");
28
+ return;
29
+ }
30
+ await queueDashboard(server, {
31
+ basePath: dashboard.path ?? "/admin/queues",
32
+ middleware: middlewareList
33
+ });
34
+ }
35
+
36
+ //#endregion
37
+ export { DEFAULT_DASHBOARD_PATH, mountQueueDashboard };
38
+ //# sourceMappingURL=dashboard-boot.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-boot.mjs","names":[],"sources":["../../../../../../queue/src/dashboard-boot.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { type DashboardServer, queueDashboard } from \"./dashboard\";\nimport { QueueDashboardUnguardedError } from \"./queue-dashboard-unguarded.error\";\nimport type { QueueConfig } from \"./types\";\n\n/** Default path the dashboard mounts on when `dashboard.path` is not set. */\nexport const DEFAULT_DASHBOARD_PATH = \"/admin/queues\";\n\n/**\n * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying\n * the safety rule: `enabled` in production with no middleware throws\n * {@link QueueDashboardUnguardedError} instead of booting exposed; outside\n * production with no middleware it logs one warning and mounts anyway.\n *\n * Called by `queueConnector()` at boot, once the HTTP server exists but\n * before it starts listening — see `queue-connector.ts`. Exported so it can\n * be unit-tested without going through the whole connector lifecycle.\n */\nexport async function mountQueueDashboard(\n server: DashboardServer | undefined,\n config: QueueConfig,\n): Promise<void> {\n const dashboard = config.dashboard;\n\n if (!dashboard?.enabled) {\n return;\n }\n\n const middlewareList = dashboard.middleware ?? [];\n\n if (middlewareList.length === 0) {\n if (process.env.NODE_ENV === \"production\") {\n throw new QueueDashboardUnguardedError();\n }\n\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete \" +\n \"jobs — add a guard middleware before this ships to production.\",\n );\n }\n\n if (!server) {\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.\",\n );\n\n return;\n }\n\n await queueDashboard(server, {\n basePath: dashboard.path ?? DEFAULT_DASHBOARD_PATH,\n middleware: middlewareList,\n });\n}\n"],"mappings":";;;;;;AAMA,MAAa,yBAAyB;;;;;;;;;;;AAYtC,eAAsB,oBACpB,QACA,QACe;CACf,MAAM,YAAY,OAAO;CAEzB,IAAI,CAAC,WAAW,SACd;CAGF,MAAM,iBAAiB,UAAU,cAAc,CAAC;CAEhD,IAAI,eAAe,WAAW,GAAG;EAC/B,IAAI,QAAQ,IAAI,aAAa,cAC3B,MAAM,IAAI,6BAA6B;EAGzC,IAAI,KACF,SACA,aACA,uJAEF;CACF;CAEA,IAAI,CAAC,QAAQ;EACX,IAAI,KACF,SACA,aACA,8FACF;EAEA;CACF;CAEA,MAAM,eAAe,QAAQ;EAC3B,UAAU,UAAU;EACpB,YAAY;CACd,CAAC;AACH"}
@@ -0,0 +1,28 @@
1
+ import { runDashboardMiddleware } from "./dashboard-middleware-adapter.mjs";
2
+
3
+ //#region ../queue/src/dashboard-guard-plugin.ts
4
+ /**
5
+ * Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`
6
+ * on every request before bull-board's own routes see it.
7
+ *
8
+ * Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify
9
+ * plugin with no hook for Warlock middleware to run through, and Fastify
10
+ * only lets an `onRequest` hook be added to a plugin scope — never spliced
11
+ * into a plugin someone else wrote. So this builds ONE plugin that adds the
12
+ * hook to its own scope and then registers bull-board's plugin as a child of
13
+ * that scope; Fastify's encapsulation runs the hook for every route the
14
+ * child registers, which is every dashboard route.
15
+ */
16
+ function buildDashboardGuardPlugin(middlewareList, bullBoardPlugin) {
17
+ return function dashboardGuardPlugin(instance, _options, done) {
18
+ if (middlewareList.length > 0) instance.addHook("onRequest", async (request, reply) => {
19
+ if (await runDashboardMiddleware(middlewareList, request, reply)) return reply;
20
+ });
21
+ instance.register(bullBoardPlugin);
22
+ done();
23
+ };
24
+ }
25
+
26
+ //#endregion
27
+ export { buildDashboardGuardPlugin };
28
+ //# sourceMappingURL=dashboard-guard-plugin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-guard-plugin.mjs","names":[],"sources":["../../../../../../queue/src/dashboard-guard-plugin.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyInstance, FastifyPluginCallback } from \"fastify\";\nimport { runDashboardMiddleware } from \"./dashboard-middleware-adapter\";\n\n/**\n * Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`\n * on every request before bull-board's own routes see it.\n *\n * Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify\n * plugin with no hook for Warlock middleware to run through, and Fastify\n * only lets an `onRequest` hook be added to a plugin scope — never spliced\n * into a plugin someone else wrote. So this builds ONE plugin that adds the\n * hook to its own scope and then registers bull-board's plugin as a child of\n * that scope; Fastify's encapsulation runs the hook for every route the\n * child registers, which is every dashboard route.\n */\nexport function buildDashboardGuardPlugin(\n middlewareList: Middleware[],\n bullBoardPlugin: unknown,\n): FastifyPluginCallback {\n return function dashboardGuardPlugin(instance: FastifyInstance, _options, done) {\n if (middlewareList.length > 0) {\n instance.addHook(\"onRequest\", async (request, reply) => {\n const handled = await runDashboardMiddleware(middlewareList, request, reply);\n\n if (handled) {\n // Returning the reply is Fastify's own way for an `onRequest` hook to\n // END the lifecycle, so bull-board's handler never runs. Relying\n // instead on Fastify noticing the reply was already sent leaves the\n // outcome to write ORDERING: a guard answering asynchronously can lose\n // that race and let the dashboard render to an unauthenticated caller.\n // The adapter returns this boolean precisely so the decision is\n // explicit rather than emergent.\n return reply;\n }\n });\n }\n\n instance.register(bullBoardPlugin as never);\n done();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,gBACA,iBACuB;CACvB,OAAO,SAAS,qBAAqB,UAA2B,UAAU,MAAM;EAC9E,IAAI,eAAe,SAAS,GAC1B,SAAS,QAAQ,aAAa,OAAO,SAAS,UAAU;GAGtD,IAAI,MAFkB,uBAAuB,gBAAgB,SAAS,KAAK,GAUzE,OAAO;EAEX,CAAC;EAGH,SAAS,SAAS,eAAwB;EAC1C,KAAK;CACP;AACF"}
@@ -0,0 +1,39 @@
1
+ //#region ../queue/src/dashboard-middleware-adapter.ts
2
+ /**
3
+ * Run a Warlock middleware list against a raw Fastify request/reply pair.
4
+ *
5
+ * Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a
6
+ * Warlock route — there is no `Route`, no validation pipeline, and none of
7
+ * `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`
8
+ * stores). Reusing that full pipeline here would pull the whole request
9
+ * machinery into a place it was never meant to run. Instead this builds the
10
+ * minimal `Request`/`Response` pair — enough for guard-style middleware
11
+ * (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the
12
+ * request and short-circuit with a response, which covers every one of
13
+ * bull-board's routes because they all sit behind the same hook.
14
+ *
15
+ * `@warlock.js/core` is imported dynamically so this module never drags
16
+ * core's runtime graph into a process that never mounts the dashboard.
17
+ *
18
+ * @returns `true` when a middleware sent a response and the caller must not
19
+ * continue (bull-board's handler must not run); `false` to continue.
20
+ */
21
+ async function runDashboardMiddleware(middlewareList, fastifyRequest, fastifyReply) {
22
+ if (middlewareList.length === 0) return false;
23
+ const { Request, Response } = await import("@warlock.js/core");
24
+ const request = new Request();
25
+ const response = new Response();
26
+ response.setResponse(fastifyReply);
27
+ request.response = response;
28
+ response.request = request;
29
+ request.setRequest(fastifyRequest);
30
+ for (const middlewareFunction of middlewareList) if (await middlewareFunction({
31
+ request,
32
+ response
33
+ })) return true;
34
+ return false;
35
+ }
36
+
37
+ //#endregion
38
+ export { runDashboardMiddleware };
39
+ //# sourceMappingURL=dashboard-middleware-adapter.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-middleware-adapter.mjs","names":[],"sources":["../../../../../../queue/src/dashboard-middleware-adapter.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\n\n/**\n * Run a Warlock middleware list against a raw Fastify request/reply pair.\n *\n * Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a\n * Warlock route — there is no `Route`, no validation pipeline, and none of\n * `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`\n * stores). Reusing that full pipeline here would pull the whole request\n * machinery into a place it was never meant to run. Instead this builds the\n * minimal `Request`/`Response` pair — enough for guard-style middleware\n * (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the\n * request and short-circuit with a response, which covers every one of\n * bull-board's routes because they all sit behind the same hook.\n *\n * `@warlock.js/core` is imported dynamically so this module never drags\n * core's runtime graph into a process that never mounts the dashboard.\n *\n * @returns `true` when a middleware sent a response and the caller must not\n * continue (bull-board's handler must not run); `false` to continue.\n */\nexport async function runDashboardMiddleware(\n middlewareList: Middleware[],\n fastifyRequest: FastifyRequest,\n fastifyReply: FastifyReply,\n): Promise<boolean> {\n if (middlewareList.length === 0) {\n return false;\n }\n\n const { Request, Response } = await import(\"@warlock.js/core\");\n\n const request = new Request();\n const response = new Response();\n\n response.setResponse(fastifyReply);\n request.response = response;\n response.request = request;\n request.setRequest(fastifyRequest);\n\n for (const middlewareFunction of middlewareList) {\n const result = await middlewareFunction({ request, response });\n\n if (result) {\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsBA,eAAsB,uBACpB,gBACA,gBACA,cACkB;CAClB,IAAI,eAAe,WAAW,GAC5B,OAAO;CAGT,MAAM,EAAE,SAAS,aAAa,MAAM,OAAO;CAE3C,MAAM,UAAU,IAAI,QAAQ;CAC5B,MAAM,WAAW,IAAI,SAAS;CAE9B,SAAS,YAAY,YAAY;CACjC,QAAQ,WAAW;CACnB,SAAS,UAAU;CACnB,QAAQ,WAAW,cAAc;CAEjC,KAAK,MAAM,sBAAsB,gBAG/B,IAAI,MAFiB,mBAAmB;EAAE;EAAS;CAAS,CAAC,GAG3D,OAAO;CAIX,OAAO;AACT"}
@@ -1,3 +1,5 @@
1
+ import { Middleware } from "@warlock.js/core";
2
+
1
3
  //#region ../queue/src/dashboard.d.ts
2
4
  /**
3
5
  * The part of a Fastify instance the dashboard needs. Warlock's HTTP server
@@ -11,6 +13,12 @@ type DashboardServer = {
11
13
  type QueueDashboardOptions = {
12
14
  /** URL prefix the dashboard is mounted on. Default `"/admin/queues"`. */basePath?: string; /** Queues to show. Default: every queue with a defined job, plus the default queue. */
13
15
  queues?: string[];
16
+ /**
17
+ * Run before every dashboard route. Applied via a wrapping Fastify plugin
18
+ * scope, since bull-board's own plugin has no hook to splice Warlock
19
+ * middleware into — see `dashboard-guard-plugin.ts`.
20
+ */
21
+ middleware?: Middleware[];
14
22
  };
15
23
  type BullBoardModules = {
16
24
  createBullBoard: (options: {
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.d.mts","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"mappings":";;AASA;;;KAAY,eAAA;EACV,QAAA,CAAS,MAAA,SAAe,OAAA;IAAW,MAAA;EAAA;AAAA;AAAA,KAGzB,qBAAA;EAHyC,yEAKnD,QAAA,WAF+B;EAI/B,MAAM;AAAA;AAAA,KAGH,gBAAA;EACH,eAAA,GAAkB,OAAA;IAAW,MAAA;IAAmB,aAAA;EAAA;EAChD,aAAA,OAAoB,KAAA;EACpB,cAAA;IACE,WAAA,CAAY,IAAA;IACZ,cAAA;EAAA;AAAA;;;;;;AAAc;AAiBlB;;;;;;;iBAAsB,cAAA,CACpB,MAAA,EAAQ,eAAA,EACR,OAAA,GAAS,qBAAA,GACR,OAAA;;;;;iBAqBmB,aAAA,CACpB,QAAA,IAAW,SAAA,aAAsB,OAAA,CAAQ,MAAA,qBACxC,OAAA,CAAQ,gBAAA"}
1
+ {"version":3,"file":"dashboard.d.mts","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"mappings":";;;;;AAWA;;KAAY,eAAA;EACV,QAAA,CAAS,MAAA,SAAe,OAAA;IAAW,MAAA;EAAA;AAAA;AAAA,KAGzB,qBAAA;EAHyC,yEAKnD,QAAA,WAFU;EAIV,MAAA;;;;;;EAMA,UAAA,GAAa,UAAU;AAAA;AAAA,KAGpB,gBAAA;EACH,eAAA,GAAkB,OAAA;IAAW,MAAA;IAAmB,aAAA;EAAA;EAChD,aAAA,OAAoB,KAAA;EACpB,cAAA;IACE,WAAA,CAAY,IAAA;IACZ,cAAA;EAAA;AAAA;;;;;;AAAc;AAiBlB;;;;;;;iBAAsB,cAAA,CACpB,MAAA,EAAQ,eAAA,EACR,OAAA,GAAS,qBAAA,GACR,OAAA;;;;;iBAuBmB,aAAA,CACpB,QAAA,IAAW,SAAA,aAAsB,OAAA,CAAQ,MAAA,qBACxC,OAAA,CAAQ,gBAAA"}
package/esm/dashboard.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { QueueDashboardDependencyError } from "./errors.mjs";
2
2
  import { defaultQueueName } from "./config.mjs";
3
+ import { buildDashboardGuardPlugin } from "./dashboard-guard-plugin.mjs";
3
4
  import { queueOf, registeredJobs } from "./job-registry.mjs";
4
5
  import { getQueue } from "./queue-manager.mjs";
5
6
 
@@ -27,7 +28,8 @@ async function queueDashboard(server, options = {}) {
27
28
  queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),
28
29
  serverAdapter
29
30
  });
30
- await server.register(serverAdapter.registerPlugin(), { prefix: basePath });
31
+ const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());
32
+ await server.register(guardedPlugin, { prefix: basePath });
31
33
  }
32
34
  /**
33
35
  * Load the optional bull-board packages. Exported for tests of the missing
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.mjs","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"sourcesContent":["import { defaultQueueName } from \"./config\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n await server.register(serverAdapter.registerPlugin() as never, { prefix: basePath });\n}\n\n/**\n * Load the optional bull-board packages. Exported for tests of the missing\n * dependency path; `importer` defaults to a real dynamic import.\n */\nexport async function loadBullBoard(\n importer: (specifier: string) => Promise<Record<string, unknown>> = (specifier) => import(specifier),\n): Promise<BullBoardModules> {\n const api = await importOptional(importer, \"@bull-board/api\");\n const adapter = await importOptional(importer, \"@bull-board/api/bullMQAdapter\");\n const fastify = await importOptional(importer, \"@bull-board/fastify\");\n\n return {\n createBullBoard: api.createBullBoard as BullBoardModules[\"createBullBoard\"],\n BullMQAdapter: adapter.BullMQAdapter as BullBoardModules[\"BullMQAdapter\"],\n FastifyAdapter: fastify.FastifyAdapter as BullBoardModules[\"FastifyAdapter\"],\n };\n}\n\nasync function importOptional(\n importer: (specifier: string) => Promise<Record<string, unknown>>,\n specifier: string,\n): Promise<Record<string, unknown>> {\n try {\n return await importer(specifier);\n } catch (error) {\n if (isModuleNotFound(error)) {\n throw new QueueDashboardDependencyError(specifier.split(\"/\").slice(0, 2).join(\"/\"));\n }\n\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA0CA,eAAsB,eACpB,QACA,UAAiC,CAAC,GACnB;CACf,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,EAAE,iBAAiB,eAAe,mBAAmB,MAAM,cAAc;CAC/E,MAAM,aACJ,QAAQ,UAAU,CAAC,GAAG,IAAI,IAAI,CAAC,iBAAiB,GAAG,GAAG,eAAe,CAAC,CAAC,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;CAErG,MAAM,gBAAgB,IAAI,eAAe;CACzC,cAAc,YAAY,QAAQ;CAElC,gBAAgB;EACd,QAAQ,WAAW,KAAK,SAAS,IAAI,cAAc,SAAS,IAAI,CAAC,CAAC;EAClE;CACF,CAAC;CAED,MAAM,OAAO,SAAS,cAAc,eAAe,GAAY,EAAE,QAAQ,SAAS,CAAC;AACrF;;;;;AAMA,eAAsB,cACpB,YAAqE,cAAc,OAAO,YAC/D;CAC3B,MAAM,MAAM,MAAM,eAAe,UAAU,iBAAiB;CAC5D,MAAM,UAAU,MAAM,eAAe,UAAU,+BAA+B;CAC9E,MAAM,UAAU,MAAM,eAAe,UAAU,qBAAqB;CAEpE,OAAO;EACL,iBAAiB,IAAI;EACrB,eAAe,QAAQ;EACvB,gBAAgB,QAAQ;CAC1B;AACF;AAEA,eAAe,eACb,UACA,WACkC;CAClC,IAAI;EACF,OAAO,MAAM,SAAS,SAAS;CACjC,SAAS,OAAO;EACd,IAAI,iBAAiB,KAAK,GACxB,MAAM,IAAI,8BAA8B,UAAU,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;EAGpF,MAAM;CACR;AACF;AAEA,SAAS,iBAAiB,OAAyB;CACjD,MAAM,OAAQ,OAA0C;CAExD,OAAO,SAAS,0BAA0B,SAAS;AACrD"}
1
+ {"version":3,"file":"dashboard.mjs","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport { defaultQueueName } from \"./config\";\nimport { buildDashboardGuardPlugin } from \"./dashboard-guard-plugin\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n /**\n * Run before every dashboard route. Applied via a wrapping Fastify plugin\n * scope, since bull-board's own plugin has no hook to splice Warlock\n * middleware into — see `dashboard-guard-plugin.ts`.\n */\n middleware?: Middleware[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());\n\n await server.register(guardedPlugin as never, { prefix: basePath });\n}\n\n/**\n * Load the optional bull-board packages. Exported for tests of the missing\n * dependency path; `importer` defaults to a real dynamic import.\n */\nexport async function loadBullBoard(\n importer: (specifier: string) => Promise<Record<string, unknown>> = (specifier) => import(specifier),\n): Promise<BullBoardModules> {\n const api = await importOptional(importer, \"@bull-board/api\");\n const adapter = await importOptional(importer, \"@bull-board/api/bullMQAdapter\");\n const fastify = await importOptional(importer, \"@bull-board/fastify\");\n\n return {\n createBullBoard: api.createBullBoard as BullBoardModules[\"createBullBoard\"],\n BullMQAdapter: adapter.BullMQAdapter as BullBoardModules[\"BullMQAdapter\"],\n FastifyAdapter: fastify.FastifyAdapter as BullBoardModules[\"FastifyAdapter\"],\n };\n}\n\nasync function importOptional(\n importer: (specifier: string) => Promise<Record<string, unknown>>,\n specifier: string,\n): Promise<Record<string, unknown>> {\n try {\n return await importer(specifier);\n } catch (error) {\n if (isModuleNotFound(error)) {\n throw new QueueDashboardDependencyError(specifier.split(\"/\").slice(0, 2).join(\"/\"));\n }\n\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkDA,eAAsB,eACpB,QACA,UAAiC,CAAC,GACnB;CACf,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,EAAE,iBAAiB,eAAe,mBAAmB,MAAM,cAAc;CAC/E,MAAM,aACJ,QAAQ,UAAU,CAAC,GAAG,IAAI,IAAI,CAAC,iBAAiB,GAAG,GAAG,eAAe,CAAC,CAAC,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;CAErG,MAAM,gBAAgB,IAAI,eAAe;CACzC,cAAc,YAAY,QAAQ;CAElC,gBAAgB;EACd,QAAQ,WAAW,KAAK,SAAS,IAAI,cAAc,SAAS,IAAI,CAAC,CAAC;EAClE;CACF,CAAC;CAED,MAAM,gBAAgB,0BAA0B,QAAQ,cAAc,CAAC,GAAG,cAAc,eAAe,CAAC;CAExG,MAAM,OAAO,SAAS,eAAwB,EAAE,QAAQ,SAAS,CAAC;AACpE;;;;;AAMA,eAAsB,cACpB,YAAqE,cAAc,OAAO,YAC/D;CAC3B,MAAM,MAAM,MAAM,eAAe,UAAU,iBAAiB;CAC5D,MAAM,UAAU,MAAM,eAAe,UAAU,+BAA+B;CAC9E,MAAM,UAAU,MAAM,eAAe,UAAU,qBAAqB;CAEpE,OAAO;EACL,iBAAiB,IAAI;EACrB,eAAe,QAAQ;EACvB,gBAAgB,QAAQ;CAC1B;AACF;AAEA,eAAe,eACb,UACA,WACkC;CAClC,IAAI;EACF,OAAO,MAAM,SAAS,SAAS;CACjC,SAAS,OAAO;EACd,IAAI,iBAAiB,KAAK,GACxB,MAAM,IAAI,8BAA8B,UAAU,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;EAGpF,MAAM;CACR;AACF;AAEA,SAAS,iBAAiB,OAAyB;CACjD,MAAM,OAAQ,OAA0C;CAExD,OAAO,SAAS,0BAA0B,SAAS;AACrD"}
@@ -41,9 +41,11 @@ function defineJob(definition) {
41
41
  };
42
42
  },
43
43
  async find(id) {
44
- const job = await getQueue(queueOf(definition)).getJob(id);
45
- if (!job || job.name !== definition.name) return;
46
- return toSnapshot(job);
44
+ const queue = getQueue(queueOf(definition));
45
+ const initialJob = await queue.getJob(id);
46
+ if (!initialJob || initialJob.name !== definition.name) return;
47
+ const state = await initialJob.getState();
48
+ return toSnapshot(await queue.getJob(id) ?? initialJob, state);
47
49
  }
48
50
  };
49
51
  }
@@ -79,14 +81,19 @@ function toBullBackoff(backoff) {
79
81
  }
80
82
  /**
81
83
  * A plain view of a BullMQ job.
84
+ *
85
+ * @param job The job to read fields from.
86
+ * @param state The job's state; pass a state read *before* `job` was
87
+ * fetched (or re-fetched) so the returned snapshot's fields are consistent
88
+ * with it. If omitted, the state is read from `job` directly.
82
89
  */
83
- async function toSnapshot(job) {
84
- const state = await job.getState();
90
+ async function toSnapshot(job, state) {
91
+ const resolvedState = state ?? await job.getState();
85
92
  return {
86
93
  id: String(job.id),
87
94
  name: job.name,
88
95
  queue: job.queueName,
89
- state,
96
+ state: resolvedState,
90
97
  payload: job.data,
91
98
  progress: job.progress,
92
99
  attemptsMade: job.attemptsMade,
@@ -1 +1 @@
1
- {"version":3,"file":"define-job.mjs","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"sourcesContent":["import type { Job, JobsOptions } from \"bullmq\";\nimport { getQueueConfig } from \"./config\";\nimport { toMilliseconds } from \"./duration\";\nimport { InvalidJobDefinitionError } from \"./errors\";\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\nimport type {\n DispatchOptions,\n JobBackoff,\n JobDefinition,\n JobOptions,\n JobSnapshot,\n JobState,\n QueueJob,\n} from \"./types\";\n\n/**\n * Define a background job.\n *\n * The definition is registered by name so any worker in the process can run\n * it; the returned object dispatches it with a typed payload.\n *\n * @example\n * export const sendInvoice = defineJob({\n * name: \"invoices.send\",\n * attempts: 5,\n * backoff: { type: \"exponential\", delay: 2000 },\n * async handle(payload: { invoiceId: string }, ctx) {\n * await ctx.progress(50);\n * },\n * });\n *\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\n */\nexport function defineJob<TPayload, TResult = unknown>(\n definition: JobDefinition<TPayload, TResult>,\n): QueueJob<TPayload, TResult> {\n assertValidDefinition(definition);\n registerJob(definition as RegisteredJob);\n\n return {\n name: definition.name,\n get queue() {\n return queueOf(definition);\n },\n async dispatch(payload, options = {}) {\n const queueName = queueOf(definition);\n const job = await getQueue(queueName).add(\n definition.name,\n payload,\n toBullJobOptions(definition, options),\n );\n\n return { id: String(job.id), name: definition.name, queue: queueName };\n },\n async find(id) {\n const job = await getQueue(queueOf(definition)).getJob(id);\n\n if (!job || job.name !== definition.name) {\n return undefined;\n }\n\n return toSnapshot<TPayload, TResult>(job);\n },\n };\n}\n\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\n }\n\n if (typeof definition.handle !== \"function\") {\n throw new InvalidJobDefinitionError(\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\n );\n }\n\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\n throw new InvalidJobDefinitionError(\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\n );\n }\n}\n\n/**\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\n */\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\n const defaults = getQueueConfig().defaultJobOptions ?? {};\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\n\n const bullOptions: JobsOptions = {};\n\n if (attempts !== undefined) bullOptions.attempts = attempts;\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\n if (options.priority !== undefined) bullOptions.priority = options.priority;\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\n\n return bullOptions;\n}\n\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\n}\n\n/**\n * A plain view of a BullMQ job.\n */\nexport async function toSnapshot<TPayload, TResult>(\n job: Job,\n): Promise<JobSnapshot<TPayload, TResult>> {\n const state = (await job.getState()) as JobState;\n\n return {\n id: String(job.id),\n name: job.name,\n queue: job.queueName,\n state,\n payload: job.data as TPayload,\n progress: job.progress as JobSnapshot[\"progress\"],\n attemptsMade: job.attemptsMade,\n result: job.returnvalue as TResult | undefined,\n failedReason: job.failedReason || undefined,\n createdAt: new Date(job.timestamp),\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,UACd,YAC6B;CAC7B,sBAAsB,UAAU;CAChC,YAAY,UAA2B;CAEvC,OAAO;EACL,MAAM,WAAW;EACjB,IAAI,QAAQ;GACV,OAAO,QAAQ,UAAU;EAC3B;EACA,MAAM,SAAS,SAAS,UAAU,CAAC,GAAG;GACpC,MAAM,YAAY,QAAQ,UAAU;GACpC,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC,CAAC,IACpC,WAAW,MACX,SACA,iBAAiB,YAAY,OAAO,CACtC;GAEA,OAAO;IAAE,IAAI,OAAO,IAAI,EAAE;IAAG,MAAM,WAAW;IAAM,OAAO;GAAU;EACvE;EACA,MAAM,KAAK,IAAI;GACb,MAAM,MAAM,MAAM,SAAS,QAAQ,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE;GAEzD,IAAI,CAAC,OAAO,IAAI,SAAS,WAAW,MAClC;GAGF,OAAO,WAA8B,GAAG;EAC1C;CACF;AACF;AAEA,SAAS,sBAAsB,YAAmD;CAChF,IAAI,OAAO,WAAW,SAAS,YAAY,WAAW,KAAK,KAAK,MAAM,IACpE,MAAM,IAAI,0BAA0B,0CAA0C;CAGhF,IAAI,OAAO,WAAW,WAAW,YAC/B,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,iDAChC;CAGF,IAAI,WAAW,aAAa,UAAa,EAAE,OAAO,UAAU,WAAW,QAAQ,KAAK,WAAW,YAAY,IACzG,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,gDAAgD,WAAW,SAAS,EACpG;AAEJ;;;;AAKA,SAAS,iBAAiB,YAAwB,SAAuC;CACvF,MAAM,WAAW,eAAe,CAAC,CAAC,qBAAqB,CAAC;CACxD,MAAM,WAAW,QAAQ,YAAY,WAAW,YAAY,SAAS;CACrE,MAAM,UAAU,QAAQ,WAAW,WAAW,WAAW,SAAS;CAClE,MAAM,mBAAmB,WAAW,oBAAoB,SAAS;CACjE,MAAM,eAAe,WAAW,gBAAgB,SAAS;CAEzD,MAAM,cAA2B,CAAC;CAElC,IAAI,aAAa,QAAW,YAAY,WAAW;CACnD,IAAI,YAAY,QAAW,YAAY,UAAU,cAAc,OAAO;CACtE,IAAI,qBAAqB,QAAW,YAAY,mBAAmB;CACnE,IAAI,iBAAiB,QAAW,YAAY,eAAe;CAC3D,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,eAAe,QAAQ,KAAK;CACjF,IAAI,QAAQ,aAAa,QAAW,YAAY,WAAW,QAAQ;CACnE,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,QAAQ;CAE7D,OAAO;AACT;AAEA,SAAS,cAAc,SAA6C;CAClE,OAAO,OAAO,YAAY,WAAW;EAAE,MAAM;EAAS,OAAO;CAAQ,IAAI;AAC3E;;;;AAKA,eAAsB,WACpB,KACyC;CACzC,MAAM,QAAS,MAAM,IAAI,SAAS;CAElC,OAAO;EACL,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX;EACA,SAAS,IAAI;EACb,UAAU,IAAI;EACd,cAAc,IAAI;EAClB,QAAQ,IAAI;EACZ,cAAc,IAAI,gBAAgB;EAClC,WAAW,IAAI,KAAK,IAAI,SAAS;EACjC,YAAY,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,IAAI;CAC1D;AACF"}
1
+ {"version":3,"file":"define-job.mjs","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"sourcesContent":["import type { Job, JobsOptions } from \"bullmq\";\r\nimport { getQueueConfig } from \"./config\";\r\nimport { toMilliseconds } from \"./duration\";\r\nimport { InvalidJobDefinitionError } from \"./errors\";\r\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\r\nimport { getQueue } from \"./queue-manager\";\r\nimport type {\r\n DispatchOptions,\r\n JobBackoff,\r\n JobDefinition,\r\n JobOptions,\r\n JobSnapshot,\r\n JobState,\r\n QueueJob,\r\n} from \"./types\";\r\n\r\n/**\r\n * Define a background job.\r\n *\r\n * The definition is registered by name so any worker in the process can run\r\n * it; the returned object dispatches it with a typed payload.\r\n *\r\n * @example\r\n * export const sendInvoice = defineJob({\r\n * name: \"invoices.send\",\r\n * attempts: 5,\r\n * backoff: { type: \"exponential\", delay: 2000 },\r\n * async handle(payload: { invoiceId: string }, ctx) {\r\n * await ctx.progress(50);\r\n * },\r\n * });\r\n *\r\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\r\n */\r\nexport function defineJob<TPayload, TResult = unknown>(\r\n definition: JobDefinition<TPayload, TResult>,\r\n): QueueJob<TPayload, TResult> {\r\n assertValidDefinition(definition);\r\n registerJob(definition as RegisteredJob);\r\n\r\n return {\r\n name: definition.name,\r\n get queue() {\r\n return queueOf(definition);\r\n },\r\n async dispatch(payload, options = {}) {\r\n const queueName = queueOf(definition);\r\n const job = await getQueue(queueName).add(\r\n definition.name,\r\n payload,\r\n toBullJobOptions(definition, options),\r\n );\r\n\r\n return { id: String(job.id), name: definition.name, queue: queueName };\r\n },\r\n async find(id) {\r\n const queue = getQueue(queueOf(definition));\r\n const initialJob = await queue.getJob(id);\r\n\r\n if (!initialJob || initialJob.name !== definition.name) {\r\n return undefined;\r\n }\r\n\r\n // Read the state first, then (re)fetch the job. BullMQ writes a job's\r\n // result/attemptsMade/finishedOn fields *before* it becomes visible\r\n // under a new state, so re-reading the job after the state is known\r\n // guarantees those fields are consistent with the reported state\r\n // (rather than reflecting a moment before the job finished).\r\n const state = (await initialJob.getState()) as JobState;\r\n const job = (await queue.getJob(id)) ?? initialJob;\r\n\r\n return toSnapshot<TPayload, TResult>(job, state);\r\n },\r\n };\r\n}\r\n\r\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\r\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\r\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\r\n }\r\n\r\n if (typeof definition.handle !== \"function\") {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\r\n );\r\n }\r\n\r\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\r\n */\r\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\r\n const defaults = getQueueConfig().defaultJobOptions ?? {};\r\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\r\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\r\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\r\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\r\n\r\n const bullOptions: JobsOptions = {};\r\n\r\n if (attempts !== undefined) bullOptions.attempts = attempts;\r\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\r\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\r\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\r\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\r\n if (options.priority !== undefined) bullOptions.priority = options.priority;\r\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\r\n\r\n return bullOptions;\r\n}\r\n\r\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\r\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\r\n}\r\n\r\n/**\r\n * A plain view of a BullMQ job.\r\n *\r\n * @param job The job to read fields from.\r\n * @param state The job's state; pass a state read *before* `job` was\r\n * fetched (or re-fetched) so the returned snapshot's fields are consistent\r\n * with it. If omitted, the state is read from `job` directly.\r\n */\r\nexport async function toSnapshot<TPayload, TResult>(\r\n job: Job,\r\n state?: JobState,\r\n): Promise<JobSnapshot<TPayload, TResult>> {\r\n const resolvedState = state ?? ((await job.getState()) as JobState);\r\n\r\n return {\r\n id: String(job.id),\r\n name: job.name,\r\n queue: job.queueName,\r\n state: resolvedState,\r\n payload: job.data as TPayload,\r\n progress: job.progress as JobSnapshot[\"progress\"],\r\n attemptsMade: job.attemptsMade,\r\n result: job.returnvalue as TResult | undefined,\r\n failedReason: job.failedReason || undefined,\r\n createdAt: new Date(job.timestamp),\r\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\r\n };\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,UACd,YAC6B;CAC7B,sBAAsB,UAAU;CAChC,YAAY,UAA2B;CAEvC,OAAO;EACL,MAAM,WAAW;EACjB,IAAI,QAAQ;GACV,OAAO,QAAQ,UAAU;EAC3B;EACA,MAAM,SAAS,SAAS,UAAU,CAAC,GAAG;GACpC,MAAM,YAAY,QAAQ,UAAU;GACpC,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC,CAAC,IACpC,WAAW,MACX,SACA,iBAAiB,YAAY,OAAO,CACtC;GAEA,OAAO;IAAE,IAAI,OAAO,IAAI,EAAE;IAAG,MAAM,WAAW;IAAM,OAAO;GAAU;EACvE;EACA,MAAM,KAAK,IAAI;GACb,MAAM,QAAQ,SAAS,QAAQ,UAAU,CAAC;GAC1C,MAAM,aAAa,MAAM,MAAM,OAAO,EAAE;GAExC,IAAI,CAAC,cAAc,WAAW,SAAS,WAAW,MAChD;GAQF,MAAM,QAAS,MAAM,WAAW,SAAS;GAGzC,OAAO,WAFM,MAAM,MAAM,OAAO,EAAE,KAAM,YAEE,KAAK;EACjD;CACF;AACF;AAEA,SAAS,sBAAsB,YAAmD;CAChF,IAAI,OAAO,WAAW,SAAS,YAAY,WAAW,KAAK,KAAK,MAAM,IACpE,MAAM,IAAI,0BAA0B,0CAA0C;CAGhF,IAAI,OAAO,WAAW,WAAW,YAC/B,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,iDAChC;CAGF,IAAI,WAAW,aAAa,UAAa,EAAE,OAAO,UAAU,WAAW,QAAQ,KAAK,WAAW,YAAY,IACzG,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,gDAAgD,WAAW,SAAS,EACpG;AAEJ;;;;AAKA,SAAS,iBAAiB,YAAwB,SAAuC;CACvF,MAAM,WAAW,eAAe,CAAC,CAAC,qBAAqB,CAAC;CACxD,MAAM,WAAW,QAAQ,YAAY,WAAW,YAAY,SAAS;CACrE,MAAM,UAAU,QAAQ,WAAW,WAAW,WAAW,SAAS;CAClE,MAAM,mBAAmB,WAAW,oBAAoB,SAAS;CACjE,MAAM,eAAe,WAAW,gBAAgB,SAAS;CAEzD,MAAM,cAA2B,CAAC;CAElC,IAAI,aAAa,QAAW,YAAY,WAAW;CACnD,IAAI,YAAY,QAAW,YAAY,UAAU,cAAc,OAAO;CACtE,IAAI,qBAAqB,QAAW,YAAY,mBAAmB;CACnE,IAAI,iBAAiB,QAAW,YAAY,eAAe;CAC3D,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,eAAe,QAAQ,KAAK;CACjF,IAAI,QAAQ,aAAa,QAAW,YAAY,WAAW,QAAQ;CACnE,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,QAAQ;CAE7D,OAAO;AACT;AAEA,SAAS,cAAc,SAA6C;CAClE,OAAO,OAAO,YAAY,WAAW;EAAE,MAAM;EAAS,OAAO;CAAQ,IAAI;AAC3E;;;;;;;;;AAUA,eAAsB,WACpB,KACA,OACyC;CACzC,MAAM,gBAAgB,SAAW,MAAM,IAAI,SAAS;CAEpD,OAAO;EACL,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,OAAO;EACP,SAAS,IAAI;EACb,UAAU,IAAI;EACd,cAAc,IAAI;EAClB,QAAQ,IAAI;EACZ,cAAc,IAAI,gBAAgB;EAClC,WAAW,IAAI,KAAK,IAAI,SAAS;EACjC,YAAY,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,IAAI;CAC1D;AACF"}
package/esm/index.d.mts CHANGED
@@ -1,10 +1,12 @@
1
- import { DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QueueConfig, QueueJob, QueueWorkersConfig } from "./types.mjs";
1
+ import { DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QueueConfig, QueueDashboardConfig, QueueJob, QueueWorkersConfig } from "./types.mjs";
2
2
  import { defaultQueueName, getQueueConfig, resetQueueConfig, setQueueConfig } from "./config.mjs";
3
3
  import { DashboardServer, QueueDashboardOptions, loadBullBoard, queueDashboard } from "./dashboard.mjs";
4
+ import { DEFAULT_DASHBOARD_PATH, mountQueueDashboard } from "./dashboard-boot.mjs";
4
5
  import { defineJob } from "./define-job.mjs";
5
6
  import { toMilliseconds } from "./duration.mjs";
6
7
  import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError } from "./errors.mjs";
7
8
  import { FailedJobsOptions, failedJobs, retryFailedJob } from "./failed-jobs.mjs";
8
9
  import { QUEUE_CONNECTOR_PRIORITY, QueueConnectorOptions, queueConnector } from "./queue-connector.mjs";
10
+ import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
9
11
  import { CloseQueueOptions, closeQueue, getQueue, runningWorkers, startWorkers } from "./queue-manager.mjs";
10
- export { type CloseQueueOptions, DashboardServer, DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, FailedJobNotFoundError, FailedJobsOptions, InvalidDurationError, InvalidJobDefinitionError, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QUEUE_CONNECTOR_PRIORITY, QueueConfig, QueueConnectorOptions, QueueDashboardDependencyError, QueueDashboardOptions, QueueJob, QueueNotConfiguredError, QueueWorkersConfig, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
12
+ export { type CloseQueueOptions, DEFAULT_DASHBOARD_PATH, DashboardServer, DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, FailedJobNotFoundError, FailedJobsOptions, InvalidDurationError, InvalidJobDefinitionError, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QUEUE_CONNECTOR_PRIORITY, QueueConfig, QueueConnectorOptions, QueueDashboardConfig, QueueDashboardDependencyError, QueueDashboardOptions, QueueDashboardUnguardedError, QueueJob, QueueNotConfiguredError, QueueWorkersConfig, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
package/esm/index.mjs CHANGED
@@ -2,9 +2,11 @@ import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError
2
2
  import { defaultQueueName, getQueueConfig, resetQueueConfig, setQueueConfig } from "./config.mjs";
3
3
  import { closeQueue, getQueue, runningWorkers, startWorkers } from "./queue-manager.mjs";
4
4
  import { loadBullBoard, queueDashboard } from "./dashboard.mjs";
5
+ import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
6
+ import { DEFAULT_DASHBOARD_PATH, mountQueueDashboard } from "./dashboard-boot.mjs";
5
7
  import { toMilliseconds } from "./duration.mjs";
6
8
  import { defineJob } from "./define-job.mjs";
7
9
  import { failedJobs, retryFailedJob } from "./failed-jobs.mjs";
8
10
  import { QUEUE_CONNECTOR_PRIORITY, queueConnector } from "./queue-connector.mjs";
9
11
 
10
- export { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QUEUE_CONNECTOR_PRIORITY, QueueDashboardDependencyError, QueueNotConfiguredError, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
12
+ export { DEFAULT_DASHBOARD_PATH, FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QUEUE_CONNECTOR_PRIORITY, QueueDashboardDependencyError, QueueDashboardUnguardedError, QueueNotConfiguredError, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
@@ -1 +1 @@
1
- {"version":3,"file":"queue-connector.d.mts","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"mappings":";;;;;;;AA4BsB;AAkBtB;cA3Ba,wBAAA;AAAA,KAID,qBAAA;EAuBkE;;;;EAlB5E,MAAA,GAAS,WAAW;AAAA;;;;;;;;;;;;;;;;iBAkBN,cAAA,CAAe,OAAA,GAAS,qBAAA,GAA6B,SAAS"}
1
+ {"version":3,"file":"queue-connector.d.mts","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"mappings":";;;;;;;AA6BsB;AAkBtB;cA3Ba,wBAAA;AAAA,KAID,qBAAA;EAuBkE;;;;EAlB5E,MAAA,GAAS,WAAW;AAAA;;;;;;;;;;;;;;;;iBAkBN,cAAA,CAAe,OAAA,GAAS,qBAAA,GAA6B,SAAS"}
@@ -1,5 +1,6 @@
1
1
  import { resetQueueConfig, setQueueConfig } from "./config.mjs";
2
2
  import { closeQueue, startWorkers } from "./queue-manager.mjs";
3
+ import { mountQueueDashboard } from "./dashboard-boot.mjs";
3
4
  import { log } from "@warlock.js/logger";
4
5
 
5
6
  //#region ../queue/src/queue-connector.ts
@@ -32,7 +33,22 @@ function queueConnector(options = {}) {
32
33
  priority: 11,
33
34
  lifecyclePhase: "late",
34
35
  isActive: () => active,
35
- boot: () => void 0,
36
+ /**
37
+ * Mounts the dashboard, when configured, here rather than in `start()`:
38
+ * `boot()` runs for every late-phase connector, in priority order, before
39
+ * any of them `start()`s — so by the time this runs, the HTTP connector
40
+ * (priority 5, before queue's 11) has already built its Fastify instance
41
+ * and registered its own plugins, but has not yet called `listen()`.
42
+ * Fastify refuses new plugin registrations after `listen()`, so this is
43
+ * the only point in the boot sequence where mounting is possible.
44
+ */
45
+ async boot() {
46
+ const queueConfig = options.config ?? await readQueueConfig();
47
+ if (!queueConfig?.dashboard?.enabled) return;
48
+ setQueueConfig(queueConfig);
49
+ const { getHttpServer } = await import("@warlock.js/core");
50
+ await mountQueueDashboard(getHttpServer(), queueConfig);
51
+ },
36
52
  async start() {
37
53
  const queueConfig = options.config ?? await readQueueConfig();
38
54
  if (!queueConfig) {
@@ -1 +1 @@
1
- {"version":3,"file":"queue-connector.mjs","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["/**\n * The queue's connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\n * at config-load time. Core is imported lazily inside `start()`, where the\n * app has already loaded it.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\nimport { closeQueue, startWorkers } from \"./queue-manager\";\nimport type { QueueConfig } from \"./types\";\n\n/**\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\n * the logger is up and anything a job handler needs is already connected;\n * shuts down before them for the same reason.\n */\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\n\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\n\nexport type QueueConnectorOptions = {\n /**\n * Supply the configuration directly instead of reading the `queue` config\n * key (`src/config/queue.ts`).\n */\n config?: QueueConfig;\n};\n\n/**\n * Construct the queue connector.\n *\n * Runs in the `late` lifecycle phase — after app code is imported — so every\n * `defineJob` in the app has registered before workers start. At start it\n * reads the `queue` config and starts in-process workers unless\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\n *\n * @example\n * // warlock.config.ts\n * import { queueConnector } from \"@warlock.js/queue\";\n *\n * export default defineConfig({ connectors: [queueConnector()] });\n */\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\n let active = false;\n\n const connector: Connector = {\n name: \"queue\",\n priority: QUEUE_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\n // module stays free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n boot: () => undefined,\n async start() {\n const queueConfig = options.config ?? (await readQueueConfig());\n\n if (!queueConfig) {\n log.warn(\n \"queue\",\n \"configured\",\n \"queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started\",\n );\n return;\n }\n\n setQueueConfig(queueConfig);\n const started = await startWorkers();\n active = true;\n\n log.info(\n \"queue\",\n \"configured\",\n started.length > 0\n ? `Queue workers running for: ${started.join(\", \")}`\n : \"Queue configured (no in-process workers)\",\n );\n },\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n if (!active) {\n return;\n }\n\n await closeQueue();\n resetQueueConfig();\n active = false;\n },\n shouldRestart(changedFiles) {\n return changedFiles.some((file) => {\n const normalized = file.replace(/\\\\/g, \"/\");\n\n return WATCHED_FILES.some((watched) => normalized === watched || normalized.endsWith(`/${watched}`));\n });\n },\n };\n\n return connector;\n}\n\nasync function readQueueConfig(): Promise<QueueConfig | undefined> {\n const { config } = await import(\"@warlock.js/core\");\n\n return config.get<QueueConfig | undefined>(\"queue\");\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAa,2BAA2B;AAExC,MAAM,gBAAgB,CAAC,qBAAqB;;;;;;;;;;;;;;;;AAyB5C,SAAgB,eAAe,UAAiC,CAAC,GAAc;CAC7E,IAAI,SAAS;CAEb,MAAM,YAAuB;EAC3B,MAAM;EACN;EAGA,gBAAgB;EAChB,gBAAgB;EAChB,YAAY;EACZ,MAAM,QAAQ;GACZ,MAAM,cAAc,QAAQ,UAAW,MAAM,gBAAgB;GAE7D,IAAI,CAAC,aAAa;IAChB,IAAI,KACF,SACA,cACA,yGACF;IACA;GACF;GAEA,eAAe,WAAW;GAC1B,MAAM,UAAU,MAAM,aAAa;GACnC,SAAS;GAET,IAAI,KACF,SACA,cACA,QAAQ,SAAS,IACb,8BAA8B,QAAQ,KAAK,IAAI,MAC/C,0CACN;EACF;EACA,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,MAAM;EACxB;EACA,MAAM,WAAW;GACf,IAAI,CAAC,QACH;GAGF,MAAM,WAAW;GACjB,iBAAiB;GACjB,SAAS;EACX;EACA,cAAc,cAAc;GAC1B,OAAO,aAAa,MAAM,SAAS;IACjC,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;IAE1C,OAAO,cAAc,MAAM,YAAY,eAAe,WAAW,WAAW,SAAS,IAAI,SAAS,CAAC;GACrG,CAAC;EACH;CACF;CAEA,OAAO;AACT;AAEA,eAAe,kBAAoD;CACjE,MAAM,EAAE,WAAW,MAAM,OAAO;CAEhC,OAAO,OAAO,IAA6B,OAAO;AACpD"}
1
+ {"version":3,"file":"queue-connector.mjs","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["/**\r\n * The queue's connector for `warlock.config.ts > connectors`.\r\n *\r\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\r\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\r\n * at config-load time. Core is imported lazily inside `start()`, where the\r\n * app has already loaded it.\r\n */\r\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\r\nimport { mountQueueDashboard } from \"./dashboard-boot\";\r\nimport { closeQueue, startWorkers } from \"./queue-manager\";\r\nimport type { QueueConfig } from \"./types\";\r\n\r\n/**\r\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\r\n * the logger is up and anything a job handler needs is already connected;\r\n * shuts down before them for the same reason.\r\n */\r\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\r\n\r\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\r\n\r\nexport type QueueConnectorOptions = {\r\n /**\r\n * Supply the configuration directly instead of reading the `queue` config\r\n * key (`src/config/queue.ts`).\r\n */\r\n config?: QueueConfig;\r\n};\r\n\r\n/**\r\n * Construct the queue connector.\r\n *\r\n * Runs in the `late` lifecycle phase — after app code is imported — so every\r\n * `defineJob` in the app has registered before workers start. At start it\r\n * reads the `queue` config and starts in-process workers unless\r\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\r\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\r\n *\r\n * @example\r\n * // warlock.config.ts\r\n * import { queueConnector } from \"@warlock.js/queue\";\r\n *\r\n * export default defineConfig({ connectors: [queueConnector()] });\r\n */\r\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\r\n let active = false;\r\n\r\n const connector: Connector = {\r\n name: \"queue\",\r\n priority: QUEUE_CONNECTOR_PRIORITY,\r\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\r\n // module stays free of a runtime import of core.\r\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\r\n isActive: () => active,\r\n /**\r\n * Mounts the dashboard, when configured, here rather than in `start()`:\r\n * `boot()` runs for every late-phase connector, in priority order, before\r\n * any of them `start()`s so by the time this runs, the HTTP connector\r\n * (priority 5, before queue's 11) has already built its Fastify instance\r\n * and registered its own plugins, but has not yet called `listen()`.\r\n * Fastify refuses new plugin registrations after `listen()`, so this is\r\n * the only point in the boot sequence where mounting is possible.\r\n */\r\n async boot() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig?.dashboard?.enabled) {\r\n return;\r\n }\r\n\r\n // The dashboard resolves queues through the active config, so it has to\r\n // be registered here rather than only in `start()`, which runs after\r\n // every late connector has booted. `start()` sets it again; the setter\r\n // is idempotent for the same object.\r\n setQueueConfig(queueConfig);\r\n\r\n const { getHttpServer } = await import(\"@warlock.js/core\");\r\n\r\n await mountQueueDashboard(getHttpServer(), queueConfig);\r\n },\r\n async start() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig) {\r\n log.warn(\r\n \"queue\",\r\n \"configured\",\r\n \"queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started\",\r\n );\r\n return;\r\n }\r\n\r\n setQueueConfig(queueConfig);\r\n const started = await startWorkers();\r\n active = true;\r\n\r\n log.info(\r\n \"queue\",\r\n \"configured\",\r\n started.length > 0\r\n ? `Queue workers running for: ${started.join(\", \")}`\r\n : \"Queue configured (no in-process workers)\",\r\n );\r\n },\r\n async restart() {\r\n await connector.shutdown();\r\n await connector.start();\r\n },\r\n async shutdown() {\r\n if (!active) {\r\n return;\r\n }\r\n\r\n await closeQueue();\r\n resetQueueConfig();\r\n active = false;\r\n },\r\n shouldRestart(changedFiles) {\r\n return changedFiles.some((file) => {\r\n const normalized = file.replace(/\\\\/g, \"/\");\r\n\r\n return WATCHED_FILES.some((watched) => normalized === watched || normalized.endsWith(`/${watched}`));\r\n });\r\n },\r\n };\r\n\r\n return connector;\r\n}\r\n\r\nasync function readQueueConfig(): Promise<QueueConfig | undefined> {\r\n const { config } = await import(\"@warlock.js/core\");\r\n\r\n return config.get<QueueConfig | undefined>(\"queue\");\r\n}\r\n"],"mappings":";;;;;;;;;;;AAoBA,MAAa,2BAA2B;AAExC,MAAM,gBAAgB,CAAC,qBAAqB;;;;;;;;;;;;;;;;AAyB5C,SAAgB,eAAe,UAAiC,CAAC,GAAc;CAC7E,IAAI,SAAS;CAEb,MAAM,YAAuB;EAC3B,MAAM;EACN;EAGA,gBAAgB;EAChB,gBAAgB;;;;;;;;;;EAUhB,MAAM,OAAO;GACX,MAAM,cAAc,QAAQ,UAAW,MAAM,gBAAgB;GAE7D,IAAI,CAAC,aAAa,WAAW,SAC3B;GAOF,eAAe,WAAW;GAE1B,MAAM,EAAE,kBAAkB,MAAM,OAAO;GAEvC,MAAM,oBAAoB,cAAc,GAAG,WAAW;EACxD;EACA,MAAM,QAAQ;GACZ,MAAM,cAAc,QAAQ,UAAW,MAAM,gBAAgB;GAE7D,IAAI,CAAC,aAAa;IAChB,IAAI,KACF,SACA,cACA,yGACF;IACA;GACF;GAEA,eAAe,WAAW;GAC1B,MAAM,UAAU,MAAM,aAAa;GACnC,SAAS;GAET,IAAI,KACF,SACA,cACA,QAAQ,SAAS,IACb,8BAA8B,QAAQ,KAAK,IAAI,MAC/C,0CACN;EACF;EACA,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,MAAM;EACxB;EACA,MAAM,WAAW;GACf,IAAI,CAAC,QACH;GAGF,MAAM,WAAW;GACjB,iBAAiB;GACjB,SAAS;EACX;EACA,cAAc,cAAc;GAC1B,OAAO,aAAa,MAAM,SAAS;IACjC,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;IAE1C,OAAO,cAAc,MAAM,YAAY,eAAe,WAAW,WAAW,SAAS,IAAI,SAAS,CAAC;GACrG,CAAC;EACH;CACF;CAEA,OAAO;AACT;AAEA,eAAe,kBAAoD;CACjE,MAAM,EAAE,WAAW,MAAM,OAAO;CAEhC,OAAO,OAAO,IAA6B,OAAO;AACpD"}
@@ -0,0 +1,13 @@
1
+ //#region ../queue/src/queue-dashboard-unguarded.error.d.ts
2
+ /**
3
+ * Thrown at boot when `queue.dashboard.enabled` is `true` in production with
4
+ * no guard middleware. The dashboard can retry and delete jobs; mounting it
5
+ * on the open internet without a guard is a production incident waiting to
6
+ * happen, so this fails the boot instead of shipping the hole.
7
+ */
8
+ declare class QueueDashboardUnguardedError extends Error {
9
+ constructor();
10
+ }
11
+ //#endregion
12
+ export { QueueDashboardUnguardedError };
13
+ //# sourceMappingURL=queue-dashboard-unguarded.error.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-dashboard-unguarded.error.d.mts","names":[],"sources":["../../../../../../queue/src/queue-dashboard-unguarded.error.ts"],"mappings":";;AAMA;;;;;cAAa,4BAAA,SAAqC,KAAK;EAAL,WAAA;AAAA"}
@@ -0,0 +1,17 @@
1
+ //#region ../queue/src/queue-dashboard-unguarded.error.ts
2
+ /**
3
+ * Thrown at boot when `queue.dashboard.enabled` is `true` in production with
4
+ * no guard middleware. The dashboard can retry and delete jobs; mounting it
5
+ * on the open internet without a guard is a production incident waiting to
6
+ * happen, so this fails the boot instead of shipping the hole.
7
+ */
8
+ var QueueDashboardUnguardedError = class extends Error {
9
+ constructor() {
10
+ super("queue.dashboard.enabled is true in production with no middleware. The dashboard can retry and delete jobs, so it must be guarded before it is exposed.\n\nAdd a guard middleware:\n\n import { middleware } from \"@warlock.js/core\";\n import { authMiddleware } from \"@warlock.js/auth\";\n\n const queueConfig: QueueConfig = {\n // ...\n dashboard: {\n enabled: true,\n middleware: [authMiddleware(\"admin\")],\n },\n };\n");
11
+ this.name = "QueueDashboardUnguardedError";
12
+ }
13
+ };
14
+
15
+ //#endregion
16
+ export { QueueDashboardUnguardedError };
17
+ //# sourceMappingURL=queue-dashboard-unguarded.error.mjs.map