@warlock.js/queue 5.13.0 → 5.14.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.
- package/CHANGELOG.md +17 -0
- package/README.md +46 -7
- package/cjs/{define-job-DideGKQK.cjs → define-job-33RNLEqL.cjs} +14 -7
- package/cjs/{define-job-DideGKQK.cjs.map → define-job-33RNLEqL.cjs.map} +1 -1
- package/cjs/index.cjs +130 -3
- package/cjs/index.cjs.map +1 -1
- package/cjs/notifications/index.cjs +25 -1
- package/cjs/notifications/index.cjs.map +1 -1
- package/esm/dashboard-boot.d.mts +20 -0
- package/esm/dashboard-boot.d.mts.map +1 -0
- package/esm/dashboard-boot.mjs +38 -0
- package/esm/dashboard-boot.mjs.map +1 -0
- package/esm/dashboard-guard-plugin.mjs +28 -0
- package/esm/dashboard-guard-plugin.mjs.map +1 -0
- package/esm/dashboard-middleware-adapter.mjs +39 -0
- package/esm/dashboard-middleware-adapter.mjs.map +1 -0
- package/esm/dashboard.d.mts +8 -0
- package/esm/dashboard.d.mts.map +1 -1
- package/esm/dashboard.mjs +3 -1
- package/esm/dashboard.mjs.map +1 -1
- package/esm/define-job.mjs +13 -6
- package/esm/define-job.mjs.map +1 -1
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +3 -1
- package/esm/notifications/queue-notification-dispatcher.d.mts +8 -0
- package/esm/notifications/queue-notification-dispatcher.d.mts.map +1 -1
- package/esm/notifications/queue-notification-dispatcher.mjs +24 -0
- package/esm/notifications/queue-notification-dispatcher.mjs.map +1 -1
- package/esm/queue-connector.d.mts.map +1 -1
- package/esm/queue-connector.mjs +17 -1
- package/esm/queue-connector.mjs.map +1 -1
- package/esm/queue-dashboard-unguarded.error.d.mts +13 -0
- package/esm/queue-dashboard-unguarded.error.d.mts.map +1 -0
- package/esm/queue-dashboard-unguarded.error.mjs +17 -0
- package/esm/queue-dashboard-unguarded.error.mjs.map +1 -0
- package/esm/types.d.mts +19 -2
- package/esm/types.d.mts.map +1 -1
- package/llms-full.txt +45 -9
- package/llms.txt +2 -2
- package/package.json +4 -4
- package/skills/configure-queue/SKILL.md +4 -0
- package/skills/manage-failed-jobs/SKILL.md +33 -4
- package/skills/overview/SKILL.md +1 -1
- package/skills/queue-notifications/SKILL.md +7 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { queueDashboard } from "./dashboard.mjs";
|
|
2
|
+
import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
|
|
3
|
+
import { log } from "@warlock.js/logger";
|
|
4
|
+
|
|
5
|
+
//#region ../queue/src/dashboard-boot.ts
|
|
6
|
+
/** Default path the dashboard mounts on when `dashboard.path` is not set. */
|
|
7
|
+
const DEFAULT_DASHBOARD_PATH = "/admin/queues";
|
|
8
|
+
/**
|
|
9
|
+
* Mount the bull-board dashboard for `queue.dashboard` in `config`, applying
|
|
10
|
+
* the safety rule: `enabled` in production with no middleware throws
|
|
11
|
+
* {@link QueueDashboardUnguardedError} instead of booting exposed; outside
|
|
12
|
+
* production with no middleware it logs one warning and mounts anyway.
|
|
13
|
+
*
|
|
14
|
+
* Called by `queueConnector()` at boot, once the HTTP server exists but
|
|
15
|
+
* before it starts listening — see `queue-connector.ts`. Exported so it can
|
|
16
|
+
* be unit-tested without going through the whole connector lifecycle.
|
|
17
|
+
*/
|
|
18
|
+
async function mountQueueDashboard(server, config) {
|
|
19
|
+
const dashboard = config.dashboard;
|
|
20
|
+
if (!dashboard?.enabled) return;
|
|
21
|
+
const middlewareList = dashboard.middleware ?? [];
|
|
22
|
+
if (middlewareList.length === 0) {
|
|
23
|
+
if (process.env.NODE_ENV === "production") throw new QueueDashboardUnguardedError();
|
|
24
|
+
log.warn("queue", "dashboard", "queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete jobs — add a guard middleware before this ships to production.");
|
|
25
|
+
}
|
|
26
|
+
if (!server) {
|
|
27
|
+
log.warn("queue", "dashboard", "queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await queueDashboard(server, {
|
|
31
|
+
basePath: dashboard.path ?? "/admin/queues",
|
|
32
|
+
middleware: middlewareList
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { DEFAULT_DASHBOARD_PATH, mountQueueDashboard };
|
|
38
|
+
//# sourceMappingURL=dashboard-boot.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-boot.mjs","names":[],"sources":["../../../../../../queue/src/dashboard-boot.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { type DashboardServer, queueDashboard } from \"./dashboard\";\nimport { QueueDashboardUnguardedError } from \"./queue-dashboard-unguarded.error\";\nimport type { QueueConfig } from \"./types\";\n\n/** Default path the dashboard mounts on when `dashboard.path` is not set. */\nexport const DEFAULT_DASHBOARD_PATH = \"/admin/queues\";\n\n/**\n * Mount the bull-board dashboard for `queue.dashboard` in `config`, applying\n * the safety rule: `enabled` in production with no middleware throws\n * {@link QueueDashboardUnguardedError} instead of booting exposed; outside\n * production with no middleware it logs one warning and mounts anyway.\n *\n * Called by `queueConnector()` at boot, once the HTTP server exists but\n * before it starts listening — see `queue-connector.ts`. Exported so it can\n * be unit-tested without going through the whole connector lifecycle.\n */\nexport async function mountQueueDashboard(\n server: DashboardServer | undefined,\n config: QueueConfig,\n): Promise<void> {\n const dashboard = config.dashboard;\n\n if (!dashboard?.enabled) {\n return;\n }\n\n const middlewareList = dashboard.middleware ?? [];\n\n if (middlewareList.length === 0) {\n if (process.env.NODE_ENV === \"production\") {\n throw new QueueDashboardUnguardedError();\n }\n\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true with no middleware. The dashboard can retry and delete \" +\n \"jobs — add a guard middleware before this ships to production.\",\n );\n }\n\n if (!server) {\n log.warn(\n \"queue\",\n \"dashboard\",\n \"queue.dashboard.enabled is true but no HTTP server was found; the dashboard was not mounted.\",\n );\n\n return;\n }\n\n await queueDashboard(server, {\n basePath: dashboard.path ?? DEFAULT_DASHBOARD_PATH,\n middleware: middlewareList,\n });\n}\n"],"mappings":";;;;;;AAMA,MAAa,yBAAyB;;;;;;;;;;;AAYtC,eAAsB,oBACpB,QACA,QACe;CACf,MAAM,YAAY,OAAO;CAEzB,IAAI,CAAC,WAAW,SACd;CAGF,MAAM,iBAAiB,UAAU,cAAc,CAAC;CAEhD,IAAI,eAAe,WAAW,GAAG;EAC/B,IAAI,QAAQ,IAAI,aAAa,cAC3B,MAAM,IAAI,6BAA6B;EAGzC,IAAI,KACF,SACA,aACA,uJAEF;CACF;CAEA,IAAI,CAAC,QAAQ;EACX,IAAI,KACF,SACA,aACA,8FACF;EAEA;CACF;CAEA,MAAM,eAAe,QAAQ;EAC3B,UAAU,UAAU;EACpB,YAAY;CACd,CAAC;AACH"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { runDashboardMiddleware } from "./dashboard-middleware-adapter.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../queue/src/dashboard-guard-plugin.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`
|
|
6
|
+
* on every request before bull-board's own routes see it.
|
|
7
|
+
*
|
|
8
|
+
* Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify
|
|
9
|
+
* plugin with no hook for Warlock middleware to run through, and Fastify
|
|
10
|
+
* only lets an `onRequest` hook be added to a plugin scope — never spliced
|
|
11
|
+
* into a plugin someone else wrote. So this builds ONE plugin that adds the
|
|
12
|
+
* hook to its own scope and then registers bull-board's plugin as a child of
|
|
13
|
+
* that scope; Fastify's encapsulation runs the hook for every route the
|
|
14
|
+
* child registers, which is every dashboard route.
|
|
15
|
+
*/
|
|
16
|
+
function buildDashboardGuardPlugin(middlewareList, bullBoardPlugin) {
|
|
17
|
+
return function dashboardGuardPlugin(instance, _options, done) {
|
|
18
|
+
if (middlewareList.length > 0) instance.addHook("onRequest", async (request, reply) => {
|
|
19
|
+
await runDashboardMiddleware(middlewareList, request, reply);
|
|
20
|
+
});
|
|
21
|
+
instance.register(bullBoardPlugin);
|
|
22
|
+
done();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { buildDashboardGuardPlugin };
|
|
28
|
+
//# sourceMappingURL=dashboard-guard-plugin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-guard-plugin.mjs","names":[],"sources":["../../../../../../queue/src/dashboard-guard-plugin.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyInstance, FastifyPluginCallback } from \"fastify\";\nimport { runDashboardMiddleware } from \"./dashboard-middleware-adapter\";\n\n/**\n * Wrap bull-board's Fastify plugin in a scope that runs `middlewareList`\n * on every request before bull-board's own routes see it.\n *\n * Bull-board's `FastifyAdapter.registerPlugin()` returns a plain Fastify\n * plugin with no hook for Warlock middleware to run through, and Fastify\n * only lets an `onRequest` hook be added to a plugin scope — never spliced\n * into a plugin someone else wrote. So this builds ONE plugin that adds the\n * hook to its own scope and then registers bull-board's plugin as a child of\n * that scope; Fastify's encapsulation runs the hook for every route the\n * child registers, which is every dashboard route.\n */\nexport function buildDashboardGuardPlugin(\n middlewareList: Middleware[],\n bullBoardPlugin: unknown,\n): FastifyPluginCallback {\n return function dashboardGuardPlugin(instance: FastifyInstance, _options, done) {\n if (middlewareList.length > 0) {\n instance.addHook(\"onRequest\", async (request, reply) => {\n await runDashboardMiddleware(middlewareList, request, reply);\n });\n }\n\n instance.register(bullBoardPlugin as never);\n done();\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,gBACA,iBACuB;CACvB,OAAO,SAAS,qBAAqB,UAA2B,UAAU,MAAM;EAC9E,IAAI,eAAe,SAAS,GAC1B,SAAS,QAAQ,aAAa,OAAO,SAAS,UAAU;GACtD,MAAM,uBAAuB,gBAAgB,SAAS,KAAK;EAC7D,CAAC;EAGH,SAAS,SAAS,eAAwB;EAC1C,KAAK;CACP;AACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region ../queue/src/dashboard-middleware-adapter.ts
|
|
2
|
+
/**
|
|
3
|
+
* Run a Warlock middleware list against a raw Fastify request/reply pair.
|
|
4
|
+
*
|
|
5
|
+
* Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a
|
|
6
|
+
* Warlock route — there is no `Route`, no validation pipeline, and none of
|
|
7
|
+
* `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`
|
|
8
|
+
* stores). Reusing that full pipeline here would pull the whole request
|
|
9
|
+
* machinery into a place it was never meant to run. Instead this builds the
|
|
10
|
+
* minimal `Request`/`Response` pair — enough for guard-style middleware
|
|
11
|
+
* (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the
|
|
12
|
+
* request and short-circuit with a response, which covers every one of
|
|
13
|
+
* bull-board's routes because they all sit behind the same hook.
|
|
14
|
+
*
|
|
15
|
+
* `@warlock.js/core` is imported dynamically so this module never drags
|
|
16
|
+
* core's runtime graph into a process that never mounts the dashboard.
|
|
17
|
+
*
|
|
18
|
+
* @returns `true` when a middleware sent a response and the caller must not
|
|
19
|
+
* continue (bull-board's handler must not run); `false` to continue.
|
|
20
|
+
*/
|
|
21
|
+
async function runDashboardMiddleware(middlewareList, fastifyRequest, fastifyReply) {
|
|
22
|
+
if (middlewareList.length === 0) return false;
|
|
23
|
+
const { Request, Response } = await import("@warlock.js/core");
|
|
24
|
+
const request = new Request();
|
|
25
|
+
const response = new Response();
|
|
26
|
+
response.setResponse(fastifyReply);
|
|
27
|
+
request.response = response;
|
|
28
|
+
response.request = request;
|
|
29
|
+
request.setRequest(fastifyRequest);
|
|
30
|
+
for (const middlewareFunction of middlewareList) if (await middlewareFunction({
|
|
31
|
+
request,
|
|
32
|
+
response
|
|
33
|
+
})) return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { runDashboardMiddleware };
|
|
39
|
+
//# sourceMappingURL=dashboard-middleware-adapter.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-middleware-adapter.mjs","names":[],"sources":["../../../../../../queue/src/dashboard-middleware-adapter.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\n\n/**\n * Run a Warlock middleware list against a raw Fastify request/reply pair.\n *\n * Bull-board's `FastifyAdapter` hands back a plain Fastify plugin, not a\n * Warlock route — there is no `Route`, no validation pipeline, and none of\n * `createRequestStore`'s context wiring (CSP, tracing, `@warlock.js/context`\n * stores). Reusing that full pipeline here would pull the whole request\n * machinery into a place it was never meant to run. Instead this builds the\n * minimal `Request`/`Response` pair — enough for guard-style middleware\n * (`authMiddleware`, `ipFilterMiddleware`, a custom check) to read the\n * request and short-circuit with a response, which covers every one of\n * bull-board's routes because they all sit behind the same hook.\n *\n * `@warlock.js/core` is imported dynamically so this module never drags\n * core's runtime graph into a process that never mounts the dashboard.\n *\n * @returns `true` when a middleware sent a response and the caller must not\n * continue (bull-board's handler must not run); `false` to continue.\n */\nexport async function runDashboardMiddleware(\n middlewareList: Middleware[],\n fastifyRequest: FastifyRequest,\n fastifyReply: FastifyReply,\n): Promise<boolean> {\n if (middlewareList.length === 0) {\n return false;\n }\n\n const { Request, Response } = await import(\"@warlock.js/core\");\n\n const request = new Request();\n const response = new Response();\n\n response.setResponse(fastifyReply);\n request.response = response;\n response.request = request;\n request.setRequest(fastifyRequest);\n\n for (const middlewareFunction of middlewareList) {\n const result = await middlewareFunction({ request, response });\n\n if (result) {\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsBA,eAAsB,uBACpB,gBACA,gBACA,cACkB;CAClB,IAAI,eAAe,WAAW,GAC5B,OAAO;CAGT,MAAM,EAAE,SAAS,aAAa,MAAM,OAAO;CAE3C,MAAM,UAAU,IAAI,QAAQ;CAC5B,MAAM,WAAW,IAAI,SAAS;CAE9B,SAAS,YAAY,YAAY;CACjC,QAAQ,WAAW;CACnB,SAAS,UAAU;CACnB,QAAQ,WAAW,cAAc;CAEjC,KAAK,MAAM,sBAAsB,gBAG/B,IAAI,MAFiB,mBAAmB;EAAE;EAAS;CAAS,CAAC,GAG3D,OAAO;CAIX,OAAO;AACT"}
|
package/esm/dashboard.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Middleware } from "@warlock.js/core";
|
|
2
|
+
|
|
1
3
|
//#region ../queue/src/dashboard.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* The part of a Fastify instance the dashboard needs. Warlock's HTTP server
|
|
@@ -11,6 +13,12 @@ type DashboardServer = {
|
|
|
11
13
|
type QueueDashboardOptions = {
|
|
12
14
|
/** URL prefix the dashboard is mounted on. Default `"/admin/queues"`. */basePath?: string; /** Queues to show. Default: every queue with a defined job, plus the default queue. */
|
|
13
15
|
queues?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Run before every dashboard route. Applied via a wrapping Fastify plugin
|
|
18
|
+
* scope, since bull-board's own plugin has no hook to splice Warlock
|
|
19
|
+
* middleware into — see `dashboard-guard-plugin.ts`.
|
|
20
|
+
*/
|
|
21
|
+
middleware?: Middleware[];
|
|
14
22
|
};
|
|
15
23
|
type BullBoardModules = {
|
|
16
24
|
createBullBoard: (options: {
|
package/esm/dashboard.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.d.mts","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"dashboard.d.mts","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"mappings":";;;;;AAWA;;KAAY,eAAA;EACV,QAAA,CAAS,MAAA,SAAe,OAAA;IAAW,MAAA;EAAA;AAAA;AAAA,KAGzB,qBAAA;EAHyC,yEAKnD,QAAA,WAFU;EAIV,MAAA;;;;;;EAMA,UAAA,GAAa,UAAU;AAAA;AAAA,KAGpB,gBAAA;EACH,eAAA,GAAkB,OAAA;IAAW,MAAA;IAAmB,aAAA;EAAA;EAChD,aAAA,OAAoB,KAAA;EACpB,cAAA;IACE,WAAA,CAAY,IAAA;IACZ,cAAA;EAAA;AAAA;;;;;;AAAc;AAiBlB;;;;;;;iBAAsB,cAAA,CACpB,MAAA,EAAQ,eAAA,EACR,OAAA,GAAS,qBAAA,GACR,OAAA;;;;;iBAuBmB,aAAA,CACpB,QAAA,IAAW,SAAA,aAAsB,OAAA,CAAQ,MAAA,qBACxC,OAAA,CAAQ,gBAAA"}
|
package/esm/dashboard.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { QueueDashboardDependencyError } from "./errors.mjs";
|
|
2
2
|
import { defaultQueueName } from "./config.mjs";
|
|
3
|
+
import { buildDashboardGuardPlugin } from "./dashboard-guard-plugin.mjs";
|
|
3
4
|
import { queueOf, registeredJobs } from "./job-registry.mjs";
|
|
4
5
|
import { getQueue } from "./queue-manager.mjs";
|
|
5
6
|
|
|
@@ -27,7 +28,8 @@ async function queueDashboard(server, options = {}) {
|
|
|
27
28
|
queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),
|
|
28
29
|
serverAdapter
|
|
29
30
|
});
|
|
30
|
-
|
|
31
|
+
const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());
|
|
32
|
+
await server.register(guardedPlugin, { prefix: basePath });
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* Load the optional bull-board packages. Exported for tests of the missing
|
package/esm/dashboard.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.mjs","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"sourcesContent":["import { defaultQueueName } from \"./config\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n
|
|
1
|
+
{"version":3,"file":"dashboard.mjs","names":[],"sources":["../../../../../../queue/src/dashboard.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport { defaultQueueName } from \"./config\";\nimport { buildDashboardGuardPlugin } from \"./dashboard-guard-plugin\";\nimport { QueueDashboardDependencyError } from \"./errors\";\nimport { queueOf, registeredJobs } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\n\n/**\n * The part of a Fastify instance the dashboard needs. Warlock's HTTP server\n * (`getHttpServer()` from `@warlock.js/core`) satisfies it.\n */\nexport type DashboardServer = {\n register(plugin: never, options: { prefix: string }): unknown;\n};\n\nexport type QueueDashboardOptions = {\n /** URL prefix the dashboard is mounted on. Default `\"/admin/queues\"`. */\n basePath?: string;\n /** Queues to show. Default: every queue with a defined job, plus the default queue. */\n queues?: string[];\n /**\n * Run before every dashboard route. Applied via a wrapping Fastify plugin\n * scope, since bull-board's own plugin has no hook to splice Warlock\n * middleware into — see `dashboard-guard-plugin.ts`.\n */\n middleware?: Middleware[];\n};\n\ntype BullBoardModules = {\n createBullBoard: (options: { queues: unknown[]; serverAdapter: unknown }) => unknown;\n BullMQAdapter: new (queue: unknown) => unknown;\n FastifyAdapter: new () => {\n setBasePath(path: string): unknown;\n registerPlugin(): unknown;\n };\n};\n\n/**\n * Mount the bull-board failed/active/delayed job UI on Warlock's Fastify\n * server. Requires the optional packages `@bull-board/api` and\n * `@bull-board/fastify`; they are loaded only when this is called, and a\n * missing one throws {@link QueueDashboardDependencyError}.\n *\n * Call it before the HTTP server starts listening, and put it behind your\n * own authentication — the dashboard can retry and delete jobs.\n *\n * @example\n * import { getHttpServer } from \"@warlock.js/core\";\n * await queueDashboard(getHttpServer(), { basePath: \"/admin/queues\" });\n */\nexport async function queueDashboard(\n server: DashboardServer,\n options: QueueDashboardOptions = {},\n): Promise<void> {\n const basePath = options.basePath ?? \"/admin/queues\";\n const { createBullBoard, BullMQAdapter, FastifyAdapter } = await loadBullBoard();\n const queueNames =\n options.queues ?? [...new Set([defaultQueueName(), ...registeredJobs().map((job) => queueOf(job))])];\n\n const serverAdapter = new FastifyAdapter();\n serverAdapter.setBasePath(basePath);\n\n createBullBoard({\n queues: queueNames.map((name) => new BullMQAdapter(getQueue(name))),\n serverAdapter,\n });\n\n const guardedPlugin = buildDashboardGuardPlugin(options.middleware ?? [], serverAdapter.registerPlugin());\n\n await server.register(guardedPlugin as never, { prefix: basePath });\n}\n\n/**\n * Load the optional bull-board packages. Exported for tests of the missing\n * dependency path; `importer` defaults to a real dynamic import.\n */\nexport async function loadBullBoard(\n importer: (specifier: string) => Promise<Record<string, unknown>> = (specifier) => import(specifier),\n): Promise<BullBoardModules> {\n const api = await importOptional(importer, \"@bull-board/api\");\n const adapter = await importOptional(importer, \"@bull-board/api/bullMQAdapter\");\n const fastify = await importOptional(importer, \"@bull-board/fastify\");\n\n return {\n createBullBoard: api.createBullBoard as BullBoardModules[\"createBullBoard\"],\n BullMQAdapter: adapter.BullMQAdapter as BullBoardModules[\"BullMQAdapter\"],\n FastifyAdapter: fastify.FastifyAdapter as BullBoardModules[\"FastifyAdapter\"],\n };\n}\n\nasync function importOptional(\n importer: (specifier: string) => Promise<Record<string, unknown>>,\n specifier: string,\n): Promise<Record<string, unknown>> {\n try {\n return await importer(specifier);\n } catch (error) {\n if (isModuleNotFound(error)) {\n throw new QueueDashboardDependencyError(specifier.split(\"/\").slice(0, 2).join(\"/\"));\n }\n\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown): boolean {\n const code = (error as { code?: unknown } | undefined)?.code;\n\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkDA,eAAsB,eACpB,QACA,UAAiC,CAAC,GACnB;CACf,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,EAAE,iBAAiB,eAAe,mBAAmB,MAAM,cAAc;CAC/E,MAAM,aACJ,QAAQ,UAAU,CAAC,GAAG,IAAI,IAAI,CAAC,iBAAiB,GAAG,GAAG,eAAe,CAAC,CAAC,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;CAErG,MAAM,gBAAgB,IAAI,eAAe;CACzC,cAAc,YAAY,QAAQ;CAElC,gBAAgB;EACd,QAAQ,WAAW,KAAK,SAAS,IAAI,cAAc,SAAS,IAAI,CAAC,CAAC;EAClE;CACF,CAAC;CAED,MAAM,gBAAgB,0BAA0B,QAAQ,cAAc,CAAC,GAAG,cAAc,eAAe,CAAC;CAExG,MAAM,OAAO,SAAS,eAAwB,EAAE,QAAQ,SAAS,CAAC;AACpE;;;;;AAMA,eAAsB,cACpB,YAAqE,cAAc,OAAO,YAC/D;CAC3B,MAAM,MAAM,MAAM,eAAe,UAAU,iBAAiB;CAC5D,MAAM,UAAU,MAAM,eAAe,UAAU,+BAA+B;CAC9E,MAAM,UAAU,MAAM,eAAe,UAAU,qBAAqB;CAEpE,OAAO;EACL,iBAAiB,IAAI;EACrB,eAAe,QAAQ;EACvB,gBAAgB,QAAQ;CAC1B;AACF;AAEA,eAAe,eACb,UACA,WACkC;CAClC,IAAI;EACF,OAAO,MAAM,SAAS,SAAS;CACjC,SAAS,OAAO;EACd,IAAI,iBAAiB,KAAK,GACxB,MAAM,IAAI,8BAA8B,UAAU,MAAM,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;EAGpF,MAAM;CACR;AACF;AAEA,SAAS,iBAAiB,OAAyB;CACjD,MAAM,OAAQ,OAA0C;CAExD,OAAO,SAAS,0BAA0B,SAAS;AACrD"}
|
package/esm/define-job.mjs
CHANGED
|
@@ -41,9 +41,11 @@ function defineJob(definition) {
|
|
|
41
41
|
};
|
|
42
42
|
},
|
|
43
43
|
async find(id) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const queue = getQueue(queueOf(definition));
|
|
45
|
+
const initialJob = await queue.getJob(id);
|
|
46
|
+
if (!initialJob || initialJob.name !== definition.name) return;
|
|
47
|
+
const state = await initialJob.getState();
|
|
48
|
+
return toSnapshot(await queue.getJob(id) ?? initialJob, state);
|
|
47
49
|
}
|
|
48
50
|
};
|
|
49
51
|
}
|
|
@@ -79,14 +81,19 @@ function toBullBackoff(backoff) {
|
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
83
|
* A plain view of a BullMQ job.
|
|
84
|
+
*
|
|
85
|
+
* @param job The job to read fields from.
|
|
86
|
+
* @param state The job's state; pass a state read *before* `job` was
|
|
87
|
+
* fetched (or re-fetched) so the returned snapshot's fields are consistent
|
|
88
|
+
* with it. If omitted, the state is read from `job` directly.
|
|
82
89
|
*/
|
|
83
|
-
async function toSnapshot(job) {
|
|
84
|
-
const
|
|
90
|
+
async function toSnapshot(job, state) {
|
|
91
|
+
const resolvedState = state ?? await job.getState();
|
|
85
92
|
return {
|
|
86
93
|
id: String(job.id),
|
|
87
94
|
name: job.name,
|
|
88
95
|
queue: job.queueName,
|
|
89
|
-
state,
|
|
96
|
+
state: resolvedState,
|
|
90
97
|
payload: job.data,
|
|
91
98
|
progress: job.progress,
|
|
92
99
|
attemptsMade: job.attemptsMade,
|
package/esm/define-job.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-job.mjs","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"sourcesContent":["import type { Job, JobsOptions } from \"bullmq\";\nimport { getQueueConfig } from \"./config\";\nimport { toMilliseconds } from \"./duration\";\nimport { InvalidJobDefinitionError } from \"./errors\";\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\nimport { getQueue } from \"./queue-manager\";\nimport type {\n DispatchOptions,\n JobBackoff,\n JobDefinition,\n JobOptions,\n JobSnapshot,\n JobState,\n QueueJob,\n} from \"./types\";\n\n/**\n * Define a background job.\n *\n * The definition is registered by name so any worker in the process can run\n * it; the returned object dispatches it with a typed payload.\n *\n * @example\n * export const sendInvoice = defineJob({\n * name: \"invoices.send\",\n * attempts: 5,\n * backoff: { type: \"exponential\", delay: 2000 },\n * async handle(payload: { invoiceId: string }, ctx) {\n * await ctx.progress(50);\n * },\n * });\n *\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\n */\nexport function defineJob<TPayload, TResult = unknown>(\n definition: JobDefinition<TPayload, TResult>,\n): QueueJob<TPayload, TResult> {\n assertValidDefinition(definition);\n registerJob(definition as RegisteredJob);\n\n return {\n name: definition.name,\n get queue() {\n return queueOf(definition);\n },\n async dispatch(payload, options = {}) {\n const queueName = queueOf(definition);\n const job = await getQueue(queueName).add(\n definition.name,\n payload,\n toBullJobOptions(definition, options),\n );\n\n return { id: String(job.id), name: definition.name, queue: queueName };\n },\n async find(id) {\n const
|
|
1
|
+
{"version":3,"file":"define-job.mjs","names":[],"sources":["../../../../../../queue/src/define-job.ts"],"sourcesContent":["import type { Job, JobsOptions } from \"bullmq\";\r\nimport { getQueueConfig } from \"./config\";\r\nimport { toMilliseconds } from \"./duration\";\r\nimport { InvalidJobDefinitionError } from \"./errors\";\r\nimport { queueOf, registerJob, type RegisteredJob } from \"./job-registry\";\r\nimport { getQueue } from \"./queue-manager\";\r\nimport type {\r\n DispatchOptions,\r\n JobBackoff,\r\n JobDefinition,\r\n JobOptions,\r\n JobSnapshot,\r\n JobState,\r\n QueueJob,\r\n} from \"./types\";\r\n\r\n/**\r\n * Define a background job.\r\n *\r\n * The definition is registered by name so any worker in the process can run\r\n * it; the returned object dispatches it with a typed payload.\r\n *\r\n * @example\r\n * export const sendInvoice = defineJob({\r\n * name: \"invoices.send\",\r\n * attempts: 5,\r\n * backoff: { type: \"exponential\", delay: 2000 },\r\n * async handle(payload: { invoiceId: string }, ctx) {\r\n * await ctx.progress(50);\r\n * },\r\n * });\r\n *\r\n * await sendInvoice.dispatch({ invoiceId: \"42\" }, { delay: \"10m\", priority: 1 });\r\n */\r\nexport function defineJob<TPayload, TResult = unknown>(\r\n definition: JobDefinition<TPayload, TResult>,\r\n): QueueJob<TPayload, TResult> {\r\n assertValidDefinition(definition);\r\n registerJob(definition as RegisteredJob);\r\n\r\n return {\r\n name: definition.name,\r\n get queue() {\r\n return queueOf(definition);\r\n },\r\n async dispatch(payload, options = {}) {\r\n const queueName = queueOf(definition);\r\n const job = await getQueue(queueName).add(\r\n definition.name,\r\n payload,\r\n toBullJobOptions(definition, options),\r\n );\r\n\r\n return { id: String(job.id), name: definition.name, queue: queueName };\r\n },\r\n async find(id) {\r\n const queue = getQueue(queueOf(definition));\r\n const initialJob = await queue.getJob(id);\r\n\r\n if (!initialJob || initialJob.name !== definition.name) {\r\n return undefined;\r\n }\r\n\r\n // Read the state first, then (re)fetch the job. BullMQ writes a job's\r\n // result/attemptsMade/finishedOn fields *before* it becomes visible\r\n // under a new state, so re-reading the job after the state is known\r\n // guarantees those fields are consistent with the reported state\r\n // (rather than reflecting a moment before the job finished).\r\n const state = (await initialJob.getState()) as JobState;\r\n const job = (await queue.getJob(id)) ?? initialJob;\r\n\r\n return toSnapshot<TPayload, TResult>(job, state);\r\n },\r\n };\r\n}\r\n\r\nfunction assertValidDefinition(definition: JobDefinition<unknown, unknown>): void {\r\n if (typeof definition.name !== \"string\" || definition.name.trim() === \"\") {\r\n throw new InvalidJobDefinitionError(\"defineJob() requires a non-empty `name`.\");\r\n }\r\n\r\n if (typeof definition.handle !== \"function\") {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\") requires a \\`handle(payload, ctx)\\` function.`,\r\n );\r\n }\r\n\r\n if (definition.attempts !== undefined && !(Number.isInteger(definition.attempts) && definition.attempts >= 1)) {\r\n throw new InvalidJobDefinitionError(\r\n `defineJob(\"${definition.name}\"): \\`attempts\\` must be an integer >= 1, got ${definition.attempts}.`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Merge app defaults < job definition < dispatch options into BullMQ's shape.\r\n */\r\nfunction toBullJobOptions(definition: JobOptions, options: DispatchOptions): JobsOptions {\r\n const defaults = getQueueConfig().defaultJobOptions ?? {};\r\n const attempts = options.attempts ?? definition.attempts ?? defaults.attempts;\r\n const backoff = options.backoff ?? definition.backoff ?? defaults.backoff;\r\n const removeOnComplete = definition.removeOnComplete ?? defaults.removeOnComplete;\r\n const removeOnFail = definition.removeOnFail ?? defaults.removeOnFail;\r\n\r\n const bullOptions: JobsOptions = {};\r\n\r\n if (attempts !== undefined) bullOptions.attempts = attempts;\r\n if (backoff !== undefined) bullOptions.backoff = toBullBackoff(backoff);\r\n if (removeOnComplete !== undefined) bullOptions.removeOnComplete = removeOnComplete;\r\n if (removeOnFail !== undefined) bullOptions.removeOnFail = removeOnFail;\r\n if (options.delay !== undefined) bullOptions.delay = toMilliseconds(options.delay);\r\n if (options.priority !== undefined) bullOptions.priority = options.priority;\r\n if (options.jobId !== undefined) bullOptions.jobId = options.jobId;\r\n\r\n return bullOptions;\r\n}\r\n\r\nfunction toBullBackoff(backoff: JobBackoff): JobsOptions[\"backoff\"] {\r\n return typeof backoff === \"number\" ? { type: \"fixed\", delay: backoff } : backoff;\r\n}\r\n\r\n/**\r\n * A plain view of a BullMQ job.\r\n *\r\n * @param job The job to read fields from.\r\n * @param state The job's state; pass a state read *before* `job` was\r\n * fetched (or re-fetched) so the returned snapshot's fields are consistent\r\n * with it. If omitted, the state is read from `job` directly.\r\n */\r\nexport async function toSnapshot<TPayload, TResult>(\r\n job: Job,\r\n state?: JobState,\r\n): Promise<JobSnapshot<TPayload, TResult>> {\r\n const resolvedState = state ?? ((await job.getState()) as JobState);\r\n\r\n return {\r\n id: String(job.id),\r\n name: job.name,\r\n queue: job.queueName,\r\n state: resolvedState,\r\n payload: job.data as TPayload,\r\n progress: job.progress as JobSnapshot[\"progress\"],\r\n attemptsMade: job.attemptsMade,\r\n result: job.returnvalue as TResult | undefined,\r\n failedReason: job.failedReason || undefined,\r\n createdAt: new Date(job.timestamp),\r\n finishedAt: job.finishedOn ? new Date(job.finishedOn) : undefined,\r\n };\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,UACd,YAC6B;CAC7B,sBAAsB,UAAU;CAChC,YAAY,UAA2B;CAEvC,OAAO;EACL,MAAM,WAAW;EACjB,IAAI,QAAQ;GACV,OAAO,QAAQ,UAAU;EAC3B;EACA,MAAM,SAAS,SAAS,UAAU,CAAC,GAAG;GACpC,MAAM,YAAY,QAAQ,UAAU;GACpC,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC,CAAC,IACpC,WAAW,MACX,SACA,iBAAiB,YAAY,OAAO,CACtC;GAEA,OAAO;IAAE,IAAI,OAAO,IAAI,EAAE;IAAG,MAAM,WAAW;IAAM,OAAO;GAAU;EACvE;EACA,MAAM,KAAK,IAAI;GACb,MAAM,QAAQ,SAAS,QAAQ,UAAU,CAAC;GAC1C,MAAM,aAAa,MAAM,MAAM,OAAO,EAAE;GAExC,IAAI,CAAC,cAAc,WAAW,SAAS,WAAW,MAChD;GAQF,MAAM,QAAS,MAAM,WAAW,SAAS;GAGzC,OAAO,WAFM,MAAM,MAAM,OAAO,EAAE,KAAM,YAEE,KAAK;EACjD;CACF;AACF;AAEA,SAAS,sBAAsB,YAAmD;CAChF,IAAI,OAAO,WAAW,SAAS,YAAY,WAAW,KAAK,KAAK,MAAM,IACpE,MAAM,IAAI,0BAA0B,0CAA0C;CAGhF,IAAI,OAAO,WAAW,WAAW,YAC/B,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,iDAChC;CAGF,IAAI,WAAW,aAAa,UAAa,EAAE,OAAO,UAAU,WAAW,QAAQ,KAAK,WAAW,YAAY,IACzG,MAAM,IAAI,0BACR,cAAc,WAAW,KAAK,gDAAgD,WAAW,SAAS,EACpG;AAEJ;;;;AAKA,SAAS,iBAAiB,YAAwB,SAAuC;CACvF,MAAM,WAAW,eAAe,CAAC,CAAC,qBAAqB,CAAC;CACxD,MAAM,WAAW,QAAQ,YAAY,WAAW,YAAY,SAAS;CACrE,MAAM,UAAU,QAAQ,WAAW,WAAW,WAAW,SAAS;CAClE,MAAM,mBAAmB,WAAW,oBAAoB,SAAS;CACjE,MAAM,eAAe,WAAW,gBAAgB,SAAS;CAEzD,MAAM,cAA2B,CAAC;CAElC,IAAI,aAAa,QAAW,YAAY,WAAW;CACnD,IAAI,YAAY,QAAW,YAAY,UAAU,cAAc,OAAO;CACtE,IAAI,qBAAqB,QAAW,YAAY,mBAAmB;CACnE,IAAI,iBAAiB,QAAW,YAAY,eAAe;CAC3D,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,eAAe,QAAQ,KAAK;CACjF,IAAI,QAAQ,aAAa,QAAW,YAAY,WAAW,QAAQ;CACnE,IAAI,QAAQ,UAAU,QAAW,YAAY,QAAQ,QAAQ;CAE7D,OAAO;AACT;AAEA,SAAS,cAAc,SAA6C;CAClE,OAAO,OAAO,YAAY,WAAW;EAAE,MAAM;EAAS,OAAO;CAAQ,IAAI;AAC3E;;;;;;;;;AAUA,eAAsB,WACpB,KACA,OACyC;CACzC,MAAM,gBAAgB,SAAW,MAAM,IAAI,SAAS;CAEpD,OAAO;EACL,IAAI,OAAO,IAAI,EAAE;EACjB,MAAM,IAAI;EACV,OAAO,IAAI;EACX,OAAO;EACP,SAAS,IAAI;EACb,UAAU,IAAI;EACd,cAAc,IAAI;EAClB,QAAQ,IAAI;EACZ,cAAc,IAAI,gBAAgB;EAClC,WAAW,IAAI,KAAK,IAAI,SAAS;EACjC,YAAY,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,IAAI;CAC1D;AACF"}
|
package/esm/index.d.mts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QueueConfig, QueueJob, QueueWorkersConfig } from "./types.mjs";
|
|
1
|
+
import { DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QueueConfig, QueueDashboardConfig, QueueJob, QueueWorkersConfig } from "./types.mjs";
|
|
2
2
|
import { defaultQueueName, getQueueConfig, resetQueueConfig, setQueueConfig } from "./config.mjs";
|
|
3
3
|
import { DashboardServer, QueueDashboardOptions, loadBullBoard, queueDashboard } from "./dashboard.mjs";
|
|
4
|
+
import { DEFAULT_DASHBOARD_PATH, mountQueueDashboard } from "./dashboard-boot.mjs";
|
|
4
5
|
import { defineJob } from "./define-job.mjs";
|
|
5
6
|
import { toMilliseconds } from "./duration.mjs";
|
|
6
7
|
import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QueueDashboardDependencyError, QueueNotConfiguredError } from "./errors.mjs";
|
|
7
8
|
import { FailedJobsOptions, failedJobs, retryFailedJob } from "./failed-jobs.mjs";
|
|
8
9
|
import { QUEUE_CONNECTOR_PRIORITY, QueueConnectorOptions, queueConnector } from "./queue-connector.mjs";
|
|
10
|
+
import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
|
|
9
11
|
import { CloseQueueOptions, closeQueue, getQueue, runningWorkers, startWorkers } from "./queue-manager.mjs";
|
|
10
|
-
export { type CloseQueueOptions, DashboardServer, DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, FailedJobNotFoundError, FailedJobsOptions, InvalidDurationError, InvalidJobDefinitionError, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QUEUE_CONNECTOR_PRIORITY, QueueConfig, QueueConnectorOptions, QueueDashboardDependencyError, QueueDashboardOptions, QueueJob, QueueNotConfiguredError, QueueWorkersConfig, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
|
|
12
|
+
export { type CloseQueueOptions, DEFAULT_DASHBOARD_PATH, DashboardServer, DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, FailedJobNotFoundError, FailedJobsOptions, InvalidDurationError, InvalidJobDefinitionError, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QUEUE_CONNECTOR_PRIORITY, QueueConfig, QueueConnectorOptions, QueueDashboardConfig, QueueDashboardDependencyError, QueueDashboardOptions, QueueDashboardUnguardedError, QueueJob, QueueNotConfiguredError, QueueWorkersConfig, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
|
package/esm/index.mjs
CHANGED
|
@@ -2,9 +2,11 @@ import { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError
|
|
|
2
2
|
import { defaultQueueName, getQueueConfig, resetQueueConfig, setQueueConfig } from "./config.mjs";
|
|
3
3
|
import { closeQueue, getQueue, runningWorkers, startWorkers } from "./queue-manager.mjs";
|
|
4
4
|
import { loadBullBoard, queueDashboard } from "./dashboard.mjs";
|
|
5
|
+
import { QueueDashboardUnguardedError } from "./queue-dashboard-unguarded.error.mjs";
|
|
6
|
+
import { DEFAULT_DASHBOARD_PATH, mountQueueDashboard } from "./dashboard-boot.mjs";
|
|
5
7
|
import { toMilliseconds } from "./duration.mjs";
|
|
6
8
|
import { defineJob } from "./define-job.mjs";
|
|
7
9
|
import { failedJobs, retryFailedJob } from "./failed-jobs.mjs";
|
|
8
10
|
import { QUEUE_CONNECTOR_PRIORITY, queueConnector } from "./queue-connector.mjs";
|
|
9
11
|
|
|
10
|
-
export { FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QUEUE_CONNECTOR_PRIORITY, QueueDashboardDependencyError, QueueNotConfiguredError, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
|
|
12
|
+
export { DEFAULT_DASHBOARD_PATH, FailedJobNotFoundError, InvalidDurationError, InvalidJobDefinitionError, QUEUE_CONNECTOR_PRIORITY, QueueDashboardDependencyError, QueueDashboardUnguardedError, QueueNotConfiguredError, closeQueue, defaultQueueName, defineJob, failedJobs, getQueue, getQueueConfig, loadBullBoard, mountQueueDashboard, queueConnector, queueDashboard, resetQueueConfig, retryFailedJob, runningWorkers, setQueueConfig, startWorkers, toMilliseconds };
|
|
@@ -20,6 +20,14 @@ type QueueNotificationDispatcherOptions = {
|
|
|
20
20
|
* - A channel missing from the worker's notifications config fails at once,
|
|
21
21
|
* without retries.
|
|
22
22
|
*
|
|
23
|
+
* @deprecated Import `bullmqQueue` from `@warlock.js/notifications` instead —
|
|
24
|
+
* vendor integrations now live as lazy drivers inside the feature package:
|
|
25
|
+
*
|
|
26
|
+
* queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } })
|
|
27
|
+
*
|
|
28
|
+
* This subpath keeps working for one release and then goes away. See the
|
|
29
|
+
* queue CHANGELOG.
|
|
30
|
+
*
|
|
23
31
|
* @example src/config/notifications.ts
|
|
24
32
|
* import { queueNotificationDispatcher } from "@warlock.js/queue/notifications";
|
|
25
33
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue-notification-dispatcher.d.mts","names":[],"sources":["../../../../../../../queue/src/notifications/queue-notification-dispatcher.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"queue-notification-dispatcher.d.mts","names":[],"sources":["../../../../../../../queue/src/notifications/queue-notification-dispatcher.ts"],"mappings":";;;;AAqB+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;AA6BtB;;;;;;;;AAEkB;;;;;;;;;;;;;;;;;;AAFlB,iBAAgB,2BAAA,CACd,OAAA,GAAS,kCAAA,GACR,eAAe"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toMilliseconds } from "../duration.mjs";
|
|
2
2
|
import { defineJob } from "../define-job.mjs";
|
|
3
|
+
import { log } from "@warlock.js/logger";
|
|
3
4
|
import { UnrecoverableError } from "bullmq";
|
|
4
5
|
import { getNotificationConfig } from "@warlock.js/notifications";
|
|
5
6
|
|
|
@@ -27,6 +28,14 @@ const NOTIFICATION_JOB_NAME = "warlock.notifications.deliver";
|
|
|
27
28
|
* - A channel missing from the worker's notifications config fails at once,
|
|
28
29
|
* without retries.
|
|
29
30
|
*
|
|
31
|
+
* @deprecated Import `bullmqQueue` from `@warlock.js/notifications` instead —
|
|
32
|
+
* vendor integrations now live as lazy drivers inside the feature package:
|
|
33
|
+
*
|
|
34
|
+
* queue: bullmqQueue({ attempts: 3, backoff: { type: "exponential", delay: 5000 } })
|
|
35
|
+
*
|
|
36
|
+
* This subpath keeps working for one release and then goes away. See the
|
|
37
|
+
* queue CHANGELOG.
|
|
38
|
+
*
|
|
30
39
|
* @example src/config/notifications.ts
|
|
31
40
|
* import { queueNotificationDispatcher } from "@warlock.js/queue/notifications";
|
|
32
41
|
*
|
|
@@ -36,6 +45,7 @@ const NOTIFICATION_JOB_NAME = "warlock.notifications.deliver";
|
|
|
36
45
|
* };
|
|
37
46
|
*/
|
|
38
47
|
function queueNotificationDispatcher(options = {}) {
|
|
48
|
+
warnDeprecatedOnce();
|
|
39
49
|
const deliver = defineNotificationJob(options);
|
|
40
50
|
return { async dispatch(job) {
|
|
41
51
|
await deliver.dispatch(job, { delay: job.options.delay === void 0 ? void 0 : notificationDelay(job.options.delay) });
|
|
@@ -61,6 +71,20 @@ function defineNotificationJob(options) {
|
|
|
61
71
|
function notificationDelay(delay) {
|
|
62
72
|
return typeof delay === "number" ? delay * 1e3 : toMilliseconds(delay);
|
|
63
73
|
}
|
|
74
|
+
let warnedDeprecated = false;
|
|
75
|
+
/**
|
|
76
|
+
* Warn once per process that `@warlock.js/queue/notifications` is deprecated
|
|
77
|
+
* in favour of `bullmqQueue` from `@warlock.js/notifications`. Kept thin —
|
|
78
|
+
* this file still owns the actual dispatch logic (delegating to notifications
|
|
79
|
+
* would create a runtime import cycle: notifications' `bullmqQueue` lazily
|
|
80
|
+
* imports `@warlock.js/queue`, which this file already statically imports
|
|
81
|
+
* `@warlock.js/notifications` from).
|
|
82
|
+
*/
|
|
83
|
+
function warnDeprecatedOnce() {
|
|
84
|
+
if (warnedDeprecated) return;
|
|
85
|
+
warnedDeprecated = true;
|
|
86
|
+
log.warn("queue", "notifications.deprecated", "\"queueNotificationDispatcher\" from \"@warlock.js/queue/notifications\" is deprecated and will be removed in the next release. Use the notifications-owned driver instead: import { bullmqQueue } from \"@warlock.js/notifications\"; queue: bullmqQueue({ attempts, backoff })");
|
|
87
|
+
}
|
|
64
88
|
|
|
65
89
|
//#endregion
|
|
66
90
|
export { NOTIFICATION_JOB_NAME, queueNotificationDispatcher };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue-notification-dispatcher.mjs","names":[],"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":"
|
|
1
|
+
{"version":3,"file":"queue-notification-dispatcher.mjs","names":[],"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 { log } from \"@warlock.js/logger\";\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 * @deprecated Import `bullmqQueue` from `@warlock.js/notifications` instead —\n * vendor integrations now live as lazy drivers inside the feature package:\n *\n * queue: bullmqQueue({ attempts: 3, backoff: { type: \"exponential\", delay: 5000 } })\n *\n * This subpath keeps working for one release and then goes away. See the\n * queue CHANGELOG.\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 warnDeprecatedOnce();\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\nlet warnedDeprecated = false;\n\n/**\n * Warn once per process that `@warlock.js/queue/notifications` is deprecated\n * in favour of `bullmqQueue` from `@warlock.js/notifications`. Kept thin —\n * this file still owns the actual dispatch logic (delegating to notifications\n * would create a runtime import cycle: notifications' `bullmqQueue` lazily\n * imports `@warlock.js/queue`, which this file already statically imports\n * `@warlock.js/notifications` from).\n */\nfunction warnDeprecatedOnce(): void {\n if (warnedDeprecated) {\n return;\n }\n\n warnedDeprecated = true;\n log.warn(\n \"queue\",\n \"notifications.deprecated\",\n '\"queueNotificationDispatcher\" from \"@warlock.js/queue/notifications\" is deprecated and will be ' +\n \"removed in the next release. Use the notifications-owned driver instead: \" +\n 'import { bullmqQueue } from \"@warlock.js/notifications\"; ' +\n 'queue: bullmqQueue({ attempts, backoff })',\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCrC,SAAgB,4BACd,UAA8C,CAAC,GAC9B;CACjB,mBAAmB;CACnB,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,OAAO,UAAwC;EAC7C,MAAM;EACN,OAAO,QAAQ;EACf,UAAU,QAAQ;EAClB,SAAS,QAAQ;EACjB,MAAM,OAAO,KAAK;GAKhB,MAAM,UAJW,sBAAsB,CAAC,CAAC,SAIhB,IAAI;GAE7B,IAAI,CAAC,SACH,MAAM,IAAI,mBACR,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,MAAQ,eAAe,KAAK;AACzE;AAEA,IAAI,mBAAmB;;;;;;;;;AAUvB,SAAS,qBAA2B;CAClC,IAAI,kBACF;CAGF,mBAAmB;CACnB,IAAI,KACF,SACA,4BACA,kRAIF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue-connector.d.mts","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"queue-connector.d.mts","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"mappings":";;;;;;;AA6BsB;AAkBtB;cA3Ba,wBAAA;AAAA,KAID,qBAAA;EAuBkE;;;;EAlB5E,MAAA,GAAS,WAAW;AAAA;;;;;;;;;;;;;;;;iBAkBN,cAAA,CAAe,OAAA,GAAS,qBAAA,GAA6B,SAAS"}
|
package/esm/queue-connector.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resetQueueConfig, setQueueConfig } from "./config.mjs";
|
|
2
2
|
import { closeQueue, startWorkers } from "./queue-manager.mjs";
|
|
3
|
+
import { mountQueueDashboard } from "./dashboard-boot.mjs";
|
|
3
4
|
import { log } from "@warlock.js/logger";
|
|
4
5
|
|
|
5
6
|
//#region ../queue/src/queue-connector.ts
|
|
@@ -32,7 +33,22 @@ function queueConnector(options = {}) {
|
|
|
32
33
|
priority: 11,
|
|
33
34
|
lifecyclePhase: "late",
|
|
34
35
|
isActive: () => active,
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Mounts the dashboard, when configured, here rather than in `start()`:
|
|
38
|
+
* `boot()` runs for every late-phase connector, in priority order, before
|
|
39
|
+
* any of them `start()`s — so by the time this runs, the HTTP connector
|
|
40
|
+
* (priority 5, before queue's 11) has already built its Fastify instance
|
|
41
|
+
* and registered its own plugins, but has not yet called `listen()`.
|
|
42
|
+
* Fastify refuses new plugin registrations after `listen()`, so this is
|
|
43
|
+
* the only point in the boot sequence where mounting is possible.
|
|
44
|
+
*/
|
|
45
|
+
async boot() {
|
|
46
|
+
const queueConfig = options.config ?? await readQueueConfig();
|
|
47
|
+
if (!queueConfig?.dashboard?.enabled) return;
|
|
48
|
+
setQueueConfig(queueConfig);
|
|
49
|
+
const { getHttpServer } = await import("@warlock.js/core");
|
|
50
|
+
await mountQueueDashboard(getHttpServer(), queueConfig);
|
|
51
|
+
},
|
|
36
52
|
async start() {
|
|
37
53
|
const queueConfig = options.config ?? await readQueueConfig();
|
|
38
54
|
if (!queueConfig) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue-connector.mjs","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["/**\n * The queue's connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\n * at config-load time. Core is imported lazily inside `start()`, where the\n * app has already loaded it.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\nimport { closeQueue, startWorkers } from \"./queue-manager\";\nimport type { QueueConfig } from \"./types\";\n\n/**\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\n * the logger is up and anything a job handler needs is already connected;\n * shuts down before them for the same reason.\n */\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\n\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\n\nexport type QueueConnectorOptions = {\n /**\n * Supply the configuration directly instead of reading the `queue` config\n * key (`src/config/queue.ts`).\n */\n config?: QueueConfig;\n};\n\n/**\n * Construct the queue connector.\n *\n * Runs in the `late` lifecycle phase — after app code is imported — so every\n * `defineJob` in the app has registered before workers start. At start it\n * reads the `queue` config and starts in-process workers unless\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\n *\n * @example\n * // warlock.config.ts\n * import { queueConnector } from \"@warlock.js/queue\";\n *\n * export default defineConfig({ connectors: [queueConnector()] });\n */\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\n let active = false;\n\n const connector: Connector = {\n name: \"queue\",\n priority: QUEUE_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\n // module stays free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n boot
|
|
1
|
+
{"version":3,"file":"queue-connector.mjs","names":[],"sources":["../../../../../../queue/src/queue-connector.ts"],"sourcesContent":["/**\r\n * The queue's connector for `warlock.config.ts > connectors`.\r\n *\r\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\r\n * file that constructs it must not drag core's runtime graph (or BullMQ) in\r\n * at config-load time. Core is imported lazily inside `start()`, where the\r\n * app has already loaded it.\r\n */\r\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { resetQueueConfig, setQueueConfig } from \"./config\";\r\nimport { mountQueueDashboard } from \"./dashboard-boot\";\r\nimport { closeQueue, startWorkers } from \"./queue-manager\";\r\nimport type { QueueConfig } from \"./types\";\r\n\r\n/**\r\n * Boots after every built-in connector (`ConnectorPriority.AI` is `10`), so\r\n * the logger is up and anything a job handler needs is already connected;\r\n * shuts down before them for the same reason.\r\n */\r\nexport const QUEUE_CONNECTOR_PRIORITY = 11;\r\n\r\nconst WATCHED_FILES = [\"src/config/queue.ts\"];\r\n\r\nexport type QueueConnectorOptions = {\r\n /**\r\n * Supply the configuration directly instead of reading the `queue` config\r\n * key (`src/config/queue.ts`).\r\n */\r\n config?: QueueConfig;\r\n};\r\n\r\n/**\r\n * Construct the queue connector.\r\n *\r\n * Runs in the `late` lifecycle phase — after app code is imported — so every\r\n * `defineJob` in the app has registered before workers start. At start it\r\n * reads the `queue` config and starts in-process workers unless\r\n * `workers.enabled` is `false`; at shutdown it closes workers (waiting for\r\n * active jobs, bounded by `workers.shutdownTimeout`) and then the queues.\r\n *\r\n * @example\r\n * // warlock.config.ts\r\n * import { queueConnector } from \"@warlock.js/queue\";\r\n *\r\n * export default defineConfig({ connectors: [queueConnector()] });\r\n */\r\nexport function queueConnector(options: QueueConnectorOptions = {}): Connector {\r\n let active = false;\r\n\r\n const connector: Connector = {\r\n name: \"queue\",\r\n priority: QUEUE_CONNECTOR_PRIORITY,\r\n // Core's `ConnectorLifecyclePhase.Late`; the value is spelled out so this\r\n // module stays free of a runtime import of core.\r\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\r\n isActive: () => active,\r\n /**\r\n * Mounts the dashboard, when configured, here rather than in `start()`:\r\n * `boot()` runs for every late-phase connector, in priority order, before\r\n * any of them `start()`s — so by the time this runs, the HTTP connector\r\n * (priority 5, before queue's 11) has already built its Fastify instance\r\n * and registered its own plugins, but has not yet called `listen()`.\r\n * Fastify refuses new plugin registrations after `listen()`, so this is\r\n * the only point in the boot sequence where mounting is possible.\r\n */\r\n async boot() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig?.dashboard?.enabled) {\r\n return;\r\n }\r\n\r\n // The dashboard resolves queues through the active config, so it has to\r\n // be registered here rather than only in `start()`, which runs after\r\n // every late connector has booted. `start()` sets it again; the setter\r\n // is idempotent for the same object.\r\n setQueueConfig(queueConfig);\r\n\r\n const { getHttpServer } = await import(\"@warlock.js/core\");\r\n\r\n await mountQueueDashboard(getHttpServer(), queueConfig);\r\n },\r\n async start() {\r\n const queueConfig = options.config ?? (await readQueueConfig());\r\n\r\n if (!queueConfig) {\r\n log.warn(\r\n \"queue\",\r\n \"configured\",\r\n \"queueConnector() is registered but no `queue` config was found (src/config/queue.ts); queue not started\",\r\n );\r\n return;\r\n }\r\n\r\n setQueueConfig(queueConfig);\r\n const started = await startWorkers();\r\n active = true;\r\n\r\n log.info(\r\n \"queue\",\r\n \"configured\",\r\n started.length > 0\r\n ? `Queue workers running for: ${started.join(\", \")}`\r\n : \"Queue configured (no in-process workers)\",\r\n );\r\n },\r\n async restart() {\r\n await connector.shutdown();\r\n await connector.start();\r\n },\r\n async shutdown() {\r\n if (!active) {\r\n return;\r\n }\r\n\r\n await closeQueue();\r\n resetQueueConfig();\r\n active = false;\r\n },\r\n shouldRestart(changedFiles) {\r\n return changedFiles.some((file) => {\r\n const normalized = file.replace(/\\\\/g, \"/\");\r\n\r\n return WATCHED_FILES.some((watched) => normalized === watched || normalized.endsWith(`/${watched}`));\r\n });\r\n },\r\n };\r\n\r\n return connector;\r\n}\r\n\r\nasync function readQueueConfig(): Promise<QueueConfig | undefined> {\r\n const { config } = await import(\"@warlock.js/core\");\r\n\r\n return config.get<QueueConfig | undefined>(\"queue\");\r\n}\r\n"],"mappings":";;;;;;;;;;;AAoBA,MAAa,2BAA2B;AAExC,MAAM,gBAAgB,CAAC,qBAAqB;;;;;;;;;;;;;;;;AAyB5C,SAAgB,eAAe,UAAiC,CAAC,GAAc;CAC7E,IAAI,SAAS;CAEb,MAAM,YAAuB;EAC3B,MAAM;EACN;EAGA,gBAAgB;EAChB,gBAAgB;;;;;;;;;;EAUhB,MAAM,OAAO;GACX,MAAM,cAAc,QAAQ,UAAW,MAAM,gBAAgB;GAE7D,IAAI,CAAC,aAAa,WAAW,SAC3B;GAOF,eAAe,WAAW;GAE1B,MAAM,EAAE,kBAAkB,MAAM,OAAO;GAEvC,MAAM,oBAAoB,cAAc,GAAG,WAAW;EACxD;EACA,MAAM,QAAQ;GACZ,MAAM,cAAc,QAAQ,UAAW,MAAM,gBAAgB;GAE7D,IAAI,CAAC,aAAa;IAChB,IAAI,KACF,SACA,cACA,yGACF;IACA;GACF;GAEA,eAAe,WAAW;GAC1B,MAAM,UAAU,MAAM,aAAa;GACnC,SAAS;GAET,IAAI,KACF,SACA,cACA,QAAQ,SAAS,IACb,8BAA8B,QAAQ,KAAK,IAAI,MAC/C,0CACN;EACF;EACA,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,MAAM;EACxB;EACA,MAAM,WAAW;GACf,IAAI,CAAC,QACH;GAGF,MAAM,WAAW;GACjB,iBAAiB;GACjB,SAAS;EACX;EACA,cAAc,cAAc;GAC1B,OAAO,aAAa,MAAM,SAAS;IACjC,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;IAE1C,OAAO,cAAc,MAAM,YAAY,eAAe,WAAW,WAAW,SAAS,IAAI,SAAS,CAAC;GACrG,CAAC;EACH;CACF;CAEA,OAAO;AACT;AAEA,eAAe,kBAAoD;CACjE,MAAM,EAAE,WAAW,MAAM,OAAO;CAEhC,OAAO,OAAO,IAA6B,OAAO;AACpD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region ../queue/src/queue-dashboard-unguarded.error.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown at boot when `queue.dashboard.enabled` is `true` in production with
|
|
4
|
+
* no guard middleware. The dashboard can retry and delete jobs; mounting it
|
|
5
|
+
* on the open internet without a guard is a production incident waiting to
|
|
6
|
+
* happen, so this fails the boot instead of shipping the hole.
|
|
7
|
+
*/
|
|
8
|
+
declare class QueueDashboardUnguardedError extends Error {
|
|
9
|
+
constructor();
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { QueueDashboardUnguardedError };
|
|
13
|
+
//# sourceMappingURL=queue-dashboard-unguarded.error.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-dashboard-unguarded.error.d.mts","names":[],"sources":["../../../../../../queue/src/queue-dashboard-unguarded.error.ts"],"mappings":";;AAMA;;;;;cAAa,4BAAA,SAAqC,KAAK;EAAL,WAAA;AAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region ../queue/src/queue-dashboard-unguarded.error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown at boot when `queue.dashboard.enabled` is `true` in production with
|
|
4
|
+
* no guard middleware. The dashboard can retry and delete jobs; mounting it
|
|
5
|
+
* on the open internet without a guard is a production incident waiting to
|
|
6
|
+
* happen, so this fails the boot instead of shipping the hole.
|
|
7
|
+
*/
|
|
8
|
+
var QueueDashboardUnguardedError = class extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("queue.dashboard.enabled is true in production with no middleware. The dashboard can retry and delete jobs, so it must be guarded before it is exposed.\n\nAdd a guard middleware:\n\n import { middleware } from \"@warlock.js/core\";\n import { authMiddleware } from \"@warlock.js/auth\";\n\n const queueConfig: QueueConfig = {\n // ...\n dashboard: {\n enabled: true,\n middleware: [authMiddleware(\"admin\")],\n },\n };\n");
|
|
11
|
+
this.name = "QueueDashboardUnguardedError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { QueueDashboardUnguardedError };
|
|
17
|
+
//# sourceMappingURL=queue-dashboard-unguarded.error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-dashboard-unguarded.error.mjs","names":[],"sources":["../../../../../../queue/src/queue-dashboard-unguarded.error.ts"],"sourcesContent":["/**\n * Thrown at boot when `queue.dashboard.enabled` is `true` in production with\n * no guard middleware. The dashboard can retry and delete jobs; mounting it\n * on the open internet without a guard is a production incident waiting to\n * happen, so this fails the boot instead of shipping the hole.\n */\nexport class QueueDashboardUnguardedError extends Error {\n public constructor() {\n super(\n \"queue.dashboard.enabled is true in production with no middleware. The dashboard can \" +\n \"retry and delete jobs, so it must be guarded before it is exposed.\\n\\n\" +\n \"Add a guard middleware:\\n\\n\" +\n \" import { middleware } from \\\"@warlock.js/core\\\";\\n\" +\n \" import { authMiddleware } from \\\"@warlock.js/auth\\\";\\n\\n\" +\n \" const queueConfig: QueueConfig = {\\n\" +\n \" // ...\\n\" +\n \" dashboard: {\\n\" +\n \" enabled: true,\\n\" +\n \" middleware: [authMiddleware(\\\"admin\\\")],\\n\" +\n \" },\\n\" +\n \" };\\n\",\n );\n this.name = \"QueueDashboardUnguardedError\";\n }\n}\n"],"mappings":";;;;;;;AAMA,IAAa,+BAAb,cAAkD,MAAM;CACtD,AAAO,cAAc;EACnB,MACE,6bAYF;EACA,KAAK,OAAO;CACd;AACF"}
|
package/esm/types.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConnectionOptions } from "bullmq";
|
|
2
|
+
import { Middleware } from "@warlock.js/core";
|
|
2
3
|
|
|
3
4
|
//#region ../queue/src/types.d.ts
|
|
4
5
|
/** Units accepted in a {@link Duration} string. */
|
|
@@ -47,6 +48,21 @@ type QueueWorkersConfig = {
|
|
|
47
48
|
*/
|
|
48
49
|
shutdownTimeout?: number;
|
|
49
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* The bull-board dashboard, mounted automatically by `queueConnector()` when
|
|
53
|
+
* `enabled` is `true`. Equivalent to calling `queueDashboard()` yourself at
|
|
54
|
+
* boot, driven by config instead — see `warlock add bull-board`.
|
|
55
|
+
*/
|
|
56
|
+
type QueueDashboardConfig = {
|
|
57
|
+
/** Mount the dashboard at boot. Default `false`. */enabled?: boolean; /** URL path the dashboard is mounted on. Default `"/admin/queues"`. */
|
|
58
|
+
path?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Run before every dashboard route — this is how the dashboard is guarded.
|
|
61
|
+
* The dashboard can retry and delete jobs, so `NODE_ENV === "production"`
|
|
62
|
+
* with an empty list throws `QueueDashboardUnguardedError` at boot.
|
|
63
|
+
*/
|
|
64
|
+
middleware?: Middleware[];
|
|
65
|
+
};
|
|
50
66
|
/**
|
|
51
67
|
* The `queue` configuration key — `src/config/queue.ts`.
|
|
52
68
|
*/
|
|
@@ -59,7 +75,8 @@ type QueueConfig = {
|
|
|
59
75
|
prefix?: string; /** Queue name used when a job does not name one. Default `"default"`. */
|
|
60
76
|
defaultQueue?: string; /** Defaults merged under every job's own options. */
|
|
61
77
|
defaultJobOptions?: JobOptions; /** In-process workers. */
|
|
62
|
-
workers?: QueueWorkersConfig;
|
|
78
|
+
workers?: QueueWorkersConfig; /** The bull-board job dashboard. */
|
|
79
|
+
dashboard?: QueueDashboardConfig;
|
|
63
80
|
};
|
|
64
81
|
/** A progress value: a number (e.g. a percentage) or a JSON object. */
|
|
65
82
|
type JobProgress = number | Record<string, unknown>;
|
|
@@ -145,5 +162,5 @@ type QueueJob<TPayload, TResult = unknown> = {
|
|
|
145
162
|
find(id: string): Promise<JobSnapshot<TPayload, TResult> | undefined>;
|
|
146
163
|
};
|
|
147
164
|
//#endregion
|
|
148
|
-
export { DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QueueConfig, QueueJob, QueueWorkersConfig };
|
|
165
|
+
export { DispatchOptions, DispatchedJob, Duration, DurationUnit, FailedJob, JobBackoff, JobContext, JobDefinition, JobOptions, JobProgress, JobRetention, JobSnapshot, JobState, QueueConfig, QueueDashboardConfig, QueueJob, QueueWorkersConfig };
|
|
149
166
|
//# sourceMappingURL=types.d.mts.map
|
package/esm/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../queue/src/types.ts"],"mappings":"
|
|
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,UAM0B;EAJ1B,WAAA,UAUU;EARV,QAAA,CAAS,KAAA,EAAO,WAAA,GAAc,OAAA,QAQP;EANvB,GAAA,CAAI,IAAA,WAAe,OAAA;AAAA;;;;KAMT,aAAA,sBAAmC,UAAA;EAM6B,+CAJ1E,IAAA,UAFwB;EAIxB,KAAA,WAJ6C;EAM7C,MAAA,CAAO,OAAA,EAAS,QAAA,EAAU,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,OAAA,IAAW,OAAA;AAAA;;;;KAMzD,eAAA;EANgB,6CAQ1B,KAAA,GAAQ,QAAA;EARgD;;;AAAkB;EAa1E,QAAA;EAPyB;;;;EAYzB,KAAA,WALA;EAOA,QAAA;EAEA,OAAA,GAAU,UAAU;AAAA;;KAIV,aAAA;EACV,EAAA;EACA,IAAA;EACA,KAAA;AAAA;;KAIU,QAAA;;KAWA,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,EAnBX;EAqBA,KAAA,IAAS,OAAA;AAAA;;;;KAMC,QAAA;EAAA,SACD,IAAA;EAAA,SACA,KAAA,UAxBT;EA0BA,QAAA,CAAS,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,aAAA,GAzBrD;EA2BX,IAAA,CAAK,EAAA,WAAa,OAAA,CAAQ,WAAA,CAAY,QAAA,EAAU,OAAA;AAAA"}
|