@rei-standard/amsg-server 2.6.0-next.15 → 2.6.0-next.16
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 +109 -1
- package/dist/adapters/d1.d.ts +24 -0
- package/dist/adapters/interface.d.ts +17 -0
- package/dist/adapters/neon.d.ts +10 -0
- package/dist/adapters/pg.d.ts +10 -0
- package/dist/adapters/schema.sqlite.d.ts +12 -0
- package/dist/{chunk-TCKPLM3P.cjs → chunk-H4V7OXHE.cjs} +256 -52
- package/dist/{chunk-D3DQF647.mjs → chunk-KVHR3RCU.mjs} +255 -51
- package/dist/cloudflare/single-user-worker.d.ts +33 -2
- package/dist/cloudflare.cjs +16 -2
- package/dist/cloudflare.d.ts +3 -1
- package/dist/cloudflare.mjs +17 -3
- package/dist/index.cjs +34 -26
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +11 -3
- package/dist/lib/errors.d.ts +43 -0
- package/dist/lib/run-tick.d.ts +12 -2
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/{neon-ZWA2U7IZ.cjs → neon-G3G4TDAY.cjs} +15 -0
- package/dist/{neon-XLWK27RL.mjs → neon-GBUF4G76.mjs} +15 -0
- package/dist/{pg-Q4VU6D75.cjs → pg-C3JZKBUM.cjs} +14 -0
- package/dist/{pg-X63HFEV7.mjs → pg-MRXP5MHJ.mjs} +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -382,6 +382,108 @@ await runScheduledTick({
|
|
|
382
382
|
|
|
383
383
|
`lease_until` / `retry_after` / `serialize_group`(都可空)。走 `POST /init-tenant`(或任何一次 `initSchema`)会自动给已有的表补上,跑几次都没事;手工维护表结构的看 `examples/cloudflare-single-user/schema.sql`,Postgres 侧对应三句 `ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS …`。分组串行还多一个索引 `idx_serialize_group_lease`,`initSchema` 一并建。
|
|
384
384
|
|
|
385
|
+
## 表结构自查(`getSchemaVersion` / `ensureSchema`)
|
|
386
|
+
|
|
387
|
+
建表语句是 `CREATE TABLE IF NOT EXISTS`,已经存在的表不会被改动,后加的列靠 `initSchema()` 里的 ALTER 补。升级到新版之后没人再跑一次建表的话,这个部署的表就停在旧形状上——cron 每分钟挂在缺的那一列上,任务一条都不发,而前端界面一切正常。
|
|
388
|
+
|
|
389
|
+
这两个函数把「我需要什么 / 现在是什么 / 帮我补齐」露出来:
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
import { getSchemaVersion, ensureSchema } from '@rei-standard/amsg-server/cloudflare';
|
|
393
|
+
|
|
394
|
+
const state = await getSchemaVersion(db);
|
|
395
|
+
// → { current: '2.6.0' | null, required: '2.6.0', ok: true | false, missing: [] }
|
|
396
|
+
|
|
397
|
+
if (!state.ok) {
|
|
398
|
+
const fixed = await ensureSchema(db); // 建表 + 补列 + 建索引,重复调没事
|
|
399
|
+
// → { current, required, ok, missing, migrated: true, schema }
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
| 字段 | 是什么 |
|
|
404
|
+
|---|---|
|
|
405
|
+
| `required` | 这一版代码需要的表结构版本(导出常量 `SCHEMA_VERSION`)。表结构自己的版本号,只在表 / 列 / 关键索引变化时抬,与包版本各走各的 |
|
|
406
|
+
| `current` | 活库当前满足的版本:够用就是 `required` 那个值,缺东西就是 `null`(只知道不够用,不知道它停在哪一版) |
|
|
407
|
+
| `ok` | 需要的表 / 列 / 关键索引是不是都在 |
|
|
408
|
+
| `missing` | 缺什么,形如 `table:message_outbox` / `column:scheduled_messages.last_error` / `index:uidx_uuid`。整张表缺席时只报这一张表,不逐列展开 |
|
|
409
|
+
| `migrated`(只有 `ensureSchema` 有) | 这次有没有真的跑 `initSchema()`。本来就够用 → `false`,`schema` 也是 `null` |
|
|
410
|
+
|
|
411
|
+
「需要什么」是从建表语句里解析出来的,不是另抄一份清单:抄的那份漏掉新列的话,自查会对着一个缺列的库回「一切正常」。
|
|
412
|
+
|
|
413
|
+
什么时候调、缺了怎么提示用户,由宿主决定——库不会在每次请求里偷偷迁移。`POST /init-tenant` 的行为一点没变(它内部做的就是 `initSchema()`)。
|
|
414
|
+
|
|
415
|
+
单用户 Worker 上有走 `env` 的同名方法:`worker.getSchemaVersion(env)` / `worker.ensureSchema(env)`,省得自己再造一次适配器。
|
|
416
|
+
|
|
417
|
+
自查要求适配器实现 `describeSchema()`(活库里现在有哪些表 / 列 / 索引,只读)。内置适配器里目前只有 D1 实现了;别的适配器调这两个函数会抛错,而不是假装一切正常。
|
|
418
|
+
|
|
419
|
+
## 只跑指定那一条任务(`runTask`)
|
|
420
|
+
|
|
421
|
+
`scheduled()` 的语义是「扫一遍所有到期任务」。刚落库的任务想立刻跑起来时,触发一次全量扫描是能达到目的,但那样多个执行者会去扫同一批任务,宿主只能退回单实例串行才不重复发。
|
|
422
|
+
|
|
423
|
+
`runTask` 只跑指定那一条,走的是 cron 完全同一条投递链(占位、租约心跳、过期守卫、失败重试 / 终态、hook 全套):
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
// 单用户 Worker:从 env 拿库和配置
|
|
427
|
+
const result = await worker.runTask(uuid, env);
|
|
428
|
+
|
|
429
|
+
// 自己攒 ctx 的宿主(和 runScheduledTick 同一份 ctx)
|
|
430
|
+
import { runTask } from '@rei-standard/amsg-server/cloudflare';
|
|
431
|
+
const result = await runTask(ctx, uuid);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
跑起来了是 `{ ran: true, summary }`(`summary` 与 `runScheduledTick` 的返回同构,`totalTasks` 恒为 1)。不跑的几种情形分开回报,宿主不用猜:
|
|
435
|
+
|
|
436
|
+
| `reason` | 什么意思 | 附带 |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| `not_found` | 没有这条 uuid。一次性任务发完即删,所以「已经发完了」的那条也落在这里 | — |
|
|
439
|
+
| `already_settled` | 行还在,但已经是终态 | `status`(`sent` / `failed`) |
|
|
440
|
+
| `not_due` | 还没到触发时刻 | `nextSendAt` |
|
|
441
|
+
| `retry_pending` | 上次投递失败,还在退避窗口里 | `retryAfter` |
|
|
442
|
+
| `not_configured` | VAPID / webpush 没配齐,跑了也只是白扣这条任务一次重试(只有 `worker.runTask` 有这一种) | — |
|
|
443
|
+
|
|
444
|
+
这个入口只是换了个触发器,不是绕过排期的后门:没到点、在退避窗口里的都不跑。`already_settled` 要求适配器实现 `getTaskStatusByUuidOnly(uuid)`(D1 / pg / neon 都实现了);不实现的自定义适配器把这种情况并进 `not_found`。
|
|
445
|
+
|
|
446
|
+
## 出错时的真实原因
|
|
447
|
+
|
|
448
|
+
`fetch()` 兜底的 500 一直是 `{ success: false, error: { code: 'INTERNAL_ERROR', message: '服务器内部错误' } }`。这两个字段一个没动,真因加在 `error.cause` 上:
|
|
449
|
+
|
|
450
|
+
```json
|
|
451
|
+
{
|
|
452
|
+
"success": false,
|
|
453
|
+
"error": {
|
|
454
|
+
"code": "INTERNAL_ERROR",
|
|
455
|
+
"message": "服务器内部错误",
|
|
456
|
+
"cause": {
|
|
457
|
+
"stage": "request",
|
|
458
|
+
"name": "Error",
|
|
459
|
+
"message": "D1_ERROR: no such table: message_outbox"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
| 字段 | 是什么 |
|
|
466
|
+
|---|---|
|
|
467
|
+
| `stage` | 在哪一段炸的:`config` = 构建配置时(少了 binding、环境变量丢了),`request` = 路由或处理器抛错 |
|
|
468
|
+
| `name` | 错误类型(`error.name`,认不出来时是 `Error`) |
|
|
469
|
+
| `message` | 错误消息,长得像凭据的串已遮掉、超长截断到 500 字符 |
|
|
470
|
+
| `code` | 错误自带的 `code` 字符串,有才带 |
|
|
471
|
+
|
|
472
|
+
只带错误类型和消息文本:密钥、用户数据、任务正文都不在 `error.message` 上,也不往这里放。
|
|
473
|
+
|
|
474
|
+
cron 那条路上没有调用方能读到响应,所以另开两个出口:
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
export default createSingleUserCloudflareWorker(buildConfig, {
|
|
478
|
+
onError({ stage, error, cause, path }) {
|
|
479
|
+
// fetch / cron 任何一段出错都会调一次(best-effort,自身抛错只记日志)
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
- `onError` 放在工厂的第二个参数上,而不是 `buildConfig` 的返回值里:`buildConfig` 自己抛错时配置里的东西一个都读不到,而那恰恰是最需要被看见的一种故障。`stage` 在 cron 路径上是 `config`(配置构建失败 / VAPID 没配齐)或 `tick`(那一跳抛错)。VAPID 没配齐这一支没有异常对象,`error` 为 `null`、`cause.name` 是 `VapidNotConfigured`。
|
|
485
|
+
- `scheduled()` 现在有返回值:`{ ok: true, summary }` 或 `{ ok: false, cause }`。Cloudflare 不看它,是给「自己包一层再转调 `scheduled`」的宿主和测试用的。
|
|
486
|
+
|
|
385
487
|
## 导出 API(Exports)
|
|
386
488
|
|
|
387
489
|
包根(`@rei-standard/amsg-server`):
|
|
@@ -391,6 +493,10 @@ await runScheduledTick({
|
|
|
391
493
|
- `createSingleUserCloudflareWorker` — 单用户 Cloudflare Worker 一键装配(`fetch` + `scheduled` 两个入口)
|
|
392
494
|
- `createAdapter` / `createD1Adapter` — pg·neon / Cloudflare D1 数据库适配器
|
|
393
495
|
- `runScheduledTick` — 手动触发一轮到期任务投递(自定义 cron 宿主、要调 `claimLeaseMs` 时用)
|
|
496
|
+
- `runTask` — 只跑指定那一条任务(与 cron 同一条投递链)
|
|
497
|
+
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION` — 表结构自查与补齐
|
|
498
|
+
- `summarizeErrorCause` — 把异常压成响应体里 `error.cause` 那个形状(自己包一层路由、想回同样形状时用同一份)
|
|
499
|
+
- `NonRetryableError` / `isNonRetryableError` — hook 侧标注「重试也好不了」的失败
|
|
394
500
|
- `createWebCryptoWebPush` — 纯 Web Crypto 的 Web Push 发送器(不依赖 `web-push` 包)
|
|
395
501
|
- `createTenantToken` / `verifyTenantToken`
|
|
396
502
|
- `deriveUserEncryptionKey` / `decryptPayload` / `encryptForStorage` / `decryptFromStorage`
|
|
@@ -401,7 +507,9 @@ await runScheduledTick({
|
|
|
401
507
|
|
|
402
508
|
`@rei-standard/amsg-server/cloudflare` 子路径 —— 只含「单用户 + D1 + Web Crypto 推送」这条子图,不引用多租户装配线和 pg / neon / `web-push`,所以 D1-only 安装(不装可选数据库 peer)也能干净打包,Worker 不需要 `nodejs_compat` 兼容 flag:
|
|
403
509
|
|
|
404
|
-
- `createSingleUserCloudflareWorker` / `createSingleUserServer` / `createD1Adapter` / `runScheduledTick`
|
|
510
|
+
- `createSingleUserCloudflareWorker` / `createSingleUserServer` / `createD1Adapter` / `runScheduledTick` / `runTask`
|
|
511
|
+
- `getSchemaVersion` / `ensureSchema` / `SCHEMA_VERSION`
|
|
512
|
+
- `summarizeErrorCause` / `NonRetryableError` / `isNonRetryableError`
|
|
405
513
|
- `createWebCryptoWebPush` / `measurePushPayload` / `MAX_PUSH_PAYLOAD_BYTES` / `WEB_PUSH_MAX_BODY_BYTES` / `WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES`
|
|
406
514
|
- `deriveUserEncryptionKey` / `decryptPayload` / `encryptForStorage` / `decryptFromStorage`
|
|
407
515
|
|
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -36,6 +36,20 @@ export class D1Adapter {
|
|
|
36
36
|
error: any;
|
|
37
37
|
})[];
|
|
38
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* 活库里现在实际有哪些表 / 列 / 索引。
|
|
41
|
+
*
|
|
42
|
+
* 只读不写,纯粹如实回报:拿它跟这一版需要的清单对照的活儿在
|
|
43
|
+
* lib/schema-version.js(`getSchemaVersion` / `ensureSchema`)。库升级后表结
|
|
44
|
+
* 构变了而老部署没跑过 initSchema 时,cron 会每分钟静默挂在缺的那一列上,
|
|
45
|
+
* 界面上一切正常——这个方法就是让宿主查得出来。
|
|
46
|
+
*
|
|
47
|
+
* @returns {Promise<{ tables: Record<string, string[]>, indexes: string[] }>}
|
|
48
|
+
*/
|
|
49
|
+
describeSchema(): Promise<{
|
|
50
|
+
tables: Record<string, string[]>;
|
|
51
|
+
indexes: string[];
|
|
52
|
+
}>;
|
|
39
53
|
dropSchema(): Promise<void>;
|
|
40
54
|
createTask(params: any): Promise<any>;
|
|
41
55
|
/**
|
|
@@ -53,6 +67,16 @@ export class D1Adapter {
|
|
|
53
67
|
createTaskSuperseding(params: import("./interface.js").InsertTaskParams, supersedesUuid: string): Promise<any>;
|
|
54
68
|
getTaskByUuid(uuid: any, userId: any): Promise<any>;
|
|
55
69
|
getTaskByUuidOnly(uuid: any): Promise<any>;
|
|
70
|
+
/**
|
|
71
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(getTaskByUuidOnly 只看
|
|
72
|
+
* pending 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
73
|
+
*
|
|
74
|
+
* @param {string} uuid
|
|
75
|
+
* @returns {Promise<{ status: string }|null>}
|
|
76
|
+
*/
|
|
77
|
+
getTaskStatusByUuidOnly(uuid: string): Promise<{
|
|
78
|
+
status: string;
|
|
79
|
+
} | null>;
|
|
56
80
|
updateTaskById(taskId: any, updates: any): Promise<any>;
|
|
57
81
|
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<{
|
|
58
82
|
uuid: any;
|
|
@@ -29,6 +29,15 @@ export type DbAdapter = {
|
|
|
29
29
|
* Create the scheduled_messages table and all indexes.
|
|
30
30
|
*/
|
|
31
31
|
initSchema: () => Promise<InitSchemaResult>;
|
|
32
|
+
/**
|
|
33
|
+
* (可选)活库里现在实际有哪些表 / 列 / 索引,只读不写。
|
|
34
|
+
* `getSchemaVersion` / `ensureSchema`(lib/schema-version.js)拿它跟这一版
|
|
35
|
+
* 需要的清单对照;不实现的适配器调那两个函数会抛错。内置只有 D1 实现。
|
|
36
|
+
*/
|
|
37
|
+
describeSchema?: () => Promise<{
|
|
38
|
+
tables: Record<string, string[]>;
|
|
39
|
+
indexes: string[];
|
|
40
|
+
}>;
|
|
32
41
|
/**
|
|
33
42
|
* Drop the scheduled_messages table (CASCADE).
|
|
34
43
|
*/
|
|
@@ -45,6 +54,14 @@ export type DbAdapter = {
|
|
|
45
54
|
* Fetch a single pending task by uuid only (used by instant processing).
|
|
46
55
|
*/
|
|
47
56
|
getTaskByUuidOnly: (uuid: string) => Promise<TaskRow | null>;
|
|
57
|
+
/**
|
|
58
|
+
* (可选)这条 uuid 现在是什么状态——不限用户,也不限状态。`runTask` 用它
|
|
59
|
+
* 把「这条已经跑完进终态了」和「压根没这条」分开回报;不实现时两种都归
|
|
60
|
+
* `not_found`。
|
|
61
|
+
*/
|
|
62
|
+
getTaskStatusByUuidOnly?: (uuid: string) => Promise<{
|
|
63
|
+
status: string;
|
|
64
|
+
} | null>;
|
|
48
65
|
/**
|
|
49
66
|
* Partially update a task row by its numeric id.
|
|
50
67
|
* 实现了 `claimTask` 的适配器还要认 `lease_until` 和 `retry_after`(含写
|
package/dist/adapters/neon.d.ts
CHANGED
|
@@ -35,6 +35,16 @@ export class NeonAdapter {
|
|
|
35
35
|
createTask(params: any): Promise<Record<string, any>>;
|
|
36
36
|
getTaskByUuid(uuid: any, userId: any): Promise<Record<string, any>>;
|
|
37
37
|
getTaskByUuidOnly(uuid: any): Promise<Record<string, any>>;
|
|
38
|
+
/**
|
|
39
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
40
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
41
|
+
*
|
|
42
|
+
* @param {string} uuid
|
|
43
|
+
* @returns {Promise<{ status: string }|null>}
|
|
44
|
+
*/
|
|
45
|
+
getTaskStatusByUuidOnly(uuid: string): Promise<{
|
|
46
|
+
status: string;
|
|
47
|
+
} | null>;
|
|
38
48
|
updateTaskById(taskId: any, updates: any): Promise<Record<string, any>>;
|
|
39
49
|
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<Record<string, any>>;
|
|
40
50
|
deleteTaskById(taskId: any): Promise<boolean>;
|
package/dist/adapters/pg.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ export class PgAdapter {
|
|
|
37
37
|
createTask(params: any): Promise<any[]>;
|
|
38
38
|
getTaskByUuid(uuid: any, userId: any): Promise<any[]>;
|
|
39
39
|
getTaskByUuidOnly(uuid: any): Promise<any[]>;
|
|
40
|
+
/**
|
|
41
|
+
* 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
|
|
42
|
+
* 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
|
|
43
|
+
*
|
|
44
|
+
* @param {string} uuid
|
|
45
|
+
* @returns {Promise<{ status: string }|null>}
|
|
46
|
+
*/
|
|
47
|
+
getTaskStatusByUuidOnly(uuid: string): Promise<{
|
|
48
|
+
status: string;
|
|
49
|
+
} | null>;
|
|
40
50
|
updateTaskById(taskId: any, updates: any): Promise<any[]>;
|
|
41
51
|
updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<any[]>;
|
|
42
52
|
deleteTaskById(taskId: any): Promise<boolean>;
|
|
@@ -34,3 +34,15 @@ export const CLIENT_STATE_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS client_stat
|
|
|
34
34
|
export const PUSH_SUBSCRIPTION_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS push_subscriptions (\n user_id TEXT PRIMARY KEY,\n subscription TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n )\n";
|
|
35
35
|
export const MESSAGE_OUTBOX_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS message_outbox (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n task_uuid TEXT,\n session_id TEXT,\n message_index INTEGER,\n total_messages INTEGER,\n payload TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n delivered_at INTEGER,\n acked_at INTEGER,\n UNIQUE (user_id, message_id)\n )\n";
|
|
36
36
|
export const MESSAGE_OUTBOX_INDEX_SQL: "\n CREATE INDEX IF NOT EXISTS idx_outbox_unacked\n ON message_outbox (user_id, id)\n WHERE acked_at IS NULL\n";
|
|
37
|
+
/**
|
|
38
|
+
* 这一版代码跑起来需要的表 / 列 / 索引。
|
|
39
|
+
*
|
|
40
|
+
* 索引只列 critical 的那几个(uidx_uuid 之类):其余索引缺了只是慢,缺了它则
|
|
41
|
+
* 是正确性问题。
|
|
42
|
+
*
|
|
43
|
+
* @type {{ tables: Record<string, string[]>, indexes: string[] }}
|
|
44
|
+
*/
|
|
45
|
+
export const SQLITE_REQUIRED_SCHEMA: {
|
|
46
|
+
tables: Record<string, string[]>;
|
|
47
|
+
indexes: string[];
|
|
48
|
+
};
|