@faapi/task-bullmq 6.33.0 → 6.34.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/README.md +1 -0
- package/dist/index.js +230 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ export function POST(body, tasks) {
|
|
|
46
46
|
| `startWorker(name, { concurrency, process })` | `new Worker(name, handler, { connection, concurrency, prefix })`;attempt 取 `job.attemptsStarted`(跨实例/重启准确) |
|
|
47
47
|
| `stop(timeoutMs)` | workers.close() + queues.close()(race 超时),超时 abort 在跑任务的 signal |
|
|
48
48
|
| `stopWorkers()` | 仅关 Worker(dev 热替换重注册用),Queue 连接保持 |
|
|
49
|
+
| `groups`(任务组记账,语义见主包 taskGroups.md) | 两组 Redis key:`<prefix>:group:<id>`(组行 hash)+ `<prefix>:group-members:<id>`(成员行 hash,jobId→状态幂等守卫),常驻不自动清理;经 `defineCommand`/`runCommand`(BullMQ 类型化 Lua 扩展口,专用连接复用连接池不加新连接)原子计账。成员组标识以载荷包装随 data 存储(`{ __faapiGroup, __faapiPayload }`,交付/查询还原);`cancelRemaining` 仅对 waiting/delayed 成员 `job.remove()`,移除成功才落定 |
|
|
49
50
|
| 失败重试 | BullMQ 侧执行(attempts = retries + 1,指数退避 500ms 起) |
|
|
50
51
|
|
|
51
52
|
注意:BullMQ 不提供执行中任务的取消信号,但 `stop` 超时路径会 abort `taskCtx.signal`(任务监听 signal 可尽快退出)。
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,72 @@ import {
|
|
|
4
4
|
Worker
|
|
5
5
|
} from "bullmq";
|
|
6
6
|
var DEFAULT_KEEP_AGE_SECONDS = 7 * 24 * 3600;
|
|
7
|
+
var GROUP_KEY = "__faapiGroup";
|
|
8
|
+
var PAYLOAD_KEY = "__faapiPayload";
|
|
9
|
+
function wrapGroupPayload(groupId, payload) {
|
|
10
|
+
return { [GROUP_KEY]: groupId, [PAYLOAD_KEY]: payload };
|
|
11
|
+
}
|
|
12
|
+
function unwrapGroupPayload(data) {
|
|
13
|
+
if (data !== null && typeof data === "object" && !Array.isArray(data) && data[GROUP_KEY] !== void 0 && typeof data[GROUP_KEY] === "string" && data[PAYLOAD_KEY] !== void 0 && Object.keys(data).length === 2) {
|
|
14
|
+
const wrapped = data;
|
|
15
|
+
return { payload: wrapped[PAYLOAD_KEY], groupId: wrapped[GROUP_KEY] };
|
|
16
|
+
}
|
|
17
|
+
return { payload: data };
|
|
18
|
+
}
|
|
19
|
+
var LUA_CREATE = `
|
|
20
|
+
if redis.call('EXISTS', KEYS[1]) == 1 then
|
|
21
|
+
if redis.call('HGET', KEYS[1], 'task') == ARGV[1]
|
|
22
|
+
and redis.call('HGET', KEYS[1], 'total') == ARGV[2]
|
|
23
|
+
and (redis.call('HGET', KEYS[1], 'onComplete') or '') == ARGV[3]
|
|
24
|
+
and redis.call('HGET', KEYS[1], 'onFailure') == ARGV[4] then
|
|
25
|
+
return 0
|
|
26
|
+
end
|
|
27
|
+
return -1
|
|
28
|
+
end
|
|
29
|
+
redis.call('HSET', KEYS[1],
|
|
30
|
+
'task', ARGV[1], 'total', ARGV[2], 'onComplete', ARGV[3], 'onFailure', ARGV[4],
|
|
31
|
+
'done', 0, 'failed', 0, 'cancelled', 0, 'completionEnqueued', 0)
|
|
32
|
+
return 1`;
|
|
33
|
+
var LUA_SETTLE = `
|
|
34
|
+
if redis.call('EXISTS', KEYS[1]) == 0 then return {-1} end
|
|
35
|
+
local prev = redis.call('HGET', KEYS[2], ARGV[1])
|
|
36
|
+
if prev and prev ~= 'pending' then
|
|
37
|
+
return {0,
|
|
38
|
+
tonumber(redis.call('HGET', KEYS[1], 'done')) or 0,
|
|
39
|
+
tonumber(redis.call('HGET', KEYS[1], 'failed')) or 0,
|
|
40
|
+
tonumber(redis.call('HGET', KEYS[1], 'cancelled')) or 0,
|
|
41
|
+
tonumber(redis.call('HGET', KEYS[1], 'total')) or 0,
|
|
42
|
+
redis.call('HGET', KEYS[1], 'onComplete') or '',
|
|
43
|
+
redis.call('HGET', KEYS[1], 'onFailure') or 'run-to-completion',
|
|
44
|
+
tonumber(redis.call('HGET', KEYS[1], 'completionEnqueued')) or 0}
|
|
45
|
+
end
|
|
46
|
+
redis.call('HSET', KEYS[2], ARGV[1], 's:' .. ARGV[2])
|
|
47
|
+
redis.call('HINCRBY', KEYS[1], ARGV[2], 1)
|
|
48
|
+
return {1,
|
|
49
|
+
tonumber(redis.call('HGET', KEYS[1], 'done')) or 0,
|
|
50
|
+
tonumber(redis.call('HGET', KEYS[1], 'failed')) or 0,
|
|
51
|
+
tonumber(redis.call('HGET', KEYS[1], 'cancelled')) or 0,
|
|
52
|
+
tonumber(redis.call('HGET', KEYS[1], 'total')) or 0,
|
|
53
|
+
redis.call('HGET', KEYS[1], 'onComplete') or '',
|
|
54
|
+
redis.call('HGET', KEYS[1], 'onFailure') or 'run-to-completion',
|
|
55
|
+
tonumber(redis.call('HGET', KEYS[1], 'completionEnqueued')) or 0}`;
|
|
56
|
+
var LUA_UNSETTLE = `
|
|
57
|
+
if redis.call('EXISTS', KEYS[1]) == 0 then return -1 end
|
|
58
|
+
local prev = redis.call('HGET', KEYS[2], ARGV[1])
|
|
59
|
+
if not prev or string.sub(prev, 1, 2) ~= 's:' then return 0 end
|
|
60
|
+
redis.call('HSET', KEYS[2], ARGV[1], 'pending')
|
|
61
|
+
redis.call('HINCRBY', KEYS[1], string.sub(prev, 3), -1)
|
|
62
|
+
return 1`;
|
|
63
|
+
var LUA_HSETNX = `
|
|
64
|
+
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 then
|
|
65
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
66
|
+
return 1
|
|
67
|
+
end
|
|
68
|
+
return 0`;
|
|
69
|
+
var GROUP_CREATE_CMD = "faapiGroupCreate";
|
|
70
|
+
var GROUP_SETTLE_CMD = "faapiGroupSettle";
|
|
71
|
+
var GROUP_UNSETTLE_CMD = "faapiGroupUnsettle";
|
|
72
|
+
var GROUP_HSETNX_CMD = "faapiGroupMemberHsetnx";
|
|
7
73
|
function createBullMQDriver(options) {
|
|
8
74
|
if (!options?.connection) {
|
|
9
75
|
throw new Error("[faapi] @faapi/task-bullmq requires `bullmq.connection` in config.task");
|
|
@@ -23,24 +89,167 @@ function createBullMQDriver(options) {
|
|
|
23
89
|
}
|
|
24
90
|
return q;
|
|
25
91
|
}
|
|
92
|
+
const groupKey = (groupId) => `${prefix}:group:${groupId}`;
|
|
93
|
+
const groupMembersKey = (groupId) => `${prefix}:group-members:${groupId}`;
|
|
94
|
+
let groupOpsQueue = null;
|
|
95
|
+
async function groupClient() {
|
|
96
|
+
if (!groupOpsQueue) {
|
|
97
|
+
groupOpsQueue = new Queue("__faapi_group_ops__", {
|
|
98
|
+
connection: options.connection,
|
|
99
|
+
prefix
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const client = await groupOpsQueue.backend.client;
|
|
103
|
+
client.defineCommand(GROUP_CREATE_CMD, { numberOfKeys: 2, lua: LUA_CREATE });
|
|
104
|
+
client.defineCommand(GROUP_SETTLE_CMD, { numberOfKeys: 2, lua: LUA_SETTLE });
|
|
105
|
+
client.defineCommand(GROUP_UNSETTLE_CMD, { numberOfKeys: 2, lua: LUA_UNSETTLE });
|
|
106
|
+
client.defineCommand(GROUP_HSETNX_CMD, { numberOfKeys: 1, lua: LUA_HSETNX });
|
|
107
|
+
return client;
|
|
108
|
+
}
|
|
109
|
+
function settleRowToSnapshot(groupId, row) {
|
|
110
|
+
const num = (i) => Number(row[i]);
|
|
111
|
+
const done = num(1);
|
|
112
|
+
const failed = num(2);
|
|
113
|
+
const cancelled = num(3);
|
|
114
|
+
const total = num(4);
|
|
115
|
+
const onComplete = String(row[5] ?? "");
|
|
116
|
+
const onFailure = String(row[6] ?? "run-to-completion");
|
|
117
|
+
const completionEnqueued = num(7) === 1;
|
|
118
|
+
const settled = done + failed + cancelled;
|
|
119
|
+
return {
|
|
120
|
+
groupId,
|
|
121
|
+
task: "",
|
|
122
|
+
total,
|
|
123
|
+
done,
|
|
124
|
+
failed,
|
|
125
|
+
cancelled,
|
|
126
|
+
settled,
|
|
127
|
+
status: settled >= total ? "settled" : "open",
|
|
128
|
+
completionEnqueued,
|
|
129
|
+
...onComplete !== "" ? { onComplete } : {},
|
|
130
|
+
onFailure
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function createGroupOps() {
|
|
134
|
+
return {
|
|
135
|
+
async create(decl) {
|
|
136
|
+
const c = await groupClient();
|
|
137
|
+
const result = await c.runCommand(GROUP_CREATE_CMD, [
|
|
138
|
+
groupKey(decl.id),
|
|
139
|
+
groupMembersKey(decl.id),
|
|
140
|
+
decl.task,
|
|
141
|
+
String(decl.total),
|
|
142
|
+
decl.onComplete ?? "",
|
|
143
|
+
decl.onFailure
|
|
144
|
+
]);
|
|
145
|
+
if (result === 0) return;
|
|
146
|
+
if (result === -1) {
|
|
147
|
+
const existing = await c.hgetall(groupKey(decl.id));
|
|
148
|
+
throw new Error(
|
|
149
|
+
`[faapi] Task group "${decl.id}" already exists with different options (existing: task=${existing.task}, total=${existing.total}, onComplete=${existing.onComplete || "(none)"}, onFailure=${existing.onFailure})`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
async settle(groupId, jobId, outcome) {
|
|
154
|
+
const c = await groupClient();
|
|
155
|
+
const row = await c.runCommand(GROUP_SETTLE_CMD, [
|
|
156
|
+
groupKey(groupId),
|
|
157
|
+
groupMembersKey(groupId),
|
|
158
|
+
jobId,
|
|
159
|
+
outcome
|
|
160
|
+
]);
|
|
161
|
+
if (Number(row[0]) === -1) {
|
|
162
|
+
throw new Error(`[faapi] Task group "${groupId}" not found`);
|
|
163
|
+
}
|
|
164
|
+
const snap = settleRowToSnapshot(groupId, row);
|
|
165
|
+
return { ...snap, isLast: snap.settled >= snap.total };
|
|
166
|
+
},
|
|
167
|
+
async unsettle(groupId, jobId) {
|
|
168
|
+
const c = await groupClient();
|
|
169
|
+
await c.runCommand(GROUP_UNSETTLE_CMD, [
|
|
170
|
+
groupKey(groupId),
|
|
171
|
+
groupMembersKey(groupId),
|
|
172
|
+
jobId
|
|
173
|
+
]);
|
|
174
|
+
},
|
|
175
|
+
async markCompletionEnqueued(groupId) {
|
|
176
|
+
const c = await groupClient();
|
|
177
|
+
await c.hset(groupKey(groupId), { completionEnqueued: 1 });
|
|
178
|
+
},
|
|
179
|
+
async get(groupId) {
|
|
180
|
+
const c = await groupClient();
|
|
181
|
+
const row = await c.hgetall(groupKey(groupId));
|
|
182
|
+
if (!row || Object.keys(row).length === 0) return void 0;
|
|
183
|
+
const done = Number(row.done ?? 0);
|
|
184
|
+
const failed = Number(row.failed ?? 0);
|
|
185
|
+
const cancelled = Number(row.cancelled ?? 0);
|
|
186
|
+
const total = Number(row.total ?? 0);
|
|
187
|
+
const settled = done + failed + cancelled;
|
|
188
|
+
return {
|
|
189
|
+
groupId,
|
|
190
|
+
task: row.task ?? "",
|
|
191
|
+
total,
|
|
192
|
+
done,
|
|
193
|
+
failed,
|
|
194
|
+
cancelled,
|
|
195
|
+
settled,
|
|
196
|
+
status: settled >= total ? "settled" : "open",
|
|
197
|
+
completionEnqueued: row.completionEnqueued === "1",
|
|
198
|
+
...row.onComplete ? { onComplete: row.onComplete } : {},
|
|
199
|
+
onFailure: row.onFailure ?? "run-to-completion"
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
async cancelRemaining(groupId) {
|
|
203
|
+
const snap = await this.get(groupId);
|
|
204
|
+
if (snap === void 0) {
|
|
205
|
+
throw new Error(`[faapi] Task group "${groupId}" not found`);
|
|
206
|
+
}
|
|
207
|
+
if (snap.onFailure !== "fail-fast") return snap;
|
|
208
|
+
const c = await groupClient();
|
|
209
|
+
const members = await c.hgetall(groupMembersKey(groupId));
|
|
210
|
+
for (const [jobId, memberState] of Object.entries(members)) {
|
|
211
|
+
if (memberState !== "pending") continue;
|
|
212
|
+
const job = await getQueue(snap.task).getJob(jobId);
|
|
213
|
+
if (!job) continue;
|
|
214
|
+
const jobState = await job.getState();
|
|
215
|
+
if (jobState === "waiting" || jobState === "delayed") {
|
|
216
|
+
await job.remove();
|
|
217
|
+
await this.settle(groupId, jobId, "cancelled");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const after = await this.get(groupId);
|
|
221
|
+
return after;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
26
225
|
return {
|
|
27
226
|
async enqueue(name, payload, opts) {
|
|
28
227
|
if (stopped) {
|
|
29
228
|
throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
|
|
30
229
|
}
|
|
31
230
|
const queue = getQueue(name);
|
|
32
|
-
const job = await queue.add(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
231
|
+
const job = await queue.add(
|
|
232
|
+
name,
|
|
233
|
+
// 组投递成员以载荷包装携带组标识(交付时还原,业务 payload 不变)
|
|
234
|
+
opts?.groupId !== void 0 ? wrapGroupPayload(opts.groupId, payload) : payload,
|
|
235
|
+
{
|
|
236
|
+
// BullMQ attempts 含首次执行,retries 是额外重试次数
|
|
237
|
+
attempts: (opts?.retries ?? 0) + 1,
|
|
238
|
+
backoff: { type: "exponential", delay: 500 },
|
|
239
|
+
// 终态任务定时移除:防 Redis 无界增长 + 限定 dedupId 去重的存活期
|
|
240
|
+
removeOnComplete,
|
|
241
|
+
removeOnFail,
|
|
242
|
+
// dedupId 幂等键:同 jobId 的 job 存活期内 add 被忽略(BullMQ 原生去重)
|
|
243
|
+
...opts?.dedupId ? { jobId: opts.dedupId } : {},
|
|
244
|
+
...opts?.delayMs ? { delay: opts.delayMs } : {}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
const id = job.id ?? crypto.randomUUID();
|
|
248
|
+
if (opts?.groupId !== void 0) {
|
|
249
|
+
const c = await groupClient();
|
|
250
|
+
await c.runCommand(GROUP_HSETNX_CMD, [groupMembersKey(opts.groupId), id, "pending"]);
|
|
251
|
+
}
|
|
252
|
+
return id;
|
|
44
253
|
},
|
|
45
254
|
async startWorker(name, workerOpts) {
|
|
46
255
|
const process = workerOpts.process;
|
|
@@ -53,15 +262,17 @@ function createBullMQDriver(options) {
|
|
|
53
262
|
async (job) => {
|
|
54
263
|
const id = job.id ?? "";
|
|
55
264
|
const attempt = job.attemptsStarted;
|
|
265
|
+
const { payload, groupId } = unwrapGroupPayload(job.data);
|
|
56
266
|
const controller = new AbortController();
|
|
57
267
|
inflight.set(id, controller);
|
|
58
268
|
try {
|
|
59
269
|
return await process({
|
|
60
270
|
id,
|
|
61
271
|
name,
|
|
62
|
-
payload
|
|
272
|
+
payload,
|
|
63
273
|
attempt,
|
|
64
|
-
signal: controller.signal
|
|
274
|
+
signal: controller.signal,
|
|
275
|
+
...groupId !== void 0 ? { groupId } : {}
|
|
65
276
|
});
|
|
66
277
|
} finally {
|
|
67
278
|
inflight.delete(id);
|
|
@@ -120,14 +331,16 @@ function createBullMQDriver(options) {
|
|
|
120
331
|
const jobs = await queue.getJobs(types, 0, limit - 1);
|
|
121
332
|
for (const job of jobs) {
|
|
122
333
|
if (!job) continue;
|
|
334
|
+
const { payload, groupId } = unwrapGroupPayload(job.data);
|
|
123
335
|
records.push({
|
|
124
336
|
id: job.id ?? "",
|
|
125
337
|
name: job.name,
|
|
126
|
-
payload
|
|
338
|
+
payload,
|
|
127
339
|
status,
|
|
128
340
|
attempts: job.attemptsStarted ?? job.attemptsMade ?? 0,
|
|
129
341
|
...job.returnvalue !== void 0 && job.returnvalue !== null ? { result: job.returnvalue } : {},
|
|
130
342
|
...job.failedReason ? { error: job.failedReason } : {},
|
|
343
|
+
...groupId !== void 0 ? { groupId } : {},
|
|
131
344
|
createdAt: job.timestamp,
|
|
132
345
|
...job.processedOn ? { runAt: job.processedOn } : {}
|
|
133
346
|
});
|
|
@@ -156,7 +369,8 @@ function createBullMQDriver(options) {
|
|
|
156
369
|
throw new Error(`[faapi] Task "${name}" job "${id}" not found`);
|
|
157
370
|
}
|
|
158
371
|
await job.retry();
|
|
159
|
-
}
|
|
372
|
+
},
|
|
373
|
+
groups: createGroupOps()
|
|
160
374
|
};
|
|
161
375
|
}
|
|
162
376
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { TaskDriver, TaskDriverProcess, TaskDriverRecord, TaskJobStatus } from '@faapi/faapi';\nimport {\n Queue,\n Worker,\n type ConnectionOptions,\n type Job,\n type JobType,\n type KeepJobs,\n} 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 * 终态(completed)任务清理策略,透传 BullMQ `removeOnComplete`。\n * 默认 7 天后移除——BullMQ 默认永久保留终态任务,Redis 无界增长,且 dedupId\n * (jobId)去重在任务存活期内一直生效,周期性复用同一 dedupId 的投递(如\n * nightly-sync)会被永久静默忽略。传 `false` 恢复 BullMQ 默认(永不清理)。\n */\n removeOnComplete?: boolean | number | KeepJobs;\n /** 终态(failed)任务清理策略,透传 BullMQ `removeOnFail`,默认同上 */\n removeOnFail?: boolean | number | KeepJobs;\n}\n\n/** 终态任务默认保留秒数(7 天,与 pg-boss 默认保留策略同量级) */\nconst DEFAULT_KEEP_AGE_SECONDS = 7 * 24 * 3600;\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, removeOnComplete, removeOnFail })`(每任务一个 Queue)\n * - `startWorker` → `new Worker(name, handler, { connection, concurrency })`;attempt 取 job.attemptsStarted\n * - `stop` → workers.close() + queues.close()(等 in-flight;超时 abort 在跑任务的 signal)\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 /** 在跑任务的取消控制器(stop 超时 abort——run 监听 signal 可尽快退出) */\n const inflight = new Map<string, AbortController>();\n /** 终态清理策略(默认 7 天移除,false 透传恢复 BullMQ 永不清理) */\n const removeOnComplete = options.removeOnComplete ?? { age: DEFAULT_KEEP_AGE_SECONDS };\n const removeOnFail = options.removeOnFail ?? { age: DEFAULT_KEEP_AGE_SECONDS };\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 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 // 终态任务定时移除:防 Redis 无界增长 + 限定 dedupId 去重的存活期\n removeOnComplete,\n removeOnFail,\n // dedupId 幂等键:同 jobId 的 job 存活期内 add 被忽略(BullMQ 原生去重)\n ...(opts?.dedupId ? { jobId: opts.dedupId } : {}),\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 id = job.id ?? '';\n // attemptsStarted 由 BullMQ 维护(首次执行为 1,重试递增,跨实例/重启准确)——\n // 进程内自计数会内存泄漏且多实例/重启后失真\n const attempt = job.attemptsStarted;\n // 信号由驱动自管:stop 超时 abort(BullMQ 自身不提供执行中任务的取消能力)\n const controller = new AbortController();\n inflight.set(id, controller);\n try {\n return await process({\n id,\n name,\n payload: job.data,\n attempt,\n signal: controller.signal,\n });\n } finally {\n inflight.delete(id);\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 // 等待超时到点 abort 在跑任务(run 监听 signal 可尽快退出);等待结束后兜底 abort 残留\n const abortTimer = setTimeout(() => {\n for (const controller of inflight.values()) controller.abort();\n }, timeoutMs);\n try {\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 } finally {\n clearTimeout(abortTimer);\n for (const controller of inflight.values()) controller.abort();\n inflight.clear();\n }\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 async list(opts) {\n const limit = opts?.limit ?? 50;\n // 目标队列:指定 name 时按需创建 Queue 实例;不传时遍历本进程已创建的\n // (BullMQ 队列按 Redis key 寻址,额外实例不影响驱动内的队列)\n const targets: Queue[] = opts?.name ? [getQueue(opts.name)] : [...queues.values()];\n // faapi 语义状态 → BullMQ JobType 分组(cancel 为 job.remove,无 cancelled 可查;\n // 重试等待中的 job 处于 delayed → 归入 pending)\n const stateTypes: Partial<Record<TaskJobStatus, JobType[]>> = {\n pending: ['waiting', 'delayed'],\n running: ['active'],\n done: ['completed'],\n failed: ['failed'],\n };\n const wanted = (opts?.state ? [opts.state] : ['pending', 'running', 'done', 'failed']).filter(\n (s): s is TaskJobStatus => (stateTypes[s as TaskJobStatus]?.length ?? 0) > 0,\n );\n\n const records: TaskDriverRecord[] = [];\n for (const status of wanted) {\n const types = stateTypes[status]!;\n for (const queue of targets) {\n const jobs = await queue.getJobs(types, 0, limit - 1);\n for (const job of jobs) {\n if (!job) continue;\n records.push({\n id: job.id ?? '',\n name: job.name,\n payload: job.data,\n status,\n attempts: job.attemptsStarted ?? job.attemptsMade ?? 0,\n ...(job.returnvalue !== undefined && job.returnvalue !== null\n ? { result: job.returnvalue }\n : {}),\n ...(job.failedReason ? { error: job.failedReason } : {}),\n createdAt: job.timestamp,\n ...(job.processedOn ? { runAt: job.processedOn } : {}),\n });\n }\n }\n }\n return records.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);\n },\n\n async cancel(name, id) {\n if (stopped) {\n throw new Error(\n '[faapi] Task queue is stopped and no longer accepts management operations',\n );\n }\n // BullMQ 无 cancelled 状态——取消即移除(等待/延迟中的不再执行;active 受锁限制由 BullMQ 抛错)\n const job = await getQueue(name).getJob(id);\n if (job) await job.remove();\n },\n\n async retry(name, id) {\n if (stopped) {\n throw new Error(\n '[faapi] Task queue is stopped and no longer accepts management operations',\n );\n }\n const job = await getQueue(name).getJob(id);\n if (!job) {\n throw new Error(`[faapi] Task \"${name}\" job \"${id}\" not found`);\n }\n await job.retry(); // 仅 failed 可重试;其余状态由 BullMQ 抛错\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type {\n TaskDriver,\n TaskDriverGroupCreate,\n TaskDriverGroupOps,\n TaskDriverProcess,\n TaskDriverRecord,\n TaskGroupSnapshot,\n TaskJobStatus,\n} from '@faapi/faapi';\nimport {\n Queue,\n Worker,\n type ConnectionOptions,\n type Job,\n type JobType,\n type KeepJobs,\n} 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 * 终态(completed)任务清理策略,透传 BullMQ `removeOnComplete`。\n * 默认 7 天后移除——BullMQ 默认永久保留终态任务,Redis 无界增长,且 dedupId\n * (jobId)去重在任务存活期内一直生效,周期性复用同一 dedupId 的投递(如\n * nightly-sync)会被永久静默忽略。传 `false` 恢复 BullMQ 默认(永不清理)。\n */\n removeOnComplete?: boolean | number | KeepJobs;\n /** 终态(failed)任务清理策略,透传 BullMQ `removeOnFail`,默认同上 */\n removeOnFail?: boolean | number | KeepJobs;\n}\n\n/** 终态任务默认保留秒数(7 天,与 pg-boss 默认保留策略同量级) */\nconst DEFAULT_KEEP_AGE_SECONDS = 7 * 24 * 3600;\n\n/**\n * 成员组标识的载荷包装(组投递成员的传输载体)\n *\n * BullMQ 的 job 除 data(payload)外无任意元数据字段,组标识随载荷一起存储:\n * `enqueue(opts.groupId)` 时包装、worker 交付 / getJobs 查询时还原。仅组任务包装\n * (存量行为不变);包装对业务不可见,`TaskDriverRecord.payload` 恒为业务原始\n * payload。业务 payload 恰为该形状(两保留键 + 无其他键)会被误解包——保留键为\n * 框架命名空间,业务 payload 不应占用。\n */\nconst GROUP_KEY = '__faapiGroup';\nconst PAYLOAD_KEY = '__faapiPayload';\n\nfunction wrapGroupPayload(groupId: string, payload: unknown): object {\n return { [GROUP_KEY]: groupId, [PAYLOAD_KEY]: payload };\n}\n\nfunction unwrapGroupPayload(data: unknown): { payload: unknown; groupId?: string } {\n if (\n data !== null &&\n typeof data === 'object' &&\n !Array.isArray(data) &&\n (data as Record<string, unknown>)[GROUP_KEY] !== undefined &&\n typeof (data as Record<string, unknown>)[GROUP_KEY] === 'string' &&\n (data as Record<string, unknown>)[PAYLOAD_KEY] !== undefined &&\n Object.keys(data as Record<string, unknown>).length === 2\n ) {\n const wrapped = data as Record<string, unknown>;\n return { payload: wrapped[PAYLOAD_KEY], groupId: wrapped[GROUP_KEY] as string };\n }\n return { payload: data };\n}\n\n/**\n * 任务组记账存储(两组 Redis key,经 `Queue.client` 复用驱动连接——不加新连接):\n * - `<prefix>:group:<id>`:组行(task/total/计数器/onComplete/onFailure/completionEnqueued)\n * - `<prefix>:group-members:<id>`:成员行(jobId → 'pending' | 's:<outcome>'),\n * 落定记账的幂等守卫(同一成员重复 settle 不重复计数)\n *\n * 组记录常驻 Redis(框架不自动清理——自动删业务可能还要查的记账是静默丢数据),\n * 业务方按保留策略自行清理。\n */\n\n/** 组记账 Lua:原子 create(同 id 参数一致幂等返回 0 / 不一致返回 -1) */\nconst LUA_CREATE = `\nif redis.call('EXISTS', KEYS[1]) == 1 then\n if redis.call('HGET', KEYS[1], 'task') == ARGV[1]\n and redis.call('HGET', KEYS[1], 'total') == ARGV[2]\n and (redis.call('HGET', KEYS[1], 'onComplete') or '') == ARGV[3]\n and redis.call('HGET', KEYS[1], 'onFailure') == ARGV[4] then\n return 0\n end\n return -1\nend\nredis.call('HSET', KEYS[1],\n 'task', ARGV[1], 'total', ARGV[2], 'onComplete', ARGV[3], 'onFailure', ARGV[4],\n 'done', 0, 'failed', 0, 'cancelled', 0, 'completionEnqueued', 0)\nreturn 1`;\n\n/** 组记账 Lua:原子 settle(成员行守卫幂等 + 计数递增;返回记账后计数) */\nconst LUA_SETTLE = `\nif redis.call('EXISTS', KEYS[1]) == 0 then return {-1} end\nlocal prev = redis.call('HGET', KEYS[2], ARGV[1])\nif prev and prev ~= 'pending' then\n return {0,\n tonumber(redis.call('HGET', KEYS[1], 'done')) or 0,\n tonumber(redis.call('HGET', KEYS[1], 'failed')) or 0,\n tonumber(redis.call('HGET', KEYS[1], 'cancelled')) or 0,\n tonumber(redis.call('HGET', KEYS[1], 'total')) or 0,\n redis.call('HGET', KEYS[1], 'onComplete') or '',\n redis.call('HGET', KEYS[1], 'onFailure') or 'run-to-completion',\n tonumber(redis.call('HGET', KEYS[1], 'completionEnqueued')) or 0}\nend\nredis.call('HSET', KEYS[2], ARGV[1], 's:' .. ARGV[2])\nredis.call('HINCRBY', KEYS[1], ARGV[2], 1)\nreturn {1,\n tonumber(redis.call('HGET', KEYS[1], 'done')) or 0,\n tonumber(redis.call('HGET', KEYS[1], 'failed')) or 0,\n tonumber(redis.call('HGET', KEYS[1], 'cancelled')) or 0,\n tonumber(redis.call('HGET', KEYS[1], 'total')) or 0,\n redis.call('HGET', KEYS[1], 'onComplete') or '',\n redis.call('HGET', KEYS[1], 'onFailure') or 'run-to-completion',\n tonumber(redis.call('HGET', KEYS[1], 'completionEnqueued')) or 0}`;\n\n/** 组记账 Lua:原子 unsettle(成员行守卫 + 按原 outcome 递减;未落定 no-op) */\nconst LUA_UNSETTLE = `\nif redis.call('EXISTS', KEYS[1]) == 0 then return -1 end\nlocal prev = redis.call('HGET', KEYS[2], ARGV[1])\nif not prev or string.sub(prev, 1, 2) ~= 's:' then return 0 end\nredis.call('HSET', KEYS[2], ARGV[1], 'pending')\nredis.call('HINCRBY', KEYS[1], string.sub(prev, 3), -1)\nreturn 1`;\n\n/** 组记账 Lua:成员登记的 set-if-absent(IRedisClient 无 hsetnx——防覆盖已落定成员态) */\nconst LUA_HSETNX = `\nif redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 then\n redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])\n return 1\nend\nreturn 0`;\n\n/** defineCommand 注册名(组记账脚本经 runCommand 调用) */\nconst GROUP_CREATE_CMD = 'faapiGroupCreate';\nconst GROUP_SETTLE_CMD = 'faapiGroupSettle';\nconst GROUP_UNSETTLE_CMD = 'faapiGroupUnsettle';\nconst GROUP_HSETNX_CMD = 'faapiGroupMemberHsetnx';\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, removeOnComplete, removeOnFail })`(每任务一个 Queue)\n * - `startWorker` → `new Worker(name, handler, { connection, concurrency })`;attempt 取 job.attemptsStarted\n * - `stop` → workers.close() + queues.close()(等 in-flight;超时 abort 在跑任务的 signal)\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 /** 在跑任务的取消控制器(stop 超时 abort——run 监听 signal 可尽快退出) */\n const inflight = new Map<string, AbortController>();\n /** 终态清理策略(默认 7 天移除,false 透传恢复 BullMQ 永不清理) */\n const removeOnComplete = options.removeOnComplete ?? { age: DEFAULT_KEEP_AGE_SECONDS };\n const removeOnFail = options.removeOnFail ?? { age: DEFAULT_KEEP_AGE_SECONDS };\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 const groupKey = (groupId: string) => `${prefix}:group:${groupId}`;\n const groupMembersKey = (groupId: string) => `${prefix}:group-members:${groupId}`;\n\n /**\n * 组记账专用连接(BullMQ 底层 ioredis client——经 backend.client 复用连接池,\n * 不加新连接)。独立 Queue 实例且不进 `queues` map(避免泄入 list() 的已建\n * 队列遍历);Queue 构造惰性建连,仅组记账命令实际触达 Redis。\n */\n let groupOpsQueue: Queue | null = null;\n async function groupClient() {\n if (!groupOpsQueue) {\n groupOpsQueue = new Queue('__faapi_group_ops__', {\n connection: options.connection,\n prefix,\n });\n }\n const client = await groupOpsQueue.backend.client;\n // 组记账脚本按 BullMQ 类型化扩展口注册(defineCommand + runCommand)——每个\n // 连接实例注册一次(同名重复注册幂等)\n client.defineCommand(GROUP_CREATE_CMD, { numberOfKeys: 2, lua: LUA_CREATE });\n client.defineCommand(GROUP_SETTLE_CMD, { numberOfKeys: 2, lua: LUA_SETTLE });\n client.defineCommand(GROUP_UNSETTLE_CMD, { numberOfKeys: 2, lua: LUA_UNSETTLE });\n client.defineCommand(GROUP_HSETNX_CMD, { numberOfKeys: 1, lua: LUA_HSETNX });\n return client;\n }\n\n /** settle Lua 返回行([changed, done, failed, cancelled, total, onComplete, onFailure, ce])→ 快照 */\n function settleRowToSnapshot(groupId: string, row: unknown[]): TaskGroupSnapshot {\n const num = (i: number) => Number(row[i]);\n const done = num(1);\n const failed = num(2);\n const cancelled = num(3);\n const total = num(4);\n const onComplete = String(row[5] ?? '');\n const onFailure = String(row[6] ?? 'run-to-completion');\n const completionEnqueued = num(7) === 1;\n const settled = done + failed + cancelled;\n return {\n groupId,\n task: '',\n total,\n done,\n failed,\n cancelled,\n settled,\n status: settled >= total ? 'settled' : 'open',\n completionEnqueued,\n ...(onComplete !== '' ? { onComplete } : {}),\n onFailure: onFailure as TaskGroupSnapshot['onFailure'],\n };\n }\n\n /**\n * 组记账实现(TaskDriver.groups)——经 backend.client 的 defineCommand/runCommand\n * (BullMQ 类型化 Lua 扩展口)计账。落定幂等由成员行状态守卫承载,成员翻转 +\n * 计数递增在 Lua 内原子;记账状态持久化在 Redis,跨实例/重启正确。\n */\n function createGroupOps(): TaskDriverGroupOps {\n return {\n async create(decl: TaskDriverGroupCreate): Promise<void> {\n const c = await groupClient();\n const result = (await c.runCommand(GROUP_CREATE_CMD, [\n groupKey(decl.id),\n groupMembersKey(decl.id),\n decl.task,\n String(decl.total),\n decl.onComplete ?? '',\n decl.onFailure,\n ])) as number;\n if (result === 0) return;\n if (result === -1) {\n // 同 id 已存在且参数漂移——静默沿用旧声明会让 fan-in 指向错误的回调任务\n const existing = (await c.hgetall(groupKey(decl.id))) as Record<string, string>;\n throw new Error(\n `[faapi] Task group \"${decl.id}\" already exists with different options ` +\n `(existing: task=${existing.task}, total=${existing.total}, ` +\n `onComplete=${existing.onComplete || '(none)'}, onFailure=${existing.onFailure})`,\n );\n }\n },\n\n async settle(groupId, jobId, outcome) {\n const c = await groupClient();\n const row = (await c.runCommand(GROUP_SETTLE_CMD, [\n groupKey(groupId),\n groupMembersKey(groupId),\n jobId,\n outcome,\n ])) as unknown[];\n if (Number(row[0]) === -1) {\n throw new Error(`[faapi] Task group \"${groupId}\" not found`);\n }\n const snap = settleRowToSnapshot(groupId, row);\n return { ...snap, isLast: snap.settled >= snap.total };\n },\n\n async unsettle(groupId, jobId) {\n const c = await groupClient();\n await c.runCommand(GROUP_UNSETTLE_CMD, [\n groupKey(groupId),\n groupMembersKey(groupId),\n jobId,\n ]);\n },\n\n async markCompletionEnqueued(groupId) {\n const c = await groupClient();\n await c.hset(groupKey(groupId), { completionEnqueued: 1 });\n },\n\n async get(groupId) {\n const c = await groupClient();\n const row = (await c.hgetall(groupKey(groupId))) as Record<string, string>;\n if (!row || Object.keys(row).length === 0) return undefined;\n const done = Number(row.done ?? 0);\n const failed = Number(row.failed ?? 0);\n const cancelled = Number(row.cancelled ?? 0);\n const total = Number(row.total ?? 0);\n const settled = done + failed + cancelled;\n return {\n groupId,\n task: row.task ?? '',\n total,\n done,\n failed,\n cancelled,\n settled,\n status: settled >= total ? 'settled' : 'open',\n completionEnqueued: row.completionEnqueued === '1',\n ...(row.onComplete ? { onComplete: row.onComplete } : {}),\n onFailure: (row.onFailure ?? 'run-to-completion') as TaskGroupSnapshot['onFailure'],\n };\n },\n\n async cancelRemaining(groupId) {\n const snap = await this.get(groupId);\n if (snap === undefined) {\n throw new Error(`[faapi] Task group \"${groupId}\" not found`);\n }\n // 仅 fail-fast 组执行取消语义(run-to-completion 为 no-op,返回当前快照)\n if (snap.onFailure !== 'fail-fast') return snap;\n const c = await groupClient();\n const members = (await c.hgetall(groupMembersKey(groupId))) as Record<string, string>;\n for (const [jobId, memberState] of Object.entries(members)) {\n if (memberState !== 'pending') continue;\n const job = await getQueue(snap.task).getJob(jobId);\n if (!job) continue;\n // 仅取消等待/延迟中的成员(在跑的自然跑完;仅此两状态取消语义可靠),\n // 实际移除成功才落定 cancelled(幂等守卫防与运行实例的落定竞态重复计数)\n const jobState = await job.getState();\n if (jobState === 'waiting' || jobState === 'delayed') {\n await job.remove();\n await this.settle(groupId, jobId, 'cancelled');\n }\n }\n const after = await this.get(groupId);\n return after!;\n },\n };\n }\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(\n name,\n // 组投递成员以载荷包装携带组标识(交付时还原,业务 payload 不变)\n opts?.groupId !== undefined ? wrapGroupPayload(opts.groupId, payload) : payload,\n {\n // BullMQ attempts 含首次执行,retries 是额外重试次数\n attempts: (opts?.retries ?? 0) + 1,\n backoff: { type: 'exponential', delay: 500 },\n // 终态任务定时移除:防 Redis 无界增长 + 限定 dedupId 去重的存活期\n removeOnComplete,\n removeOnFail,\n // dedupId 幂等键:同 jobId 的 job 存活期内 add 被忽略(BullMQ 原生去重)\n ...(opts?.dedupId ? { jobId: opts.dedupId } : {}),\n ...(opts?.delayMs ? { delay: opts.delayMs } : {}),\n },\n );\n const id = job.id ?? crypto.randomUUID();\n // 组投递成员登记(成员 hash = 落定记账的幂等守卫载体;set-if-absent——\n // 幂等命中不覆盖已落定成员态,重复投递不重复登记)\n if (opts?.groupId !== undefined) {\n const c = await groupClient();\n await c.runCommand(GROUP_HSETNX_CMD, [groupMembersKey(opts.groupId), id, 'pending']);\n }\n return id;\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 id = job.id ?? '';\n // attemptsStarted 由 BullMQ 维护(首次执行为 1,重试递增,跨实例/重启准确)——\n // 进程内自计数会内存泄漏且多实例/重启后失真\n const attempt = job.attemptsStarted;\n // 组投递成员:从载荷包装还原业务 payload 与组标识(语义层据此记账)\n const { payload, groupId } = unwrapGroupPayload(job.data);\n // 信号由驱动自管:stop 超时 abort(BullMQ 自身不提供执行中任务的取消能力)\n const controller = new AbortController();\n inflight.set(id, controller);\n try {\n return await process({\n id,\n name,\n payload,\n attempt,\n signal: controller.signal,\n ...(groupId !== undefined ? { groupId } : {}),\n });\n } finally {\n inflight.delete(id);\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 // 等待超时到点 abort 在跑任务(run 监听 signal 可尽快退出);等待结束后兜底 abort 残留\n const abortTimer = setTimeout(() => {\n for (const controller of inflight.values()) controller.abort();\n }, timeoutMs);\n try {\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 } finally {\n clearTimeout(abortTimer);\n for (const controller of inflight.values()) controller.abort();\n inflight.clear();\n }\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 async list(opts) {\n const limit = opts?.limit ?? 50;\n // 目标队列:指定 name 时按需创建 Queue 实例;不传时遍历本进程已创建的\n // (BullMQ 队列按 Redis key 寻址,额外实例不影响驱动内的队列)\n const targets: Queue[] = opts?.name ? [getQueue(opts.name)] : [...queues.values()];\n // faapi 语义状态 → BullMQ JobType 分组(cancel 为 job.remove,无 cancelled 可查;\n // 重试等待中的 job 处于 delayed → 归入 pending)\n const stateTypes: Partial<Record<TaskJobStatus, JobType[]>> = {\n pending: ['waiting', 'delayed'],\n running: ['active'],\n done: ['completed'],\n failed: ['failed'],\n };\n const wanted = (opts?.state ? [opts.state] : ['pending', 'running', 'done', 'failed']).filter(\n (s): s is TaskJobStatus => (stateTypes[s as TaskJobStatus]?.length ?? 0) > 0,\n );\n\n const records: TaskDriverRecord[] = [];\n for (const status of wanted) {\n const types = stateTypes[status]!;\n for (const queue of targets) {\n const jobs = await queue.getJobs(types, 0, limit - 1);\n for (const job of jobs) {\n if (!job) continue;\n // 组投递成员:从载荷包装还原业务 payload 与组标识(记录对业务不可见包装)\n const { payload, groupId } = unwrapGroupPayload(job.data);\n records.push({\n id: job.id ?? '',\n name: job.name,\n payload,\n status,\n attempts: job.attemptsStarted ?? job.attemptsMade ?? 0,\n ...(job.returnvalue !== undefined && job.returnvalue !== null\n ? { result: job.returnvalue }\n : {}),\n ...(job.failedReason ? { error: job.failedReason } : {}),\n ...(groupId !== undefined ? { groupId } : {}),\n createdAt: job.timestamp,\n ...(job.processedOn ? { runAt: job.processedOn } : {}),\n });\n }\n }\n }\n return records.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);\n },\n\n async cancel(name, id) {\n if (stopped) {\n throw new Error(\n '[faapi] Task queue is stopped and no longer accepts management operations',\n );\n }\n // BullMQ 无 cancelled 状态——取消即移除(等待/延迟中的不再执行;active 受锁限制由 BullMQ 抛错)\n const job = await getQueue(name).getJob(id);\n if (job) await job.remove();\n },\n\n async retry(name, id) {\n if (stopped) {\n throw new Error(\n '[faapi] Task queue is stopped and no longer accepts management operations',\n );\n }\n const job = await getQueue(name).getJob(id);\n if (!job) {\n throw new Error(`[faapi] Task \"${name}\" job \"${id}\" not found`);\n }\n await job.retry(); // 仅 failed 可重试;其余状态由 BullMQ 抛错\n },\n\n groups: createGroupOps(),\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faapi/task-bullmq",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.34.0",
|
|
4
4
|
"description": "BullMQ task queue driver for faapi — persistent queue on Redis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"tsup": "^8.4.0",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^5.0.3",
|
|
27
|
-
"@faapi/faapi": "6.
|
|
27
|
+
"@faapi/faapi": "6.34.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"bullmq": "^6.0.0",
|
|
31
|
-
"@faapi/faapi": "^6.
|
|
31
|
+
"@faapi/faapi": "^6.34.0"
|
|
32
32
|
},
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"repository": {
|