@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
@@ -1,468 +0,0 @@
1
- let _warlock_js_logger = require("@warlock.js/logger");
2
- let bullmq = require("bullmq");
3
-
4
- //#region ../queue/src/errors.ts
5
- /**
6
- * Thrown when the queue is used before `setQueueConfig` (or the queue
7
- * connector) supplied a configuration.
8
- */
9
- var QueueNotConfiguredError = class extends Error {
10
- constructor() {
11
- super("@warlock.js/queue is not configured. Add src/config/queue.ts exporting a QueueConfig and register queueConnector() in warlock.config.ts > connectors, or call setQueueConfig() yourself.");
12
- this.name = "QueueNotConfiguredError";
13
- }
14
- };
15
- /**
16
- * Thrown for a malformed duration such as `"10 minutes"`.
17
- */
18
- var InvalidDurationError = class extends Error {
19
- constructor(value) {
20
- super(`Invalid duration ${JSON.stringify(value)}: expected milliseconds as a non-negative number or a string like "500ms", "30s", "10m", "2h", "1d".`);
21
- this.name = "InvalidDurationError";
22
- }
23
- };
24
- /**
25
- * Thrown by `defineJob` for an invalid definition.
26
- */
27
- var InvalidJobDefinitionError = class extends Error {
28
- constructor(message) {
29
- super(message);
30
- this.name = "InvalidJobDefinitionError";
31
- }
32
- };
33
- /**
34
- * Thrown by `retryFailedJob` when no failed job has the given id.
35
- */
36
- var FailedJobNotFoundError = class extends Error {
37
- constructor(id, queue) {
38
- super(`No failed job with id "${id}" on queue "${queue}".`);
39
- this.name = "FailedJobNotFoundError";
40
- }
41
- };
42
- /**
43
- * Thrown by `queueDashboard` when an optional bull-board package is not
44
- * installed.
45
- */
46
- var QueueDashboardDependencyError = class extends Error {
47
- constructor(missing) {
48
- super(`The queue dashboard needs the optional package "${missing}", which is not installed.\nInstall both bull-board packages:
49
-
50
- npm install @bull-board/api @bull-board/fastify
51
- `);
52
- this.name = "QueueDashboardDependencyError";
53
- }
54
- };
55
-
56
- //#endregion
57
- //#region ../queue/src/config.ts
58
- let activeConfig;
59
- /**
60
- * Set the active queue configuration. In a Warlock app the queue connector
61
- * calls this at boot with `src/config/queue.ts`; scripts and tests may call
62
- * it directly. Replaces (does not merge) any previous configuration.
63
- */
64
- function setQueueConfig(config) {
65
- activeConfig = config;
66
- }
67
- /**
68
- * The active queue configuration. Throws {@link QueueNotConfiguredError}
69
- * when none was set.
70
- */
71
- function getQueueConfig() {
72
- if (!activeConfig) throw new QueueNotConfiguredError();
73
- return activeConfig;
74
- }
75
- /** Forget the active configuration. */
76
- function resetQueueConfig() {
77
- activeConfig = void 0;
78
- }
79
- /** The queue a job runs on when it names none. */
80
- function defaultQueueName() {
81
- return activeConfig?.defaultQueue ?? "default";
82
- }
83
-
84
- //#endregion
85
- //#region ../queue/src/job-registry.ts
86
- const jobs = /* @__PURE__ */ new Map();
87
- const listeners = /* @__PURE__ */ new Set();
88
- /**
89
- * Register a definition under its name.
90
- *
91
- * Re-registering a name REPLACES the previous definition: in development a
92
- * job module is re-evaluated on every reload, and refusing the second
93
- * evaluation would break the reload. Job names must therefore be unique
94
- * across the app — two different modules using one name leave only the
95
- * later handler active.
96
- */
97
- function registerJob(job) {
98
- jobs.set(job.name, job);
99
- for (const listener of listeners) listener(job);
100
- }
101
- /** The definition registered under `name`, if any. */
102
- function findRegisteredJob(name) {
103
- return jobs.get(name);
104
- }
105
- /** Every registered definition. */
106
- function registeredJobs() {
107
- return [...jobs.values()];
108
- }
109
- /** The queue a definition runs on, resolved against the active config. */
110
- function queueOf(job) {
111
- return job.queue ?? defaultQueueName();
112
- }
113
- /** Be told whenever a job is registered. Returns an unsubscribe function. */
114
- function onJobRegistered(listener) {
115
- listeners.add(listener);
116
- return () => listeners.delete(listener);
117
- }
118
-
119
- //#endregion
120
- //#region ../queue/src/process-job.ts
121
- /**
122
- * The single BullMQ processor every worker runs: route the job to the
123
- * handler registered under its name.
124
- *
125
- * A name with no handler in this process fails with `UnrecoverableError` —
126
- * retrying cannot make a missing definition appear, so it must not burn
127
- * through its attempts.
128
- */
129
- async function processJob(job) {
130
- const definition = findRegisteredJob(job.name);
131
- if (!definition) throw new bullmq.UnrecoverableError(`No job named "${job.name}" is defined in this process. Make sure the module that calls defineJob() is imported by the worker process.`);
132
- const context = {
133
- id: String(job.id),
134
- name: job.name,
135
- queue: job.queueName,
136
- attempt: job.attemptsMade + 1,
137
- maxAttempts: job.opts.attempts ?? 1,
138
- progress: (value) => job.updateProgress(value),
139
- log: async (line) => {
140
- await job.log(line);
141
- }
142
- };
143
- return definition.handle(job.data, context);
144
- }
145
-
146
- //#endregion
147
- //#region ../queue/src/queue-manager.ts
148
- const DEFAULT_SHUTDOWN_TIMEOUT = 3e4;
149
- const queues = /* @__PURE__ */ new Map();
150
- const workers = /* @__PURE__ */ new Map();
151
- let stopListening;
152
- /**
153
- * The BullMQ queue for `name`, created on first use with the configured
154
- * connection and prefix.
155
- */
156
- function getQueue(name) {
157
- let queue = queues.get(name);
158
- if (!queue) {
159
- const config = getQueueConfig();
160
- queue = new bullmq.Queue(name, {
161
- connection: config.connection,
162
- prefix: config.prefix ?? "warlock"
163
- });
164
- queue.on("error", (error) => {
165
- _warlock_js_logger.log.error("queue", "connection", error);
166
- });
167
- queues.set(name, queue);
168
- }
169
- return queue;
170
- }
171
- /**
172
- * Start one worker per queue that has a registered job, and keep starting
173
- * workers for queues whose first job is defined later.
174
- *
175
- * A no-op returning `[]` when `workers.enabled` is `false`. Calling it again
176
- * while workers run starts only the missing ones.
177
- *
178
- * @returns the queue names that now have a worker in this process.
179
- */
180
- async function startWorkers() {
181
- if (getQueueConfig().workers?.enabled === false) return [];
182
- for (const job of registeredJobs()) ensureWorker(queueOf(job));
183
- stopListening ??= onJobRegistered((job) => {
184
- ensureWorker(queueOf(job));
185
- });
186
- await Promise.all([...workers.values()].map((worker) => worker.waitUntilReady()));
187
- return [...workers.keys()];
188
- }
189
- /** The queue names with a running worker in this process. */
190
- function runningWorkers() {
191
- return [...workers.keys()];
192
- }
193
- function ensureWorker(queueName) {
194
- if (workers.has(queueName)) return;
195
- const config = getQueueConfig();
196
- const worker = new bullmq.Worker(queueName, processJob, {
197
- connection: config.connection,
198
- prefix: config.prefix ?? "warlock",
199
- concurrency: config.workers?.concurrency ?? 1
200
- });
201
- worker.on("error", (error) => {
202
- _warlock_js_logger.log.error("queue", "worker", error);
203
- });
204
- worker.on("failed", (job, error) => {
205
- _warlock_js_logger.log.error("queue", "job.failed", `${job?.name ?? "unknown"} (${job?.id ?? "?"}): ${error.message}`);
206
- });
207
- workers.set(queueName, worker);
208
- }
209
- /**
210
- * Graceful shutdown: stop workers taking new jobs and wait for active ones
211
- * (bounded by `timeout`, then force-close), then close every queue
212
- * connection. Safe to call when nothing was started, and more than once.
213
- */
214
- async function closeQueue(options = {}) {
215
- stopListening?.();
216
- stopListening = void 0;
217
- const timeout = options.timeout ?? configuredShutdownTimeout();
218
- const closingWorkers = [...workers.values()];
219
- workers.clear();
220
- await Promise.all(closingWorkers.map((worker) => closeWorker(worker, timeout)));
221
- const closingQueues = [...queues.values()];
222
- queues.clear();
223
- await Promise.all(closingQueues.map((queue) => queue.close()));
224
- }
225
- function configuredShutdownTimeout() {
226
- try {
227
- return getQueueConfig().workers?.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
228
- } catch {
229
- return DEFAULT_SHUTDOWN_TIMEOUT;
230
- }
231
- }
232
- async function closeWorker(worker, timeout) {
233
- let timer;
234
- const timedOut = new Promise((resolve) => {
235
- timer = setTimeout(() => resolve("timeout"), timeout);
236
- });
237
- const closing = worker.close().then(() => "closed");
238
- closing.catch(() => void 0);
239
- const outcome = await Promise.race([closing, timedOut]);
240
- clearTimeout(timer);
241
- if (outcome === "timeout") {
242
- _warlock_js_logger.log.warn("queue", "shutdown", `Worker for "${worker.name}" still had active jobs after ${timeout}ms; disconnecting. Those jobs are retried once their lock expires.`);
243
- await worker.disconnect();
244
- }
245
- }
246
-
247
- //#endregion
248
- //#region ../queue/src/duration.ts
249
- const UNIT_MILLISECONDS = {
250
- ms: 1,
251
- s: 1e3,
252
- m: 6e4,
253
- h: 36e5,
254
- d: 864e5
255
- };
256
- const DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/;
257
- /**
258
- * Convert a {@link Duration} to milliseconds. Numbers are already
259
- * milliseconds. Anything else is rejected loudly rather than guessed at.
260
- */
261
- function toMilliseconds(value) {
262
- if (typeof value === "number") {
263
- if (!Number.isFinite(value) || value < 0) throw new InvalidDurationError(value);
264
- return Math.round(value);
265
- }
266
- const match = typeof value === "string" ? DURATION_PATTERN.exec(value.trim()) : null;
267
- if (!match) throw new InvalidDurationError(value);
268
- return Math.round(Number(match[1]) * UNIT_MILLISECONDS[match[2]]);
269
- }
270
-
271
- //#endregion
272
- //#region ../queue/src/define-job.ts
273
- /**
274
- * Define a background job.
275
- *
276
- * The definition is registered by name so any worker in the process can run
277
- * it; the returned object dispatches it with a typed payload.
278
- *
279
- * @example
280
- * export const sendInvoice = defineJob({
281
- * name: "invoices.send",
282
- * attempts: 5,
283
- * backoff: { type: "exponential", delay: 2000 },
284
- * async handle(payload: { invoiceId: string }, ctx) {
285
- * await ctx.progress(50);
286
- * },
287
- * });
288
- *
289
- * await sendInvoice.dispatch({ invoiceId: "42" }, { delay: "10m", priority: 1 });
290
- */
291
- function defineJob(definition) {
292
- assertValidDefinition(definition);
293
- registerJob(definition);
294
- return {
295
- name: definition.name,
296
- get queue() {
297
- return queueOf(definition);
298
- },
299
- async dispatch(payload, options = {}) {
300
- const queueName = queueOf(definition);
301
- const job = await getQueue(queueName).add(definition.name, payload, toBullJobOptions(definition, options));
302
- return {
303
- id: String(job.id),
304
- name: definition.name,
305
- queue: queueName
306
- };
307
- },
308
- async find(id) {
309
- const job = await getQueue(queueOf(definition)).getJob(id);
310
- if (!job || job.name !== definition.name) return;
311
- return toSnapshot(job);
312
- }
313
- };
314
- }
315
- function assertValidDefinition(definition) {
316
- if (typeof definition.name !== "string" || definition.name.trim() === "") throw new InvalidJobDefinitionError("defineJob() requires a non-empty `name`.");
317
- if (typeof definition.handle !== "function") throw new InvalidJobDefinitionError(`defineJob("${definition.name}") requires a \`handle(payload, ctx)\` function.`);
318
- if (definition.attempts !== void 0 && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) throw new InvalidJobDefinitionError(`defineJob("${definition.name}"): \`attempts\` must be an integer >= 1, got ${definition.attempts}.`);
319
- }
320
- /**
321
- * Merge app defaults < job definition < dispatch options into BullMQ's shape.
322
- */
323
- function toBullJobOptions(definition, options) {
324
- const defaults = getQueueConfig().defaultJobOptions ?? {};
325
- const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;
326
- const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;
327
- const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;
328
- const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;
329
- const bullOptions = {};
330
- if (attempts !== void 0) bullOptions.attempts = attempts;
331
- if (backoff !== void 0) bullOptions.backoff = toBullBackoff(backoff);
332
- if (removeOnComplete !== void 0) bullOptions.removeOnComplete = removeOnComplete;
333
- if (removeOnFail !== void 0) bullOptions.removeOnFail = removeOnFail;
334
- if (options.delay !== void 0) bullOptions.delay = toMilliseconds(options.delay);
335
- if (options.priority !== void 0) bullOptions.priority = options.priority;
336
- if (options.jobId !== void 0) bullOptions.jobId = options.jobId;
337
- return bullOptions;
338
- }
339
- function toBullBackoff(backoff) {
340
- return typeof backoff === "number" ? {
341
- type: "fixed",
342
- delay: backoff
343
- } : backoff;
344
- }
345
- /**
346
- * A plain view of a BullMQ job.
347
- */
348
- async function toSnapshot(job) {
349
- const state = await job.getState();
350
- return {
351
- id: String(job.id),
352
- name: job.name,
353
- queue: job.queueName,
354
- state,
355
- payload: job.data,
356
- progress: job.progress,
357
- attemptsMade: job.attemptsMade,
358
- result: job.returnvalue,
359
- failedReason: job.failedReason || void 0,
360
- createdAt: new Date(job.timestamp),
361
- finishedAt: job.finishedOn ? new Date(job.finishedOn) : void 0
362
- };
363
- }
364
-
365
- //#endregion
366
- Object.defineProperty(exports, 'FailedJobNotFoundError', {
367
- enumerable: true,
368
- get: function () {
369
- return FailedJobNotFoundError;
370
- }
371
- });
372
- Object.defineProperty(exports, 'InvalidDurationError', {
373
- enumerable: true,
374
- get: function () {
375
- return InvalidDurationError;
376
- }
377
- });
378
- Object.defineProperty(exports, 'InvalidJobDefinitionError', {
379
- enumerable: true,
380
- get: function () {
381
- return InvalidJobDefinitionError;
382
- }
383
- });
384
- Object.defineProperty(exports, 'QueueDashboardDependencyError', {
385
- enumerable: true,
386
- get: function () {
387
- return QueueDashboardDependencyError;
388
- }
389
- });
390
- Object.defineProperty(exports, 'QueueNotConfiguredError', {
391
- enumerable: true,
392
- get: function () {
393
- return QueueNotConfiguredError;
394
- }
395
- });
396
- Object.defineProperty(exports, 'closeQueue', {
397
- enumerable: true,
398
- get: function () {
399
- return closeQueue;
400
- }
401
- });
402
- Object.defineProperty(exports, 'defaultQueueName', {
403
- enumerable: true,
404
- get: function () {
405
- return defaultQueueName;
406
- }
407
- });
408
- Object.defineProperty(exports, 'defineJob', {
409
- enumerable: true,
410
- get: function () {
411
- return defineJob;
412
- }
413
- });
414
- Object.defineProperty(exports, 'getQueue', {
415
- enumerable: true,
416
- get: function () {
417
- return getQueue;
418
- }
419
- });
420
- Object.defineProperty(exports, 'getQueueConfig', {
421
- enumerable: true,
422
- get: function () {
423
- return getQueueConfig;
424
- }
425
- });
426
- Object.defineProperty(exports, 'queueOf', {
427
- enumerable: true,
428
- get: function () {
429
- return queueOf;
430
- }
431
- });
432
- Object.defineProperty(exports, 'registeredJobs', {
433
- enumerable: true,
434
- get: function () {
435
- return registeredJobs;
436
- }
437
- });
438
- Object.defineProperty(exports, 'resetQueueConfig', {
439
- enumerable: true,
440
- get: function () {
441
- return resetQueueConfig;
442
- }
443
- });
444
- Object.defineProperty(exports, 'runningWorkers', {
445
- enumerable: true,
446
- get: function () {
447
- return runningWorkers;
448
- }
449
- });
450
- Object.defineProperty(exports, 'setQueueConfig', {
451
- enumerable: true,
452
- get: function () {
453
- return setQueueConfig;
454
- }
455
- });
456
- Object.defineProperty(exports, 'startWorkers', {
457
- enumerable: true,
458
- get: function () {
459
- return startWorkers;
460
- }
461
- });
462
- Object.defineProperty(exports, 'toMilliseconds', {
463
- enumerable: true,
464
- get: function () {
465
- return toMilliseconds;
466
- }
467
- });
468
- //# sourceMappingURL=define-job-DideGKQK.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"define-job-DideGKQK.cjs","names":["UnrecoverableError","Queue","Worker"],"sources":["../../../../../../queue/src/errors.ts","../../../../../../queue/src/config.ts","../../../../../../queue/src/job-registry.ts","../../../../../../queue/src/process-job.ts","../../../../../../queue/src/queue-manager.ts","../../../../../../queue/src/duration.ts","../../../../../../queue/src/define-job.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 { 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 { 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\";\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":"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"}
@@ -1,68 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_define_job = require('../define-job-DideGKQK.cjs');
3
- let bullmq = require("bullmq");
4
- let _warlock_js_notifications = require("@warlock.js/notifications");
5
-
6
- //#region ../queue/src/notifications/queue-notification-dispatcher.ts
7
- /**
8
- * BullMQ-backed `QueueDispatcher` for `@warlock.js/notifications`.
9
- *
10
- * Notifications renders the payload and resolves the route BEFORE handing a
11
- * job to its dispatcher, so the job (`{ channel, route, payload, options }`)
12
- * is plain JSON. This adapter enqueues it as a queue job; the job's handler,
13
- * running in any worker process, looks the channel up by name in that
14
- * process's notifications config and calls `channel.send`.
15
- *
16
- * Notifications itself has no dependency on this package.
17
- */
18
- /** The job name notification deliveries run under. */
19
- const NOTIFICATION_JOB_NAME = "warlock.notifications.deliver";
20
- /**
21
- * Create the dispatcher for `NotificationConfig.queue`.
22
- *
23
- * - `SendOptions.delay` is honoured: a number is SECONDS (notifications'
24
- * convention), a string is a duration such as `"10m"`.
25
- * - A failing `channel.send` throws, so the delivery is retried per
26
- * `attempts` / `backoff` and ends in `failedJobs()` when exhausted.
27
- * - A channel missing from the worker's notifications config fails at once,
28
- * without retries.
29
- *
30
- * @example src/config/notifications.ts
31
- * import { queueNotificationDispatcher } from "@warlock.js/queue/notifications";
32
- *
33
- * const config: NotificationConfig = {
34
- * channels: { mail: mailChannel() },
35
- * queue: queueNotificationDispatcher({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
36
- * };
37
- */
38
- function queueNotificationDispatcher(options = {}) {
39
- const deliver = defineNotificationJob(options);
40
- return { async dispatch(job) {
41
- await deliver.dispatch(job, { delay: job.options.delay === void 0 ? void 0 : notificationDelay(job.options.delay) });
42
- } };
43
- }
44
- function defineNotificationJob(options) {
45
- return require_define_job.defineJob({
46
- name: NOTIFICATION_JOB_NAME,
47
- queue: options.queue,
48
- attempts: options.attempts,
49
- backoff: options.backoff,
50
- async handle(job) {
51
- const channel = (0, _warlock_js_notifications.getNotificationConfig)().channels[job.channel];
52
- if (!channel) throw new bullmq.UnrecoverableError(`Notification channel "${job.channel}" is not configured in this worker's notifications config.`);
53
- await channel.send({
54
- payload: job.payload,
55
- route: job.route,
56
- options: job.options
57
- });
58
- }
59
- });
60
- }
61
- function notificationDelay(delay) {
62
- return typeof delay === "number" ? delay * 1e3 : require_define_job.toMilliseconds(delay);
63
- }
64
-
65
- //#endregion
66
- exports.NOTIFICATION_JOB_NAME = NOTIFICATION_JOB_NAME;
67
- exports.queueNotificationDispatcher = queueNotificationDispatcher;
68
- //# sourceMappingURL=index.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.cjs","names":["defineJob","UnrecoverableError","toMilliseconds"],"sources":["../../../../../../../queue/src/notifications/queue-notification-dispatcher.ts"],"sourcesContent":["/**\n * BullMQ-backed `QueueDispatcher` for `@warlock.js/notifications`.\n *\n * Notifications renders the payload and resolves the route BEFORE handing a\n * job to its dispatcher, so the job (`{ channel, route, payload, options }`)\n * is plain JSON. This adapter enqueues it as a queue job; the job's handler,\n * running in any worker process, looks the channel up by name in that\n * process's notifications config and calls `channel.send`.\n *\n * Notifications itself has no dependency on this package.\n */\nimport { getNotificationConfig, type QueueDispatcher } from \"@warlock.js/notifications\";\nimport { UnrecoverableError } from \"bullmq\";\nimport { defineJob } from \"../define-job\";\nimport { toMilliseconds } from \"../duration\";\nimport type { Duration, JobBackoff, QueueJob } from \"../types\";\n\n/** The job name notification deliveries run under. */\nexport const NOTIFICATION_JOB_NAME = \"warlock.notifications.deliver\";\n\nexport type NotificationJobPayload = Parameters<QueueDispatcher[\"dispatch\"]>[0];\n\nexport type QueueNotificationDispatcherOptions = {\n /** Queue to deliver on. Default: the default queue. */\n queue?: string;\n /** Attempts per delivery. Default: `queue.defaultJobOptions.attempts`, else `1`. */\n attempts?: number;\n /** Backoff between attempts. */\n backoff?: JobBackoff;\n};\n\n/**\n * Create the dispatcher for `NotificationConfig.queue`.\n *\n * - `SendOptions.delay` is honoured: a number is SECONDS (notifications'\n * convention), a string is a duration such as `\"10m\"`.\n * - A failing `channel.send` throws, so the delivery is retried per\n * `attempts` / `backoff` and ends in `failedJobs()` when exhausted.\n * - A channel missing from the worker's notifications config fails at once,\n * without retries.\n *\n * @example src/config/notifications.ts\n * import { queueNotificationDispatcher } from \"@warlock.js/queue/notifications\";\n *\n * const config: NotificationConfig = {\n * channels: { mail: mailChannel() },\n * queue: queueNotificationDispatcher({ attempts: 3, backoff: { type: \"exponential\", delay: 5000 } }),\n * };\n */\nexport function queueNotificationDispatcher(\n options: QueueNotificationDispatcherOptions = {},\n): QueueDispatcher {\n const deliver = defineNotificationJob(options);\n\n return {\n async dispatch(job) {\n await deliver.dispatch(job, {\n delay: job.options.delay === undefined ? undefined : notificationDelay(job.options.delay),\n });\n },\n };\n}\n\nfunction defineNotificationJob(\n options: QueueNotificationDispatcherOptions,\n): QueueJob<NotificationJobPayload, void> {\n return defineJob<NotificationJobPayload, void>({\n name: NOTIFICATION_JOB_NAME,\n queue: options.queue,\n attempts: options.attempts,\n backoff: options.backoff,\n async handle(job) {\n const channels = getNotificationConfig().channels as Record<\n string,\n { send(context: never): Promise<void> } | undefined\n >;\n const channel = channels[job.channel];\n\n if (!channel) {\n throw new UnrecoverableError(\n `Notification channel \"${job.channel}\" is not configured in this worker's notifications config.`,\n );\n }\n\n await channel.send({ payload: job.payload, route: job.route, options: job.options } as never);\n },\n });\n}\n\nfunction notificationDelay(delay: number | string): Duration {\n return typeof delay === \"number\" ? delay * 1_000 : toMilliseconds(delay);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;AA+BrC,SAAgB,4BACd,UAA8C,CAAC,GAC9B;CACjB,MAAM,UAAU,sBAAsB,OAAO;CAE7C,OAAO,EACL,MAAM,SAAS,KAAK;EAClB,MAAM,QAAQ,SAAS,KAAK,EAC1B,OAAO,IAAI,QAAQ,UAAU,SAAY,SAAY,kBAAkB,IAAI,QAAQ,KAAK,EAC1F,CAAC;CACH,EACF;AACF;AAEA,SAAS,sBACP,SACwC;CACxC,OAAOA,6BAAwC;EAC7C,MAAM;EACN,OAAO,QAAQ;EACf,UAAU,QAAQ;EAClB,SAAS,QAAQ;EACjB,MAAM,OAAO,KAAK;GAKhB,MAAM,+DAJiC,CAAC,CAAC,SAIhB,IAAI;GAE7B,IAAI,CAAC,SACH,MAAM,IAAIC,0BACR,yBAAyB,IAAI,QAAQ,2DACvC;GAGF,MAAM,QAAQ,KAAK;IAAE,SAAS,IAAI;IAAS,OAAO,IAAI;IAAO,SAAS,IAAI;GAAQ,CAAU;EAC9F;CACF,CAAC;AACH;AAEA,SAAS,kBAAkB,OAAkC;CAC3D,OAAO,OAAO,UAAU,WAAW,QAAQ,MAAQC,kCAAe,KAAK;AACzE"}
@@ -1,2 +0,0 @@
1
- import { NOTIFICATION_JOB_NAME, NotificationJobPayload, QueueNotificationDispatcherOptions, queueNotificationDispatcher } from "./queue-notification-dispatcher.mjs";
2
- export { NOTIFICATION_JOB_NAME, NotificationJobPayload, QueueNotificationDispatcherOptions, queueNotificationDispatcher };
@@ -1,3 +0,0 @@
1
- import { NOTIFICATION_JOB_NAME, queueNotificationDispatcher } from "./queue-notification-dispatcher.mjs";
2
-
3
- export { NOTIFICATION_JOB_NAME, queueNotificationDispatcher };
@@ -1,34 +0,0 @@
1
- import { JobBackoff } from "../types.mjs";
2
- import { QueueDispatcher } from "@warlock.js/notifications";
3
-
4
- //#region ../queue/src/notifications/queue-notification-dispatcher.d.ts
5
- /** The job name notification deliveries run under. */
6
- declare const NOTIFICATION_JOB_NAME = "warlock.notifications.deliver";
7
- type NotificationJobPayload = Parameters<QueueDispatcher["dispatch"]>[0];
8
- type QueueNotificationDispatcherOptions = {
9
- /** Queue to deliver on. Default: the default queue. */queue?: string; /** Attempts per delivery. Default: `queue.defaultJobOptions.attempts`, else `1`. */
10
- attempts?: number; /** Backoff between attempts. */
11
- backoff?: JobBackoff;
12
- };
13
- /**
14
- * Create the dispatcher for `NotificationConfig.queue`.
15
- *
16
- * - `SendOptions.delay` is honoured: a number is SECONDS (notifications'
17
- * convention), a string is a duration such as `"10m"`.
18
- * - A failing `channel.send` throws, so the delivery is retried per
19
- * `attempts` / `backoff` and ends in `failedJobs()` when exhausted.
20
- * - A channel missing from the worker's notifications config fails at once,
21
- * without retries.
22
- *
23
- * @example src/config/notifications.ts
24
- * import { queueNotificationDispatcher } from "@warlock.js/queue/notifications";
25
- *
26
- * const config: NotificationConfig = {
27
- * channels: { mail: mailChannel() },
28
- * queue: queueNotificationDispatcher({ attempts: 3, backoff: { type: "exponential", delay: 5000 } }),
29
- * };
30
- */
31
- declare function queueNotificationDispatcher(options?: QueueNotificationDispatcherOptions): QueueDispatcher;
32
- //#endregion
33
- export { NOTIFICATION_JOB_NAME, NotificationJobPayload, QueueNotificationDispatcherOptions, queueNotificationDispatcher };
34
- //# sourceMappingURL=queue-notification-dispatcher.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"queue-notification-dispatcher.d.mts","names":[],"sources":["../../../../../../../queue/src/notifications/queue-notification-dispatcher.ts"],"mappings":";;;;AAoB+D;AAAA,cAFlD,qBAAA;AAAA,KAED,sBAAA,GAAyB,UAAU,CAAC,eAAA;AAAA,KAEpC,kCAAA;EAMU,uDAJpB,KAAA,WAEA;EAAA,QAAA,WAEU;EAAV,OAAA,GAAU,UAAU;AAAA;AAqBtB;;;;;;;;AAEkB;;;;;;;;;;AAFlB,iBAAgB,2BAAA,CACd,OAAA,GAAS,kCAAA,GACR,eAAe"}