@faapi/task-bullmq 0.0.0-canary.0 → 6.3.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/LICENSE +21 -0
- package/README.md +50 -1
- package/dist/index.d.ts +36 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 faapi contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
1
|
# @faapi/task-bullmq
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
faapi 任务子系统的 **BullMQ 驱动**(Redis 持久化队列)。
|
|
4
|
+
|
|
5
|
+
基于 Redis 的任务队列——任务不丢(持久化)、多实例天然防重跑(worker 领取制)、支持延迟任务与指数退避重试。适合已有 Redis 基础设施或需要高吞吐的项目。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @faapi/task-bullmq bullmq
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 使用
|
|
14
|
+
|
|
15
|
+
任务定义不变(主包文件约定 `src/tasks/<name>/task.ts`),只切驱动:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// faapi.config.ts
|
|
19
|
+
import type { FaapiConfig } from '@faapi/faapi';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
task: {
|
|
23
|
+
driver: 'bullmq',
|
|
24
|
+
bullmq: { connection: { host: '127.0.0.1', port: 6379 } },
|
|
25
|
+
// 可选:同 Redis 下多应用隔离
|
|
26
|
+
// bullmq: { connection: {...}, prefix: 'myapp' },
|
|
27
|
+
},
|
|
28
|
+
} satisfies FaapiConfig;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// handler 里用法完全不变
|
|
33
|
+
export function POST(body, tasks) {
|
|
34
|
+
return tasks.enqueue('send-email', { to: body.email });
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 语义映射
|
|
39
|
+
|
|
40
|
+
| faapi 驱动接口 | BullMQ |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `enqueue(name, payload, { retries, delayMs })` | `queue.add(name, payload, { attempts: retries + 1, backoff: exponential 500ms, delay })` |
|
|
43
|
+
| `startWorker(name, { concurrency, process })` | `new Worker(name, handler, { connection, concurrency, prefix })` |
|
|
44
|
+
| `stop(timeoutMs)` | workers.close() + queues.close()(race 超时) |
|
|
45
|
+
| `stopWorkers()` | 仅关 Worker(dev 热替换重注册用),Queue 连接保持 |
|
|
46
|
+
| 失败重试 | BullMQ 侧执行(attempts = retries + 1,指数退避 500ms 起) |
|
|
47
|
+
|
|
48
|
+
注意:BullMQ 不提供执行中任务的取消信号——`run` 的 `taskCtx.signal` 永不 abort,长任务请自行做超时控制。
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TaskDriver } from '@faapi/faapi';
|
|
2
|
+
import { ConnectionOptions } from 'bullmq';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BullMQ 驱动选项
|
|
6
|
+
*/
|
|
7
|
+
interface BullMQDriverOptions {
|
|
8
|
+
/** Redis 连接配置(透传给 Queue / Worker 的 connection) */
|
|
9
|
+
connection: ConnectionOptions;
|
|
10
|
+
/** 队列名前缀(默认 `faapi`——同一 Redis 下多个 faapi 应用隔离用) */
|
|
11
|
+
prefix?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* faapi 任务队列 BullMQ 驱动(Redis 持久化队列)
|
|
15
|
+
*
|
|
16
|
+
* 与 faapi 主包 `config.task.driver: 'bullmq'` 配合使用:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* // faapi.config.ts
|
|
20
|
+
* export default {
|
|
21
|
+
* task: {
|
|
22
|
+
* driver: 'bullmq',
|
|
23
|
+
* bullmq: { connection: { host: '127.0.0.1', port: 6379 } },
|
|
24
|
+
* },
|
|
25
|
+
* } satisfies FaapiConfig;
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* 语义映射(详见包根 README):
|
|
29
|
+
* - `enqueue` → `queue.add(name, payload, { delay, attempts, backoff })`(每任务一个 Queue)
|
|
30
|
+
* - `startWorker` → `new Worker(name, handler, { connection, concurrency })`
|
|
31
|
+
* - `stop` → workers.close() + queues.close()(等 in-flight;超时由 BullMQ 处置)
|
|
32
|
+
* - 重试 → BullMQ 侧执行(attempts = retries + 1,指数退避 500ms 起)
|
|
33
|
+
*/
|
|
34
|
+
declare function createBullMQDriver(options: BullMQDriverOptions): TaskDriver;
|
|
35
|
+
|
|
36
|
+
export { type BullMQDriverOptions, createBullMQDriver };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Queue, Worker } from "bullmq";
|
|
3
|
+
function createBullMQDriver(options) {
|
|
4
|
+
if (!options?.connection) {
|
|
5
|
+
throw new Error("[faapi] @faapi/task-bullmq requires `bullmq.connection` in config.task");
|
|
6
|
+
}
|
|
7
|
+
const prefix = options.prefix ?? "faapi";
|
|
8
|
+
let stopped = false;
|
|
9
|
+
const queues = /* @__PURE__ */ new Map();
|
|
10
|
+
const workers = /* @__PURE__ */ new Map();
|
|
11
|
+
function getQueue(name) {
|
|
12
|
+
let q = queues.get(name);
|
|
13
|
+
if (!q) {
|
|
14
|
+
q = new Queue(name, { connection: options.connection, prefix });
|
|
15
|
+
queues.set(name, q);
|
|
16
|
+
}
|
|
17
|
+
return q;
|
|
18
|
+
}
|
|
19
|
+
const attemptCounts = /* @__PURE__ */ new Map();
|
|
20
|
+
return {
|
|
21
|
+
async enqueue(name, payload, opts) {
|
|
22
|
+
if (stopped) {
|
|
23
|
+
throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
|
|
24
|
+
}
|
|
25
|
+
const queue = getQueue(name);
|
|
26
|
+
const job = await queue.add(name, payload, {
|
|
27
|
+
// BullMQ attempts 含首次执行,retries 是额外重试次数
|
|
28
|
+
attempts: (opts?.retries ?? 0) + 1,
|
|
29
|
+
backoff: { type: "exponential", delay: 500 },
|
|
30
|
+
...opts?.delayMs ? { delay: opts.delayMs } : {}
|
|
31
|
+
});
|
|
32
|
+
return job.id ?? crypto.randomUUID();
|
|
33
|
+
},
|
|
34
|
+
async startWorker(name, workerOpts) {
|
|
35
|
+
const process = workerOpts.process;
|
|
36
|
+
const existing = workers.get(name);
|
|
37
|
+
if (existing) {
|
|
38
|
+
await existing.close();
|
|
39
|
+
}
|
|
40
|
+
const worker = new Worker(
|
|
41
|
+
name,
|
|
42
|
+
async (job) => {
|
|
43
|
+
const attempt = (attemptCounts.get(job.id ?? "") ?? 0) + 1;
|
|
44
|
+
attemptCounts.set(job.id ?? "", attempt);
|
|
45
|
+
return await process({
|
|
46
|
+
id: job.id ?? "",
|
|
47
|
+
name,
|
|
48
|
+
payload: job.data,
|
|
49
|
+
attempt,
|
|
50
|
+
signal: new AbortController().signal
|
|
51
|
+
// BullMQ 不提供执行中任务的取消信号
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
{ connection: options.connection, concurrency: workerOpts.concurrency, prefix }
|
|
55
|
+
);
|
|
56
|
+
worker.on("error", (err) => {
|
|
57
|
+
console.error(`[faapi] bullmq worker error for task "${name}":`, err);
|
|
58
|
+
});
|
|
59
|
+
workers.set(name, worker);
|
|
60
|
+
},
|
|
61
|
+
async stop(timeoutMs = 1e4) {
|
|
62
|
+
stopped = true;
|
|
63
|
+
const closes = [
|
|
64
|
+
...Array.from(workers.values()).map((w) => w.close()),
|
|
65
|
+
...Array.from(queues.values()).map((q) => q.close())
|
|
66
|
+
];
|
|
67
|
+
workers.clear();
|
|
68
|
+
const timeout = new Promise((resolve) => {
|
|
69
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
70
|
+
timer.unref?.();
|
|
71
|
+
});
|
|
72
|
+
await Promise.race([Promise.all(closes), timeout]);
|
|
73
|
+
attemptCounts.clear();
|
|
74
|
+
},
|
|
75
|
+
async stopWorkers() {
|
|
76
|
+
const closes = Array.from(workers.values()).map((w) => w.close());
|
|
77
|
+
workers.clear();
|
|
78
|
+
await Promise.all(closes);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
createBullMQDriver
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { TaskDriver, TaskDriverProcess } from '@faapi/faapi';\nimport { Queue, Worker, type ConnectionOptions, type Job } from 'bullmq';\n\n/**\n * BullMQ 驱动选项\n */\nexport interface BullMQDriverOptions {\n /** Redis 连接配置(透传给 Queue / Worker 的 connection) */\n connection: ConnectionOptions;\n /** 队列名前缀(默认 `faapi`——同一 Redis 下多个 faapi 应用隔离用) */\n prefix?: string;\n}\n\n/**\n * faapi 任务队列 BullMQ 驱动(Redis 持久化队列)\n *\n * 与 faapi 主包 `config.task.driver: 'bullmq'` 配合使用:\n *\n * ```ts\n * // faapi.config.ts\n * export default {\n * task: {\n * driver: 'bullmq',\n * bullmq: { connection: { host: '127.0.0.1', port: 6379 } },\n * },\n * } satisfies FaapiConfig;\n * ```\n *\n * 语义映射(详见包根 README):\n * - `enqueue` → `queue.add(name, payload, { delay, attempts, backoff })`(每任务一个 Queue)\n * - `startWorker` → `new Worker(name, handler, { connection, concurrency })`\n * - `stop` → workers.close() + queues.close()(等 in-flight;超时由 BullMQ 处置)\n * - 重试 → BullMQ 侧执行(attempts = retries + 1,指数退避 500ms 起)\n */\nexport function createBullMQDriver(options: BullMQDriverOptions): TaskDriver {\n if (!options?.connection) {\n throw new Error('[faapi] @faapi/task-bullmq requires `bullmq.connection` in config.task');\n }\n const prefix = options.prefix ?? 'faapi';\n\n let stopped = false;\n /** 每任务一个 Queue(按名缓存) */\n const queues = new Map<string, Queue>();\n /** 已创建的 Worker(stopWorkers / stop 时关闭) */\n const workers = new Map<string, Worker>();\n\n function getQueue(name: string): Queue {\n let q = queues.get(name);\n if (!q) {\n q = new Queue(name, { connection: options.connection, prefix });\n queues.set(name, q);\n }\n return q;\n }\n\n /** 驱动侧 attempt 计数(兼容不同 BullMQ 版本的 attemptsMade/attemptsStarted 字段差异) */\n const attemptCounts = new Map<string, number>();\n\n return {\n async enqueue(name, payload, opts) {\n if (stopped) {\n throw new Error('[faapi] Task queue is stopped and no longer accepts jobs');\n }\n const queue = getQueue(name);\n const job = await queue.add(name, payload, {\n // BullMQ attempts 含首次执行,retries 是额外重试次数\n attempts: (opts?.retries ?? 0) + 1,\n backoff: { type: 'exponential', delay: 500 },\n ...(opts?.delayMs ? { delay: opts.delayMs } : {}),\n });\n return job.id ?? crypto.randomUUID();\n },\n\n async startWorker(name, workerOpts) {\n const process: TaskDriverProcess = workerOpts.process;\n // 二次注册(reload 场景)先关旧 Worker\n const existing = workers.get(name);\n if (existing) {\n await existing.close();\n }\n const worker = new Worker(\n name,\n async (job: Job) => {\n const attempt = (attemptCounts.get(job.id ?? '') ?? 0) + 1;\n attemptCounts.set(job.id ?? '', attempt);\n return await process({\n id: job.id ?? '',\n name,\n payload: job.data,\n attempt,\n signal: new AbortController().signal, // BullMQ 不提供执行中任务的取消信号\n });\n },\n { connection: options.connection, concurrency: workerOpts.concurrency, prefix },\n );\n // 处理器内部异常已由语义层包装记录;这里兜底防止 unhandled error 事件崩进程\n worker.on('error', (err) => {\n console.error(`[faapi] bullmq worker error for task \"${name}\":`, err);\n });\n workers.set(name, worker);\n },\n\n async stop(timeoutMs = 10_000) {\n stopped = true;\n const closes = [\n ...Array.from(workers.values()).map((w) => w.close()),\n ...Array.from(queues.values()).map((q) => q.close()),\n ];\n workers.clear();\n const timeout = new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, timeoutMs);\n timer.unref?.();\n });\n await Promise.race([Promise.all(closes), timeout]);\n attemptCounts.clear();\n },\n\n async stopWorkers() {\n const closes = Array.from(workers.values()).map((w) => w.close());\n workers.clear();\n await Promise.all(closes);\n },\n };\n}\n"],"mappings":";AACA,SAAS,OAAO,cAAgD;AAiCzD,SAAS,mBAAmB,SAA0C;AAC3E,MAAI,CAAC,SAAS,YAAY;AACxB,UAAM,IAAI,MAAM,wEAAwE;AAAA,EAC1F;AACA,QAAM,SAAS,QAAQ,UAAU;AAEjC,MAAI,UAAU;AAEd,QAAM,SAAS,oBAAI,IAAmB;AAEtC,QAAM,UAAU,oBAAI,IAAoB;AAExC,WAAS,SAAS,MAAqB;AACrC,QAAI,IAAI,OAAO,IAAI,IAAI;AACvB,QAAI,CAAC,GAAG;AACN,UAAI,IAAI,MAAM,MAAM,EAAE,YAAY,QAAQ,YAAY,OAAO,CAAC;AAC9D,aAAO,IAAI,MAAM,CAAC;AAAA,IACpB;AACA,WAAO;AAAA,EACT;AAGA,QAAM,gBAAgB,oBAAI,IAAoB;AAE9C,SAAO;AAAA,IACL,MAAM,QAAQ,MAAM,SAAS,MAAM;AACjC,UAAI,SAAS;AACX,cAAM,IAAI,MAAM,0DAA0D;AAAA,MAC5E;AACA,YAAM,QAAQ,SAAS,IAAI;AAC3B,YAAM,MAAM,MAAM,MAAM,IAAI,MAAM,SAAS;AAAA;AAAA,QAEzC,WAAW,MAAM,WAAW,KAAK;AAAA,QACjC,SAAS,EAAE,MAAM,eAAe,OAAO,IAAI;AAAA,QAC3C,GAAI,MAAM,UAAU,EAAE,OAAO,KAAK,QAAQ,IAAI,CAAC;AAAA,MACjD,CAAC;AACD,aAAO,IAAI,MAAM,OAAO,WAAW;AAAA,IACrC;AAAA,IAEA,MAAM,YAAY,MAAM,YAAY;AAClC,YAAM,UAA6B,WAAW;AAE9C,YAAM,WAAW,QAAQ,IAAI,IAAI;AACjC,UAAI,UAAU;AACZ,cAAM,SAAS,MAAM;AAAA,MACvB;AACA,YAAM,SAAS,IAAI;AAAA,QACjB;AAAA,QACA,OAAO,QAAa;AAClB,gBAAM,WAAW,cAAc,IAAI,IAAI,MAAM,EAAE,KAAK,KAAK;AACzD,wBAAc,IAAI,IAAI,MAAM,IAAI,OAAO;AACvC,iBAAO,MAAM,QAAQ;AAAA,YACnB,IAAI,IAAI,MAAM;AAAA,YACd;AAAA,YACA,SAAS,IAAI;AAAA,YACb;AAAA,YACA,QAAQ,IAAI,gBAAgB,EAAE;AAAA;AAAA,UAChC,CAAC;AAAA,QACH;AAAA,QACA,EAAE,YAAY,QAAQ,YAAY,aAAa,WAAW,aAAa,OAAO;AAAA,MAChF;AAEA,aAAO,GAAG,SAAS,CAAC,QAAQ;AAC1B,gBAAQ,MAAM,yCAAyC,IAAI,MAAM,GAAG;AAAA,MACtE,CAAC;AACD,cAAQ,IAAI,MAAM,MAAM;AAAA,IAC1B;AAAA,IAEA,MAAM,KAAK,YAAY,KAAQ;AAC7B,gBAAU;AACV,YAAM,SAAS;AAAA,QACb,GAAG,MAAM,KAAK,QAAQ,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AAAA,QACpD,GAAG,MAAM,KAAK,OAAO,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AAAA,MACrD;AACA,cAAQ,MAAM;AACd,YAAM,UAAU,IAAI,QAAc,CAAC,YAAY;AAC7C,cAAM,QAAQ,WAAW,SAAS,SAAS;AAC3C,cAAM,QAAQ;AAAA,MAChB,CAAC;AACD,YAAM,QAAQ,KAAK,CAAC,QAAQ,IAAI,MAAM,GAAG,OAAO,CAAC;AACjD,oBAAc,MAAM;AAAA,IACtB;AAAA,IAEA,MAAM,cAAc;AAClB,YAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AAChE,cAAQ,MAAM;AACd,YAAM,QAAQ,IAAI,MAAM;AAAA,IAC1B;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faapi/task-bullmq",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "6.3.0",
|
|
4
|
+
"description": "BullMQ task queue driver for faapi — persistent queue on Redis",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./index.js",
|
|
7
|
-
"
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.3.0",
|
|
23
|
+
"bullmq": "^5.58.5",
|
|
24
|
+
"tsup": "^8.4.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"vitest": "^4.1.11",
|
|
27
|
+
"@faapi/faapi": "6.3.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"bullmq": "^5.0.0",
|
|
31
|
+
"@faapi/faapi": "^6.3.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/faapi/faapi.git",
|
|
37
|
+
"directory": "packages/task-bullmq"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/faapi/faapi/issues"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"faapi",
|
|
44
|
+
"task-queue",
|
|
45
|
+
"bullmq",
|
|
46
|
+
"redis",
|
|
47
|
+
"driver"
|
|
48
|
+
],
|
|
49
|
+
"sideEffects": false,
|
|
8
50
|
"publishConfig": {
|
|
9
|
-
"access": "public"
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup",
|
|
56
|
+
"test": "vitest run --passWithNoTests",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"typecheck:test": "tsc --noEmit -p tsconfig.test.json"
|
|
10
59
|
}
|
|
11
|
-
}
|
|
60
|
+
}
|