@rei-standard/amsg-server 2.6.0-next.9 → 3.0.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 +822 -41
- package/dist/adapters/d1.d.ts +693 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +384 -0
- package/dist/adapters/neon.d.ts +105 -0
- package/dist/adapters/pg-shared.d.ts +194 -0
- package/dist/adapters/pg.d.ts +101 -0
- package/dist/adapters/schema.d.ts +57 -0
- package/dist/adapters/schema.sqlite.d.ts +80 -0
- package/dist/chunk-7NMQFTDJ.cjs +148 -0
- package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
- package/dist/chunk-RL7V7VSS.mjs +8784 -0
- package/dist/chunk-TKDISKCF.cjs +163 -0
- package/dist/chunk-XKUWMYVF.mjs +163 -0
- package/dist/chunk-YBDQJVFQ.cjs +8784 -0
- package/dist/cloudflare/single-user-worker.d.ts +39 -0
- package/dist/cloudflare.cjs +35 -2
- package/dist/cloudflare.d.ts +11 -2
- package/dist/cloudflare.mjs +36 -3
- package/dist/handlers/cancel-message.d.ts +3 -0
- package/dist/handlers/capabilities.d.ts +4 -0
- package/dist/handlers/client-state-namespaces.d.ts +5 -0
- package/dist/handlers/client-state.d.ts +7 -0
- package/dist/handlers/cloud-data.d.ts +4 -0
- package/dist/handlers/get-message.d.ts +3 -0
- package/dist/handlers/get-user-key.d.ts +3 -0
- package/dist/handlers/init-tenant.d.ts +23 -0
- package/dist/handlers/llm-credentials.d.ts +5 -0
- package/dist/handlers/messages.d.ts +3 -0
- package/dist/handlers/outbox.d.ts +9 -0
- package/dist/handlers/push-subscription.d.ts +5 -0
- package/dist/handlers/schedule-message.d.ts +3 -0
- package/dist/handlers/send-notifications.d.ts +3 -0
- package/dist/handlers/single-user-init.d.ts +13 -0
- package/dist/handlers/update-message.d.ts +3 -0
- package/dist/handlers/vapid-public-key.d.ts +19 -0
- package/dist/index.cjs +133 -44
- package/dist/index.d.cts +145 -766
- package/dist/index.d.ts +145 -766
- package/dist/index.mjs +125 -36
- package/dist/lib/agentic-fire.d.ts +177 -0
- package/dist/lib/client-state-store.d.ts +88 -0
- package/dist/lib/cloud-data-cleanup.d.ts +36 -0
- package/dist/lib/cloud-data-guard.d.ts +14 -0
- package/dist/lib/cloud-data-inventory.d.ts +15 -0
- package/dist/lib/constant-time.d.ts +14 -0
- package/dist/lib/db-errors.d.ts +10 -0
- package/dist/lib/encryption.d.ts +48 -0
- package/dist/lib/errors.d.ts +273 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +12 -0
- package/dist/lib/message-processor.d.ts +82 -0
- package/dist/lib/outbox-store.d.ts +162 -0
- package/dist/lib/push-policy.d.ts +11 -0
- package/dist/lib/push-subscription-store.d.ts +94 -0
- package/dist/lib/recurrence.d.ts +51 -0
- package/dist/lib/request.d.ts +182 -0
- package/dist/lib/result-emitter.d.ts +60 -0
- package/dist/lib/retry-policy.d.ts +11 -0
- package/dist/lib/run-tick.d.ts +121 -0
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/lib/state-accessors.d.ts +70 -0
- package/dist/lib/state-chunks.d.ts +65 -0
- package/dist/lib/task-projection.d.ts +92 -0
- package/dist/lib/validation.d.ts +97 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/webcrypto-utils.d.ts +1 -0
- package/dist/lib/webpush-webcrypto.d.ts +78 -0
- package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
- package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
- package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
- package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
- package/dist/single-user.d.ts +106 -0
- package/dist/tenant/blob-store.d.ts +30 -0
- package/dist/tenant/context.d.ts +47 -0
- package/dist/tenant/single-user-context.d.ts +29 -0
- package/dist/tenant/token.d.ts +26 -0
- package/package.json +3 -3
- package/dist/chunk-3QRVP4JE.cjs +0 -2834
- package/dist/chunk-DCKCAGFV.cjs +0 -81
- package/dist/chunk-S6O3MXQO.mjs +0 -2834
- package/dist/cloudflare-kUY6z-k5.d.cts +0 -4460
- package/dist/cloudflare-kUY6z-k5.d.ts +0 -4460
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-D8Z2Nn97.d.cts +0 -295
- package/dist/neon-PqNFJIz2.d.ts +0 -295
- package/dist/pg-C48FwCWW.d.cts +0 -289
- package/dist/pg-C5gtWUfU.d.ts +0 -289
- package/dist/schema-BHuyH_6Z.d.cts +0 -88
- package/dist/schema-BHuyH_6Z.d.ts +0 -88
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** 适配器支持 outbox 吗(写入侧要用的两个方法都在才算)。 */
|
|
2
|
+
export function supportsOutbox(db: any): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* 发送前把这一批 push 落进 outbox。push 对象须已定稿(messageId / sessionId /
|
|
5
|
+
* messageIndex / totalMessages / 任务身份都已补齐)——落进去的密文就是客户端
|
|
6
|
+
* 补收时拿到的那一份。
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} args
|
|
9
|
+
* @param {Object} args.db
|
|
10
|
+
* @param {string} args.userId
|
|
11
|
+
* @param {CryptoKey|string} args.userKey - per-user 存储密钥
|
|
12
|
+
* @param {Object[]} args.pushes - 定稿后的 push 对象
|
|
13
|
+
* @returns {Promise<boolean>} 是否真的落了行(适配器不支持 / 落行失败 → false)
|
|
14
|
+
*/
|
|
15
|
+
export function appendPushesToOutbox({ db, userId, userKey, pushes }: {
|
|
16
|
+
db: any;
|
|
17
|
+
userId: string;
|
|
18
|
+
userKey: CryptoKey | string;
|
|
19
|
+
pushes: any[];
|
|
20
|
+
}): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* 把 push 对象转成 message_outbox 的行(payload 加密)。
|
|
23
|
+
*
|
|
24
|
+
* 抽出来是给 `ctx.emitResult()` 用的:它落的行要跟推送链路落的完全同构——
|
|
25
|
+
* 列一样、加密方式一样、身份字段的取法一样,客户端补收时才不用区分是谁写的。
|
|
26
|
+
*
|
|
27
|
+
* @param {Object[]} pushes - 定稿后的 push 对象
|
|
28
|
+
* @param {CryptoKey|string} userKey
|
|
29
|
+
* @param {number} createdAt - epoch 毫秒
|
|
30
|
+
* @returns {Promise<Object[]>}
|
|
31
|
+
*/
|
|
32
|
+
export function toOutboxRows(pushes: any[], userKey: CryptoKey | string, createdAt: number): Promise<any[]>;
|
|
33
|
+
/**
|
|
34
|
+
* 把发出去的那部分标成 delivered(发送半途失败时只标已发出的段)。
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} args
|
|
37
|
+
* @param {Object} args.db
|
|
38
|
+
* @param {string} args.userId
|
|
39
|
+
* @param {string[]} args.messageIds
|
|
40
|
+
*/
|
|
41
|
+
export function markPushesDelivered({ db, userId, messageIds }: {
|
|
42
|
+
db: any;
|
|
43
|
+
userId: string;
|
|
44
|
+
messageIds: string[];
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* 把这一批 push 里「没发出去的那些」从 outbox 撤掉。
|
|
48
|
+
*
|
|
49
|
+
* 取消撞上投递时用:整批 push 在开发之前就落进了 outbox,取消只拦住了 Web
|
|
50
|
+
* Push 这一路,剩下的行不撤掉,客户端下一次 GET /outbox 会照样把它们拉回去
|
|
51
|
+
* ——用户看到的就是「取消接口回了成功,消息还是来了」。
|
|
52
|
+
*
|
|
53
|
+
* 「哪些算没发出去」这条判据收在这里:投递侧只要把整批 push 和已发出的
|
|
54
|
+
* messageId 交过来就行。
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} args
|
|
57
|
+
* @param {Object} args.db
|
|
58
|
+
* @param {string} args.userId
|
|
59
|
+
* @param {Array<{ messageId: string }>} args.pushes - 落进 outbox 的整批 push
|
|
60
|
+
* @param {string[]} args.sentIds - 已经发出去的 messageId
|
|
61
|
+
*/
|
|
62
|
+
export function discardUndeliveredPushes({ db, userId, pushes, sentIds }: {
|
|
63
|
+
db: any;
|
|
64
|
+
userId: string;
|
|
65
|
+
pushes: Array<{
|
|
66
|
+
messageId: string;
|
|
67
|
+
}>;
|
|
68
|
+
sentIds: string[];
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* 把某条任务名下「还没发出去的」行从 outbox 撤掉。
|
|
72
|
+
*
|
|
73
|
+
* 用在取消 / 顶替之后的每条收尾路上(`DELETE /message`、`supersedesUuid`、
|
|
74
|
+
* fire 内的 cancelTask、投递侧发现行已被取消):任务此前投递到一半失败过的话,
|
|
75
|
+
* 没发出去的那几段还躺在 outbox 里等重试,任务行删掉它们也不会跟着走。不撤的
|
|
76
|
+
* 话客户端下一次 `GET /outbox` 照样把它们补收回去——用户看到的就是「取消接口回
|
|
77
|
+
* 了成功,消息还是来了」。
|
|
78
|
+
*
|
|
79
|
+
* 判据与 discardUndeliveredPushes 一致:只撤 delivered_at 为 null 的行。已经推
|
|
80
|
+
* 给设备的那几条撤不回来,行留着让客户端照常 ack——取消的意思是「别再发后面
|
|
81
|
+
* 的」,不是「把已经收到的从收件箱里抹掉」。
|
|
82
|
+
*
|
|
83
|
+
* 优先走适配器的按任务删除(discardUndeliveredOutboxForTask):一个来回,且不
|
|
84
|
+
* 受未 ack 积压量的影响。没有这个方法的适配器(宿主自带的旧实现)退回翻页扫
|
|
85
|
+
* 描——被取消任务的行通常是最新的,积压超过扫描上限时正好扫不到它们,所以扫到
|
|
86
|
+
* 上限还没扫完必须吵出来,不能装作清干净了。
|
|
87
|
+
*
|
|
88
|
+
* 同样是 best-effort:适配器缺读/删任一侧就静默跳过,出错只记日志。取消 / 顶
|
|
89
|
+
* 替本身已经生效了,不该因为账本没清干净被翻成失败。
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} args
|
|
92
|
+
* @param {Object} args.db
|
|
93
|
+
* @param {string} args.userId
|
|
94
|
+
* @param {string} args.taskUuid - 被取消 / 被顶替的任务 uuid
|
|
95
|
+
*/
|
|
96
|
+
export function discardUndeliveredPushesForTask({ db, userId, taskUuid }: {
|
|
97
|
+
db: any;
|
|
98
|
+
userId: string;
|
|
99
|
+
taskUuid: string;
|
|
100
|
+
}): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {Object} CommittedBatchEntry
|
|
103
|
+
* @property {Object} push - 落进 outbox 的那条 push(解密后,与当初要发的是同一份)
|
|
104
|
+
* @property {boolean} delivered - Web Push 已经发出去过(delivered_at 不为空)
|
|
105
|
+
* @property {boolean} acked - 客户端已经确认收到(acked_at 不为空)
|
|
106
|
+
*/
|
|
107
|
+
/**
|
|
108
|
+
* 找这次触发已经落进 outbox 的那一批 push。
|
|
109
|
+
*
|
|
110
|
+
* 认行的规矩(三条都要满足):
|
|
111
|
+
* - 行上 `total_messages` 不为空。推送链路定稿时每条都会写 messageIndex /
|
|
112
|
+
* totalMessages(agentic 与冻结 prompt 两条路都是),`ctx.emitResult()` 落的
|
|
113
|
+
* 结果行没有;
|
|
114
|
+
* - 解开之后 `taskUuid` / `occurrenceMs` 与本次触发对得上(这两个字段由库覆盖
|
|
115
|
+
* 写,宿主改不了,见 agentic-fire.js 的 stampTaskIdentity);
|
|
116
|
+
* - `messageKind` 不是 `result`(再挡一道:emitResult 的 payload 是宿主的形状,
|
|
117
|
+
* 万一自己带了 totalMessages 也不会被认成聊天分段)。
|
|
118
|
+
*
|
|
119
|
+
* 老链路的思考过程(reasoning)那条不带 totalMessages,不在认领范围内:批次落
|
|
120
|
+
* 定时它本来就只落收件箱、不推送,补推也轮不到它。
|
|
121
|
+
*
|
|
122
|
+
* 已经被客户端 ack 的行也要读回来:客户端在两次重试之间上线、把整批补收并 ack
|
|
123
|
+
* 了的话,这一批仍然算落定过——这时候要做的是「什么都不用推了」,而不是当成没
|
|
124
|
+
* 生成过、再生成一份新的。
|
|
125
|
+
*
|
|
126
|
+
* 读不到(适配器没有 outbox、查询抛错)→ null,调用方退回重新生成的老行为:查
|
|
127
|
+
* 不了不等于有,拿「可能多花一次」换「表结构没跟上的部署照样能重试送达」。
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} args
|
|
130
|
+
* @param {Object} args.db
|
|
131
|
+
* @param {string} args.userId
|
|
132
|
+
* @param {CryptoKey|string} args.userKey
|
|
133
|
+
* @param {string|null|undefined} args.taskUuid
|
|
134
|
+
* @param {number|null} args.occurrenceMs - 本次触发的名义时刻
|
|
135
|
+
* @param {number} [args.now] - 当前时刻(epoch 毫秒,测试可注入)
|
|
136
|
+
* @returns {Promise<{ entries: CommittedBatchEntry[] } | null>} entries 按
|
|
137
|
+
* messageIndex 升序;这次触发没有落定过批次 → null
|
|
138
|
+
*/
|
|
139
|
+
export function findCommittedBatch({ db, userId, userKey, taskUuid, occurrenceMs, now }: {
|
|
140
|
+
db: any;
|
|
141
|
+
userId: string;
|
|
142
|
+
userKey: CryptoKey | string;
|
|
143
|
+
taskUuid: string | null | undefined;
|
|
144
|
+
occurrenceMs: number | null;
|
|
145
|
+
now?: number;
|
|
146
|
+
}): Promise<{
|
|
147
|
+
entries: CommittedBatchEntry[];
|
|
148
|
+
} | null>;
|
|
149
|
+
export type CommittedBatchEntry = {
|
|
150
|
+
/**
|
|
151
|
+
* - 落进 outbox 的那条 push(解密后,与当初要发的是同一份)
|
|
152
|
+
*/
|
|
153
|
+
push: any;
|
|
154
|
+
/**
|
|
155
|
+
* - Web Push 已经发出去过(delivered_at 不为空)
|
|
156
|
+
*/
|
|
157
|
+
delivered: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* - 客户端已经确认收到(acked_at 不为空)
|
|
160
|
+
*/
|
|
161
|
+
acked: boolean;
|
|
162
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 这条 push 要不要真的发出去。
|
|
3
|
+
*
|
|
4
|
+
* @param {Record<string, unknown>} push - 定稿后的 push 对象
|
|
5
|
+
* @param {Object} options
|
|
6
|
+
* @param {boolean} options.outboxed - 这一批是否真的落进了 outbox
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function shouldSendPush(push: Record<string, unknown>, { outboxed }: {
|
|
10
|
+
outboxed: boolean;
|
|
11
|
+
}): boolean;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** 适配器支不支持用户级订阅存储。 */
|
|
2
|
+
export function supportsPushSubscriptionStore(db: any): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* 一个对象长得像不像 Web Push 订阅。只看投递必需的部分:没有 endpoint 就
|
|
5
|
+
* 谈不上往哪推。keys 缺失留给推送服务去拒,这里不替它判。
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} subscription
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isPushSubscriptionShape(subscription: unknown): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* 写入(覆盖)这个用户的订阅。
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} args
|
|
15
|
+
* @param {import('../adapters/interface.js').DbAdapter} args.db
|
|
16
|
+
* @param {string} args.userId
|
|
17
|
+
* @param {string} args.userKey
|
|
18
|
+
* @param {Object} args.subscription - 明文订阅对象
|
|
19
|
+
* @param {number} [args.updatedAt] - epoch 毫秒,默认取当前时刻
|
|
20
|
+
* @returns {Promise<{ updatedAt: number }>}
|
|
21
|
+
*/
|
|
22
|
+
export function savePushSubscription({ db, userId, userKey, subscription, updatedAt }: {
|
|
23
|
+
db: import("../adapters/interface.js").DbAdapter;
|
|
24
|
+
userId: string;
|
|
25
|
+
userKey: string;
|
|
26
|
+
subscription: any;
|
|
27
|
+
updatedAt?: number;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* 把一行 push_subscriptions 解成明文订阅。行不在、或订阅列是空的 → null;
|
|
33
|
+
* 密文解不开(换过 masterKey 之类)、解出来不是 JSON → 抛。
|
|
34
|
+
*
|
|
35
|
+
* 单独拎出来是为了让调用方分得开两类失败:`db.getPushSubscription()` 抛出来
|
|
36
|
+
* 的是基础设施问题(表没建、读超时),换个时候重试有救;这里抛出来的是这一
|
|
37
|
+
* 行的密文废了,重试多少次都一样。两者压在同一个 try 里就只能一起处理,读不
|
|
38
|
+
* 到库会被当成「这个用户没登记过」。
|
|
39
|
+
*
|
|
40
|
+
* @param {{ subscription?: unknown, updated_at?: unknown }|null|undefined} row - `getPushSubscription()` 读回的行
|
|
41
|
+
* @param {string} userKey
|
|
42
|
+
* @returns {Promise<{ subscription: Object, updatedAt: number|null }|null>}
|
|
43
|
+
*/
|
|
44
|
+
export function decodePushSubscriptionRow(row: {
|
|
45
|
+
subscription?: unknown;
|
|
46
|
+
updated_at?: unknown;
|
|
47
|
+
} | null | undefined, userKey: string): Promise<{
|
|
48
|
+
subscription: any;
|
|
49
|
+
updatedAt: number | null;
|
|
50
|
+
} | null>;
|
|
51
|
+
/**
|
|
52
|
+
* 读回这个用户的订阅(解密后的明文对象)。没有登记过 → null。
|
|
53
|
+
*
|
|
54
|
+
* 读库失败和解密失败都原样抛出去:投递链路上这两种都得让任务失败,分不分得
|
|
55
|
+
* 开无所谓;要分开处理的调用方(GET /push-subscription)自己走上面两步。
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} args
|
|
58
|
+
* @param {import('../adapters/interface.js').DbAdapter} args.db
|
|
59
|
+
* @param {string} args.userId
|
|
60
|
+
* @param {string} args.userKey
|
|
61
|
+
* @returns {Promise<{ subscription: Object, updatedAt: number|null }|null>}
|
|
62
|
+
*/
|
|
63
|
+
export function loadPushSubscription({ db, userId, userKey }: {
|
|
64
|
+
db: import("../adapters/interface.js").DbAdapter;
|
|
65
|
+
userId: string;
|
|
66
|
+
userKey: string;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
subscription: any;
|
|
69
|
+
updatedAt: number | null;
|
|
70
|
+
} | null>;
|
|
71
|
+
/**
|
|
72
|
+
* 投递前取订阅。取不到就抛——静默不发会让任务「成功」地什么都没做,用户
|
|
73
|
+
* 只看到消息凭空消失。抛出去走既有的重试 / 标记逻辑,原因也会记进 payload
|
|
74
|
+
* 的 lastError,`GET /messages` 上看得见。抛出的错误带稳定的 `code` 属性
|
|
75
|
+
* ('PUSH_SUBSCRIPTION_MISSING' / 'PUSH_SUBSCRIPTION_STORE_UNSUPPORTED'),
|
|
76
|
+
* 按类别分支请用它,别匹配 message 文案。
|
|
77
|
+
*
|
|
78
|
+
* `legacyFallback`:用户级存储里没有订阅时的兜底(升级前创建的任务把订阅
|
|
79
|
+
* 冻结在自己的 payload 里,这份订阅仍然有效)。存储里有订阅时永远用存储的
|
|
80
|
+
* 那份——它是用户最近一次登记的。
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} args
|
|
83
|
+
* @param {import('../adapters/interface.js').DbAdapter} args.db
|
|
84
|
+
* @param {string} args.userId
|
|
85
|
+
* @param {string} args.userKey
|
|
86
|
+
* @param {unknown} [args.legacyFallback] - 旧任务 payload 里内嵌的订阅(可选)
|
|
87
|
+
* @returns {Promise<Object>} 明文订阅对象
|
|
88
|
+
*/
|
|
89
|
+
export function resolvePushSubscription({ db, userId, userKey, legacyFallback }: {
|
|
90
|
+
db: import("../adapters/interface.js").DbAdapter;
|
|
91
|
+
userId: string;
|
|
92
|
+
userKey: string;
|
|
93
|
+
legacyFallback?: unknown;
|
|
94
|
+
}): Promise<any>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 这个字符串能不能当 IANA 时区 id 用。建任务 / 改任务时校验,运行时不再猜。
|
|
3
|
+
*
|
|
4
|
+
* @param {unknown} tzId
|
|
5
|
+
* @returns {boolean}
|
|
6
|
+
*/
|
|
7
|
+
export function isValidTimeZoneId(tzId: unknown): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 从名义触发时刻往后推 `periods` 个周期。
|
|
10
|
+
*
|
|
11
|
+
* @param {number} occurrenceMs - 名义触发时刻(epoch 毫秒)
|
|
12
|
+
* @param {'daily'|'weekly'} recurrenceType
|
|
13
|
+
* @param {number} periods - 推几个周期(≥ 1)
|
|
14
|
+
* @param {string|null} [tzId] - IANA 时区 id;缺省按 UTC 推进
|
|
15
|
+
* @returns {number} 推进之后的时刻(epoch 毫秒)
|
|
16
|
+
*/
|
|
17
|
+
export function advanceOccurrence(occurrenceMs: number, recurrenceType: "daily" | "weekly", periods: number, tzId?: string | null): number;
|
|
18
|
+
/**
|
|
19
|
+
* 从名义触发时刻往后推,找到第一个在 `nowMs` 之后的 occurrence,并顺带
|
|
20
|
+
* 报出中间跳过了哪些。
|
|
21
|
+
*
|
|
22
|
+
* 推进基准永远是名义时刻本身(不是「现在」),所以停摆多久都不会漂到别的
|
|
23
|
+
* 钟点上。
|
|
24
|
+
*
|
|
25
|
+
* @param {number} occurrenceMs
|
|
26
|
+
* @param {'daily'|'weekly'} recurrenceType
|
|
27
|
+
* @param {number} nowMs
|
|
28
|
+
* @param {string|null} [tzId]
|
|
29
|
+
* @returns {{ nextMs: number, skippedCount: number, skippedOccurrences: number[], skippedTruncated: boolean }}
|
|
30
|
+
* `skippedCount` 含传进来的这一次(它自己就是没发出去的那一次)。
|
|
31
|
+
* `skippedOccurrences` 最多列 {@link MAX_LISTED_SKIPPED_OCCURRENCES} 个,
|
|
32
|
+
* 超了只给首末两个并把 `skippedTruncated` 置 true。
|
|
33
|
+
*/
|
|
34
|
+
export function planNextOccurrence(occurrenceMs: number, recurrenceType: "daily" | "weekly", nowMs: number, tzId?: string | null): {
|
|
35
|
+
nextMs: number;
|
|
36
|
+
skippedCount: number;
|
|
37
|
+
skippedOccurrences: number[];
|
|
38
|
+
skippedTruncated: boolean;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* 下一次触发时刻的 ISO 字符串(写库用)。
|
|
42
|
+
*
|
|
43
|
+
* @param {number} occurrenceMs
|
|
44
|
+
* @param {'daily'|'weekly'} recurrenceType
|
|
45
|
+
* @param {number} nowMs
|
|
46
|
+
* @param {string|null} [tzId]
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function nextFutureOccurrence(occurrenceMs: number, recurrenceType: "daily" | "weekly", nowMs: number, tzId?: string | null): string;
|
|
50
|
+
/** 快进时最多逐条列出多少个被跳过的名义时刻,超了只给首末两个。 */
|
|
51
|
+
export const MAX_LISTED_SKIPPED_OCCURRENCES: 32;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把请求正文读成字符串,`Content-Encoding: gzip` 的在这一步还原。
|
|
3
|
+
*
|
|
4
|
+
* 客户端把大 body(一次 fire_pack、一整批 client_state)压了再传能省下几倍
|
|
5
|
+
* 传输量,代价只有服务端这一次解压。所有带 body 的端点走同一个入口,谁都不用
|
|
6
|
+
* 各自判一遍这个头。
|
|
7
|
+
*
|
|
8
|
+
* 说是 gzip、字节却不是 gzip 开头时按明文处理:有些边缘网关会替你把请求体解
|
|
9
|
+
* 开却留着原来的 `Content-Encoding` 头,再解一次只会解出乱码。魔数判断两种情
|
|
10
|
+
* 况都接得住。
|
|
11
|
+
*
|
|
12
|
+
* 只认 gzip。deflate / br 回 415 而不是猜着解——猜错解出来的是一段乱码,会变
|
|
13
|
+
* 成一句让人找不着北的「请求体不是有效的 JSON」。
|
|
14
|
+
*
|
|
15
|
+
* @param {{ headers?: { get?: (name: string) => string|null }, text: () => Promise<string>, arrayBuffer: () => Promise<ArrayBuffer> }} request
|
|
16
|
+
* Fetch API 的 Request(或形状相同的对象)。
|
|
17
|
+
* @param {{ maxBytes?: number }} [options] - `maxBytes`:解压后的字节上限,
|
|
18
|
+
* 默认 {@link DEFAULT_MAX_REQUEST_BODY_BYTES}。
|
|
19
|
+
* @returns {Promise<{ ok: true, body: string } | { ok: false, error: { status: number, body: Object } }>}
|
|
20
|
+
* `error` 就是可以直接回给调用方的那个信封(与 handler 的返回同构)。
|
|
21
|
+
*/
|
|
22
|
+
export function readRequestBody(request: {
|
|
23
|
+
headers?: {
|
|
24
|
+
get?: (name: string) => string | null;
|
|
25
|
+
};
|
|
26
|
+
text: () => Promise<string>;
|
|
27
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
28
|
+
}, options?: {
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
ok: true;
|
|
32
|
+
body: string;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
error: {
|
|
36
|
+
status: number;
|
|
37
|
+
body: any;
|
|
38
|
+
};
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {{ code: string, message: string }} ValidationError
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {{
|
|
45
|
+
* invalidJson?: ValidationError,
|
|
46
|
+
* invalidType?: ValidationError
|
|
47
|
+
* }} ParseBodyOptions
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {{
|
|
51
|
+
* ok: true,
|
|
52
|
+
* data: Record<string, any>
|
|
53
|
+
* } | {
|
|
54
|
+
* ok: false,
|
|
55
|
+
* error: ValidationError
|
|
56
|
+
* }} ParseBodyResult
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Parse body into a JSON object.
|
|
60
|
+
*
|
|
61
|
+
* @param {unknown} body
|
|
62
|
+
* @param {ParseBodyOptions} [options]
|
|
63
|
+
* @returns {ParseBodyResult}
|
|
64
|
+
*/
|
|
65
|
+
export function parseBodyAsObject(body: unknown, options?: ParseBodyOptions): ParseBodyResult;
|
|
66
|
+
/**
|
|
67
|
+
* Parse a standard JSON object body.
|
|
68
|
+
*
|
|
69
|
+
* @param {unknown} body
|
|
70
|
+
* @returns {ParseBodyResult}
|
|
71
|
+
*/
|
|
72
|
+
export function parseJsonBody(body: unknown): ParseBodyResult;
|
|
73
|
+
/**
|
|
74
|
+
* Check if a value is a plain object (and not null/array).
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} value
|
|
77
|
+
* @returns {value is Record<string, any>}
|
|
78
|
+
*/
|
|
79
|
+
export function isPlainObject(value: unknown): value is Record<string, any>;
|
|
80
|
+
/**
|
|
81
|
+
* Check if an object follows the encrypted payload envelope shape.
|
|
82
|
+
*
|
|
83
|
+
* @param {unknown} payload
|
|
84
|
+
* @returns {payload is { iv: string, authTag: string, encryptedData: string }}
|
|
85
|
+
*/
|
|
86
|
+
export function isEncryptedEnvelope(payload: unknown): payload is {
|
|
87
|
+
iv: string;
|
|
88
|
+
authTag: string;
|
|
89
|
+
encryptedData: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Parse and validate an encrypted payload envelope.
|
|
93
|
+
*
|
|
94
|
+
* @param {unknown} body
|
|
95
|
+
* @returns {ParseBodyResult}
|
|
96
|
+
*/
|
|
97
|
+
export function parseEncryptedBody(body: unknown): ParseBodyResult;
|
|
98
|
+
/**
|
|
99
|
+
* 标准错误信封:{ status, body: { success: false, error: { code, message, details? } } }。
|
|
100
|
+
*
|
|
101
|
+
* @param {number} status
|
|
102
|
+
* @param {string} code
|
|
103
|
+
* @param {string} message
|
|
104
|
+
* @param {Object} [details]
|
|
105
|
+
*/
|
|
106
|
+
export function errorResponse(status: number, code: string, message: string, details?: any): {
|
|
107
|
+
status: number;
|
|
108
|
+
body: {
|
|
109
|
+
success: boolean;
|
|
110
|
+
error: {
|
|
111
|
+
code: string;
|
|
112
|
+
message: string;
|
|
113
|
+
details?: undefined;
|
|
114
|
+
} | {
|
|
115
|
+
code: string;
|
|
116
|
+
message: string;
|
|
117
|
+
details: any;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* X-User-Id 门禁。所有按用户读写的端点共用这一份:规则(必填 + UUID v4)和
|
|
123
|
+
* 文案只此一处,改口径不用挨个 handler 找复制粘贴的副本。
|
|
124
|
+
*
|
|
125
|
+
* @param {Record<string, any>} headers
|
|
126
|
+
* @returns {{ userId: string, error?: undefined } | { error: ReturnType<typeof errorResponse> }}
|
|
127
|
+
*/
|
|
128
|
+
export function requireUserId(headers: Record<string, any>): {
|
|
129
|
+
userId: string;
|
|
130
|
+
error?: undefined;
|
|
131
|
+
} | {
|
|
132
|
+
error: ReturnType<typeof errorResponse>;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Read a header value case-insensitively.
|
|
136
|
+
*
|
|
137
|
+
* @param {Record<string, any>} headers
|
|
138
|
+
* @param {string} name
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function getHeader(headers: Record<string, any>, name: string): string;
|
|
142
|
+
export namespace REQUEST_ERRORS {
|
|
143
|
+
namespace INVALID_JSON {
|
|
144
|
+
let code: string;
|
|
145
|
+
let message: string;
|
|
146
|
+
}
|
|
147
|
+
namespace INVALID_REQUEST_BODY {
|
|
148
|
+
let code_1: string;
|
|
149
|
+
export { code_1 as code };
|
|
150
|
+
let message_1: string;
|
|
151
|
+
export { message_1 as message };
|
|
152
|
+
}
|
|
153
|
+
namespace INVALID_ENCRYPTED_PAYLOAD {
|
|
154
|
+
let code_2: string;
|
|
155
|
+
export { code_2 as code };
|
|
156
|
+
let message_2: string;
|
|
157
|
+
export { message_2 as message };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 解压后请求体的字节上限(默认 32MB)。
|
|
162
|
+
*
|
|
163
|
+
* 只管 gzip 那条路:压缩数据能用很小的体积展开成极大的正文(几百 KB 换几个
|
|
164
|
+
* GB),不设上限等于把内存交给调用方决定。不压缩的请求体不受这个数约束,行为
|
|
165
|
+
* 与以前一致——那条路的体积本来就摆在传输量上,平台自己的请求大小限制盖得住。
|
|
166
|
+
*/
|
|
167
|
+
export const DEFAULT_MAX_REQUEST_BODY_BYTES: number;
|
|
168
|
+
export type ValidationError = {
|
|
169
|
+
code: string;
|
|
170
|
+
message: string;
|
|
171
|
+
};
|
|
172
|
+
export type ParseBodyOptions = {
|
|
173
|
+
invalidJson?: ValidationError;
|
|
174
|
+
invalidType?: ValidationError;
|
|
175
|
+
};
|
|
176
|
+
export type ParseBodyResult = {
|
|
177
|
+
ok: true;
|
|
178
|
+
data: Record<string, any>;
|
|
179
|
+
} | {
|
|
180
|
+
ok: false;
|
|
181
|
+
error: ValidationError;
|
|
182
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ResultEmitter
|
|
3
|
+
* @property {(payload: Object) => Promise<{ messageId: string, pushed: boolean }>} emitResult
|
|
4
|
+
* 把一条结果落进收件箱并推送出去。`payload` 是宿主自己的形状,只有两个约束:
|
|
5
|
+
* 必须是普通对象,且带一个非空的 `resultKind`(这类结果的名字,客户端按它
|
|
6
|
+
* 分流)。返回 `messageId`(补收和去重的键)与 `pushed`(这次推送有没有真
|
|
7
|
+
* 的发出去;`false` 只表示没能当场送达,行还在收件箱里等补收)。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 造一份作用于某条任务的结果出口。
|
|
11
|
+
*
|
|
12
|
+
* 身份字段(`taskId` / `taskUuid` / `recurrenceType` / `occurrenceMs` /
|
|
13
|
+
* `messageKind`)由库覆盖写:它们描述的是这条任务行的事实,不是内容。
|
|
14
|
+
* `messageId` / `sessionId` / `timestamp` 宿主没给才补——`messageId` 的缺省
|
|
15
|
+
* 值掺了任务 id 与本次名义触发时刻,同一次触发重试时组出同一个 id,收件箱靠
|
|
16
|
+
* `(user_id, message_id)` 唯一约束天然去重,不会补出第二条。
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} args
|
|
19
|
+
* @param {import('../adapters/interface.js').DbAdapter} args.db
|
|
20
|
+
* @param {Object} args.task - 数据库任务行
|
|
21
|
+
* @param {string} args.userKey - per-user 存储密钥
|
|
22
|
+
* @param {Object} args.decryptedPayload - 解密后的任务 payload(取 messageType / recurrenceType / 老任务里的订阅)
|
|
23
|
+
* @param {string} args.messageIdBase - messageId 的前缀(与推送链路同一份)
|
|
24
|
+
* @param {string} args.sessionId
|
|
25
|
+
* @param {number|null} args.occurrenceMs
|
|
26
|
+
* @param {{ sendNotification: Function }|null} args.webpush
|
|
27
|
+
* @param {() => number} [args.now] - 取当前时刻(测试可注入假时钟)
|
|
28
|
+
* @param {(() => boolean)|null} [args.isCancelled] - 「这条任务是否已在投递期间
|
|
29
|
+
* 被取消 / 顶替」(run-tick 的租约心跳信号)。推送那条路的取消检查在
|
|
30
|
+
* webpush 那层(guardWebpushWithLease),但 `notification.show: false` 的结
|
|
31
|
+
* 果不发推送,只有这个显式读法能拦得住它——不给的话那条路上没有任何取消检
|
|
32
|
+
* 查点。
|
|
33
|
+
* @returns {ResultEmitter}
|
|
34
|
+
*/
|
|
35
|
+
export function createResultEmitter({ db, task, userKey, decryptedPayload, messageIdBase, sessionId, occurrenceMs, webpush, now, isCancelled, }: {
|
|
36
|
+
db: import("../adapters/interface.js").DbAdapter;
|
|
37
|
+
task: any;
|
|
38
|
+
userKey: string;
|
|
39
|
+
decryptedPayload: any;
|
|
40
|
+
messageIdBase: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
occurrenceMs: number | null;
|
|
43
|
+
webpush: {
|
|
44
|
+
sendNotification: Function;
|
|
45
|
+
} | null;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
isCancelled?: (() => boolean) | null;
|
|
48
|
+
}): ResultEmitter;
|
|
49
|
+
export type ResultEmitter = {
|
|
50
|
+
/**
|
|
51
|
+
* 把一条结果落进收件箱并推送出去。`payload` 是宿主自己的形状,只有两个约束:
|
|
52
|
+
* 必须是普通对象,且带一个非空的 `resultKind`(这类结果的名字,客户端按它
|
|
53
|
+
* 分流)。返回 `messageId`(补收和去重的键)与 `pushed`(这次推送有没有真
|
|
54
|
+
* 的发出去;`false` 只表示没能当场送达,行还在收件箱里等补收)。
|
|
55
|
+
*/
|
|
56
|
+
emitResult: (payload: any) => Promise<{
|
|
57
|
+
messageId: string;
|
|
58
|
+
pushed: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function resolveMaxDeliveryRetries(ctx: any): any;
|
|
2
|
+
/** Resolve once per attempt, before any generation hook or model request. */
|
|
3
|
+
export function resolveMaxGenerationRetries(ctx: any, safeTask: any): any;
|
|
4
|
+
/** Shared by fire receipts and both delivery entry points; never alters errors. */
|
|
5
|
+
export function failureRetryDecision(state: any, error: any): {
|
|
6
|
+
failureStage: string;
|
|
7
|
+
retryLimit: any;
|
|
8
|
+
willRetry: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Number of retries after the first attempt when no policy is configured. */
|
|
11
|
+
export const DEFAULT_MAX_DELIVERY_RETRIES: 3;
|