@faapi/task-pgboss 0.0.0-canary.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -1
- package/dist/index.d.ts +33 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 faapi contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,50 @@
|
|
|
1
1
|
# @faapi/task-pgboss
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
faapi 任务子系统的 **pg-boss 驱动**(PostgreSQL 持久化队列)。
|
|
4
|
+
|
|
5
|
+
基于 PostgreSQL 的任务队列——任务不丢(持久化)、多实例天然防重跑(worker 领取制)、cron 重复投递由 pg-boss 消费语义兜底。复用业务方已有的 Postgres,不引入 Redis。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @faapi/task-pgboss pgboss
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 使用
|
|
14
|
+
|
|
15
|
+
任务定义不变(主包文件约定 `src/tasks/<name>/task.ts`),只切驱动:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// faapi.config.ts
|
|
19
|
+
import type { FaapiConfig } from '@faapi/faapi';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
task: {
|
|
23
|
+
driver: 'pgboss',
|
|
24
|
+
pgboss: { connectionString: 'postgres://localhost:5432/app' }, // 透传给 new PgBoss(...)
|
|
25
|
+
},
|
|
26
|
+
} satisfies FaapiConfig;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// handler 里用法完全不变
|
|
31
|
+
export function POST(body, tasks) {
|
|
32
|
+
return tasks.enqueue('send-email', { to: body.email });
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 语义映射
|
|
37
|
+
|
|
38
|
+
| faapi 驱动接口 | pg-boss |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `enqueue(name, payload, { retries, delayMs })` | `boss.send(name, payload, { retryLimit, retryDelay: 1, retryBackoff: true, startAfter })` |
|
|
41
|
+
| `startWorker(name, { concurrency, process })` | `boss.work(name, { batchSize: concurrency }, handler)` |
|
|
42
|
+
| `stop(timeoutMs)` | 停 work + `boss.stop({ close: true, timeout })` |
|
|
43
|
+
| `stopWorkers()` | `offWork()`(不断开连接,dev 热替换重注册用) |
|
|
44
|
+
| 失败重试 | pg-boss 侧执行(retryLimit + retryBackoff 指数退避) |
|
|
45
|
+
|
|
46
|
+
注意:pg-boss 不提供执行中任务的取消信号——`run` 的 `taskCtx.signal` 永不 abort,长任务请自行做超时控制。
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TaskDriver } from '@faapi/faapi';
|
|
2
|
+
import PgBoss from 'pg-boss';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* pg-boss 驱动选项(透传给 PgBoss 构造函数)
|
|
6
|
+
*
|
|
7
|
+
* 常用:`connectionString`;其余见 pg-boss 文档(ConstructorOptions)。
|
|
8
|
+
*/
|
|
9
|
+
type PgBossDriverOptions = PgBoss.ConstructorOptions;
|
|
10
|
+
/**
|
|
11
|
+
* faapi 任务队列 pg-boss 驱动(PostgreSQL 持久化队列)
|
|
12
|
+
*
|
|
13
|
+
* 与 faapi 主包 `config.task.driver: 'pgboss'` 配合使用:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* // faapi.config.ts
|
|
17
|
+
* export default {
|
|
18
|
+
* task: {
|
|
19
|
+
* driver: 'pgboss',
|
|
20
|
+
* pgboss: { connectionString: 'postgres://localhost:5432/app' },
|
|
21
|
+
* },
|
|
22
|
+
* } satisfies FaapiConfig;
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* 语义映射(详见包根 README):
|
|
26
|
+
* - `enqueue` → `boss.send(name, payload, { retryLimit, retryDelay, retryBackoff, startAfter })`
|
|
27
|
+
* - `startWorker` → `boss.work(name, { batchSize: concurrency, includeMetadata: true }, handler)`
|
|
28
|
+
* - `stop` → `offWork` + `boss.stop({ close: true, graceful: true, timeout })`
|
|
29
|
+
* - 重试 → pg-boss 侧执行(retryLimit + retryBackoff 指数退避)
|
|
30
|
+
*/
|
|
31
|
+
declare function createPgBossDriver(options?: PgBossDriverOptions): TaskDriver;
|
|
32
|
+
|
|
33
|
+
export { type PgBossDriverOptions, createPgBossDriver };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import PgBoss from "pg-boss";
|
|
3
|
+
function createPgBossDriver(options = {}) {
|
|
4
|
+
let boss = null;
|
|
5
|
+
let stopped = false;
|
|
6
|
+
const workerIds = /* @__PURE__ */ new Map();
|
|
7
|
+
async function ensureBoss() {
|
|
8
|
+
if (!boss) {
|
|
9
|
+
boss = new PgBoss(options);
|
|
10
|
+
await boss.start();
|
|
11
|
+
}
|
|
12
|
+
return boss;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
async enqueue(name, payload, opts) {
|
|
16
|
+
if (stopped) {
|
|
17
|
+
throw new Error("[faapi] Task queue is stopped and no longer accepts jobs");
|
|
18
|
+
}
|
|
19
|
+
const b = await ensureBoss();
|
|
20
|
+
const sendOptions = {
|
|
21
|
+
retryLimit: opts?.retries ?? 0,
|
|
22
|
+
retryDelay: 1,
|
|
23
|
+
// 秒;配合 retryBackoff 指数退避
|
|
24
|
+
retryBackoff: true,
|
|
25
|
+
...opts?.delayMs ? { startAfter: new Date(Date.now() + opts.delayMs) } : {}
|
|
26
|
+
};
|
|
27
|
+
const id = await b.send(name, payload, sendOptions);
|
|
28
|
+
if (!id) {
|
|
29
|
+
throw new Error(`[faapi] pg-boss send failed for task "${name}"`);
|
|
30
|
+
}
|
|
31
|
+
return id;
|
|
32
|
+
},
|
|
33
|
+
async startWorker(name, workerOpts) {
|
|
34
|
+
const b = await ensureBoss();
|
|
35
|
+
const process = workerOpts.process;
|
|
36
|
+
const existing = workerIds.get(name);
|
|
37
|
+
if (existing) {
|
|
38
|
+
await b.offWork(existing);
|
|
39
|
+
}
|
|
40
|
+
const workerId = await b.work(
|
|
41
|
+
name,
|
|
42
|
+
{ batchSize: workerOpts.concurrency, includeMetadata: true },
|
|
43
|
+
async (jobs) => {
|
|
44
|
+
for (const job of jobs) {
|
|
45
|
+
const attempt = job.retryCount + 1;
|
|
46
|
+
await process({
|
|
47
|
+
id: job.id,
|
|
48
|
+
name,
|
|
49
|
+
payload: job.data,
|
|
50
|
+
attempt,
|
|
51
|
+
signal: new AbortController().signal
|
|
52
|
+
// pg-boss 不提供执行中任务的取消信号
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
workerIds.set(name, workerId);
|
|
58
|
+
},
|
|
59
|
+
async stop(timeoutMs = 1e4) {
|
|
60
|
+
stopped = true;
|
|
61
|
+
const b = boss;
|
|
62
|
+
if (b) {
|
|
63
|
+
for (const workerId of workerIds.values()) {
|
|
64
|
+
await b.offWork(workerId).catch(() => {
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
workerIds.clear();
|
|
68
|
+
await b.stop({ close: true, graceful: true, timeout: Math.floor(timeoutMs / 1e3) });
|
|
69
|
+
boss = null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async stopWorkers() {
|
|
73
|
+
const b = boss;
|
|
74
|
+
if (b) {
|
|
75
|
+
for (const workerId of workerIds.values()) {
|
|
76
|
+
await b.offWork(workerId).catch(() => {
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
workerIds.clear();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export {
|
|
85
|
+
createPgBossDriver
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { TaskDriver, TaskDriverProcess } from '@faapi/faapi';\nimport PgBoss from 'pg-boss';\n\n/**\n * pg-boss 驱动选项(透传给 PgBoss 构造函数)\n *\n * 常用:`connectionString`;其余见 pg-boss 文档(ConstructorOptions)。\n */\nexport type PgBossDriverOptions = PgBoss.ConstructorOptions;\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` → `boss.send(name, payload, { retryLimit, retryDelay, retryBackoff, startAfter })`\n * - `startWorker` → `boss.work(name, { batchSize: concurrency, includeMetadata: true }, handler)`\n * - `stop` → `offWork` + `boss.stop({ close: true, graceful: true, timeout })`\n * - 重试 → pg-boss 侧执行(retryLimit + retryBackoff 指数退避)\n */\nexport function createPgBossDriver(options: PgBossDriverOptions = {}): TaskDriver {\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\n async function ensureBoss(): Promise<PgBoss> {\n if (!boss) {\n boss = new PgBoss(options);\n // pg-boss 惰性连接:start 后才连库;驱动在首次 enqueue/startWorker 前建立\n await boss.start();\n }\n return boss;\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 const sendOptions: PgBoss.SendOptions = {\n retryLimit: opts?.retries ?? 0,\n retryDelay: 1, // 秒;配合 retryBackoff 指数退避\n retryBackoff: true,\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 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 const process: TaskDriverProcess = workerOpts.process;\n // 二次注册(reload 场景)先 offWork 旧 worker\n const existing = workerIds.get(name);\n if (existing) {\n await b.offWork(existing);\n }\n const workerId = await b.work<unknown>(\n name,\n { batchSize: workerOpts.concurrency, includeMetadata: true },\n async (jobs) => {\n for (const job of jobs) {\n // retryCount 从 0 起(首次执行为 0)→ faapi attempt 从 1 起\n const attempt = job.retryCount + 1;\n await process({\n id: job.id,\n name,\n payload: job.data,\n attempt,\n signal: new AbortController().signal, // pg-boss 不提供执行中任务的取消信号\n });\n }\n // handler 正常返回 = 本批全部完成;抛错 = 整批失败由 pg-boss 按 retryLimit 重试\n },\n );\n workerIds.set(name, workerId);\n },\n\n async stop(timeoutMs = 10_000) {\n stopped = true;\n const b = boss;\n if (b) {\n for (const workerId of workerIds.values()) {\n await b.offWork(workerId).catch(() => {});\n }\n workerIds.clear();\n // graceful: 等 in-flight 任务完成;timeout 秒后强制处置\n await b.stop({ close: true, graceful: true, timeout: Math.floor(timeoutMs / 1000) });\n boss = null;\n }\n },\n\n async stopWorkers() {\n const b = boss;\n if (b) {\n for (const workerId of workerIds.values()) {\n await b.offWork(workerId).catch(() => {});\n }\n }\n workerIds.clear();\n },\n };\n}\n"],"mappings":";AACA,OAAO,YAAY;AA8BZ,SAAS,mBAAmB,UAA+B,CAAC,GAAe;AAChF,MAAI,OAAsB;AAC1B,MAAI,UAAU;AAEd,QAAM,YAAY,oBAAI,IAAoB;AAE1C,iBAAe,aAA8B;AAC3C,QAAI,CAAC,MAAM;AACT,aAAO,IAAI,OAAO,OAAO;AAEzB,YAAM,KAAK,MAAM;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,QAAQ,MAAM,SAAS,MAAM;AACjC,UAAI,SAAS;AACX,cAAM,IAAI,MAAM,0DAA0D;AAAA,MAC5E;AACA,YAAM,IAAI,MAAM,WAAW;AAC3B,YAAM,cAAkC;AAAA,QACtC,YAAY,MAAM,WAAW;AAAA,QAC7B,YAAY;AAAA;AAAA,QACZ,cAAc;AAAA,QACd,GAAI,MAAM,UAAU,EAAE,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE,IAAI,CAAC;AAAA,MAC7E;AAEA,YAAM,KAAK,MAAM,EAAE,KAAK,MAAM,SAAmB,WAAW;AAC5D,UAAI,CAAC,IAAI;AACP,cAAM,IAAI,MAAM,yCAAyC,IAAI,GAAG;AAAA,MAClE;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,YAAY,MAAM,YAAY;AAClC,YAAM,IAAI,MAAM,WAAW;AAC3B,YAAM,UAA6B,WAAW;AAE9C,YAAM,WAAW,UAAU,IAAI,IAAI;AACnC,UAAI,UAAU;AACZ,cAAM,EAAE,QAAQ,QAAQ;AAAA,MAC1B;AACA,YAAM,WAAW,MAAM,EAAE;AAAA,QACvB;AAAA,QACA,EAAE,WAAW,WAAW,aAAa,iBAAiB,KAAK;AAAA,QAC3D,OAAO,SAAS;AACd,qBAAW,OAAO,MAAM;AAEtB,kBAAM,UAAU,IAAI,aAAa;AACjC,kBAAM,QAAQ;AAAA,cACZ,IAAI,IAAI;AAAA,cACR;AAAA,cACA,SAAS,IAAI;AAAA,cACb;AAAA,cACA,QAAQ,IAAI,gBAAgB,EAAE;AAAA;AAAA,YAChC,CAAC;AAAA,UACH;AAAA,QAEF;AAAA,MACF;AACA,gBAAU,IAAI,MAAM,QAAQ;AAAA,IAC9B;AAAA,IAEA,MAAM,KAAK,YAAY,KAAQ;AAC7B,gBAAU;AACV,YAAM,IAAI;AACV,UAAI,GAAG;AACL,mBAAW,YAAY,UAAU,OAAO,GAAG;AACzC,gBAAM,EAAE,QAAQ,QAAQ,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QAC1C;AACA,kBAAU,MAAM;AAEhB,cAAM,EAAE,KAAK,EAAE,OAAO,MAAM,UAAU,MAAM,SAAS,KAAK,MAAM,YAAY,GAAI,EAAE,CAAC;AACnF,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,cAAc;AAClB,YAAM,IAAI;AACV,UAAI,GAAG;AACL,mBAAW,YAAY,UAAU,OAAO,GAAG;AACzC,gBAAM,EAAE,QAAQ,QAAQ,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QAC1C;AAAA,MACF;AACA,gBAAU,MAAM;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faapi/task-pgboss",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "6.3.0",
|
|
4
|
+
"description": "pg-boss task queue driver for faapi — persistent queue on PostgreSQL",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./index.js",
|
|
7
|
-
"
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.3.0",
|
|
23
|
+
"pg-boss": "^10.1.5",
|
|
24
|
+
"tsup": "^8.4.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"vitest": "^4.1.11",
|
|
27
|
+
"@faapi/faapi": "6.3.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"pg-boss": "^10.0.0",
|
|
31
|
+
"@faapi/faapi": "^6.3.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/faapi/faapi.git",
|
|
37
|
+
"directory": "packages/task-pgboss"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/faapi/faapi/issues"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"faapi",
|
|
44
|
+
"task-queue",
|
|
45
|
+
"pg-boss",
|
|
46
|
+
"postgres",
|
|
47
|
+
"driver"
|
|
48
|
+
],
|
|
49
|
+
"sideEffects": false,
|
|
8
50
|
"publishConfig": {
|
|
9
|
-
"access": "public"
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup",
|
|
56
|
+
"test": "vitest run --passWithNoTests",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"typecheck:test": "tsc --noEmit -p tsconfig.test.json"
|
|
10
59
|
}
|
|
11
|
-
}
|
|
60
|
+
}
|