@faapi/task-bullmq 6.17.0 → 6.18.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 CHANGED
@@ -39,13 +39,13 @@ export function POST(body, tasks) {
39
39
 
40
40
  | faapi 驱动接口 | BullMQ |
41
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 超时) |
42
+ | `enqueue(name, payload, { retries, delayMs, dedupId })` | `queue.add(name, payload, { attempts: retries + 1, backoff: exponential 500ms, delay, removeOnComplete, removeOnFail, jobId })`。终态任务默认 7 天后移除(`removeOnComplete`/`removeOnFail` 可覆盖,`false` 恢复永不清理)——BullMQ 默认永久保留终态任务,Redis 无界增长且 dedupId 去重在存活期内一直生效 |
43
+ | `startWorker(name, { concurrency, process })` | `new Worker(name, handler, { connection, concurrency, prefix })`;attempt 取 `job.attemptsStarted`(跨实例/重启准确) |
44
+ | `stop(timeoutMs)` | workers.close() + queues.close()(race 超时),超时 abort 在跑任务的 signal |
45
45
  | `stopWorkers()` | 仅关 Worker(dev 热替换重注册用),Queue 连接保持 |
46
46
  | 失败重试 | BullMQ 侧执行(attempts = retries + 1,指数退避 500ms 起) |
47
47
 
48
- 注意:BullMQ 不提供执行中任务的取消信号——`run` 的 `taskCtx.signal` 永不 abort,长任务请自行做超时控制。
48
+ 注意:BullMQ 不提供执行中任务的取消信号,但 `stop` 超时路径会 abort `taskCtx.signal`(任务监听 signal 可尽快退出)。
49
49
 
50
50
  ## License
51
51
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TaskDriver } from '@faapi/faapi';
2
- import { ConnectionOptions } from 'bullmq';
2
+ import { ConnectionOptions, KeepJobs } from 'bullmq';
3
3
 
4
4
  /**
5
5
  * BullMQ 驱动选项
@@ -9,6 +9,15 @@ interface BullMQDriverOptions {
9
9
  connection: ConnectionOptions;
10
10
  /** 队列名前缀(默认 `faapi`——同一 Redis 下多个 faapi 应用隔离用) */
11
11
  prefix?: string;
12
+ /**
13
+ * 终态(completed)任务清理策略,透传 BullMQ `removeOnComplete`。
14
+ * 默认 7 天后移除——BullMQ 默认永久保留终态任务,Redis 无界增长,且 dedupId
15
+ * (jobId)去重在任务存活期内一直生效,周期性复用同一 dedupId 的投递(如
16
+ * nightly-sync)会被永久静默忽略。传 `false` 恢复 BullMQ 默认(永不清理)。
17
+ */
18
+ removeOnComplete?: boolean | number | KeepJobs;
19
+ /** 终态(failed)任务清理策略,透传 BullMQ `removeOnFail`,默认同上 */
20
+ removeOnFail?: boolean | number | KeepJobs;
12
21
  }
13
22
  /**
14
23
  * faapi 任务队列 BullMQ 驱动(Redis 持久化队列)
@@ -26,8 +35,8 @@ interface BullMQDriverOptions {
26
35
  * ```
27
36
  *
28
37
  * 语义映射(详见包根 README):
29
- * - `enqueue` → `queue.add(name, payload, { delay, attempts, backoff })`(每任务一个 Queue)
30
- * - `startWorker` → `new Worker(name, handler, { connection, concurrency })`
38
+ * - `enqueue` → `queue.add(name, payload, { delay, attempts, backoff, removeOnComplete, removeOnFail })`(每任务一个 Queue)
39
+ * - `startWorker` → `new Worker(name, handler, { connection, concurrency })`;attempt 取 job.attemptsStarted
31
40
  * - `stop` → workers.close() + queues.close()(等 in-flight;超时 abort 在跑任务的 signal)
32
41
  * - 重试 → BullMQ 侧执行(attempts = retries + 1,指数退避 500ms 起)
33
42
  */
package/dist/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  // src/index.ts
2
- import { Queue, Worker } from "bullmq";
2
+ import {
3
+ Queue,
4
+ Worker
5
+ } from "bullmq";
6
+ var DEFAULT_KEEP_AGE_SECONDS = 7 * 24 * 3600;
3
7
  function createBullMQDriver(options) {
4
8
  if (!options?.connection) {
5
9
  throw new Error("[faapi] @faapi/task-bullmq requires `bullmq.connection` in config.task");
@@ -9,6 +13,8 @@ function createBullMQDriver(options) {
9
13
  const queues = /* @__PURE__ */ new Map();
10
14
  const workers = /* @__PURE__ */ new Map();
11
15
  const inflight = /* @__PURE__ */ new Map();
16
+ const removeOnComplete = options.removeOnComplete ?? { age: DEFAULT_KEEP_AGE_SECONDS };
17
+ const removeOnFail = options.removeOnFail ?? { age: DEFAULT_KEEP_AGE_SECONDS };
12
18
  function getQueue(name) {
13
19
  let q = queues.get(name);
14
20
  if (!q) {
@@ -17,7 +23,6 @@ function createBullMQDriver(options) {
17
23
  }
18
24
  return q;
19
25
  }
20
- const attemptCounts = /* @__PURE__ */ new Map();
21
26
  return {
22
27
  async enqueue(name, payload, opts) {
23
28
  if (stopped) {
@@ -28,6 +33,9 @@ function createBullMQDriver(options) {
28
33
  // BullMQ attempts 含首次执行,retries 是额外重试次数
29
34
  attempts: (opts?.retries ?? 0) + 1,
30
35
  backoff: { type: "exponential", delay: 500 },
36
+ // 终态任务定时移除:防 Redis 无界增长 + 限定 dedupId 去重的存活期
37
+ removeOnComplete,
38
+ removeOnFail,
31
39
  // dedupId 幂等键:同 jobId 的 job 存活期内 add 被忽略(BullMQ 原生去重)
32
40
  ...opts?.dedupId ? { jobId: opts.dedupId } : {},
33
41
  ...opts?.delayMs ? { delay: opts.delayMs } : {}
@@ -44,8 +52,7 @@ function createBullMQDriver(options) {
44
52
  name,
45
53
  async (job) => {
46
54
  const id = job.id ?? "";
47
- const attempt = (attemptCounts.get(id) ?? 0) + 1;
48
- attemptCounts.set(id, attempt);
55
+ const attempt = job.attemptsStarted;
49
56
  const controller = new AbortController();
50
57
  inflight.set(id, controller);
51
58
  try {
@@ -87,7 +94,6 @@ function createBullMQDriver(options) {
87
94
  clearTimeout(abortTimer);
88
95
  for (const controller of inflight.values()) controller.abort();
89
96
  inflight.clear();
90
- attemptCounts.clear();
91
97
  }
92
98
  },
93
99
  async stopWorkers() {
@@ -119,7 +125,7 @@ function createBullMQDriver(options) {
119
125
  name: job.name,
120
126
  payload: job.data,
121
127
  status,
122
- attempts: job.attemptsMade ?? 0,
128
+ attempts: job.attemptsStarted ?? job.attemptsMade ?? 0,
123
129
  ...job.returnvalue !== void 0 && job.returnvalue !== null ? { result: job.returnvalue } : {},
124
130
  ...job.failedReason ? { error: job.failedReason } : {},
125
131
  createdAt: job.timestamp,
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 { Queue, Worker, type ConnectionOptions, type Job, type JobType } 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;超时 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\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 // 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 const attempt = (attemptCounts.get(id) ?? 0) + 1;\n attemptCounts.set(id, attempt);\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 attemptCounts.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.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 { 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faapi/task-bullmq",
3
- "version": "6.17.0",
3
+ "version": "6.18.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": "^4.1.11",
27
- "@faapi/faapi": "6.17.0"
27
+ "@faapi/faapi": "6.18.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "bullmq": "^5.0.0",
31
- "@faapi/faapi": "^6.17.0"
31
+ "@faapi/faapi": "^6.18.0"
32
32
  },
33
33
  "license": "MIT",
34
34
  "repository": {