@faapi/task-pgboss 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 CHANGED
@@ -42,6 +42,7 @@ export function POST(body, tasks) {
42
42
  | `startWorker(name, { concurrency, process })` | 幂等 `createQueue(name)` → `boss.work(name, { batchSize: concurrency }, handler)`。批内任务并发执行、逐任务 `complete`/`fail` 结算——单个任务失败只消耗自己的重试额度,不毒化同批 |
43
43
  | `stop(timeoutMs)` | `offWork`(与 deadline 竞速,卡死任务不悬挂停机)+ `boss.stop({ close: true, graceful: true, timeout })`(timeout 单位毫秒);deadline 到点 abort 在跑任务的 signal |
44
44
  | `stopWorkers()` | `offWork()`(不断开连接,dev 热替换重注册用) |
45
+ | `groups`(任务组记账,语义见主包 taskGroups.md) | 同库两张自建表 `faapi_task_groups`(组行:计数器 + 回调声明 + 失败策略)+ `faapi_task_group_members`(成员行:settled/outcome 幂等守卫),首次组操作 `CREATE TABLE IF NOT EXISTS`;经 `boss.getDb().executeSql` 计账——落定为单条 CTE(成员行守卫翻转 + 计数递增原子)。成员组标识以载荷包装随 data 存储(`{ __faapiGroup, __faapiPayload }`,交付/查询还原);`cancelRemaining` 逐成员 `boss.cancel` + `getJobById` 核实生效才落定 |
45
46
  | 失败重试 | pg-boss 侧执行(retryLimit + retryBackoff 指数退避) |
46
47
 
47
48
  注意:pg-boss(v10 起)不再隐式建队列(`send()` 对未创建队列静默返回 null)——驱动在投递/注册 worker 前自动幂等建队列,业务方无需预建。pg-boss 自身不提供执行中任务的取消信号,但 `stop` 超时路径会 abort `taskCtx.signal`(任务监听 signal 可尽快退出;仍不退出的由 expire_in 兜底结算)。
package/dist/index.js CHANGED
@@ -1,6 +1,39 @@
1
1
  // src/index.ts
2
2
  import { createHash } from "crypto";
3
3
  import { PgBoss } from "pg-boss";
4
+ var GROUP_KEY = "__faapiGroup";
5
+ var PAYLOAD_KEY = "__faapiPayload";
6
+ function wrapGroupPayload(groupId, payload) {
7
+ return { [GROUP_KEY]: groupId, [PAYLOAD_KEY]: payload };
8
+ }
9
+ function unwrapGroupPayload(data) {
10
+ 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) {
11
+ const wrapped = data;
12
+ return { payload: wrapped[PAYLOAD_KEY], groupId: wrapped[GROUP_KEY] };
13
+ }
14
+ return { payload: data };
15
+ }
16
+ var GROUP_TABLES_SQL = [
17
+ `CREATE TABLE IF NOT EXISTS faapi_task_groups (
18
+ id text PRIMARY KEY,
19
+ task text NOT NULL,
20
+ total integer NOT NULL,
21
+ done integer NOT NULL DEFAULT 0,
22
+ failed integer NOT NULL DEFAULT 0,
23
+ cancelled integer NOT NULL DEFAULT 0,
24
+ on_complete text,
25
+ on_failure text NOT NULL,
26
+ completion_enqueued boolean NOT NULL DEFAULT false,
27
+ created_at timestamptz NOT NULL DEFAULT now()
28
+ )`,
29
+ `CREATE TABLE IF NOT EXISTS faapi_task_group_members (
30
+ group_id text NOT NULL,
31
+ job_id text NOT NULL,
32
+ settled boolean NOT NULL DEFAULT false,
33
+ outcome text,
34
+ PRIMARY KEY (group_id, job_id)
35
+ )`
36
+ ];
4
37
  function dedupIdToUuid(dedupId) {
5
38
  const h = createHash("sha1").update(dedupId).digest("hex");
6
39
  return [
@@ -25,6 +58,163 @@ function createPgBossDriver(driverOptions = {}) {
25
58
  const ensuredQueues = /* @__PURE__ */ new Set();
26
59
  const ensuring = /* @__PURE__ */ new Map();
27
60
  let bossStarting = null;
61
+ let groupTablesEnsured = false;
62
+ async function ensureGroupTables(b) {
63
+ if (groupTablesEnsured) return;
64
+ const db = b.getDb();
65
+ for (const sql of GROUP_TABLES_SQL) {
66
+ await db.executeSql(sql, []);
67
+ }
68
+ groupTablesEnsured = true;
69
+ }
70
+ function rowToSnapshot(row) {
71
+ const done = Number(row.done);
72
+ const failed = Number(row.failed);
73
+ const cancelled = Number(row.cancelled);
74
+ const total = Number(row.total);
75
+ const settled = done + failed + cancelled;
76
+ return {
77
+ groupId: String(row.id),
78
+ task: String(row.task),
79
+ total,
80
+ done,
81
+ failed,
82
+ cancelled,
83
+ settled,
84
+ status: settled >= total ? "settled" : "open",
85
+ completionEnqueued: row.completion_enqueued === true,
86
+ ...row.on_complete !== null && row.on_complete !== void 0 ? { onComplete: String(row.on_complete) } : {},
87
+ onFailure: String(row.on_failure)
88
+ };
89
+ }
90
+ function createGroupOps(getBoss) {
91
+ return {
92
+ async create(decl) {
93
+ const b = await getBoss();
94
+ await ensureGroupTables(b);
95
+ const db = b.getDb();
96
+ const inserted = await db.executeSql(
97
+ `INSERT INTO faapi_task_groups (id, task, total, on_complete, on_failure)
98
+ VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO NOTHING RETURNING id`,
99
+ [decl.id, decl.task, decl.total, decl.onComplete ?? null, decl.onFailure]
100
+ );
101
+ if (inserted.rows.length > 0) return;
102
+ const existing = await db.executeSql(
103
+ `SELECT task, total, on_complete, on_failure FROM faapi_task_groups WHERE id = $1`,
104
+ [decl.id]
105
+ );
106
+ const row = existing.rows[0];
107
+ const same = row !== void 0 && row.task === decl.task && Number(row.total) === decl.total && (row.on_complete ?? null) === (decl.onComplete ?? null) && row.on_failure === decl.onFailure;
108
+ if (!same) {
109
+ throw new Error(
110
+ `[faapi] Task group "${decl.id}" already exists with different options (existing: task=${row?.task}, total=${row?.total}, onComplete=${row?.on_complete}, onFailure=${row?.on_failure})`
111
+ );
112
+ }
113
+ },
114
+ async settle(groupId, jobId, outcome) {
115
+ const b = await getBoss();
116
+ await ensureGroupTables(b);
117
+ const db = b.getDb();
118
+ const result = await db.executeSql(
119
+ `WITH member AS (
120
+ INSERT INTO faapi_task_group_members (group_id, job_id, settled, outcome)
121
+ VALUES ($1, $2, true, $3)
122
+ ON CONFLICT (group_id, job_id) DO UPDATE
123
+ SET settled = true, outcome = EXCLUDED.outcome
124
+ WHERE faapi_task_group_members.settled = false
125
+ RETURNING job_id
126
+ )
127
+ UPDATE faapi_task_groups g
128
+ SET done = done + CASE WHEN $3 = 'done' THEN 1 ELSE 0 END,
129
+ failed = failed + CASE WHEN $3 = 'failed' THEN 1 ELSE 0 END,
130
+ cancelled = cancelled + CASE WHEN $3 = 'cancelled' THEN 1 ELSE 0 END
131
+ FROM member
132
+ WHERE g.id = $1
133
+ RETURNING g.id, g.task, g.total, g.done, g.failed, g.cancelled,
134
+ g.on_complete, g.on_failure, g.completion_enqueued`,
135
+ [groupId, jobId, outcome]
136
+ );
137
+ const row = result.rows[0];
138
+ if (row !== void 0) {
139
+ const snap = rowToSnapshot(row);
140
+ return { ...snap, isLast: snap.settled >= snap.total };
141
+ }
142
+ const current = await db.executeSql(
143
+ `SELECT id, task, total, done, failed, cancelled, on_complete, on_failure,
144
+ completion_enqueued
145
+ FROM faapi_task_groups WHERE id = $1`,
146
+ [groupId]
147
+ );
148
+ const cur = current.rows[0];
149
+ if (cur === void 0) {
150
+ throw new Error(`[faapi] Task group "${groupId}" not found`);
151
+ }
152
+ return { ...rowToSnapshot(cur), isLast: false };
153
+ },
154
+ async unsettle(groupId, jobId) {
155
+ const b = await getBoss();
156
+ const db = b.getDb();
157
+ await db.executeSql(
158
+ `WITH member AS (
159
+ UPDATE faapi_task_group_members
160
+ SET settled = false, outcome = NULL
161
+ WHERE group_id = $1 AND job_id = $2 AND settled = true
162
+ RETURNING outcome
163
+ )
164
+ UPDATE faapi_task_groups g
165
+ SET done = done - CASE WHEN member.outcome = 'done' THEN 1 ELSE 0 END,
166
+ failed = failed - CASE WHEN member.outcome = 'failed' THEN 1 ELSE 0 END,
167
+ cancelled = cancelled - CASE WHEN member.outcome = 'cancelled' THEN 1 ELSE 0 END
168
+ FROM member
169
+ WHERE g.id = $1`,
170
+ [groupId, jobId]
171
+ );
172
+ },
173
+ async markCompletionEnqueued(groupId) {
174
+ const b = await getBoss();
175
+ const db = b.getDb();
176
+ await db.executeSql(
177
+ `UPDATE faapi_task_groups SET completion_enqueued = true WHERE id = $1`,
178
+ [groupId]
179
+ );
180
+ },
181
+ async get(groupId) {
182
+ const b = await getBoss();
183
+ const db = b.getDb();
184
+ const result = await db.executeSql(
185
+ `SELECT id, task, total, done, failed, cancelled, on_complete, on_failure,
186
+ completion_enqueued
187
+ FROM faapi_task_groups WHERE id = $1`,
188
+ [groupId]
189
+ );
190
+ const row = result.rows[0];
191
+ return row === void 0 ? void 0 : rowToSnapshot(row);
192
+ },
193
+ async cancelRemaining(groupId) {
194
+ const b = await getBoss();
195
+ const db = b.getDb();
196
+ const group = await this.get(groupId);
197
+ if (group === void 0) {
198
+ throw new Error(`[faapi] Task group "${groupId}" not found`);
199
+ }
200
+ if (group.onFailure !== "fail-fast") return group;
201
+ const pending = await db.executeSql(
202
+ `SELECT job_id FROM faapi_task_group_members WHERE group_id = $1 AND settled = false`,
203
+ [groupId]
204
+ );
205
+ for (const row of pending.rows) {
206
+ const jobId = row.job_id;
207
+ await b.cancel(group.task, jobId);
208
+ const job = await b.getJobById(group.task, jobId);
209
+ if (job !== null && job.state === "cancelled") {
210
+ await this.settle(groupId, jobId, "cancelled");
211
+ }
212
+ }
213
+ const after = await this.get(groupId);
214
+ return after;
215
+ }
216
+ };
217
+ }
28
218
  async function ensureBoss() {
29
219
  if (boss) return boss;
30
220
  if (!bossStarting) {
@@ -82,14 +272,24 @@ function createPgBossDriver(driverOptions = {}) {
82
272
  ...opts?.dedupId ? { id: dedupIdToUuid(opts.dedupId) } : {},
83
273
  ...opts?.delayMs ? { startAfter: new Date(Date.now() + opts.delayMs) } : {}
84
274
  };
85
- const id = await b.send(name, payload, sendOptions);
86
- if (!id) {
87
- if (opts?.dedupId) {
88
- return dedupIdToUuid(opts.dedupId);
89
- }
275
+ const id = await b.send(
276
+ name,
277
+ opts?.groupId !== void 0 ? wrapGroupPayload(opts.groupId, payload) : payload,
278
+ sendOptions
279
+ );
280
+ const jobId = id ?? (opts?.dedupId ? dedupIdToUuid(opts.dedupId) : null);
281
+ if (!jobId) {
90
282
  throw new Error(`[faapi] pg-boss send failed for task "${name}"`);
91
283
  }
92
- return id;
284
+ if (opts?.groupId !== void 0) {
285
+ await ensureGroupTables(b);
286
+ await b.getDb().executeSql(
287
+ `INSERT INTO faapi_task_group_members (group_id, job_id)
288
+ VALUES ($1, $2) ON CONFLICT (group_id, job_id) DO NOTHING`,
289
+ [opts.groupId, jobId]
290
+ );
291
+ }
292
+ return jobId;
93
293
  },
94
294
  async startWorker(name, workerOpts) {
95
295
  const b = await ensureBoss();
@@ -106,6 +306,7 @@ function createPgBossDriver(driverOptions = {}) {
106
306
  await Promise.all(
107
307
  jobs.map(async (job) => {
108
308
  const attempt = job.retryCount + 1;
309
+ const { payload, groupId } = unwrapGroupPayload(job.data);
109
310
  const controller = new AbortController();
110
311
  inflight.set(job.id, controller);
111
312
  try {
@@ -113,9 +314,10 @@ function createPgBossDriver(driverOptions = {}) {
113
314
  await process({
114
315
  id: job.id,
115
316
  name,
116
- payload: job.data,
317
+ payload,
117
318
  attempt,
118
- signal: controller.signal
319
+ signal: controller.signal,
320
+ ...groupId !== void 0 ? { groupId } : {}
119
321
  });
120
322
  } catch (err) {
121
323
  const reason = err instanceof Error ? { name: err.name, message: err.message } : { value: String(err) };
@@ -228,6 +430,7 @@ function createPgBossDriver(driverOptions = {}) {
228
430
  const status = stateMap[job.state];
229
431
  if (!status) continue;
230
432
  if (wanted && !wanted.includes(status)) continue;
433
+ const { payload, groupId } = unwrapGroupPayload(job.data);
231
434
  const output = job.output;
232
435
  let error;
233
436
  if (status === "failed" && output !== void 0 && output !== null) {
@@ -241,7 +444,7 @@ function createPgBossDriver(driverOptions = {}) {
241
444
  records.push({
242
445
  id: job.id,
243
446
  name: job.name,
244
- payload: job.data,
447
+ payload,
245
448
  status,
246
449
  // retryCount 从 0 起(首次执行为 0)→ faapi attempts 从 1 起
247
450
  attempts: job.retryCount + 1,
@@ -249,12 +452,14 @@ function createPgBossDriver(driverOptions = {}) {
249
452
  createdAt: job.createdOn.getTime(),
250
453
  // startAfter 恒有值(send 未传时默认 now)——计划执行时间即本字段语义
251
454
  runAt: job.startAfter.getTime(),
455
+ ...groupId !== void 0 ? { groupId } : {},
252
456
  ...error !== void 0 ? { error } : {}
253
457
  });
254
458
  }
255
459
  }
256
460
  return records.sort((a, b2) => b2.createdAt - a.createdAt).slice(0, opts?.limit ?? 50);
257
- }
461
+ },
462
+ groups: createGroupOps(ensureBoss)
258
463
  };
259
464
  }
260
465
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { TaskDriver, TaskDriverProcess, TaskDriverRecord } from '@faapi/faapi';\nimport { PgBoss } from 'pg-boss';\nimport type { ConstructorOptions, SendOptions } from 'pg-boss';\n\n/**\n * pg-boss 驱动选项\n *\n * `defaultExpireSeconds` 之外全部透传给 PgBoss 构造函数(常用:`connectionString`,\n * 其余见 pg-boss 文档 ConstructorOptions)。\n */\nexport type PgBossDriverOptions = ConstructorOptions & {\n /**\n * 未声明 `timeoutMs` 的任务的 expire_in 兜底秒数(默认 24h − 1s)。\n *\n * pg-boss 以 job 的 expire_in 硬限 handler 执行(DDL 默认 15 分钟):超时判失败\n * 重试,而任务还在后台跑 → 同一任务两份并发执行。声明了 `timeoutMs` 的任务由\n * 驱动按 `timeoutMs + graceMs + 60s` 缓冲给足;未声明的任务用本兜底。\n * 默认 24h − 1s:pg-boss 12.28 前断言 `expireIn/3600 < 24`(排他),减 1 秒落\n * 界内(12.28 起放宽到允许恰好 24h,保守值对两者都安全);显式配置超过 86399\n * 在 12.28 前的版本会被 send 参数校验阶段拒绝。\n *\n * 声明侧上界由主包 `scanTasks` 构建期校验(`MAX_ISOLATED_TIMEOUT_MS` 23h),\n * 保证 `timeoutMs + graceMs + 缓冲` 的 expire 预算恒落在断言界内。\n */\n defaultExpireSeconds?: number;\n};\n\n/**\n * faapi 任务队列 pg-boss 驱动(PostgreSQL 持久化队列)\n *\n * 与 faapi 主包 `config.task.driver: 'pgboss'` 配合使用:\n *\n * ```ts\n * // faapi.config.ts\n * export default {\n * task: {\n * driver: 'pgboss',\n * pgboss: { connectionString: 'postgres://localhost:5432/app' },\n * },\n * } satisfies FaapiConfig;\n * ```\n *\n * 语义映射(详见包根 README):\n * - `enqueue` → 幂等 ensureQueue + `boss.send(name, payload, { retryLimit, retryDelay, retryBackoff, expireInSeconds, startAfter })`\n * - `startWorker` → 幂等 ensureQueue + `boss.work(name, { batchSize: concurrency, includeMetadata: true }, handler)`;\n * 批内任务并发执行、逐任务 complete/fail 结算(失败不毒化同批)\n * - `stop` → `offWork` + `boss.stop({ close: true, graceful: true, timeout })` 整体与\n * deadline 竞速;deadline 到点 abort 在跑任务的 signal\n * - 重试 → pg-boss 侧执行(retryLimit + retryBackoff 指数退避)\n * - `list` → `boss.findJobs(name)`(v12):六态精确映射(created→pending、retry→retry、\n * active→running、completed→done、failed→failed、cancelled→cancelled);不传 name\n * 遍历本进程已 ensureQueue 的任务名;createdAt 降序截断 limit\n */\n/**\n * dedupId → 确定性 UUID:pg-boss 的 send 自定义 id 要求 UUID 格式(SQL 侧 cast),\n * 任意字符串键经 SHA-1 映射为合法 UUID——同键必同 UUID(幂等),不同键碰撞可忽略\n */\nfunction dedupIdToUuid(dedupId: string): string {\n const h = createHash('sha1').update(dedupId).digest('hex');\n return [\n h.slice(0, 8),\n h.slice(8, 12),\n `5${h.slice(13, 16)}`, // version 5\n `${((parseInt(h.slice(16, 17), 16) & 0x3) | 0x8).toString(16)}${h.slice(17, 20)}`, // variant\n h.slice(20, 32),\n ].join('-');\n}\n\n/** 未声明 timeoutMs 的任务 expire_in 兜底(秒)——pg-boss 12.28 前断言 expireIn/3600 < 24\n * (严格小于),顶到 24h 整会让 send() 在参数校验阶段必抛 AssertionError,减 1 秒落界内\n * (12.28 起放宽到允许恰好 24h,保守值对两者都安全) */\nconst DEFAULT_EXPIRE_SECONDS = 24 * 60 * 60 - 1;\n/** expire_in 在任务执行预算之外的缓冲(秒)——留出调度/网络抖动余量 */\nconst EXPIRE_BUFFER_SECONDS = 60;\n/** graceMs 未声明时的默认值(与主包 taskWorker 的取消宽限期默认一致) */\nconst DEFAULT_GRACE_MS = 5000;\n\nexport function createPgBossDriver(driverOptions: PgBossDriverOptions = {}): TaskDriver {\n const { defaultExpireSeconds, ...options } = driverOptions;\n let boss: PgBoss | null = null;\n let stopped = false;\n /** 已创建的 pg-boss worker id(stopWorkers 时 offWork) */\n const workerIds = new Map<string, string>();\n /** 在跑任务的取消控制器(stop 超时 abort——run 监听 signal 可尽快退出) */\n const inflight = new Map<string, AbortController>();\n /** 已建队列名缓存(每任务名每进程一次真实 createQueue,之后短路) */\n const ensuredQueues = new Set<string>();\n /** 并发首次投递同名任务的 in-flight 去重 */\n const ensuring = new Map<string, Promise<void>>();\n /** boss.start() in-flight 共享 promise(并发首调等待同一次建连,不出现半启动实例) */\n let bossStarting: Promise<PgBoss> | null = null;\n\n async function ensureBoss(): Promise<PgBoss> {\n if (boss) return boss;\n if (!bossStarting) {\n const starting = (async () => {\n const b = new PgBoss(options);\n // pg-boss 把连接池错误(PG 重启/网络闪断)与 worker 异常 emit 为 'error' 事件,\n // EventEmitter 无监听器时 emit 即抛 uncaughtException 崩进程——必须挂监听\n b.on('error', (err: unknown) => {\n console.error(\n '[faapi] pg-boss error:',\n err instanceof Error ? (err.stack ?? err.message) : err,\n );\n });\n // pg-boss 惰性连接:start 后才连库;驱动在首次 enqueue/startWorker 前建立。\n // start 完成前实例不外借——pre-open 的 executeSql 静默 no-op(返回 undefined),\n // 建连中并发调用会在半启动实例上跑 SQL\n await b.start();\n if (stopped) {\n // 停机竞态:start 期间 stop() 已执行——不回写实例(否则泄漏一个已 start、\n // 永不 stop 的 PgBoss),关闭后让调用方拿到失败\n await b.stop().catch(() => {});\n throw new Error('[faapi] Task queue is stopped and no longer accepts jobs');\n }\n boss = b;\n return b;\n })();\n bossStarting = starting;\n // start 失败清空 in-flight:下次调用重新建连(坏实例不入缓存);\n // 身份校验防止误清 stop() 之后新建的 promise\n void starting.catch(() => {\n if (bossStarting === starting) bossStarting = null;\n });\n }\n return bossStarting;\n }\n\n /**\n * pg-boss v10 不再隐式建队列(v9 行为):send() 的 INSERT JOIN queue 对未创建队列\n * 静默返回 null,work() 只注册进程内轮询器同样不建队列——投递/注册 worker 前先建队列。\n * create_queue plpgsql 幂等(INSERT ON CONFLICT DO NOTHING,已存在直接返回),\n * 重复/并发调用安全;失败不入缓存,下次投递重试。\n */\n async function ensureQueue(b: PgBoss, name: string): Promise<void> {\n if (ensuredQueues.has(name)) return;\n const pending = ensuring.get(name);\n if (pending) return pending;\n const promise = (async () => {\n await b.createQueue(name);\n ensuredQueues.add(name);\n })().finally(() => {\n ensuring.delete(name);\n });\n ensuring.set(name, promise);\n return promise;\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 b = await ensureBoss();\n await ensureQueue(b, name);\n // pg-boss 以 job 的 expire_in 硬限 handler 执行(DDL 默认 15 分钟):超时判失败\n // 重试,而任务还在后台跑 → 同一任务两份并发执行。按任务元信息给足执行预算:\n // timeoutMs + graceMs + 缓冲;未声明 timeoutMs 的任务用 defaultExpireSeconds 兜底\n const expireInSeconds = opts?.timeoutMs\n ? Math.ceil((opts.timeoutMs + (opts.graceMs ?? DEFAULT_GRACE_MS)) / 1000) +\n EXPIRE_BUFFER_SECONDS\n : (defaultExpireSeconds ?? DEFAULT_EXPIRE_SECONDS);\n const sendOptions: SendOptions = {\n retryLimit: opts?.retries ?? 0,\n retryDelay: 1, // 秒;配合 retryBackoff 指数退避\n retryBackoff: true,\n expireInSeconds,\n ...(opts?.dedupId ? { id: dedupIdToUuid(opts.dedupId) } : {}),\n ...(opts?.delayMs ? { startAfter: new Date(Date.now() + opts.delayMs) } : {}),\n };\n // pg-boss data 形参为 object——基础类型 payload(cron 空任务等)按 JSON 语义透传\n const id = await b.send(name, payload as object, sendOptions);\n if (!id) {\n // dedupId 幂等投递:同键已存在(主键冲突 DO NOTHING → send 返回 null),\n // 返回已存在任务的确定性 id;无 dedupId 的失败投递才是异常\n if (opts?.dedupId) {\n return dedupIdToUuid(opts.dedupId);\n }\n throw new Error(`[faapi] pg-boss send failed for task \"${name}\"`);\n }\n return id;\n },\n\n async startWorker(name, workerOpts) {\n const b = await ensureBoss();\n await ensureQueue(b, name);\n const process: TaskDriverProcess = workerOpts.process;\n // 二次注册(reload 场景)先 offWork 旧 worker(v12 签名 offWork(name, { id }))\n const existing = workerIds.get(name);\n if (existing) {\n await b.offWork(name, { id: existing });\n }\n const workerId = await b.work<unknown>(\n name,\n { batchSize: workerOpts.concurrency, includeMetadata: true },\n async (jobs) => {\n // 批内并发执行 + 逐任务结算:complete/fail 按单 job id 调用(pg-boss 的结算\n // SQL 带 state 守卫——complete 仅 active、fail 仅 state<completed,handler\n // 正常返回后 onFetch 的批量 complete 对已结算任务是 no-op)。单个任务失败只\n // 消耗自己的重试额度,不毒化同批其他任务;concurrency 语义与 BullMQ 驱动一致\n await Promise.all(\n jobs.map(async (job) => {\n // retryCount 从 0 起(首次执行为 0)→ faapi attempt 从 1 起\n const attempt = job.retryCount + 1;\n // 信号由驱动自管:stop 超时 abort(pg-boss 自身不提供执行中任务的取消能力)\n const controller = new AbortController();\n inflight.set(job.id, controller);\n try {\n try {\n await process({\n id: job.id,\n name,\n payload: job.data,\n attempt,\n signal: controller.signal,\n });\n } catch (err) {\n // fail 的 data 存 jsonb:传可序列化的错误摘要(原始 Error 序列化为 {} 丢信息)\n const reason =\n err instanceof Error\n ? { name: err.name, message: err.message }\n : { value: String(err) };\n try {\n await b.fail(name, job.id, reason);\n } catch (failErr) {\n // fail 结算失败:任务留在 active,由 pg-boss 的 expire_in 兜底结算\n console.error(\n `[faapi] pg-boss fail() failed for job ${job.id} of \"${name}\":`,\n failErr,\n );\n }\n return;\n }\n try {\n await b.complete(name, job.id);\n } catch (completeErr) {\n // complete 结算失败:任务留在 active,由 expire_in 兜底(不标失败——\n // 任务已成功执行,标失败会触发重试导致重复执行)\n console.error(\n `[faapi] pg-boss complete() failed for job ${job.id} of \"${name}\":`,\n completeErr,\n );\n }\n } finally {\n inflight.delete(job.id);\n }\n }),\n );\n },\n );\n workerIds.set(name, workerId);\n },\n\n async stop(timeoutMs = 10_000) {\n stopped = true;\n const b = boss;\n bossStarting = null; // 停机后 ensureBoss 不再复用旧实例/旧建连 promise\n if (!b) return;\n\n let deadlineTimer: ReturnType<typeof setTimeout> | undefined;\n let aborted = false;\n const deadline = new Promise<void>((resolve) => {\n deadlineTimer = setTimeout(() => {\n aborted = true;\n // deadline 到点 abort 在跑任务:监听 signal 的任务尽快退出;仍不退出的由\n // pg-boss 的 expire_in 兜底结算(resolveWithinSeconds 判超时失败并重试)\n for (const controller of inflight.values()) controller.abort();\n resolve();\n }, timeoutMs);\n });\n try {\n // offWork 等待在跑 handler 结束——卡死的任务会让它永久悬挂,必须与 deadline\n // 竞速保证 stop() 有界返回\n await Promise.race([\n (async () => {\n for (const [taskName, workerId] of workerIds.entries()) {\n await b.offWork(taskName, { id: workerId }).catch(() => {});\n }\n })(),\n deadline,\n ]);\n workerIds.clear();\n // graceful 收尾:pg-boss stop 的 timeout 单位是毫秒(其实现以 Date.now() 差值\n // 比较,超时 failWip 处置在跑任务)。整体与 deadline 竞速,超时即放弃等待\n await Promise.race([b.stop({ close: true, graceful: true, timeout: timeoutMs }), deadline]);\n boss = null;\n } finally {\n clearTimeout(deadlineTimer);\n if (!aborted) {\n for (const controller of inflight.values()) controller.abort();\n }\n inflight.clear();\n }\n },\n\n async stopWorkers() {\n const b = boss;\n if (b) {\n for (const [taskName, workerId] of workerIds.entries()) {\n await b.offWork(taskName, { id: workerId }).catch(() => {});\n }\n }\n workerIds.clear();\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 const b = await ensureBoss();\n await b.cancel(name, id);\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 b = await ensureBoss();\n // pg-boss v10 语义:resume 恢复 cancelled 任务;failed 任务无原生重试 API\n await b.resume(name, id);\n },\n\n async list(opts) {\n const b = await ensureBoss();\n // findJobs 按队列名查(无全局列出):不传 name 遍历本进程已 ensureQueue 的\n // 任务名(与 BullMQ 驱动遍历已建 Queue 实例同语义);FindJobsOptions 无\n // state/limit 过滤,映射后自行过滤 + 降序截断\n const targets = opts?.name ? [opts.name] : [...ensuredQueues];\n // pg-boss 六态 → faapi 六态精确映射(BullMQ 的 delayed 无法区分重试等待,\n // 只能归 pending;pg-boss 的 retry 态可精确区分)\n const stateMap: Record<string, TaskDriverRecord['status']> = {\n created: 'pending',\n retry: 'retry',\n active: 'running',\n completed: 'done',\n failed: 'failed',\n cancelled: 'cancelled',\n };\n const wanted = opts?.state ? [opts.state] : undefined;\n const records: TaskDriverRecord[] = [];\n for (const target of targets) {\n const jobs = await b.findJobs(target);\n for (const job of jobs) {\n const status = stateMap[job.state];\n if (!status) continue;\n if (wanted && !wanted.includes(status)) continue;\n // fail 的 data 存 output jsonb:{ name, message } 或 { value }(startWorker\n // 结算形态);错误摘要优先取 message/value——提取不到(空对象/缺失)时\n // 不放 error 字段,其余形态 String 化保底\n const output: unknown = (job as { output?: unknown }).output;\n let error: string | undefined;\n if (status === 'failed' && output !== undefined && output !== null) {\n if (typeof output === 'object') {\n const extracted =\n (output as { message?: unknown; value?: unknown }).message ??\n (output as { message?: unknown; value?: unknown }).value;\n error = extracted === undefined ? undefined : String(extracted);\n } else {\n error = String(output);\n }\n }\n records.push({\n id: job.id,\n name: job.name,\n payload: job.data,\n status,\n // retryCount 从 0 起(首次执行为 0)→ faapi attempts 从 1 起\n attempts: job.retryCount + 1,\n // 驱动 complete() 不携带执行结果(语义层自记),done 记录无 result\n createdAt: job.createdOn.getTime(),\n // startAfter 恒有值(send 未传时默认 now)——计划执行时间即本字段语义\n runAt: job.startAfter.getTime(),\n ...(error !== undefined ? { error } : {}),\n });\n }\n }\n return records.sort((a, b2) => b2.createdAt - a.createdAt).slice(0, opts?.limit ?? 50);\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type {\n TaskDriver,\n TaskDriverGroupCreate,\n TaskDriverGroupOps,\n TaskDriverProcess,\n TaskDriverRecord,\n TaskGroupSnapshot,\n} from '@faapi/faapi';\nimport { PgBoss } from 'pg-boss';\nimport type { ConstructorOptions, SendOptions } from 'pg-boss';\n\n/**\n * pg-boss 驱动选项\n *\n * `defaultExpireSeconds` 之外全部透传给 PgBoss 构造函数(常用:`connectionString`,\n * 其余见 pg-boss 文档 ConstructorOptions)。\n */\nexport type PgBossDriverOptions = ConstructorOptions & {\n /**\n * 未声明 `timeoutMs` 的任务的 expire_in 兜底秒数(默认 24h − 1s)。\n *\n * pg-boss 以 job 的 expire_in 硬限 handler 执行(DDL 默认 15 分钟):超时判失败\n * 重试,而任务还在后台跑 → 同一任务两份并发执行。声明了 `timeoutMs` 的任务由\n * 驱动按 `timeoutMs + graceMs + 60s` 缓冲给足;未声明的任务用本兜底。\n * 默认 24h − 1s:pg-boss 12.28 前断言 `expireIn/3600 < 24`(排他),减 1 秒落\n * 界内(12.28 起放宽到允许恰好 24h,保守值对两者都安全);显式配置超过 86399\n * 在 12.28 前的版本会被 send 参数校验阶段拒绝。\n *\n * 声明侧上界由主包 `scanTasks` 构建期校验(`MAX_ISOLATED_TIMEOUT_MS` 23h),\n * 保证 `timeoutMs + graceMs + 缓冲` 的 expire 预算恒落在断言界内。\n */\n defaultExpireSeconds?: number;\n};\n\n/**\n * 成员组标识的载荷包装(组投递成员的传输载体)\n *\n * pg-boss 的 job 除 data(payload)外无任意元数据字段,组标识随载荷一起存储:\n * `enqueue(opts.groupId)` 时包装、work 交付 / findJobs 查询时还原。仅组任务包装\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 * 任务组记账表(同库自建,与 pg-boss 自身 schema 表无关)\n *\n * 组行 = 计数器 + 完成回调声明 + 失败策略;成员行 = job 级 settled/outcome,\n * 落定记账的幂等守卫(同一成员重复 settle 不重复计数)。首次组操作时\n * CREATE TABLE IF NOT EXISTS,业务方按保留策略自行清理(框架不自动删——\n * 自动删业务可能还要查的记账是静默丢数据)。\n */\nconst GROUP_TABLES_SQL = [\n `CREATE TABLE IF NOT EXISTS faapi_task_groups (\n id text PRIMARY KEY,\n task text NOT NULL,\n total integer NOT NULL,\n done integer NOT NULL DEFAULT 0,\n failed integer NOT NULL DEFAULT 0,\n cancelled integer NOT NULL DEFAULT 0,\n on_complete text,\n on_failure text NOT NULL,\n completion_enqueued boolean NOT NULL DEFAULT false,\n created_at timestamptz NOT NULL DEFAULT now()\n)`,\n `CREATE TABLE IF NOT EXISTS faapi_task_group_members (\n group_id text NOT NULL,\n job_id text NOT NULL,\n settled boolean NOT NULL DEFAULT false,\n outcome text,\n PRIMARY KEY (group_id, job_id)\n)`,\n];\n\n/**\n * faapi 任务队列 pg-boss 驱动(PostgreSQL 持久化队列)\n *\n * 与 faapi 主包 `config.task.driver: 'pgboss'` 配合使用:\n *\n * ```ts\n * // faapi.config.ts\n * export default {\n * task: {\n * driver: 'pgboss',\n * pgboss: { connectionString: 'postgres://localhost:5432/app' },\n * },\n * } satisfies FaapiConfig;\n * ```\n *\n * 语义映射(详见包根 README):\n * - `enqueue` → 幂等 ensureQueue + `boss.send(name, payload, { retryLimit, retryDelay, retryBackoff, expireInSeconds, startAfter })`;\n * 组投递成员(opts.groupId)以载荷包装携带组标识并在成员表登记\n * - `startWorker` → 幂等 ensureQueue + `boss.work(name, { batchSize: concurrency, includeMetadata: true }, handler)`;\n * 批内任务并发执行、逐任务 complete/fail 结算(失败不毒化同批);交付时从载荷\n * 包装还原 groupId\n * - `stop` → `offWork` + `boss.stop({ close: true, graceful: true, timeout })` 整体与\n * deadline 竞速;deadline 到点 abort 在跑任务的 signal\n * - 重试 → pg-boss 侧执行(retryLimit + retryBackoff 指数退避)\n * - `list` → `boss.findJobs(name)`(v12):六态精确映射(created→pending、retry→retry、\n * active→running、completed→done、failed→failed、cancelled→cancelled);不传 name\n * 遍历本进程已 ensureQueue 的任务名;createdAt 降序截断 limit\n * - `groups` → 同库两张表(faapi_task_groups / faapi_task_group_members)经\n * `boss.getDb().executeSql` 计账:成员行 settled 守卫保证落定幂等,CTE 内\n * 成员翻转 + 计数递增单语句原子\n */\n/**\n * dedupId → 确定性 UUID:pg-boss 的 send 自定义 id 要求 UUID 格式(SQL 侧 cast),\n * 任意字符串键经 SHA-1 映射为合法 UUID——同键必同 UUID(幂等),不同键碰撞可忽略\n */\nfunction dedupIdToUuid(dedupId: string): string {\n const h = createHash('sha1').update(dedupId).digest('hex');\n return [\n h.slice(0, 8),\n h.slice(8, 12),\n `5${h.slice(13, 16)}`, // version 5\n `${((parseInt(h.slice(16, 17), 16) & 0x3) | 0x8).toString(16)}${h.slice(17, 20)}`, // variant\n h.slice(20, 32),\n ].join('-');\n}\n\n/** 未声明 timeoutMs 的任务 expire_in 兜底(秒)——pg-boss 12.28 前断言 expireIn/3600 < 24\n * (严格小于),顶到 24h 整会让 send() 在参数校验阶段必抛 AssertionError,减 1 秒落界内\n * (12.28 起放宽到允许恰好 24h,保守值对两者都安全) */\nconst DEFAULT_EXPIRE_SECONDS = 24 * 60 * 60 - 1;\n/** expire_in 在任务执行预算之外的缓冲(秒)——留出调度/网络抖动余量 */\nconst EXPIRE_BUFFER_SECONDS = 60;\n/** graceMs 未声明时的默认值(与主包 taskWorker 的取消宽限期默认一致) */\nconst DEFAULT_GRACE_MS = 5000;\n\nexport function createPgBossDriver(driverOptions: PgBossDriverOptions = {}): TaskDriver {\n const { defaultExpireSeconds, ...options } = driverOptions;\n let boss: PgBoss | null = null;\n let stopped = false;\n /** 已创建的 pg-boss worker id(stopWorkers 时 offWork) */\n const workerIds = new Map<string, string>();\n /** 在跑任务的取消控制器(stop 超时 abort——run 监听 signal 可尽快退出) */\n const inflight = new Map<string, AbortController>();\n /** 已建队列名缓存(每任务名每进程一次真实 createQueue,之后短路) */\n const ensuredQueues = new Set<string>();\n /** 并发首次投递同名任务的 in-flight 去重 */\n const ensuring = new Map<string, Promise<void>>();\n /** boss.start() in-flight 共享 promise(并发首调等待同一次建连,不出现半启动实例) */\n let bossStarting: Promise<PgBoss> | null = null;\n /** 组记账表已确保(每进程一次 CREATE TABLE IF NOT EXISTS,之后短路) */\n let groupTablesEnsured = false;\n\n async function ensureGroupTables(b: PgBoss): Promise<void> {\n if (groupTablesEnsured) return;\n const db = b.getDb();\n for (const sql of GROUP_TABLES_SQL) {\n await db.executeSql(sql, []);\n }\n groupTablesEnsured = true;\n }\n\n /** 组行 → TaskGroupSnapshot(行字段 snake_case → 快照契约) */\n function rowToSnapshot(row: Record<string, unknown>): TaskGroupSnapshot {\n const done = Number(row.done);\n const failed = Number(row.failed);\n const cancelled = Number(row.cancelled);\n const total = Number(row.total);\n const settled = done + failed + cancelled;\n return {\n groupId: String(row.id),\n task: String(row.task),\n total,\n done,\n failed,\n cancelled,\n settled,\n status: settled >= total ? 'settled' : 'open',\n completionEnqueued: row.completion_enqueued === true,\n ...(row.on_complete !== null && row.on_complete !== undefined\n ? { onComplete: String(row.on_complete) }\n : {}),\n onFailure: String(row.on_failure) as TaskGroupSnapshot['onFailure'],\n };\n }\n\n /**\n * 组记账实现(TaskDriver.groups)——经 boss.getDb().executeSql 计账。\n * 落定幂等由成员行 settled 守卫承载;成员翻转 + 计数递增在单条 CTE 语句内原子。\n */\n function createGroupOps(getBoss: () => Promise<PgBoss>): TaskDriverGroupOps {\n return {\n async create(decl: TaskDriverGroupCreate): Promise<void> {\n const b = await getBoss();\n await ensureGroupTables(b);\n const db = b.getDb();\n const inserted = await db.executeSql(\n `INSERT INTO faapi_task_groups (id, task, total, on_complete, on_failure)\n VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO NOTHING RETURNING id`,\n [decl.id, decl.task, decl.total, decl.onComplete ?? null, decl.onFailure],\n );\n if (inserted.rows.length > 0) return;\n // 同 id 已存在:参数完全一致幂等跳过;不一致抛错(组标识是业务关联键,\n // 形状漂移属调用方错误——静默沿用旧声明会让 fan-in 指向错误的回调任务)\n const existing = await db.executeSql(\n `SELECT task, total, on_complete, on_failure FROM faapi_task_groups WHERE id = $1`,\n [decl.id],\n );\n const row = existing.rows[0] as Record<string, unknown> | undefined;\n const same =\n row !== undefined &&\n row.task === decl.task &&\n Number(row.total) === decl.total &&\n (row.on_complete ?? null) === (decl.onComplete ?? null) &&\n row.on_failure === decl.onFailure;\n if (!same) {\n throw new Error(\n `[faapi] Task group \"${decl.id}\" already exists with different options ` +\n `(existing: task=${row?.task}, total=${row?.total}, onComplete=${row?.on_complete}, ` +\n `onFailure=${row?.on_failure})`,\n );\n }\n },\n\n async settle(groupId, jobId, outcome) {\n const b = await getBoss();\n await ensureGroupTables(b);\n const db = b.getDb();\n // 单语句原子:成员行 settled 守卫翻转(已落定成员返回 0 行 → 不递增)+\n // 组行计数按 outcome 递增 + 返回记账后快照\n const result = await db.executeSql(\n `WITH member AS (\n INSERT INTO faapi_task_group_members (group_id, job_id, settled, outcome)\n VALUES ($1, $2, true, $3)\n ON CONFLICT (group_id, job_id) DO UPDATE\n SET settled = true, outcome = EXCLUDED.outcome\n WHERE faapi_task_group_members.settled = false\n RETURNING job_id\n )\n UPDATE faapi_task_groups g\n SET done = done + CASE WHEN $3 = 'done' THEN 1 ELSE 0 END,\n failed = failed + CASE WHEN $3 = 'failed' THEN 1 ELSE 0 END,\n cancelled = cancelled + CASE WHEN $3 = 'cancelled' THEN 1 ELSE 0 END\n FROM member\n WHERE g.id = $1\n RETURNING g.id, g.task, g.total, g.done, g.failed, g.cancelled,\n g.on_complete, g.on_failure, g.completion_enqueued`,\n [groupId, jobId, outcome],\n );\n const row = result.rows[0] as Record<string, unknown> | undefined;\n if (row !== undefined) {\n const snap = rowToSnapshot(row);\n return { ...snap, isLast: snap.settled >= snap.total };\n }\n // 成员已落定(重复 settle):计数不变,返回当前快照(isLast=false——\n // 重复落定不重新触发 fan-in;回调自身还有 dedupId + completionEnqueued 双守卫)\n const current = await db.executeSql(\n `SELECT id, task, total, done, failed, cancelled, on_complete, on_failure,\n completion_enqueued\n FROM faapi_task_groups WHERE id = $1`,\n [groupId],\n );\n const cur = current.rows[0] as Record<string, unknown> | undefined;\n if (cur === undefined) {\n throw new Error(`[faapi] Task group \"${groupId}\" not found`);\n }\n return { ...rowToSnapshot(cur), isLast: false };\n },\n\n async unsettle(groupId, jobId) {\n const b = await getBoss();\n const db = b.getDb();\n // 单语句原子:成员行 settled 守卫撤销(未落定成员返回 0 行 → 不递减)+\n // 组行计数按原 outcome 递减\n await db.executeSql(\n `WITH member AS (\n UPDATE faapi_task_group_members\n SET settled = false, outcome = NULL\n WHERE group_id = $1 AND job_id = $2 AND settled = true\n RETURNING outcome\n )\n UPDATE faapi_task_groups g\n SET done = done - CASE WHEN member.outcome = 'done' THEN 1 ELSE 0 END,\n failed = failed - CASE WHEN member.outcome = 'failed' THEN 1 ELSE 0 END,\n cancelled = cancelled - CASE WHEN member.outcome = 'cancelled' THEN 1 ELSE 0 END\n FROM member\n WHERE g.id = $1`,\n [groupId, jobId],\n );\n },\n\n async markCompletionEnqueued(groupId) {\n const b = await getBoss();\n const db = b.getDb();\n await db.executeSql(\n `UPDATE faapi_task_groups SET completion_enqueued = true WHERE id = $1`,\n [groupId],\n );\n },\n\n async get(groupId) {\n const b = await getBoss();\n const db = b.getDb();\n const result = await db.executeSql(\n `SELECT id, task, total, done, failed, cancelled, on_complete, on_failure,\n completion_enqueued\n FROM faapi_task_groups WHERE id = $1`,\n [groupId],\n );\n const row = result.rows[0] as Record<string, unknown> | undefined;\n return row === undefined ? undefined : rowToSnapshot(row);\n },\n\n async cancelRemaining(groupId) {\n const b = await getBoss();\n const db = b.getDb();\n const group = await this.get(groupId);\n if (group === undefined) {\n throw new Error(`[faapi] Task group \"${groupId}\" not found`);\n }\n // 仅 fail-fast 组执行取消语义(run-to-completion 为 no-op,返回当前快照)\n if (group.onFailure !== 'fail-fast') return group;\n const pending = await db.executeSql(\n `SELECT job_id FROM faapi_task_group_members WHERE group_id = $1 AND settled = false`,\n [groupId],\n );\n for (const row of pending.rows as Array<{ job_id: string }>) {\n const jobId = row.job_id;\n // cancel(等待/延迟中的不再执行)后经 getJobById 核实真实生效才落定——\n // pg-boss 对不可取消状态静默 no-op,落定前核实防与运行实例的落定竞态重复计数\n await b.cancel(group.task, jobId);\n const job = await b.getJobById(group.task, jobId);\n if (job !== null && job.state === 'cancelled') {\n await this.settle(groupId, jobId, 'cancelled');\n }\n }\n const after = await this.get(groupId);\n return after!;\n },\n };\n }\n\n async function ensureBoss(): Promise<PgBoss> {\n if (boss) return boss;\n if (!bossStarting) {\n const starting = (async () => {\n const b = new PgBoss(options);\n // pg-boss 把连接池错误(PG 重启/网络闪断)与 worker 异常 emit 为 'error' 事件,\n // EventEmitter 无监听器时 emit 即抛 uncaughtException 崩进程——必须挂监听\n b.on('error', (err: unknown) => {\n console.error(\n '[faapi] pg-boss error:',\n err instanceof Error ? (err.stack ?? err.message) : err,\n );\n });\n // pg-boss 惰性连接:start 后才连库;驱动在首次 enqueue/startWorker 前建立。\n // start 完成前实例不外借——pre-open 的 executeSql 静默 no-op(返回 undefined),\n // 建连中并发调用会在半启动实例上跑 SQL\n await b.start();\n if (stopped) {\n // 停机竞态:start 期间 stop() 已执行——不回写实例(否则泄漏一个已 start、\n // 永不 stop 的 PgBoss),关闭后让调用方拿到失败\n await b.stop().catch(() => {});\n throw new Error('[faapi] Task queue is stopped and no longer accepts jobs');\n }\n boss = b;\n return b;\n })();\n bossStarting = starting;\n // start 失败清空 in-flight:下次调用重新建连(坏实例不入缓存);\n // 身份校验防止误清 stop() 之后新建的 promise\n void starting.catch(() => {\n if (bossStarting === starting) bossStarting = null;\n });\n }\n return bossStarting;\n }\n\n /**\n * pg-boss v10 不再隐式建队列(v9 行为):send() 的 INSERT JOIN queue 对未创建队列\n * 静默返回 null,work() 只注册进程内轮询器同样不建队列——投递/注册 worker 前先建队列。\n * create_queue plpgsql 幂等(INSERT ON CONFLICT DO NOTHING,已存在直接返回),\n * 重复/并发调用安全;失败不入缓存,下次投递重试。\n */\n async function ensureQueue(b: PgBoss, name: string): Promise<void> {\n if (ensuredQueues.has(name)) return;\n const pending = ensuring.get(name);\n if (pending) return pending;\n const promise = (async () => {\n await b.createQueue(name);\n ensuredQueues.add(name);\n })().finally(() => {\n ensuring.delete(name);\n });\n ensuring.set(name, promise);\n return promise;\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 b = await ensureBoss();\n await ensureQueue(b, name);\n // pg-boss 以 job 的 expire_in 硬限 handler 执行(DDL 默认 15 分钟):超时判失败\n // 重试,而任务还在后台跑 → 同一任务两份并发执行。按任务元信息给足执行预算:\n // timeoutMs + graceMs + 缓冲;未声明 timeoutMs 的任务用 defaultExpireSeconds 兜底\n const expireInSeconds = opts?.timeoutMs\n ? Math.ceil((opts.timeoutMs + (opts.graceMs ?? DEFAULT_GRACE_MS)) / 1000) +\n EXPIRE_BUFFER_SECONDS\n : (defaultExpireSeconds ?? DEFAULT_EXPIRE_SECONDS);\n const sendOptions: SendOptions = {\n retryLimit: opts?.retries ?? 0,\n retryDelay: 1, // 秒;配合 retryBackoff 指数退避\n retryBackoff: true,\n expireInSeconds,\n ...(opts?.dedupId ? { id: dedupIdToUuid(opts.dedupId) } : {}),\n ...(opts?.delayMs ? { startAfter: new Date(Date.now() + opts.delayMs) } : {}),\n };\n // pg-boss data 形参为 object——基础类型 payload(cron 空任务等)按 JSON 语义透传;\n // 组投递成员以载荷包装携带组标识(交付时还原,业务 payload 不变)\n const id = await b.send(\n name,\n opts?.groupId !== undefined ? wrapGroupPayload(opts.groupId, payload) : (payload as object),\n sendOptions,\n );\n const jobId = id ?? (opts?.dedupId ? dedupIdToUuid(opts.dedupId) : null);\n if (!jobId) {\n // 无 dedupId 的失败投递才是异常(dedupId 幂等命中已在上方还原确定性 id)\n throw new Error(`[faapi] pg-boss send failed for task \"${name}\"`);\n }\n // 组投递成员登记(成员表行 = 落定记账的幂等守卫载体;幂等命中时\n // ON CONFLICT DO NOTHING——重复投递不重复登记)\n if (opts?.groupId !== undefined) {\n await ensureGroupTables(b);\n await b.getDb().executeSql(\n `INSERT INTO faapi_task_group_members (group_id, job_id)\n VALUES ($1, $2) ON CONFLICT (group_id, job_id) DO NOTHING`,\n [opts.groupId, jobId],\n );\n }\n return jobId;\n },\n\n async startWorker(name, workerOpts) {\n const b = await ensureBoss();\n await ensureQueue(b, name);\n const process: TaskDriverProcess = workerOpts.process;\n // 二次注册(reload 场景)先 offWork 旧 worker(v12 签名 offWork(name, { id }))\n const existing = workerIds.get(name);\n if (existing) {\n await b.offWork(name, { id: existing });\n }\n const workerId = await b.work<unknown>(\n name,\n { batchSize: workerOpts.concurrency, includeMetadata: true },\n async (jobs) => {\n // 批内并发执行 + 逐任务结算:complete/fail 按单 job id 调用(pg-boss 的结算\n // SQL 带 state 守卫——complete 仅 active、fail 仅 state<completed,handler\n // 正常返回后 onFetch 的批量 complete 对已结算任务是 no-op)。单个任务失败只\n // 消耗自己的重试额度,不毒化同批其他任务;concurrency 语义与 BullMQ 驱动一致\n await Promise.all(\n jobs.map(async (job) => {\n // retryCount 从 0 起(首次执行为 0)→ faapi attempt 从 1 起\n const attempt = job.retryCount + 1;\n // 组投递成员:从载荷包装还原业务 payload 与组标识(语义层据此记账)\n const { payload, groupId } = unwrapGroupPayload(job.data);\n // 信号由驱动自管:stop 超时 abort(pg-boss 自身不提供执行中任务的取消能力)\n const controller = new AbortController();\n inflight.set(job.id, controller);\n try {\n try {\n await process({\n id: job.id,\n name,\n payload,\n attempt,\n signal: controller.signal,\n ...(groupId !== undefined ? { groupId } : {}),\n });\n } catch (err) {\n // fail 的 data 存 jsonb:传可序列化的错误摘要(原始 Error 序列化为 {} 丢信息)\n const reason =\n err instanceof Error\n ? { name: err.name, message: err.message }\n : { value: String(err) };\n try {\n await b.fail(name, job.id, reason);\n } catch (failErr) {\n // fail 结算失败:任务留在 active,由 pg-boss 的 expire_in 兜底结算\n console.error(\n `[faapi] pg-boss fail() failed for job ${job.id} of \"${name}\":`,\n failErr,\n );\n }\n return;\n }\n try {\n await b.complete(name, job.id);\n } catch (completeErr) {\n // complete 结算失败:任务留在 active,由 expire_in 兜底(不标失败——\n // 任务已成功执行,标失败会触发重试导致重复执行)\n console.error(\n `[faapi] pg-boss complete() failed for job ${job.id} of \"${name}\":`,\n completeErr,\n );\n }\n } finally {\n inflight.delete(job.id);\n }\n }),\n );\n },\n );\n workerIds.set(name, workerId);\n },\n\n async stop(timeoutMs = 10_000) {\n stopped = true;\n const b = boss;\n bossStarting = null; // 停机后 ensureBoss 不再复用旧实例/旧建连 promise\n if (!b) return;\n\n let deadlineTimer: ReturnType<typeof setTimeout> | undefined;\n let aborted = false;\n const deadline = new Promise<void>((resolve) => {\n deadlineTimer = setTimeout(() => {\n aborted = true;\n // deadline 到点 abort 在跑任务:监听 signal 的任务尽快退出;仍不退出的由\n // pg-boss 的 expire_in 兜底结算(resolveWithinSeconds 判超时失败并重试)\n for (const controller of inflight.values()) controller.abort();\n resolve();\n }, timeoutMs);\n });\n try {\n // offWork 等待在跑 handler 结束——卡死的任务会让它永久悬挂,必须与 deadline\n // 竞速保证 stop() 有界返回\n await Promise.race([\n (async () => {\n for (const [taskName, workerId] of workerIds.entries()) {\n await b.offWork(taskName, { id: workerId }).catch(() => {});\n }\n })(),\n deadline,\n ]);\n workerIds.clear();\n // graceful 收尾:pg-boss stop 的 timeout 单位是毫秒(其实现以 Date.now() 差值\n // 比较,超时 failWip 处置在跑任务)。整体与 deadline 竞速,超时即放弃等待\n await Promise.race([b.stop({ close: true, graceful: true, timeout: timeoutMs }), deadline]);\n boss = null;\n } finally {\n clearTimeout(deadlineTimer);\n if (!aborted) {\n for (const controller of inflight.values()) controller.abort();\n }\n inflight.clear();\n }\n },\n\n async stopWorkers() {\n const b = boss;\n if (b) {\n for (const [taskName, workerId] of workerIds.entries()) {\n await b.offWork(taskName, { id: workerId }).catch(() => {});\n }\n }\n workerIds.clear();\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 const b = await ensureBoss();\n await b.cancel(name, id);\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 b = await ensureBoss();\n // pg-boss v10 语义:resume 恢复 cancelled 任务;failed 任务无原生重试 API\n await b.resume(name, id);\n },\n\n async list(opts) {\n const b = await ensureBoss();\n // findJobs 按队列名查(无全局列出):不传 name 遍历本进程已 ensureQueue 的\n // 任务名(与 BullMQ 驱动遍历已建 Queue 实例同语义);FindJobsOptions 无\n // state/limit 过滤,映射后自行过滤 + 降序截断\n const targets = opts?.name ? [opts.name] : [...ensuredQueues];\n // pg-boss 六态 → faapi 六态精确映射(BullMQ 的 delayed 无法区分重试等待,\n // 只能归 pending;pg-boss 的 retry 态可精确区分)\n const stateMap: Record<string, TaskDriverRecord['status']> = {\n created: 'pending',\n retry: 'retry',\n active: 'running',\n completed: 'done',\n failed: 'failed',\n cancelled: 'cancelled',\n };\n const wanted = opts?.state ? [opts.state] : undefined;\n const records: TaskDriverRecord[] = [];\n for (const target of targets) {\n const jobs = await b.findJobs(target);\n for (const job of jobs) {\n const status = stateMap[job.state];\n if (!status) continue;\n if (wanted && !wanted.includes(status)) continue;\n // 组投递成员:从载荷包装还原业务 payload 与组标识(记录对业务不可见包装)\n const { payload, groupId } = unwrapGroupPayload(job.data);\n // fail 的 data 存 output jsonb:{ name, message } 或 { value }(startWorker\n // 结算形态);错误摘要优先取 message/value——提取不到(空对象/缺失)时\n // 不放 error 字段,其余形态 String 化保底\n const output: unknown = (job as { output?: unknown }).output;\n let error: string | undefined;\n if (status === 'failed' && output !== undefined && output !== null) {\n if (typeof output === 'object') {\n const extracted =\n (output as { message?: unknown; value?: unknown }).message ??\n (output as { message?: unknown; value?: unknown }).value;\n error = extracted === undefined ? undefined : String(extracted);\n } else {\n error = String(output);\n }\n }\n records.push({\n id: job.id,\n name: job.name,\n payload,\n status,\n // retryCount 从 0 起(首次执行为 0)→ faapi attempts 从 1 起\n attempts: job.retryCount + 1,\n // 驱动 complete() 不携带执行结果(语义层自记),done 记录无 result\n createdAt: job.createdOn.getTime(),\n // startAfter 恒有值(send 未传时默认 now)——计划执行时间即本字段语义\n runAt: job.startAfter.getTime(),\n ...(groupId !== undefined ? { groupId } : {}),\n ...(error !== undefined ? { error } : {}),\n });\n }\n }\n return records.sort((a, b2) => b2.createdAt - a.createdAt).slice(0, opts?.limit ?? 50);\n },\n\n groups: createGroupOps(ensureBoss),\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faapi/task-pgboss",
3
- "version": "6.33.0",
3
+ "version": "6.34.0",
4
4
  "description": "pg-boss task queue driver for faapi — persistent queue on PostgreSQL",
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.33.0"
27
+ "@faapi/faapi": "6.34.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "pg-boss": "^12.0.0",
31
- "@faapi/faapi": "^6.33.0"
31
+ "@faapi/faapi": "^6.34.0"
32
32
  },
33
33
  "license": "MIT",
34
34
  "repository": {