@warlock.js/queue 5.29.0 → 5.31.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 (72) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -2
  3. package/cjs/index.cjs +44 -15
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/config.d.mts +1 -1
  6. package/esm/config.d.mts.map +1 -1
  7. package/esm/config.mjs +1 -1
  8. package/esm/config.mjs.map +1 -1
  9. package/esm/dashboard-boot.d.mts +1 -1
  10. package/esm/dashboard-boot.d.mts.map +1 -1
  11. package/esm/dashboard-boot.mjs +1 -1
  12. package/esm/dashboard-boot.mjs.map +1 -1
  13. package/esm/dashboard-guard-plugin.mjs +1 -1
  14. package/esm/dashboard-guard-plugin.mjs.map +1 -1
  15. package/esm/dashboard-middleware-adapter.mjs +1 -1
  16. package/esm/dashboard-middleware-adapter.mjs.map +1 -1
  17. package/esm/dashboard.d.mts +1 -1
  18. package/esm/dashboard.d.mts.map +1 -1
  19. package/esm/dashboard.mjs +1 -1
  20. package/esm/dashboard.mjs.map +1 -1
  21. package/esm/define-job.d.mts +1 -1
  22. package/esm/define-job.d.mts.map +1 -1
  23. package/esm/define-job.mjs +15 -1
  24. package/esm/define-job.mjs.map +1 -1
  25. package/esm/dispatch-interceptor.d.mts +35 -0
  26. package/esm/dispatch-interceptor.d.mts.map +1 -0
  27. package/esm/dispatch-interceptor.mjs +14 -0
  28. package/esm/dispatch-interceptor.mjs.map +1 -0
  29. package/esm/duration.d.mts +1 -1
  30. package/esm/duration.d.mts.map +1 -1
  31. package/esm/duration.mjs +1 -1
  32. package/esm/duration.mjs.map +1 -1
  33. package/esm/errors.d.mts +1 -1
  34. package/esm/errors.d.mts.map +1 -1
  35. package/esm/errors.mjs +1 -1
  36. package/esm/errors.mjs.map +1 -1
  37. package/esm/failed-jobs.d.mts +1 -1
  38. package/esm/failed-jobs.d.mts.map +1 -1
  39. package/esm/failed-jobs.mjs +1 -1
  40. package/esm/failed-jobs.mjs.map +1 -1
  41. package/esm/index.d.mts +3 -1
  42. package/esm/index.mjs +3 -1
  43. package/esm/job-registry.d.mts +12 -0
  44. package/esm/job-registry.d.mts.map +1 -0
  45. package/esm/job-registry.mjs +1 -1
  46. package/esm/job-registry.mjs.map +1 -1
  47. package/esm/process-job.mjs +1 -1
  48. package/esm/process-job.mjs.map +1 -1
  49. package/esm/queue-connector.d.mts +1 -1
  50. package/esm/queue-connector.d.mts.map +1 -1
  51. package/esm/queue-connector.mjs +1 -1
  52. package/esm/queue-connector.mjs.map +1 -1
  53. package/esm/queue-context.d.mts +1 -1
  54. package/esm/queue-context.d.mts.map +1 -1
  55. package/esm/queue-context.mjs +1 -1
  56. package/esm/queue-context.mjs.map +1 -1
  57. package/esm/queue-dashboard-unguarded.error.d.mts +1 -1
  58. package/esm/queue-dashboard-unguarded.error.d.mts.map +1 -1
  59. package/esm/queue-dashboard-unguarded.error.mjs +1 -1
  60. package/esm/queue-dashboard-unguarded.error.mjs.map +1 -1
  61. package/esm/queue-manager.d.mts +1 -1
  62. package/esm/queue-manager.d.mts.map +1 -1
  63. package/esm/queue-manager.mjs +1 -1
  64. package/esm/queue-manager.mjs.map +1 -1
  65. package/esm/types.d.mts +1 -1
  66. package/esm/types.d.mts.map +1 -1
  67. package/llms-full.txt +36 -7
  68. package/llms.txt +1 -1
  69. package/package.json +5 -4
  70. package/skills/define-jobs/SKILL.md +36 -6
  71. package/skills/index.md +4 -1
  72. package/skills/queue-notifications/SKILL.md +0 -1
@@ -1,6 +1,6 @@
1
1
  import { Queue } from "bullmq";
2
2
 
3
- //#region ../queue/src/queue-manager.d.ts
3
+ //#region ../../packages/queue/src/queue-manager.d.ts
4
4
  /**
5
5
  * The BullMQ queue for `name`, created on first use with the configured
6
6
  * connection and prefix.
@@ -1 +1 @@
1
- {"version":3,"file":"queue-manager.d.mts","names":[],"sources":["../../../../../../queue/src/queue-manager.ts"],"mappings":";;;;;AAgBA;;iBAAgB,QAAA,CAAS,IAAA,WAAe,KAAK;;AAAA;AAiC7C;;;;AAA6C;AAgC7C;;;;AAA8B;iBAhCR,YAAA,IAAgB,OAAO;;iBAgC7B,cAAA;AAAA,KAgCJ,iBAAA;EAKH;AAQT;;;EARE,OAAO;AAAA;;;;AAQiE;;iBAApD,UAAA,CAAW,OAAA,GAAS,iBAAA,GAAyB,OAAO"}
1
+ {"version":3,"file":"queue-manager.d.mts","names":[],"sources":["../../../../../../../packages/queue/src/queue-manager.ts"],"mappings":";;;;;AAgBA;;iBAAgB,QAAA,CAAS,IAAA,WAAe,KAAK;;AAAA;AAiC7C;;;;AAA6C;AAgC7C;;;;AAA8B;iBAhCR,YAAA,IAAgB,OAAO;;iBAgC7B,cAAA;AAAA,KAgCJ,iBAAA;EAKH;AAQT;;;EARE,OAAO;AAAA;;;;AAQiE;;iBAApD,UAAA,CAAW,OAAA,GAAS,iBAAA,GAAyB,OAAO"}
@@ -4,7 +4,7 @@ import { processJob } from "./process-job.mjs";
4
4
  import { log } from "@warlock.js/logger";
5
5
  import { Queue, Worker } from "bullmq";
6
6
 
7
- //#region ../queue/src/queue-manager.ts
7
+ //#region ../../packages/queue/src/queue-manager.ts
8
8
  const DEFAULT_SHUTDOWN_TIMEOUT = 3e4;
9
9
  const queues = /* @__PURE__ */ new Map();
10
10
  const workers = /* @__PURE__ */ new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"queue-manager.mjs","names":[],"sources":["../../../../../../queue/src/queue-manager.ts"],"sourcesContent":["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`, or when this\n * process does not serve the `worker` role (`Application.hasRole`) — a\n * process started with `--role=web`, for example, produces jobs but never\n * runs them in-process. Calling it again while workers run starts only the\n * 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 { Application } = await import(\"@warlock.js/core\");\n\n if (!Application.hasRole(\"worker\")) {\n log.info(\n \"queue\",\n \"workers\",\n `queue: workers not started (role: ${[...Application.roles].join(\",\")})`,\n );\n return [];\n }\n\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(\n \"queue\",\n \"job.failed\",\n `${job?.name ?? \"unknown\"} (${job?.id ?? \"?\"}): ${error.message}`,\n );\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"],"mappings":"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"}
1
+ {"version":3,"file":"queue-manager.mjs","names":[],"sources":["../../../../../../../packages/queue/src/queue-manager.ts"],"sourcesContent":["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`, or when this\n * process does not serve the `worker` role (`Application.hasRole`) — a\n * process started with `--role=web`, for example, produces jobs but never\n * runs them in-process. Calling it again while workers run starts only the\n * 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 { Application } = await import(\"@warlock.js/core\");\n\n if (!Application.hasRole(\"worker\")) {\n log.info(\n \"queue\",\n \"workers\",\n `queue: workers not started (role: ${[...Application.roles].join(\",\")})`,\n );\n return [];\n }\n\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(\n \"queue\",\n \"job.failed\",\n `${job?.name ?? \"unknown\"} (${job?.id ?? \"?\"}): ${error.message}`,\n );\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"],"mappings":"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"}
package/esm/types.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ConnectionOptions } from "bullmq";
2
2
  import { Middleware } from "@warlock.js/core";
3
3
 
4
- //#region ../queue/src/types.d.ts
4
+ //#region ../../packages/queue/src/types.d.ts
5
5
  /** Units accepted in a {@link Duration} string. */
6
6
  type DurationUnit = "ms" | "s" | "m" | "h" | "d";
7
7
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../queue/src/types.ts"],"mappings":";;;;;KAIY,YAAA;AAAZ;;;;AAAA,KAMY,QAAA,wBAAgC,YAAY;AAAxD;;;;AAAA,KAMY,UAAA;EAAA,uFAIN,IAAA;EAEA,KAAK;AAAA;AAOX;;;;AAAA,KAAY,YAAA;AAMZ;;;;AAAA,KAAY,UAAA;EAQK,kEANf,QAAA,WAM2B;EAJ3B,OAAA,GAAU,UAAA,EAAV;EAEA,gBAAA,GAAmB,YAAA,EAAnB;EAEA,YAAA,GAAe,YAAA;AAAA;;;AAAY;KAMjB,kBAAA;EAAkB;;;;EAK5B,OAAA,YAOA;EALA,WAAA;EAKe;AAQjB;;;EARE,eAAA;AAAA;;;;;AAkBuB;KAVb,oBAAA;EAgBW,oDAdrB,OAAA,YAmBY;EAjBZ,IAAA;EAyBU;;;;;EAnBV,UAAA,GAAa,UAAU;AAAA;;;;KAMb,WAAA;EAaA;;;;EARV,UAAA,EAAY,iBAAA,EAcF;EAZV,MAAA;EAEA,YAAA,WAUuC;EARvC,iBAAA,GAAoB,UAAA,EAaA;EAXpB,OAAA,GAAU,kBAAA,EAuBM;EArBhB,SAAA,GAAY,oBAAA;AAAA;;KAIF,WAAA,YAAuB,MAAM;;;;KAK7B,UAAA;EAUV,kBARA,EAAA,UAUgB;EARhB,IAAA,UAQ8B;EAN9B,KAAA,UAQI;EANJ,OAAA,UAQA;EANA,WAAA,UAMO;EAJP,QAAA,CAAS,KAAA,EAAO,WAAA,GAAc,OAAA,QAUP;EARvB,GAAA,CAAI,IAAA,WAAe,OAAA,QAQ0B;EAN7C,OAAA;AAAA;;;;KAMU,aAAA,sBAAmC,UAAA;EAQ6B,oEAN1E,OAAA,UAFkC;EAIlC,IAAA,UAFA;EAIA,KAAA;EAEA,MAAA,CAAO,OAAA,EAAS,QAAA,EAAU,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA;;;;KAMzD,eAAA;EAN8C,6CAQxD,KAAA,GAAQ,QAAA;EARkE;AAAA;AAM5E;;EAOE,QAAA;EASoB;;;;EAJpB,KAAA,WAEA;EAAA,QAAA,WAEU;EAAV,OAAA,GAAU,UAAU;AAAA;AAItB;AAAA,KAAY,aAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;AAAA;;KAIU,QAAA;AAJL;AAAA,KAeK,WAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,KAAA,EAAO,QAAA;EACP,OAAA,EAAS,QAAA;EACT,QAAA,EAAU,WAAA;EACV,YAAA;EACA,MAAA,GAAS,OAAA;EACT,YAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA,GAAa,IAAA;AAAA;;KAIH,SAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,OAAA,EAAS,QAAA;EACT,YAAA;EACA,YAAA;EACA,UAAA;EACA,QAAA,GAAW,IAAA,EAlBX;EAoBA,KAAA,IAAS,OAAA;AAAA;;;;KAMC,QAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA,UAvBE;EAyBX,QAAA,CAAS,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,aAAA,GAxBnD;EA0Bb,IAAA,CAAK,EAAA,WAAa,OAAA,CAAQ,WAAA,CAAY,QAAA,EAAU,OAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/queue/src/types.ts"],"mappings":";;;;;KAIY,YAAA;AAAZ;;;;AAAA,KAMY,QAAA,wBAAgC,YAAY;AAAxD;;;;AAAA,KAMY,UAAA;EAAA,uFAIN,IAAA;EAEA,KAAK;AAAA;AAOX;;;;AAAA,KAAY,YAAA;AAMZ;;;;AAAA,KAAY,UAAA;EAQK,kEANf,QAAA,WAM2B;EAJ3B,OAAA,GAAU,UAAA,EAAV;EAEA,gBAAA,GAAmB,YAAA,EAAnB;EAEA,YAAA,GAAe,YAAA;AAAA;;;AAAY;KAMjB,kBAAA;EAAkB;;;;EAK5B,OAAA,YAOA;EALA,WAAA;EAKe;AAQjB;;;EARE,eAAA;AAAA;;;;;AAkBuB;KAVb,oBAAA;EAgBW,oDAdrB,OAAA,YAmBY;EAjBZ,IAAA;EAyBU;;;;;EAnBV,UAAA,GAAa,UAAU;AAAA;;;;KAMb,WAAA;EAaA;;;;EARV,UAAA,EAAY,iBAAA,EAcF;EAZV,MAAA;EAEA,YAAA,WAUuC;EARvC,iBAAA,GAAoB,UAAA,EAaA;EAXpB,OAAA,GAAU,kBAAA,EAuBM;EArBhB,SAAA,GAAY,oBAAA;AAAA;;KAIF,WAAA,YAAuB,MAAM;;;;KAK7B,UAAA;EAUV,kBARA,EAAA,UAUgB;EARhB,IAAA,UAQ8B;EAN9B,KAAA,UAQI;EANJ,OAAA,UAQA;EANA,WAAA,UAMO;EAJP,QAAA,CAAS,KAAA,EAAO,WAAA,GAAc,OAAA,QAUP;EARvB,GAAA,CAAI,IAAA,WAAe,OAAA,QAQ0B;EAN7C,OAAA;AAAA;;;;KAMU,aAAA,sBAAmC,UAAA;EAQ6B,oEAN1E,OAAA,UAFkC;EAIlC,IAAA,UAFA;EAIA,KAAA;EAEA,MAAA,CAAO,OAAA,EAAS,QAAA,EAAU,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA;;;;KAMzD,eAAA;EAN8C,6CAQxD,KAAA,GAAQ,QAAA;EARkE;AAAA;AAM5E;;EAOE,QAAA;EASoB;;;;EAJpB,KAAA,WAEA;EAAA,QAAA,WAEU;EAAV,OAAA,GAAU,UAAU;AAAA;AAItB;AAAA,KAAY,aAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;AAAA;;KAIU,QAAA;AAJL;AAAA,KAeK,WAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,KAAA,EAAO,QAAA;EACP,OAAA,EAAS,QAAA;EACT,QAAA,EAAU,WAAA;EACV,YAAA;EACA,MAAA,GAAS,OAAA;EACT,YAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA,GAAa,IAAA;AAAA;;KAIH,SAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,OAAA,EAAS,QAAA;EACT,YAAA;EACA,YAAA;EACA,UAAA;EACA,QAAA,GAAW,IAAA,EAlBX;EAoBA,KAAA,IAAS,OAAA;AAAA;;;;KAMC,QAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA,UAvBE;EAyBX,QAAA,CAAS,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,aAAA,GAxBnD;EA0Bb,IAAA,CAAK,EAAA,WAAa,OAAA,CAAQ,WAAA,CAAY,QAAA,EAAU,OAAA;AAAA"}
package/llms-full.txt CHANGED
@@ -71,7 +71,7 @@ Every process must use the same `prefix`, or they will not see each other's jobs
71
71
 
72
72
  ---
73
73
  name: define-jobs
74
- description: 'Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — the `configure-queue` topic; failed jobs — the `manage-failed-jobs` topic.'
74
+ description: 'Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `setDispatchInterceptor`, `queueFake`, `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — the `configure-queue` topic; failed jobs — the `manage-failed-jobs` topic.'
75
75
  ---
76
76
 
77
77
  # Define and dispatch jobs
@@ -99,11 +99,14 @@ The job module must be imported by the process that runs workers — in a Warloc
99
99
  ```ts
100
100
  const { id } = await sendInvoice.dispatch({ invoiceId: "42" });
101
101
 
102
- await sendInvoice.dispatch({ invoiceId: "43" }, {
103
- delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
104
- priority: 1, // 1 runs first; larger numbers later
105
- jobId: "invoice:43", // a second dispatch with a live id is ignored
106
- });
102
+ await sendInvoice.dispatch(
103
+ { invoiceId: "43" },
104
+ {
105
+ delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
106
+ priority: 1, // 1 runs first; larger numbers later
107
+ jobId: "invoice:43", // a second dispatch with a live id is ignored
108
+ },
109
+ );
107
110
  ```
108
111
 
109
112
  Option precedence: dispatch options > `defineJob` > `queue.defaultJobOptions`.
@@ -115,6 +118,16 @@ const job = await sendInvoice.find(id);
115
118
  // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
116
119
  ```
117
120
 
121
+ ## List the defined jobs
122
+
123
+ `registeredJobs()` returns every definition `defineJob()` registered in this process, and `queueOf(definition)` the queue it runs on. Neither touches Redis; `warlock inspect --section jobs` prints the same list for a whole app.
124
+
125
+ ```ts
126
+ import { queueOf, registeredJobs } from "@warlock.js/queue";
127
+
128
+ const jobs = registeredJobs().map((job) => ({ name: job.name, queue: queueOf(job) }));
129
+ ```
130
+
118
131
  ## Carry request context into jobs
119
132
 
120
133
  ```ts
@@ -131,6 +144,23 @@ setQueueContext(
131
144
  - `defineJob({ context: false })` opts a job out.
132
145
  - Throw `UnrecoverableJobError` from a handler or `restore` to fail the job without retries.
133
146
 
147
+ ## Test dispatches without Redis
148
+
149
+ In the test environment `queueFake` from `@warlock.js/core/tests` is installed by default: dispatches are recorded, not pushed, and `find()` answers from the record.
150
+
151
+ ```ts
152
+ // with: import { queueFake } from "@warlock.js/core/tests";
153
+ // plus your job (sendInvoice) and service (placeOrderService)
154
+ it("queues the invoice", async () => {
155
+ const order = await placeOrderService({ items: ["pen"] });
156
+
157
+ queueFake.assertPushed(sendInvoice, { invoiceId: order.invoiceId });
158
+ expect(queueFake.pushed(sendInvoice)).toHaveLength(1);
159
+ });
160
+ ```
161
+
162
+ Jobs are not run — call the handler to test it. Opt out with `tests.fakes: { queue: false }`. The seam is `setDispatchInterceptor({ dispatch, find? })`: while set, every job's `dispatch()` / `find()` goes to it instead of BullMQ; set it back to `undefined` to restore the broker.
163
+
134
164
  ## Rules
135
165
 
136
166
  - A job name with no handler in the worker process fails at once, without retries.
@@ -278,4 +308,3 @@ await orderShipped.queue(user, { order }, { delay: "10m" }); // delivered in 10
278
308
  - Deliveries run under the job name `warlock.notifications.deliver`.
279
309
 
280
310
 
281
-
package/llms.txt CHANGED
@@ -7,7 +7,7 @@
7
7
  ## Skills
8
8
 
9
9
  - [configure-queue](@warlock.js/queue/configure-queue/SKILL.md): Configure `@warlock.js/queue`: the declarative `src/config/queue.ts` (`QueueConfig` — `connection`, `prefix`, `defaultQueue`, `defaultJobOptions`, `workers: { enabled, concurrency, shutdownTimeout }`), registering `queueConnector()` in `warlock.config.ts > connectors`, running a dispatch-only process, and graceful shutdown. Programmatic `setQueueConfig` / `startWorkers` / `closeQueue` for scripts and tests. Triggers: `QueueConfig`, `queueConnector`, `setQueueConfig`, `startWorkers`, `closeQueue`, `workers.enabled`, `shutdownTimeout`; "configure the queue", "connect BullMQ to Redis", "disable workers in the web process", "graceful shutdown of jobs". Skip: writing jobs — the `define-jobs` topic.
10
- - [define-jobs](@warlock.js/queue/define-jobs/SKILL.md): Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — the `configure-queue` topic; failed jobs — the `manage-failed-jobs` topic.
10
+ - [define-jobs](@warlock.js/queue/define-jobs/SKILL.md): Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `setDispatchInterceptor`, `queueFake`, `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — the `configure-queue` topic; failed jobs — the `manage-failed-jobs` topic.
11
11
  - [manage-failed-jobs](@warlock.js/queue/manage-failed-jobs/SKILL.md): Inspect and retry failed `@warlock.js/queue` jobs: `failedJobs({ queue, start, end })` returns `FailedJob[]` (`id`, `name`, `payload`, `attemptsMade`, `failedReason`, `stacktrace`, `failedAt`, `retry()`), `retryFailedJob(id, { queue })` (throws `FailedJobNotFoundError`), and the bull-board UI — `warlock add bull-board` (requires: ["queue"], so it installs queue first when missing) writes a `dashboard: { enabled, path, middleware }` block to `src/config/queue.ts`; a guard middleware is required in production or boot throws `QueueDashboardUnguardedError`. Manual `queueDashboard(server, { basePath, queues, middleware })` also available — needs `@bull-board/api` + `@bull-board/fastify`, loaded only on call, missing ones throw `QueueDashboardDependencyError`. Triggers: `failedJobs`, `retryFailedJob`, `queueDashboard`, `bull-board`, `dashboard.enabled`, `QueueDashboardUnguardedError`; "list failed jobs", "retry a failed job", "queue dashboard", "job admin UI". Skip: defining retries — the `define-jobs` topic.
12
12
  - [overview](@warlock.js/queue/overview/SKILL.md): @warlock.js/queue overview for orientation and choosing the focused skill for the task.
13
13
  - [queue-notifications](@warlock.js/queue/queue-notifications/SKILL.md): Send `@warlock.js/notifications` `.queue()` deliveries through BullMQ with `bullmqQueue({ queue?, attempts?, backoff? })` from `@warlock.js/notifications` — put it in the `queue` slot of `src/config/notifications.ts`; the queue workers deliver. Honours `SendOptions.delay` (number = seconds, or "10m"), retries a failing `channel.send`, and fails at once for a channel missing from the worker config. Triggers: `bullmqQueue`, `NotificationConfig.queue`, `QueuePackageNotInstalledError`; "queue notifications with BullMQ", "retry notification delivery", "delayed notification". Skip: the herald backend — the `queue-notifications` topic of the `warlock-js-notifications` skill.
package/package.json CHANGED
@@ -11,8 +11,8 @@
11
11
  "peerDependencies": {
12
12
  "@bull-board/api": "^9.0.0",
13
13
  "@bull-board/fastify": "^9.0.0",
14
- "@warlock.js/core": "5.29.0",
15
- "@warlock.js/logger": "5.29.0"
14
+ "@warlock.js/core": "5.31.0",
15
+ "@warlock.js/logger": "5.31.0"
16
16
  },
17
17
  "peerDependenciesMeta": {
18
18
  "@bull-board/api": {
@@ -24,7 +24,8 @@
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "https://github.com/warlockjs/queue"
27
+ "url": "git+https://github.com/warlockjs/framework.git",
28
+ "directory": "packages/queue"
28
29
  },
29
30
  "keywords": [
30
31
  "warlock.js",
@@ -37,7 +38,7 @@
37
38
  ],
38
39
  "author": "hassanzohdy",
39
40
  "license": "MIT",
40
- "version": "5.29.0",
41
+ "version": "5.31.0",
41
42
  "main": "./cjs/index.cjs",
42
43
  "module": "./esm/index.mjs",
43
44
  "types": "./esm/index.d.mts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: define-jobs
3
- description: 'Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — the `configure-queue` topic; failed jobs — the `manage-failed-jobs` topic.'
3
+ description: 'Define and dispatch background jobs with `@warlock.js/queue`: `defineJob({ name, queue?, attempts?, backoff?, removeOnComplete?, removeOnFail?, handle(payload, ctx) })` returns a typed job; `job.dispatch(payload, { delay, priority, jobId, attempts, backoff })`; the handler context (`id`, `attempt`, `maxAttempts`, `progress()`, `log()`); reading a job with `job.find(id)`. Triggers: `setDispatchInterceptor`, `queueFake`, `defineJob`, `.dispatch(`, `ctx.progress`, `JobContext`, `DispatchOptions`, `backoff`, `attempts`, `priority`, `jobId`; "run this in the background", "retry with backoff", "delay a job", "job progress", "idempotent dispatch". Skip: config and workers — the `configure-queue` topic; failed jobs — the `manage-failed-jobs` topic.'
4
4
  ---
5
5
 
6
6
  # Define and dispatch jobs
@@ -28,11 +28,14 @@ The job module must be imported by the process that runs workers — in a Warloc
28
28
  ```ts
29
29
  const { id } = await sendInvoice.dispatch({ invoiceId: "42" });
30
30
 
31
- await sendInvoice.dispatch({ invoiceId: "43" }, {
32
- delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
33
- priority: 1, // 1 runs first; larger numbers later
34
- jobId: "invoice:43", // a second dispatch with a live id is ignored
35
- });
31
+ await sendInvoice.dispatch(
32
+ { invoiceId: "43" },
33
+ {
34
+ delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
35
+ priority: 1, // 1 runs first; larger numbers later
36
+ jobId: "invoice:43", // a second dispatch with a live id is ignored
37
+ },
38
+ );
36
39
  ```
37
40
 
38
41
  Option precedence: dispatch options > `defineJob` > `queue.defaultJobOptions`.
@@ -44,6 +47,16 @@ const job = await sendInvoice.find(id);
44
47
  // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
45
48
  ```
46
49
 
50
+ ## List the defined jobs
51
+
52
+ `registeredJobs()` returns every definition `defineJob()` registered in this process, and `queueOf(definition)` the queue it runs on. Neither touches Redis; `warlock inspect --section jobs` prints the same list for a whole app.
53
+
54
+ ```ts
55
+ import { queueOf, registeredJobs } from "@warlock.js/queue";
56
+
57
+ const jobs = registeredJobs().map((job) => ({ name: job.name, queue: queueOf(job) }));
58
+ ```
59
+
47
60
  ## Carry request context into jobs
48
61
 
49
62
  ```ts
@@ -60,6 +73,23 @@ setQueueContext(
60
73
  - `defineJob({ context: false })` opts a job out.
61
74
  - Throw `UnrecoverableJobError` from a handler or `restore` to fail the job without retries.
62
75
 
76
+ ## Test dispatches without Redis
77
+
78
+ In the test environment `queueFake` from `@warlock.js/core/tests` is installed by default: dispatches are recorded, not pushed, and `find()` answers from the record.
79
+
80
+ ```ts
81
+ // with: import { queueFake } from "@warlock.js/core/tests";
82
+ // plus your job (sendInvoice) and service (placeOrderService)
83
+ it("queues the invoice", async () => {
84
+ const order = await placeOrderService({ items: ["pen"] });
85
+
86
+ queueFake.assertPushed(sendInvoice, { invoiceId: order.invoiceId });
87
+ expect(queueFake.pushed(sendInvoice)).toHaveLength(1);
88
+ });
89
+ ```
90
+
91
+ Jobs are not run — call the handler to test it. Opt out with `tests.fakes: { queue: false }`. The seam is `setDispatchInterceptor({ dispatch, find? })`: while set, every job's `dispatch()` / `find()` goes to it instead of BullMQ; set it back to `undefined` to restore the broker.
92
+
63
93
  ## Rules
64
94
 
65
95
  - A job name with no handler in the worker process fails at once, without retries.
package/skills/index.md CHANGED
@@ -1,11 +1,13 @@
1
1
  ---
2
- description: "Durable background jobs on BullMQ + Redis: retries, delays, priorities, workers, failed-job recovery. Exports `defineJob`, `queueConnector`, `startWorkers`, `closeQueue`, `getQueue`, `failedJobs`, `retryFailedJob`, `mountQueueDashboard`, `QueueConfig`, `QueueJob`. Use for: \"run this in the background\", \"retry a failed job\", \"delay a job\", \"survive restarts\", \"job dashboard\". Not this package: cron or recurring schedules → @warlock.js/scheduler; broker pub/sub messaging → @warlock.js/herald; core `Queue` is an in-memory batcher."
2
+ description: 'Durable background jobs on BullMQ + Redis: retries, delays, priorities, workers, failed-job recovery. Exports `defineJob`, `queueConnector`, `startWorkers`, `closeQueue`, `getQueue`, `failedJobs`, `retryFailedJob`, `mountQueueDashboard`, `QueueConfig`, `QueueJob`. Use for: "run this in the background", "retry a failed job", "delay a job", "survive restarts", "job dashboard". Not this package: cron or recurring schedules → @warlock.js/scheduler; broker pub/sub messaging → @warlock.js/herald; core `Queue` is an in-memory batcher.'
3
3
  ---
4
+
4
5
  # @warlock.js/queue
5
6
 
6
7
  Jobs are stored in Redis (required; not bundled), retried on failure, and run by workers, in the app process by default. `defineJob({ name, handle })` registers a handler and returns a typed job; `job.dispatch(payload, options)` enqueues it. Any process with the same definition and a worker can run it. Server-only.
7
8
 
8
9
  ## The 80% path
10
+
9
11
  1. Orient with `overview.md`.
10
12
  2. Configure `src/config/queue.ts` and register `queueConnector()`: `configure-queue.md`.
11
13
  3. Define and dispatch jobs (attempts, backoff, delay, priority, progress): `define-jobs.md`.
@@ -13,6 +15,7 @@ Jobs are stored in Redis (required; not bundled), retried on failure, and run by
13
15
  5. Back `@warlock.js/notifications` `.queue()` with BullMQ: `queue-notifications.md`.
14
16
 
15
17
  ## Conventions and pitfalls
18
+
16
19
  - Job names are the routing key; a job with no matching `defineJob` in the worker process cannot run.
17
20
  - Handlers must be idempotent: retries re-run them.
18
21
  - Shutdown lets active jobs finish up to `workers.shutdownTimeout`, then closes connections.
@@ -32,4 +32,3 @@ await orderShipped.queue(user, { order }, { delay: "10m" }); // delivered in 10
32
32
  - `channel.send` throws → retried per `attempts` / `backoff`, then listed by `failedJobs()`.
33
33
  - Channel not configured in the worker → fails at once, no retries.
34
34
  - Deliveries run under the job name `warlock.notifications.deliver`.
35
-