@warlock.js/queue 5.13.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 (63) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +121 -0
  4. package/cjs/define-job-DideGKQK.cjs +468 -0
  5. package/cjs/define-job-DideGKQK.cjs.map +1 -0
  6. package/cjs/index.cjs +180 -0
  7. package/cjs/index.cjs.map +1 -0
  8. package/cjs/notifications/index.cjs +68 -0
  9. package/cjs/notifications/index.cjs.map +1 -0
  10. package/esm/config.d.mts +21 -0
  11. package/esm/config.d.mts.map +1 -0
  12. package/esm/config.mjs +32 -0
  13. package/esm/config.mjs.map +1 -0
  14. package/esm/dashboard.d.mts +47 -0
  15. package/esm/dashboard.d.mts.map +1 -0
  16. package/esm/dashboard.mjs +61 -0
  17. package/esm/dashboard.mjs.map +1 -0
  18. package/esm/define-job.d.mts +24 -0
  19. package/esm/define-job.d.mts.map +1 -0
  20. package/esm/define-job.mjs +102 -0
  21. package/esm/define-job.mjs.map +1 -0
  22. package/esm/duration.d.mts +11 -0
  23. package/esm/duration.d.mts.map +1 -0
  24. package/esm/duration.mjs +28 -0
  25. package/esm/duration.mjs.map +1 -0
  26. package/esm/errors.d.mts +36 -0
  27. package/esm/errors.d.mts.map +1 -0
  28. package/esm/errors.mjs +55 -0
  29. package/esm/errors.mjs.map +1 -0
  30. package/esm/failed-jobs.d.mts +23 -0
  31. package/esm/failed-jobs.d.mts.map +1 -0
  32. package/esm/failed-jobs.mjs +39 -0
  33. package/esm/failed-jobs.mjs.map +1 -0
  34. package/esm/index.d.mts +10 -0
  35. package/esm/index.mjs +10 -0
  36. package/esm/job-registry.mjs +39 -0
  37. package/esm/job-registry.mjs.map +1 -0
  38. package/esm/notifications/index.d.mts +2 -0
  39. package/esm/notifications/index.mjs +3 -0
  40. package/esm/notifications/queue-notification-dispatcher.d.mts +34 -0
  41. package/esm/notifications/queue-notification-dispatcher.d.mts.map +1 -0
  42. package/esm/notifications/queue-notification-dispatcher.mjs +67 -0
  43. package/esm/notifications/queue-notification-dispatcher.mjs.map +1 -0
  44. package/esm/process-job.mjs +32 -0
  45. package/esm/process-job.mjs.map +1 -0
  46. package/esm/queue-connector.d.mts +36 -0
  47. package/esm/queue-connector.d.mts.map +1 -0
  48. package/esm/queue-connector.mjs +73 -0
  49. package/esm/queue-connector.mjs.map +1 -0
  50. package/esm/queue-manager.d.mts +36 -0
  51. package/esm/queue-manager.d.mts.map +1 -0
  52. package/esm/queue-manager.mjs +109 -0
  53. package/esm/queue-manager.mjs.map +1 -0
  54. package/esm/types.d.mts +149 -0
  55. package/esm/types.d.mts.map +1 -0
  56. package/llms-full.txt +229 -0
  57. package/llms.txt +13 -0
  58. package/package.json +70 -0
  59. package/skills/configure-queue/SKILL.md +55 -0
  60. package/skills/define-jobs/SKILL.md +51 -0
  61. package/skills/manage-failed-jobs/SKILL.md +38 -0
  62. package/skills/overview/SKILL.md +26 -0
  63. package/skills/queue-notifications/SKILL.md +33 -0
@@ -0,0 +1,109 @@
1
+ import { getQueueConfig } from "./config.mjs";
2
+ import { onJobRegistered, queueOf, registeredJobs } from "./job-registry.mjs";
3
+ import { processJob } from "./process-job.mjs";
4
+ import { log } from "@warlock.js/logger";
5
+ import { Queue, Worker } from "bullmq";
6
+
7
+ //#region ../queue/src/queue-manager.ts
8
+ const DEFAULT_SHUTDOWN_TIMEOUT = 3e4;
9
+ const queues = /* @__PURE__ */ new Map();
10
+ const workers = /* @__PURE__ */ new Map();
11
+ let stopListening;
12
+ /**
13
+ * The BullMQ queue for `name`, created on first use with the configured
14
+ * connection and prefix.
15
+ */
16
+ function getQueue(name) {
17
+ let queue = queues.get(name);
18
+ if (!queue) {
19
+ const config = getQueueConfig();
20
+ queue = new Queue(name, {
21
+ connection: config.connection,
22
+ prefix: config.prefix ?? "warlock"
23
+ });
24
+ queue.on("error", (error) => {
25
+ log.error("queue", "connection", error);
26
+ });
27
+ queues.set(name, queue);
28
+ }
29
+ return queue;
30
+ }
31
+ /**
32
+ * Start one worker per queue that has a registered job, and keep starting
33
+ * workers for queues whose first job is defined later.
34
+ *
35
+ * A no-op returning `[]` when `workers.enabled` is `false`. Calling it again
36
+ * while workers run starts only the missing ones.
37
+ *
38
+ * @returns the queue names that now have a worker in this process.
39
+ */
40
+ async function startWorkers() {
41
+ if (getQueueConfig().workers?.enabled === false) return [];
42
+ for (const job of registeredJobs()) ensureWorker(queueOf(job));
43
+ stopListening ??= onJobRegistered((job) => {
44
+ ensureWorker(queueOf(job));
45
+ });
46
+ await Promise.all([...workers.values()].map((worker) => worker.waitUntilReady()));
47
+ return [...workers.keys()];
48
+ }
49
+ /** The queue names with a running worker in this process. */
50
+ function runningWorkers() {
51
+ return [...workers.keys()];
52
+ }
53
+ function ensureWorker(queueName) {
54
+ if (workers.has(queueName)) return;
55
+ const config = getQueueConfig();
56
+ const worker = new Worker(queueName, processJob, {
57
+ connection: config.connection,
58
+ prefix: config.prefix ?? "warlock",
59
+ concurrency: config.workers?.concurrency ?? 1
60
+ });
61
+ worker.on("error", (error) => {
62
+ log.error("queue", "worker", error);
63
+ });
64
+ worker.on("failed", (job, error) => {
65
+ log.error("queue", "job.failed", `${job?.name ?? "unknown"} (${job?.id ?? "?"}): ${error.message}`);
66
+ });
67
+ workers.set(queueName, worker);
68
+ }
69
+ /**
70
+ * Graceful shutdown: stop workers taking new jobs and wait for active ones
71
+ * (bounded by `timeout`, then force-close), then close every queue
72
+ * connection. Safe to call when nothing was started, and more than once.
73
+ */
74
+ async function closeQueue(options = {}) {
75
+ stopListening?.();
76
+ stopListening = void 0;
77
+ const timeout = options.timeout ?? configuredShutdownTimeout();
78
+ const closingWorkers = [...workers.values()];
79
+ workers.clear();
80
+ await Promise.all(closingWorkers.map((worker) => closeWorker(worker, timeout)));
81
+ const closingQueues = [...queues.values()];
82
+ queues.clear();
83
+ await Promise.all(closingQueues.map((queue) => queue.close()));
84
+ }
85
+ function configuredShutdownTimeout() {
86
+ try {
87
+ return getQueueConfig().workers?.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
88
+ } catch {
89
+ return DEFAULT_SHUTDOWN_TIMEOUT;
90
+ }
91
+ }
92
+ async function closeWorker(worker, timeout) {
93
+ let timer;
94
+ const timedOut = new Promise((resolve) => {
95
+ timer = setTimeout(() => resolve("timeout"), timeout);
96
+ });
97
+ const closing = worker.close().then(() => "closed");
98
+ closing.catch(() => void 0);
99
+ const outcome = await Promise.race([closing, timedOut]);
100
+ clearTimeout(timer);
101
+ if (outcome === "timeout") {
102
+ log.warn("queue", "shutdown", `Worker for "${worker.name}" still had active jobs after ${timeout}ms; disconnecting. Those jobs are retried once their lock expires.`);
103
+ await worker.disconnect();
104
+ }
105
+ }
106
+
107
+ //#endregion
108
+ export { closeQueue, getQueue, runningWorkers, startWorkers };
109
+ //# sourceMappingURL=queue-manager.mjs.map
@@ -0,0 +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`. 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"],"mappings":";;;;;;;AAMA,MAAM,2BAA2B;AAEjC,MAAM,yBAAS,IAAI,IAAmB;AACtC,MAAM,0BAAU,IAAI,IAAoB;AACxC,IAAI;;;;;AAMJ,SAAgB,SAAS,MAAqB;CAC5C,IAAI,QAAQ,OAAO,IAAI,IAAI;CAE3B,IAAI,CAAC,OAAO;EACV,MAAM,SAAS,eAAe;EAE9B,QAAQ,IAAI,MAAM,MAAM;GACtB,YAAY,OAAO;GACnB,QAAQ,OAAO,UAAU;EAC3B,CAAC;EAED,MAAM,GAAG,UAAU,UAAU;GAC3B,IAAI,MAAM,SAAS,cAAc,KAAK;EACxC,CAAC;EAED,OAAO,IAAI,MAAM,KAAK;CACxB;CAEA,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,eAAkC;CAGtD,IAFe,eAEN,CAAC,CAAC,SAAS,YAAY,OAC9B,OAAO,CAAC;CAGV,KAAK,MAAM,OAAO,eAAe,GAC/B,aAAa,QAAQ,GAAG,CAAC;CAG3B,kBAAkB,iBAAiB,QAAQ;EACzC,aAAa,QAAQ,GAAG,CAAC;CAC3B,CAAC;CAED,MAAM,QAAQ,IAAI,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,KAAK,WAAW,OAAO,eAAe,CAAC,CAAC;CAEhF,OAAO,CAAC,GAAG,QAAQ,KAAK,CAAC;AAC3B;;AAGA,SAAgB,iBAA2B;CACzC,OAAO,CAAC,GAAG,QAAQ,KAAK,CAAC;AAC3B;AAEA,SAAS,aAAa,WAAyB;CAC7C,IAAI,QAAQ,IAAI,SAAS,GACvB;CAGF,MAAM,SAAS,eAAe;CAE9B,MAAM,SAAS,IAAI,OAAO,WAAW,YAAY;EAC/C,YAAY,OAAO;EACnB,QAAQ,OAAO,UAAU;EACzB,aAAa,OAAO,SAAS,eAAe;CAC9C,CAAC;CAED,OAAO,GAAG,UAAU,UAAU;EAC5B,IAAI,MAAM,SAAS,UAAU,KAAK;CACpC,CAAC;CAED,OAAO,GAAG,WAAW,KAAK,UAAU;EAClC,IAAI,MAAM,SAAS,cAAc,GAAG,KAAK,QAAQ,UAAU,IAAI,KAAK,MAAM,IAAI,KAAK,MAAM,SAAS;CACpG,CAAC;CAED,QAAQ,IAAI,WAAW,MAAM;AAC/B;;;;;;AAeA,eAAsB,WAAW,UAA6B,CAAC,GAAkB;CAC/E,gBAAgB;CAChB,gBAAgB;CAEhB,MAAM,UAAU,QAAQ,WAAW,0BAA0B;CAC7D,MAAM,iBAAiB,CAAC,GAAG,QAAQ,OAAO,CAAC;CAC3C,QAAQ,MAAM;CAEd,MAAM,QAAQ,IAAI,eAAe,KAAK,WAAW,YAAY,QAAQ,OAAO,CAAC,CAAC;CAE9E,MAAM,gBAAgB,CAAC,GAAG,OAAO,OAAO,CAAC;CACzC,OAAO,MAAM;CAEb,MAAM,QAAQ,IAAI,cAAc,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAC/D;AAEA,SAAS,4BAAoC;CAC3C,IAAI;EACF,OAAO,eAAe,CAAC,CAAC,SAAS,mBAAmB;CACtD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,YAAY,QAAgB,SAAgC;CACzE,IAAI;CAEJ,MAAM,WAAW,IAAI,SAAoB,YAAY;EACnD,QAAQ,iBAAiB,QAAQ,SAAS,GAAG,OAAO;CACtD,CAAC;CAKD,MAAM,UAAU,OAAO,MAAM,CAAC,CAAC,WAAW,QAAiB;CAC3D,QAAQ,YAAY,MAAS;CAE7B,MAAM,UAAU,MAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,CAAC;CAEtD,aAAa,KAAK;CAElB,IAAI,YAAY,WAAW;EACzB,IAAI,KACF,SACA,YACA,eAAe,OAAO,KAAK,gCAAgC,QAAQ,mEAErE;EAEA,MAAM,OAAO,WAAW;CAC1B;AACF"}
@@ -0,0 +1,149 @@
1
+ import { ConnectionOptions } from "bullmq";
2
+
3
+ //#region ../queue/src/types.d.ts
4
+ /** Units accepted in a {@link Duration} string. */
5
+ type DurationUnit = "ms" | "s" | "m" | "h" | "d";
6
+ /**
7
+ * A duration: milliseconds as a number, or a string such as `"500ms"`,
8
+ * `"30s"`, `"10m"`, `"2h"`, `"1d"`.
9
+ */
10
+ type Duration = number | `${number}${DurationUnit}`;
11
+ /**
12
+ * How a failed attempt waits before the next one. A number is a fixed delay in
13
+ * milliseconds.
14
+ */
15
+ type JobBackoff = number | {
16
+ /** `fixed` waits `delay` every time; `exponential` waits `delay * 2^(attempt - 1)`. */type: "fixed" | "exponential"; /** Base delay in milliseconds. */
17
+ delay: number;
18
+ };
19
+ /**
20
+ * How many finished jobs to keep: `true` removes them immediately, `false`
21
+ * keeps them all, a number keeps the newest N.
22
+ */
23
+ type JobRetention = boolean | number;
24
+ /**
25
+ * Options that shape how a job is retried and retained. Set app-wide in
26
+ * `queue.defaultJobOptions`, per job in `defineJob`.
27
+ */
28
+ type JobOptions = {
29
+ /** Total attempts including the first. Default `1` (no retry). */attempts?: number; /** Wait between attempts. */
30
+ backoff?: JobBackoff; /** Retention for completed jobs. Default keeps them. */
31
+ removeOnComplete?: JobRetention; /** Retention for failed jobs. Default keeps them, so `failedJobs()` can list them. */
32
+ removeOnFail?: JobRetention;
33
+ };
34
+ /**
35
+ * In-process worker settings.
36
+ */
37
+ type QueueWorkersConfig = {
38
+ /**
39
+ * Start workers in this process. Default `true`. Set `false` for a process
40
+ * that only dispatches jobs while another process consumes them.
41
+ */
42
+ enabled?: boolean; /** Jobs processed in parallel per queue. Default `1`. */
43
+ concurrency?: number;
44
+ /**
45
+ * How long shutdown waits for active jobs before force-closing the workers,
46
+ * in milliseconds. Default `30000`.
47
+ */
48
+ shutdownTimeout?: number;
49
+ };
50
+ /**
51
+ * The `queue` configuration key — `src/config/queue.ts`.
52
+ */
53
+ type QueueConfig = {
54
+ /**
55
+ * Redis connection. Any BullMQ connection option: `{ host, port, password, db }`,
56
+ * `{ url }`, or an ioredis instance.
57
+ */
58
+ connection: ConnectionOptions; /** Redis key prefix for every queue. Default `"warlock"`. */
59
+ prefix?: string; /** Queue name used when a job does not name one. Default `"default"`. */
60
+ defaultQueue?: string; /** Defaults merged under every job's own options. */
61
+ defaultJobOptions?: JobOptions; /** In-process workers. */
62
+ workers?: QueueWorkersConfig;
63
+ };
64
+ /** A progress value: a number (e.g. a percentage) or a JSON object. */
65
+ type JobProgress = number | Record<string, unknown>;
66
+ /**
67
+ * What a job handler receives beside its payload.
68
+ */
69
+ type JobContext = {
70
+ /** The job id. */id: string; /** The job name given to `defineJob`. */
71
+ name: string; /** The queue the job runs on. */
72
+ queue: string; /** The current attempt, starting at `1`. */
73
+ attempt: number; /** Total attempts allowed. */
74
+ maxAttempts: number; /** Record progress; readable through `job.find(id)`. */
75
+ progress(value: JobProgress): Promise<void>; /** Append a line to the job's log. */
76
+ log(line: string): Promise<void>;
77
+ };
78
+ /**
79
+ * The definition passed to `defineJob`.
80
+ */
81
+ type JobDefinition<TPayload, TResult> = JobOptions & {
82
+ /** Unique job name, e.g. `"invoices.send"`. */name: string; /** Queue to run on. Default: `queue.defaultQueue` (`"default"`). */
83
+ queue?: string; /** The work. Throw to fail the attempt; the return value is stored as the job result. */
84
+ handle(payload: TPayload, context: JobContext): Promise<TResult> | TResult;
85
+ };
86
+ /**
87
+ * Per-dispatch options.
88
+ */
89
+ type DispatchOptions = {
90
+ /** Wait before the job becomes available. */delay?: Duration;
91
+ /**
92
+ * Priority: `1` is the highest; larger numbers run later. Omit for no
93
+ * priority — such jobs run ahead of every prioritized job.
94
+ */
95
+ priority?: number;
96
+ /**
97
+ * Explicit job id. Dispatching again with an id that still exists is a
98
+ * no-op, which makes dispatch idempotent.
99
+ */
100
+ jobId?: string; /** Override the job's attempts for this dispatch. */
101
+ attempts?: number; /** Override the job's backoff for this dispatch. */
102
+ backoff?: JobBackoff;
103
+ };
104
+ /** What `dispatch` resolves with. */
105
+ type DispatchedJob = {
106
+ id: string;
107
+ name: string;
108
+ queue: string;
109
+ };
110
+ /** A job's lifecycle state. */
111
+ type JobState = "waiting" | "delayed" | "prioritized" | "active" | "completed" | "failed" | "waiting-children" | "unknown";
112
+ /** A point-in-time view of a job. */
113
+ type JobSnapshot<TPayload = unknown, TResult = unknown> = {
114
+ id: string;
115
+ name: string;
116
+ queue: string;
117
+ state: JobState;
118
+ payload: TPayload;
119
+ progress: JobProgress;
120
+ attemptsMade: number;
121
+ result?: TResult;
122
+ failedReason?: string;
123
+ createdAt: Date;
124
+ finishedAt?: Date;
125
+ };
126
+ /** A failed job, with the means to retry it. */
127
+ type FailedJob<TPayload = unknown> = {
128
+ id: string;
129
+ name: string;
130
+ queue: string;
131
+ payload: TPayload;
132
+ attemptsMade: number;
133
+ failedReason: string;
134
+ stacktrace: string[];
135
+ failedAt?: Date; /** Move the job back to waiting so a worker picks it up again. */
136
+ retry(): Promise<void>;
137
+ };
138
+ /**
139
+ * A job returned by `defineJob`.
140
+ */
141
+ type QueueJob<TPayload, TResult = unknown> = {
142
+ readonly name: string;
143
+ readonly queue: string; /** Enqueue the job. */
144
+ dispatch(payload: TPayload, options?: DispatchOptions): Promise<DispatchedJob>; /** Read a job of this type by id; `undefined` when it no longer exists. */
145
+ find(id: string): Promise<JobSnapshot<TPayload, TResult> | undefined>;
146
+ };
147
+ //#endregion
148
+ export { DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QueueConfig, QueueJob, QueueWorkersConfig };
149
+ //# sourceMappingURL=types.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../queue/src/types.ts"],"mappings":";;;;KAGY,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;AAMjB;;;EANE,eAAA;AAAA;;;;KAMU,WAAA;EAKE;;;;EAAZ,UAAA,EAAY,iBAAA,EAQZ;EANA,MAAA,WAM4B;EAJ5B,YAAA,WAQU;EANV,iBAAA,GAAoB,UAAA;EAEpB,OAAA,GAAU,kBAAA;AAAA;AASZ;AAAA,KALY,WAAA,YAAuB,MAAM;;;;KAK7B,UAAA;EAcgB,kBAZ1B,EAAA;EAEA,IAAA,UAEA;EAAA,KAAA,UAIA;EAFA,OAAA,UAIgB;EAFhB,WAAA,UAE8B;EAA9B,QAAA,CAAS,KAAA,EAAO,WAAA,GAAc,OAAA,QAE1B;EAAJ,GAAA,CAAI,IAAA,WAAe,OAAA;AAAA;AAAO;AAM5B;;AAN4B,KAMhB,aAAA,sBAAmC,UAAA;EAAA,+CAE7C,IAAA,UAImC;EAFnC,KAAA,WAEgD;EAAhD,MAAA,CAAO,OAAA,EAAS,QAAA,EAAU,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA;;;;KAMzD,eAAA;EAVV,6CAYA,KAAA,GAAQ,QAAA;EARR;;;;EAaA,QAAA;EAbgD;;;;EAkBhD,KAAA,WAZU;EAcV,QAAA;EAEA,OAAA,GAAU,UAAU;AAAA;;KAIV,aAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;AAAA;;KAIU,QAAA;AAPZ;AAAA,KAkBY,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;AAXf;AAAA,KAeY,SAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;EACA,OAAA,EAAS,QAAA;EACT,YAAA;EACA,YAAA;EACA,UAAA;EACA,QAAA,GAAW,IAAA,EAZM;EAcjB,KAAA,IAAS,OAAA;AAAA;;;;KAMC,QAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA,UA5BA;EA8BT,QAAA,CAAS,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,aAAA,GA7BtD;EA+BV,IAAA,CAAK,EAAA,WAAa,OAAA,CAAQ,WAAA,CAAY,QAAA,EAAU,OAAA;AAAA"}
package/llms-full.txt ADDED
@@ -0,0 +1,229 @@
1
+ # Warlock Queue — full skills
2
+
3
+ > Package: `@warlock.js/queue`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/queue/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## configure-queue `@warlock.js/queue/configure-queue/SKILL.md`
8
+
9
+ ---
10
+ name: configure-queue
11
+ description: '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 — `@warlock.js/queue/define-jobs/SKILL.md`.'
12
+ ---
13
+
14
+ # Configure the queue
15
+
16
+ Redis is required. The config is declarative; the connector applies it.
17
+
18
+ ```ts title="src/config/queue.ts"
19
+ import type { QueueConfig } from "@warlock.js/queue";
20
+
21
+ const queueConfig: QueueConfig = {
22
+ connection: { host: "127.0.0.1", port: 6379 }, // any BullMQ connection option, or { url }
23
+ prefix: "my-app", // Redis key prefix, default "warlock"
24
+ defaultJobOptions: { attempts: 3, backoff: { type: "exponential", delay: 1000 } },
25
+ workers: {
26
+ enabled: true, // default true
27
+ concurrency: 5, // jobs in parallel per queue, default 1
28
+ shutdownTimeout: 30_000, // ms to wait for active jobs, default 30000
29
+ },
30
+ };
31
+
32
+ export default queueConfig;
33
+ ```
34
+
35
+ ```ts title="warlock.config.ts"
36
+ import { defineConfig } from "@warlock.js/core";
37
+ import { queueConnector } from "@warlock.js/queue";
38
+
39
+ export default defineConfig({
40
+ connectors: [queueConnector()],
41
+ });
42
+ ```
43
+
44
+ ## What the connector does
45
+
46
+ - Starts in the **late** phase — after app code is imported — so every `defineJob` has registered. One worker per queue that has a job.
47
+ - No `queue` config → logs a warning and does nothing.
48
+ - On SIGINT/SIGTERM: workers stop taking jobs, active jobs get up to `shutdownTimeout` ms, then Redis connections close. A job still running after that is retried by another worker once its lock expires.
49
+
50
+ ## Dispatch-only process
51
+
52
+ Set `workers.enabled: false`. `dispatch()` still works; nothing is processed in that process. Run the workers elsewhere with the same job definitions:
53
+
54
+ ```ts
55
+ import { setQueueConfig, startWorkers, closeQueue } from "@warlock.js/queue";
56
+ import "./jobs"; // modules that call defineJob
57
+
58
+ setQueueConfig({ connection: { host: "127.0.0.1", port: 6379 } });
59
+ await startWorkers();
60
+ process.on("SIGTERM", () => closeQueue());
61
+ ```
62
+
63
+ Every process must use the same `prefix`, or they will not see each other's jobs.
64
+
65
+
66
+ ## define-jobs `@warlock.js/queue/define-jobs/SKILL.md`
67
+
68
+ ---
69
+ name: define-jobs
70
+ 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 — `@warlock.js/queue/configure-queue/SKILL.md`; failed jobs — `@warlock.js/queue/manage-failed-jobs/SKILL.md`.'
71
+ ---
72
+
73
+ # Define and dispatch jobs
74
+
75
+ ```ts title="src/app/invoices/jobs/send-invoice.job.ts"
76
+ import { defineJob } from "@warlock.js/queue";
77
+
78
+ export const sendInvoice = defineJob({
79
+ name: "invoices.send", // unique across the app
80
+ attempts: 5, // total tries, default 1
81
+ backoff: { type: "exponential", delay: 2000 }, // or a number = fixed ms
82
+ async handle(payload: { invoiceId: string }, ctx) {
83
+ await ctx.log(`attempt ${ctx.attempt} of ${ctx.maxAttempts}`);
84
+ await ctx.progress(50);
85
+ // throw to fail this attempt; it is retried until attempts run out
86
+ return { sent: true }; // stored as the job result
87
+ },
88
+ });
89
+ ```
90
+
91
+ The job module must be imported by the process that runs workers — in a Warlock app, anything under `src/app` that is loaded at boot.
92
+
93
+ ## Dispatch
94
+
95
+ ```ts
96
+ const { id } = await sendInvoice.dispatch({ invoiceId: "42" });
97
+
98
+ await sendInvoice.dispatch({ invoiceId: "43" }, {
99
+ delay: "10m", // ms number or "500ms" | "30s" | "10m" | "2h" | "1d"
100
+ priority: 1, // 1 runs first; larger numbers later
101
+ jobId: "invoice:43", // a second dispatch with a live id is ignored
102
+ });
103
+ ```
104
+
105
+ Option precedence: dispatch options > `defineJob` > `queue.defaultJobOptions`.
106
+
107
+ ## Read a job
108
+
109
+ ```ts
110
+ const job = await sendInvoice.find(id);
111
+ // { id, name, queue, state, payload, progress, attemptsMade, result, failedReason, createdAt, finishedAt }
112
+ ```
113
+
114
+ ## Rules
115
+
116
+ - A job name with no handler in the worker process fails at once, without retries.
117
+ - Payloads are stored as JSON: pass ids, not model instances.
118
+ - Defining the same name again replaces the handler (this is what keeps dev reloads working), so keep names unique.
119
+
120
+
121
+ ## manage-failed-jobs `@warlock.js/queue/manage-failed-jobs/SKILL.md`
122
+
123
+ ---
124
+ name: manage-failed-jobs
125
+ description: '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 optional bull-board UI via `queueDashboard(server, { basePath, queues })` — needs `@bull-board/api` + `@bull-board/fastify`, loaded only on call, missing ones throw `QueueDashboardDependencyError`. Triggers: `failedJobs`, `retryFailedJob`, `queueDashboard`, `bull-board`; "list failed jobs", "retry a failed job", "queue dashboard", "job admin UI". Skip: defining retries — `@warlock.js/queue/define-jobs/SKILL.md`.'
126
+ ---
127
+
128
+ # Failed jobs
129
+
130
+ A job is failed once it has used every attempt, or failed in a way that cannot be retried (for example, no handler for its name). Failed jobs are kept unless you set `removeOnFail`.
131
+
132
+ ```ts
133
+ import { failedJobs, retryFailedJob } from "@warlock.js/queue";
134
+
135
+ const failed = await failedJobs({ queue: "default", start: 0, end: 49 }); // newest first
136
+
137
+ for (const job of failed) {
138
+ console.log(job.name, job.failedReason, job.attemptsMade);
139
+ }
140
+
141
+ await failed[0]?.retry(); // back to waiting
142
+ await retryFailedJob("invoice:43"); // by id; throws FailedJobNotFoundError if not failed
143
+ ```
144
+
145
+ ## Dashboard (optional)
146
+
147
+ ```sh
148
+ npm install @bull-board/api @bull-board/fastify
149
+ ```
150
+
151
+ ```ts
152
+ import { getHttpServer } from "@warlock.js/core";
153
+ import { queueDashboard } from "@warlock.js/queue";
154
+
155
+ await queueDashboard(getHttpServer(), { basePath: "/admin/queues" });
156
+ ```
157
+
158
+ - Call it before the HTTP server starts listening.
159
+ - Shows every queue that has a job, plus the default queue, unless you pass `queues`.
160
+ - It has no authentication of its own, and it can retry and delete jobs. Protect the path.
161
+
162
+
163
+ ## overview `@warlock.js/queue/overview/SKILL.md`
164
+
165
+ ---
166
+ name: overview
167
+ description: 'Front door for `@warlock.js/queue` — durable background jobs for Warlock apps on BullMQ + Redis (Redis is required): `defineJob` + `.dispatch()`, retries/backoff, delay, priority, progress, failed-job listing/retry, in-process workers started by `queueConnector()` with graceful shutdown, a BullMQ backend for notifications `.queue()`, and an optional bull-board dashboard. TRIGGER when: importing from `@warlock.js/queue`; "background job", "job queue", "run this later", "retry failed jobs", "BullMQ in Warlock", "worker process". Skip: in-memory batching inside one process — that is core''s `Queue` class (`@warlock.js/core`); cron-style schedules — `@warlock.js/scheduler/overview/SKILL.md`; a known task — load `configure-queue`, `define-jobs`, `manage-failed-jobs` or `queue-notifications`.'
168
+ ---
169
+
170
+ # `@warlock.js/queue` — overview
171
+
172
+ Durable background jobs. Jobs are stored in **Redis** (required), retried on failure, and processed by workers — inside the app process by default.
173
+
174
+ ## Mental model
175
+
176
+ - **Job definition** — `defineJob({ name, handle })` registers a handler by name and returns a typed job.
177
+ - **Dispatch** — `job.dispatch(payload, options)` stores the job in Redis. Any process with the same definition and a worker can run it.
178
+ - **Worker** — one per queue name, started by `queueConnector()` (or `startWorkers()`); routes each job to the handler with its name.
179
+ - **Shutdown** — workers stop taking jobs, active jobs finish (up to `workers.shutdownTimeout`), then connections close.
180
+
181
+ ## Not core's `Queue`
182
+
183
+ `@warlock.js/core` exports `Queue` — an in-memory batcher that flushes items by size or interval inside one process. No storage, no retries, lost on exit. Use `@warlock.js/queue` when work must survive a restart or be retried.
184
+
185
+ ## Skills index
186
+
187
+ - [`configure-queue`](@warlock.js/queue/configure-queue/SKILL.md) — `src/config/queue.ts`, `queueConnector()`, workers on/off, shutdown.
188
+ - [`define-jobs`](@warlock.js/queue/define-jobs/SKILL.md) — `defineJob`, `dispatch` options, retries, progress, `find`.
189
+ - [`manage-failed-jobs`](@warlock.js/queue/manage-failed-jobs/SKILL.md) — `failedJobs`, `retryFailedJob`, the bull-board dashboard.
190
+ - [`queue-notifications`](@warlock.js/queue/queue-notifications/SKILL.md) — `queueNotificationDispatcher()` for notifications `.queue()`.
191
+
192
+
193
+ ## queue-notifications `@warlock.js/queue/queue-notifications/SKILL.md`
194
+
195
+ ---
196
+ name: queue-notifications
197
+ description: 'Send `@warlock.js/notifications` `.queue()` deliveries through BullMQ with `queueNotificationDispatcher({ queue?, attempts?, backoff? })` from `@warlock.js/queue/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. Notifications keeps no queue dependency. Triggers: `queueNotificationDispatcher`, `@warlock.js/queue/notifications`, `NotificationConfig.queue`; "queue notifications with BullMQ", "retry notification delivery", "delayed notification". Skip: the herald backend — `@warlock.js/notifications/queue-notifications/SKILL.md`.'
198
+ ---
199
+
200
+ # Queue notifications with BullMQ
201
+
202
+ ```ts title="src/config/notifications.ts"
203
+ import { type NotificationConfig, mailChannel } from "@warlock.js/notifications";
204
+ import { queueNotificationDispatcher } from "@warlock.js/queue/notifications";
205
+
206
+ const config: NotificationConfig = {
207
+ channels: { mail: mailChannel() },
208
+ queue: queueNotificationDispatcher({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
209
+ };
210
+
211
+ export default config;
212
+ ```
213
+
214
+ Also configure the queue itself (`configure-queue`). No separate notifications worker is needed: the delivery is an ordinary job, run by the queue workers.
215
+
216
+ ```ts
217
+ await orderShipped.queue(user, { order }); // enqueued, delivered by a worker
218
+ await orderShipped.queue(user, { order }, { delay: "10m" }); // delivered in 10 minutes
219
+ ```
220
+
221
+ ## Behaviour
222
+
223
+ - The job carries the rendered payload and resolved route; the worker looks the channel up by name in its own notifications config and calls `channel.send`.
224
+ - `delay`: a number is seconds (notifications' convention); a string is a duration.
225
+ - `channel.send` throws → retried per `attempts` / `backoff`, then listed by `failedJobs()`.
226
+ - Channel not configured in the worker → fails at once, no retries.
227
+ - Deliveries run under the job name `warlock.notifications.deliver`.
228
+
229
+
package/llms.txt ADDED
@@ -0,0 +1,13 @@
1
+ # Warlock Queue
2
+
3
+ > Package: `@warlock.js/queue`
4
+
5
+ > Durable background jobs for Warlock.js on BullMQ + Redis: defineJob, dispatch with delay/priority, retries with backoff, progress, failed-job access, in-process workers with graceful shutdown.
6
+
7
+ ## Skills
8
+
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 — `@warlock.js/queue/define-jobs/SKILL.md`.
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 — `@warlock.js/queue/configure-queue/SKILL.md`; failed jobs — `@warlock.js/queue/manage-failed-jobs/SKILL.md`.
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 optional bull-board UI via `queueDashboard(server, { basePath, queues })` — needs `@bull-board/api` + `@bull-board/fastify`, loaded only on call, missing ones throw `QueueDashboardDependencyError`. Triggers: `failedJobs`, `retryFailedJob`, `queueDashboard`, `bull-board`; "list failed jobs", "retry a failed job", "queue dashboard", "job admin UI". Skip: defining retries — `@warlock.js/queue/define-jobs/SKILL.md`.
12
+ - [overview](@warlock.js/queue/overview/SKILL.md): Front door for `@warlock.js/queue` — durable background jobs for Warlock apps on BullMQ + Redis (Redis is required): `defineJob` + `.dispatch()`, retries/backoff, delay, priority, progress, failed-job listing/retry, in-process workers started by `queueConnector()` with graceful shutdown, a BullMQ backend for notifications `.queue()`, and an optional bull-board dashboard. TRIGGER when: importing from `@warlock.js/queue`; "background job", "job queue", "run this later", "retry failed jobs", "BullMQ in Warlock", "worker process". Skip: in-memory batching inside one process — that is core's `Queue` class (`@warlock.js/core`); cron-style schedules — `@warlock.js/scheduler/overview/SKILL.md`; a known task — load `configure-queue`, `define-jobs`, `manage-failed-jobs` or `queue-notifications`.
13
+ - [queue-notifications](@warlock.js/queue/queue-notifications/SKILL.md): Send `@warlock.js/notifications` `.queue()` deliveries through BullMQ with `queueNotificationDispatcher({ queue?, attempts?, backoff? })` from `@warlock.js/queue/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. Notifications keeps no queue dependency. Triggers: `queueNotificationDispatcher`, `@warlock.js/queue/notifications`, `NotificationConfig.queue`; "queue notifications with BullMQ", "retry notification delivery", "delayed notification". Skip: the herald backend — `@warlock.js/notifications/queue-notifications/SKILL.md`.
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@warlock.js/queue",
3
+ "description": "Durable background jobs for Warlock.js on BullMQ + Redis: defineJob, dispatch with delay/priority, retries with backoff, progress, failed-job access, in-process workers with graceful shutdown.",
4
+ "warlock": {
5
+ "environment": "server"
6
+ },
7
+ "dependencies": {
8
+ "bullmq": "^6.3.6",
9
+ "ioredis": "^5.11.1"
10
+ },
11
+ "peerDependencies": {
12
+ "@bull-board/api": "^9.0.0",
13
+ "@bull-board/fastify": "^9.0.0",
14
+ "@warlock.js/core": "5.13.0",
15
+ "@warlock.js/logger": "5.13.0",
16
+ "@warlock.js/notifications": "5.13.0"
17
+ },
18
+ "peerDependenciesMeta": {
19
+ "@bull-board/api": {
20
+ "optional": true
21
+ },
22
+ "@bull-board/fastify": {
23
+ "optional": true
24
+ },
25
+ "@warlock.js/notifications": {
26
+ "optional": true
27
+ }
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/warlockjs/queue"
32
+ },
33
+ "keywords": [
34
+ "warlock.js",
35
+ "queue",
36
+ "jobs",
37
+ "background",
38
+ "bullmq",
39
+ "redis",
40
+ "worker"
41
+ ],
42
+ "author": "hassanzohdy",
43
+ "license": "MIT",
44
+ "version": "5.13.0",
45
+ "main": "./cjs/index.cjs",
46
+ "module": "./esm/index.mjs",
47
+ "types": "./esm/index.d.mts",
48
+ "exports": {
49
+ ".": {
50
+ "import": {
51
+ "types": "./esm/index.d.mts",
52
+ "default": "./esm/index.mjs"
53
+ },
54
+ "require": {
55
+ "types": "./esm/index.d.mts",
56
+ "default": "./cjs/index.cjs"
57
+ }
58
+ },
59
+ "./notifications": {
60
+ "import": {
61
+ "types": "./esm/notifications/index.d.mts",
62
+ "default": "./esm/notifications/index.mjs"
63
+ },
64
+ "require": {
65
+ "types": "./esm/notifications/index.d.mts",
66
+ "default": "./cjs/notifications/index.cjs"
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: configure-queue
3
+ description: '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 — `@warlock.js/queue/define-jobs/SKILL.md`.'
4
+ ---
5
+
6
+ # Configure the queue
7
+
8
+ Redis is required. The config is declarative; the connector applies it.
9
+
10
+ ```ts title="src/config/queue.ts"
11
+ import type { QueueConfig } from "@warlock.js/queue";
12
+
13
+ const queueConfig: QueueConfig = {
14
+ connection: { host: "127.0.0.1", port: 6379 }, // any BullMQ connection option, or { url }
15
+ prefix: "my-app", // Redis key prefix, default "warlock"
16
+ defaultJobOptions: { attempts: 3, backoff: { type: "exponential", delay: 1000 } },
17
+ workers: {
18
+ enabled: true, // default true
19
+ concurrency: 5, // jobs in parallel per queue, default 1
20
+ shutdownTimeout: 30_000, // ms to wait for active jobs, default 30000
21
+ },
22
+ };
23
+
24
+ export default queueConfig;
25
+ ```
26
+
27
+ ```ts title="warlock.config.ts"
28
+ import { defineConfig } from "@warlock.js/core";
29
+ import { queueConnector } from "@warlock.js/queue";
30
+
31
+ export default defineConfig({
32
+ connectors: [queueConnector()],
33
+ });
34
+ ```
35
+
36
+ ## What the connector does
37
+
38
+ - Starts in the **late** phase — after app code is imported — so every `defineJob` has registered. One worker per queue that has a job.
39
+ - No `queue` config → logs a warning and does nothing.
40
+ - On SIGINT/SIGTERM: workers stop taking jobs, active jobs get up to `shutdownTimeout` ms, then Redis connections close. A job still running after that is retried by another worker once its lock expires.
41
+
42
+ ## Dispatch-only process
43
+
44
+ Set `workers.enabled: false`. `dispatch()` still works; nothing is processed in that process. Run the workers elsewhere with the same job definitions:
45
+
46
+ ```ts
47
+ import { setQueueConfig, startWorkers, closeQueue } from "@warlock.js/queue";
48
+ import "./jobs"; // modules that call defineJob
49
+
50
+ setQueueConfig({ connection: { host: "127.0.0.1", port: 6379 } });
51
+ await startWorkers();
52
+ process.on("SIGTERM", () => closeQueue());
53
+ ```
54
+
55
+ Every process must use the same `prefix`, or they will not see each other's jobs.