@rei-standard/amsg-server 2.6.0-next.2 → 2.6.0-next.20

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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -24
  3. package/dist/adapters/d1.d.ts +388 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +265 -0
  6. package/dist/adapters/neon.d.ts +82 -0
  7. package/dist/adapters/pg-shared.d.ts +161 -0
  8. package/dist/adapters/pg.d.ts +84 -0
  9. package/dist/adapters/schema.d.ts +41 -0
  10. package/dist/adapters/schema.sqlite.d.ts +49 -0
  11. package/dist/chunk-5FXVSC5O.mjs +144 -0
  12. package/dist/chunk-E7OWP3VL.cjs +135 -0
  13. package/dist/chunk-HTGYGGUH.mjs +4998 -0
  14. package/dist/chunk-V6NZQ22S.cjs +144 -0
  15. package/dist/chunk-WOXSQBAM.cjs +4998 -0
  16. package/dist/chunk-ZGGF4GMA.mjs +135 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +37 -0
  18. package/dist/cloudflare.cjs +35 -2
  19. package/dist/cloudflare.d.ts +9 -2
  20. package/dist/cloudflare.mjs +36 -3
  21. package/dist/handlers/cancel-message.d.ts +3 -0
  22. package/dist/handlers/capabilities.d.ts +4 -0
  23. package/dist/handlers/client-state.d.ts +7 -0
  24. package/dist/handlers/get-message.d.ts +3 -0
  25. package/dist/handlers/get-user-key.d.ts +3 -0
  26. package/dist/handlers/init-tenant.d.ts +23 -0
  27. package/dist/handlers/llm-credentials.d.ts +5 -0
  28. package/dist/handlers/messages.d.ts +3 -0
  29. package/dist/handlers/outbox.d.ts +7 -0
  30. package/dist/handlers/push-subscription.d.ts +5 -0
  31. package/dist/handlers/schedule-message.d.ts +3 -0
  32. package/dist/handlers/send-notifications.d.ts +3 -0
  33. package/dist/handlers/single-user-init.d.ts +13 -0
  34. package/dist/handlers/update-message.d.ts +3 -0
  35. package/dist/handlers/vapid-public-key.d.ts +19 -0
  36. package/dist/index.cjs +121 -44
  37. package/dist/index.d.cts +113 -766
  38. package/dist/index.d.ts +113 -766
  39. package/dist/index.mjs +113 -36
  40. package/dist/lib/agentic-fire.d.ts +108 -0
  41. package/dist/lib/client-state-store.d.ts +61 -0
  42. package/dist/lib/constant-time.d.ts +14 -0
  43. package/dist/lib/db-errors.d.ts +10 -0
  44. package/dist/lib/encryption.d.ts +48 -0
  45. package/dist/lib/errors.d.ts +93 -0
  46. package/dist/lib/llm-credentials-store.d.ts +147 -0
  47. package/dist/lib/llm.d.ts +1 -0
  48. package/dist/lib/message-processor.d.ts +56 -0
  49. package/dist/lib/outbox-store.d.ts +33 -0
  50. package/dist/lib/push-subscription-store.d.ts +71 -0
  51. package/dist/lib/recurrence.d.ts +51 -0
  52. package/dist/lib/request.d.ts +135 -0
  53. package/dist/lib/run-tick.d.ts +106 -0
  54. package/dist/lib/schema-version.d.ts +50 -0
  55. package/dist/lib/state-accessors.d.ts +70 -0
  56. package/dist/lib/state-chunks.d.ts +55 -0
  57. package/dist/lib/task-projection.d.ts +87 -0
  58. package/dist/lib/validation.d.ts +62 -0
  59. package/dist/lib/version.d.ts +7 -0
  60. package/dist/lib/webcrypto-utils.d.ts +1 -0
  61. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  62. package/dist/{neon-BFUS25UX.cjs → neon-U6TQFKMJ.cjs} +100 -8
  63. package/dist/{neon-CU5N3CSW.mjs → neon-Y4KEZO5C.mjs} +96 -4
  64. package/dist/{pg-IIH3M4OM.cjs → pg-B324Q2R4.cjs} +89 -8
  65. package/dist/{pg-QO6NKTGL.mjs → pg-GWJZBWAM.mjs} +85 -4
  66. package/dist/single-user.d.ts +94 -0
  67. package/dist/tenant/blob-store.d.ts +30 -0
  68. package/dist/tenant/context.d.ts +47 -0
  69. package/dist/tenant/single-user-context.d.ts +29 -0
  70. package/dist/tenant/token.d.ts +26 -0
  71. package/package.json +3 -3
  72. package/dist/chunk-5ENJGVZX.mjs +0 -72
  73. package/dist/chunk-GN5QTMCL.cjs +0 -1870
  74. package/dist/chunk-PVNLB6TF.mjs +0 -1870
  75. package/dist/chunk-RGECD4OH.cjs +0 -72
  76. package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
  77. package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
  78. package/dist/cloudflare.d.cts +0 -2
  79. package/dist/neon-CJl66EGy.d.cts +0 -251
  80. package/dist/neon-DYvGnCzx.d.ts +0 -251
  81. package/dist/pg-07S-u_H4.d.cts +0 -246
  82. package/dist/pg-Du-pN_UT.d.ts +0 -246
  83. package/dist/schema-C8OnYk6j.d.cts +0 -74
  84. package/dist/schema-C8OnYk6j.d.ts +0 -74
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Create a D1 adapter from a Cloudflare D1 binding (env.DB).
3
+ * @param {{ prepare: (sql: string) => any }} db
4
+ * @returns {import('./interface.js').DbAdapter}
5
+ */
6
+ export function createD1Adapter(db: {
7
+ prepare: (sql: string) => any;
8
+ }): import("./interface.js").DbAdapter;
9
+ export class D1Adapter {
10
+ /** @param {{ prepare: (sql: string) => any }} db - Cloudflare D1 binding */
11
+ constructor(db: {
12
+ prepare: (sql: string) => any;
13
+ });
14
+ /** @private */
15
+ private _db;
16
+ /** @private */
17
+ private _now;
18
+ /** @private */
19
+ private _iso;
20
+ /**
21
+ * 组一组带 `IN (...)` 的语句:值多到一条塞不下时,按 D1 的绑定参数上限切成
22
+ * 几条(见 chunkForBoundParams)。
23
+ *
24
+ * 只有拿得到事务的绑定才切。没有 batch() 的绑定(测试 shim、自定义适配器,
25
+ * 都不是真实 D1)原样发一条不切的语句:切批是为 D1 那条上限服务的,这类绑
26
+ * 定不受它约束,切了反倒会因为没有事务而多出一个「写一半」的中间态。
27
+ *
28
+ * @private
29
+ * @param {(placeholders: string) => string} buildSql - 拿占位符串(`?, ?, ?`)组 SQL
30
+ * @param {unknown[]} leadingParams - IN 列表之前的固定参数,按出现顺序
31
+ * @param {unknown[]} values - IN 列表里的全部值
32
+ * @returns {any[]} 已经 bind 好的语句
33
+ */
34
+ private _prepareInClauseStatements;
35
+ /**
36
+ * 带 `IN (...)` 的批量写,切成几条也仍然是一次原子操作。
37
+ *
38
+ * 一条语句本来天然原子,拆开之后这份保证得自己补回来,否则就会多出「只删掉
39
+ * 前 99 个」「只 ack 了前 98 条」这类比原问题更难查的中间态。D1 的 batch()
40
+ * 是隐式事务——其中一条失败,整批回滚——正好接住这件事。
41
+ *
42
+ * 一条就够时照旧单发,跟切批以前走的是同一条路,常见调用(一次三五个 id)
43
+ * 的行为一个字节都没变。
44
+ *
45
+ * @private
46
+ * @param {(placeholders: string) => string} buildSql
47
+ * @param {unknown[]} leadingParams
48
+ * @param {unknown[]} values
49
+ * @returns {Promise<number>} 各批影响行数之和
50
+ */
51
+ private _runInClauseWrite;
52
+ initSchema(): Promise<{
53
+ columnsCreated: number;
54
+ indexesCreated: number;
55
+ indexesFailed: number;
56
+ columns: any[];
57
+ indexes: ({
58
+ name: string;
59
+ status: string;
60
+ description: string;
61
+ critical: boolean;
62
+ error?: undefined;
63
+ } | {
64
+ name: string;
65
+ status: string;
66
+ description: string;
67
+ critical: boolean;
68
+ error: any;
69
+ })[];
70
+ }>;
71
+ /**
72
+ * 活库里现在实际有哪些表 / 列 / 索引。
73
+ *
74
+ * 只读不写,纯粹如实回报:拿它跟这一版需要的清单对照的活儿在
75
+ * lib/schema-version.js(`getSchemaVersion` / `ensureSchema`)。库升级后表结
76
+ * 构变了而老部署没跑过 initSchema 时,cron 会每分钟静默挂在缺的那一列上,
77
+ * 界面上一切正常——这个方法就是让宿主查得出来。
78
+ *
79
+ * @returns {Promise<{ tables: Record<string, string[]>, indexes: string[] }>}
80
+ */
81
+ describeSchema(): Promise<{
82
+ tables: Record<string, string[]>;
83
+ indexes: string[];
84
+ }>;
85
+ dropSchema(): Promise<void>;
86
+ createTask(params: any): Promise<any>;
87
+ /**
88
+ * 建新任务的同时取消旧的那条(`POST /schedule-message` 的 supersedesUuid)。
89
+ *
90
+ * 两条语句走一次 batch(D1 的隐式事务 + 单次网络往返):不会出现「旧的删了、
91
+ * 新的没建成」的中间态——INSERT 撞 uuid 唯一索引时整个 batch 回滚,旧行原样
92
+ * 留着,调用方按既有的 409 冲突路径处理。
93
+ *
94
+ * @param {import('./interface.js').InsertTaskParams} params
95
+ * @param {string} supersedesUuid - 要取消的旧任务 uuid(同一 user_id 下)
96
+ * @returns {Promise<Object>} createTask 的返回行 + `superseded`(旧行是否
97
+ * 真的被删掉;false = 旧行本就不存在)
98
+ */
99
+ createTaskSuperseding(params: import("./interface.js").InsertTaskParams, supersedesUuid: string): Promise<any>;
100
+ getTaskByUuid(uuid: any, userId: any): Promise<any>;
101
+ getTaskByUuidOnly(uuid: any): Promise<any>;
102
+ /**
103
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(getTaskByUuidOnly 只看
104
+ * pending 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
105
+ *
106
+ * @param {string} uuid
107
+ * @returns {Promise<{ status: string }|null>}
108
+ */
109
+ getTaskStatusByUuidOnly(uuid: string): Promise<{
110
+ status: string;
111
+ } | null>;
112
+ updateTaskById(taskId: any, updates: any): Promise<any>;
113
+ updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<{
114
+ uuid: any;
115
+ updated_at: string;
116
+ }>;
117
+ deleteTaskById(taskId: any): Promise<boolean>;
118
+ deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
119
+ getPendingTasks(limit?: number): Promise<any>;
120
+ /**
121
+ * 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
122
+ * 本次投递期间别的 tick 领不走它。
123
+ *
124
+ * 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
125
+ * 任务列表要读它、循环任务推进下一次也要拿它当基准。
126
+ *
127
+ * 两个 tick 抢同一行时只有一个能改到行,另一个拿到 changes = 0,据此跳过。
128
+ * WHERE 里的两个条件各管一件事:
129
+ * - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
130
+ * 也不会把行焊死,租约到期后自然可以被接手。
131
+ * - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
132
+ * 这一跳就不该再按旧时刻发。
133
+ *
134
+ * 不加一个 'sending' 状态来表达「正在跑」:建表语句里 status 有
135
+ * CHECK (status IN ('pending','sent','failed')),加值要重建表。
136
+ *
137
+ * expectedNextSendAt 按读到的原样比对,不做时区归一化——老部署里可能还留
138
+ * 着非归一化写法的行(如 +08:00 结尾),归一化后反而对不上,那条任务会永
139
+ * 远领不到。
140
+ *
141
+ * 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
142
+ * 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
143
+ * 里完成,「先查再占」的空档天然不存在——两个 tick 同时来,只有一个改得动
144
+ * 行。分组门只看租约,不看 `retry_after`:等着重试的任务其实闲着,不该把
145
+ * 同分组的其他任务一起堵住。
146
+ *
147
+ * @param {number} taskId
148
+ * @param {string} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
149
+ * @param {string|Date} leaseUntil - 租期末尾
150
+ * @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
151
+ * @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、同分组
152
+ * 有任务正在跑、排期被改过、或行已不是 pending
153
+ */
154
+ claimTask(taskId: number, expectedNextSendAt: string, leaseUntil: string | Date, serializeGroup?: string | null): Promise<boolean>;
155
+ /**
156
+ * 投递期间的租约续期(run-tick 的心跳用)。只在行仍是 pending 且确实持有
157
+ * 租约(lease_until 非空)时生效——收尾把租约放掉之后,迟到的心跳不会把
158
+ * 租约复活。
159
+ *
160
+ * @param {number} taskId
161
+ * @param {string|Date} leaseUntil - 新的租期末尾
162
+ * @returns {Promise<boolean>} true = 续上了
163
+ */
164
+ renewTaskLease(taskId: number, leaseUntil: string | Date): Promise<boolean>;
165
+ listTasks(userId: any, opts?: {}): Promise<{
166
+ tasks: any;
167
+ total: number;
168
+ }>;
169
+ cleanupOldTasks(days?: number): Promise<any>;
170
+ getTaskStatus(uuid: any, userId: any): Promise<any>;
171
+ /**
172
+ * 状态 + 失败摘要(GET /message 用它把「为什么失败」透给已失败的行——那些
173
+ * 行 getTaskByUuid 读不到,payload 里的 lastError 也就够不着了)。
174
+ *
175
+ * @param {string} uuid
176
+ * @param {string} userId
177
+ * @returns {Promise<{ status: string, last_error: string|null }|null>}
178
+ */
179
+ getTaskStatusInfo(uuid: string, userId: string): Promise<{
180
+ status: string;
181
+ last_error: string | null;
182
+ } | null>;
183
+ /**
184
+ * Batch upsert. Last-write-wins per (namespace, key): an entry older
185
+ * than the stored row (updatedAt strictly lower) is skipped; equal or
186
+ * newer overwrites. Values arrive pre-encrypted (the handler encrypts).
187
+ *
188
+ * `cleanups` 是删除项:在同一 batch 里先于 upsert 执行,`updated_at <= ?`
189
+ * 条件保证陈旧批次删不动更新写入的行。两种形态——
190
+ * - `{ namespace, keyPrefix, updatedAt }` 删 key 前缀下的所有行,用来清掉
191
+ * 大值旧写入留下的切片行(见 lib/state-chunks.js);
192
+ * - `{ namespace, key, updatedAt }` 删这一个 key,用来删整条状态(前缀会
193
+ * 连带删掉同前缀的兄弟 key,删单条必须走精确匹配)。
194
+ *
195
+ * 删掉一整条状态就是这两种各来一条(切片行走前缀、根行走精确 key),
196
+ * `entries` 传空数组即可——见 lib/client-state-store.js。
197
+ *
198
+ * Uses D1's batch() — one network round trip for the whole set (implicit
199
+ * transaction). The client calls this endpoint inside its few-seconds
200
+ * background window, so N sequential round trips could eat the whole
201
+ * window. Bindings without batch() (e.g. the sqlite test shim, custom
202
+ * adapters) fall back to a sequential loop.
203
+ *
204
+ * @param {string} userId
205
+ * @param {Array<{ namespace: string, key: string, value: string, updatedAt: number }>} entries
206
+ * @param {Array<{ namespace: string, key?: string, keyPrefix?: string, updatedAt: number }>} [cleanups]
207
+ * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
208
+ * `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
209
+ */
210
+ upsertClientState(userId: string, entries: Array<{
211
+ namespace: string;
212
+ key: string;
213
+ value: string;
214
+ updatedAt: number;
215
+ }>, cleanups?: Array<{
216
+ namespace: string;
217
+ key?: string;
218
+ keyPrefix?: string;
219
+ updatedAt: number;
220
+ }>): Promise<{
221
+ upserted: number;
222
+ skipped: number;
223
+ outcomes: boolean[];
224
+ }>;
225
+ /**
226
+ * All entries of one namespace (values still encrypted).
227
+ *
228
+ * @param {string} userId
229
+ * @param {string} namespace
230
+ * @returns {Promise<Array<{ namespace: string, key: string, value: string, updated_at: number }>>}
231
+ */
232
+ getClientState(userId: string, namespace: string): Promise<Array<{
233
+ namespace: string;
234
+ key: string;
235
+ value: string;
236
+ updated_at: number;
237
+ }>>;
238
+ /**
239
+ * Wipe every entry of this user.
240
+ *
241
+ * @param {string} userId
242
+ * @returns {Promise<number>} rows deleted
243
+ */
244
+ clearClientState(userId: string): Promise<number>;
245
+ /**
246
+ * 这个用户当前登记的推送订阅(密文原样返回,解密在上层)。
247
+ *
248
+ * @param {string} userId
249
+ * @returns {Promise<{ subscription: string, updated_at: number }|null>}
250
+ */
251
+ getPushSubscription(userId: string): Promise<{
252
+ subscription: string;
253
+ updated_at: number;
254
+ } | null>;
255
+ /**
256
+ * 覆盖写这个用户的订阅。一个用户一行,没有 last-write-wins 之类的比较——
257
+ * 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
258
+ *
259
+ * @param {string} userId
260
+ * @param {string} encryptedSubscription
261
+ * @param {number} updatedAt - epoch 毫秒
262
+ * @returns {Promise<boolean>}
263
+ */
264
+ upsertPushSubscription(userId: string, encryptedSubscription: string, updatedAt: number): Promise<boolean>;
265
+ /**
266
+ * 删掉这个用户的订阅(设置页「停止接收推送」)。
267
+ *
268
+ * @param {string} userId
269
+ * @returns {Promise<boolean>} true = 确实删掉了一行
270
+ */
271
+ deletePushSubscription(userId: string): Promise<boolean>;
272
+ /**
273
+ * 批量 upsert 这个用户的凭据(value 已是密文,加密在上层)。已存在的行
274
+ * 覆盖 encrypted_value 并刷 updated_at,created_at 保留首次写入的时刻。
275
+ *
276
+ * @param {string} userId
277
+ * @param {Array<{ credId: string, encryptedValue: string }>} entries
278
+ * @returns {Promise<number>} 实际写入/覆盖的行数
279
+ */
280
+ upsertLlmCredentials(userId: string, entries: Array<{
281
+ credId: string;
282
+ encryptedValue: string;
283
+ }>): Promise<number>;
284
+ /**
285
+ * 按 cred_id 批量读这个用户的凭据行(`encrypted_value` 是密文,解密在上
286
+ * 层)。排程时的存在性检查和 fire 时的解析共用这一个口。
287
+ *
288
+ * @param {string} userId
289
+ * @param {string[]} credIds
290
+ * @returns {Promise<Array<{ cred_id: string, encrypted_value: string, updated_at: string }>>}
291
+ */
292
+ getLlmCredentials(userId: string, credIds: string[]): Promise<Array<{
293
+ cred_id: string;
294
+ encrypted_value: string;
295
+ updated_at: string;
296
+ }>>;
297
+ /**
298
+ * 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出
299
+ * 这个方法)。按 cred_id 排序,输出稳定。
300
+ *
301
+ * @param {string} userId
302
+ * @returns {Promise<Array<{ cred_id: string, updated_at: string }>>}
303
+ */
304
+ listLlmCredentials(userId: string): Promise<Array<{
305
+ cred_id: string;
306
+ updated_at: string;
307
+ }>>;
308
+ /**
309
+ * 删凭据。`credIds` 传数组删指定那几行;传 null 删这个用户的全部
310
+ * (「清空云端数据」用)。
311
+ *
312
+ * @param {string} userId
313
+ * @param {string[]|null} credIds
314
+ * @returns {Promise<number>} 删掉的行数
315
+ */
316
+ deleteLlmCredentials(userId: string, credIds?: string[] | null): Promise<number>;
317
+ /**
318
+ * 发送前把这一批 push 落进 outbox(一次 batch)。(user_id, message_id)
319
+ * 唯一:重试同一 occurrence 带着同一批 messageId 再来时更新 payload、不加
320
+ * 第二行;客户端已经 ack 过的行不动(ack 是终态,重试不该把它拉回未读)。
321
+ *
322
+ * @param {string} userId
323
+ * @param {Array<{ message_id: string, task_uuid?: string|null, session_id?: string|null,
324
+ * message_index?: number|null, total_messages?: number|null, payload: string, created_at: number }>} rows
325
+ * `payload` 是整条 push JSON 的 encryptForStorage 密文。
326
+ * @returns {Promise<number>} 实际写入/更新的行数
327
+ */
328
+ appendOutboxMessages(userId: string, rows: Array<{
329
+ message_id: string;
330
+ task_uuid?: string | null;
331
+ session_id?: string | null;
332
+ message_index?: number | null;
333
+ total_messages?: number | null;
334
+ payload: string;
335
+ created_at: number;
336
+ }>): Promise<number>;
337
+ /**
338
+ * 把这一批标记为「Web Push 已发出」。发送失败的段不标——delivered_at 为
339
+ * null 的行正是客户端最需要拉的那部分。
340
+ *
341
+ * @param {string} userId
342
+ * @param {string[]} messageIds
343
+ * @param {number} deliveredAt - epoch 毫秒
344
+ * @returns {Promise<number>}
345
+ */
346
+ markOutboxDelivered(userId: string, messageIds: string[], deliveredAt: number): Promise<number>;
347
+ /**
348
+ * 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
349
+ *
350
+ * @param {string} userId
351
+ * @param {number} sinceId - 上一页游标(0 = 从头)
352
+ * @param {number} limit
353
+ * @returns {Promise<Array<{ id: number, message_id: string, task_uuid: string|null,
354
+ * session_id: string|null, message_index: number|null, total_messages: number|null,
355
+ * payload: string, created_at: number, delivered_at: number|null }>>}
356
+ */
357
+ listUnackedOutbox(userId: string, sinceId?: number, limit?: number): Promise<Array<{
358
+ id: number;
359
+ message_id: string;
360
+ task_uuid: string | null;
361
+ session_id: string | null;
362
+ message_index: number | null;
363
+ total_messages: number | null;
364
+ payload: string;
365
+ created_at: number;
366
+ delivered_at: number | null;
367
+ }>>;
368
+ /**
369
+ * 客户端确认收到这一批(幂等:已 ack 的行再 ack 不动)。
370
+ *
371
+ * @param {string} userId
372
+ * @param {string[]} messageIds
373
+ * @param {number} ackedAt - epoch 毫秒
374
+ * @returns {Promise<number>} 本次真正被 ack 的行数
375
+ */
376
+ ackOutboxMessages(userId: string, messageIds: string[], ackedAt: number): Promise<number>;
377
+ /**
378
+ * outbox 的例行清理(run-tick 每跳顺手调):已 ack 的行留短一些,未 ack 的
379
+ * 也不无限留(Web Push TTL 上限四周,比它更老的推送谁也收不到了)。
380
+ *
381
+ * @param {{ ackedBeforeMs?: number, allBeforeMs?: number }} opts - epoch 毫秒阈值
382
+ * @returns {Promise<number>} 删掉的行数
383
+ */
384
+ cleanupOutbox({ ackedBeforeMs, allBeforeMs }?: {
385
+ ackedBeforeMs?: number;
386
+ allBeforeMs?: number;
387
+ }): Promise<number>;
388
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Adapter Factory
3
+ *
4
+ * Creates a database adapter instance based on the supplied configuration.
5
+ *
6
+ * @typedef {'neon'|'pg'} DriverName
7
+ *
8
+ * @typedef {Object} AdapterConfig
9
+ * @property {DriverName} driver - Which database driver to use.
10
+ * @property {string} connectionString - Database connection URL.
11
+ */
12
+ /**
13
+ * Create a database adapter.
14
+ *
15
+ * @param {AdapterConfig} config
16
+ * @returns {Promise<import('./interface.js').DbAdapter>}
17
+ */
18
+ export function createAdapter(config: AdapterConfig): Promise<import("./interface.js").DbAdapter>;
19
+ /**
20
+ * Adapter Factory
21
+ *
22
+ * Creates a database adapter instance based on the supplied configuration.
23
+ */
24
+ export type DriverName = "neon" | "pg";
25
+ /**
26
+ * Adapter Factory
27
+ *
28
+ * Creates a database adapter instance based on the supplied configuration.
29
+ */
30
+ export type AdapterConfig = {
31
+ /**
32
+ * - Which database driver to use.
33
+ */
34
+ driver: DriverName;
35
+ /**
36
+ * - Database connection URL.
37
+ */
38
+ connectionString: string;
39
+ };
@@ -0,0 +1,265 @@
1
+ export type TaskRow = {
2
+ id: number;
3
+ user_id: string;
4
+ uuid: string;
5
+ encrypted_payload: string;
6
+ message_type: string;
7
+ next_send_at: string;
8
+ status: string;
9
+ retry_count: number;
10
+ created_at: string;
11
+ updated_at: string;
12
+ };
13
+ export type InsertTaskParams = {
14
+ user_id: string;
15
+ uuid: string;
16
+ encrypted_payload: string;
17
+ next_send_at: string;
18
+ message_type: string;
19
+ };
20
+ export type InitSchemaResult = {
21
+ columnsCreated: number;
22
+ indexesCreated: number;
23
+ indexesFailed: number;
24
+ columns: any[];
25
+ indexes: any[];
26
+ };
27
+ export type DbAdapter = {
28
+ /**
29
+ * Create the scheduled_messages table and all indexes.
30
+ */
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
+ }>;
41
+ /**
42
+ * Drop the scheduled_messages table (CASCADE).
43
+ */
44
+ dropSchema: () => Promise<void>;
45
+ /**
46
+ * Insert a new task row and return the created record.
47
+ */
48
+ createTask: (params: InsertTaskParams) => Promise<TaskRow>;
49
+ /**
50
+ * Fetch a single pending task by uuid + user_id.
51
+ */
52
+ getTaskByUuid: (uuid: string, userId: string) => Promise<TaskRow | null>;
53
+ /**
54
+ * Fetch a single pending task by uuid only (used by instant processing).
55
+ */
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>;
65
+ /**
66
+ * Partially update a task row by its numeric id.
67
+ * 实现了 `claimTask` 的适配器还要认 `lease_until` 和 `retry_after`(含写
68
+ * null):投递收尾时 runScheduledTick 用前者把租约放掉,用后者写/清投递失败
69
+ * 的退避时刻。
70
+ */
71
+ updateTaskById: (taskId: number, updates: any) => Promise<TaskRow | null>;
72
+ /**
73
+ * Update a pending task's encrypted_payload (and optional index fields) by uuid + user_id.
74
+ */
75
+ updateTaskByUuid: (uuid: string, userId: string, encryptedPayload: string, extraFields?: any) => Promise<TaskRow | null>;
76
+ /**
77
+ * Delete a task by numeric id. Returns true if a row was affected.
78
+ */
79
+ deleteTaskById: (taskId: number) => Promise<boolean>;
80
+ /**
81
+ * Delete a task by uuid + user_id. Returns true if a row was affected.
82
+ */
83
+ deleteTaskByUuid: (uuid: string, userId: string) => Promise<boolean>;
84
+ /**
85
+ * Fetch pending tasks whose next_send_at <= NOW(), ordered ASC.
86
+ * 跳过两种还不该动的行:租约没到期的(`lease_until`,有人正在跑)、退避没到
87
+ * 点的(`retry_after`,上次投递失败在等重试)。两列都是空或已过期才算待发。
88
+ */
89
+ getPendingTasks: (limit?: number) => Promise<TaskRow[]>;
90
+ /**
91
+ * 领取一条到点的任务,返回是否领到。cron 每分钟一跳而一次投递可能跑几分
92
+ * 钟,runScheduledTick 靠它保证同一行同时只被一个 tick 跑。
93
+ * 三个条件同时成立才领得走:行仍是 pending;`lease_until` 为空或已过期
94
+ * (没别人正在跑);`next_send_at` 还等于 `expectedNextSendAt`(读这行时
95
+ * 拿到的值,用户中途改了排期就不发了)。领到就把 `lease_until` 写成
96
+ * `leaseUntil`,`next_send_at` 不动。
97
+ * 传了非空 `serializeGroup` 时还多一个条件:同一分组里没有别的行拿着未到期
98
+ * 的租约(同一分组同时只跑一条,`runScheduledTick` 的 `serializeBy` 用)。
99
+ * 领到时把这个值写进 `serialize_group` 列。判定和占位必须在同一条语句里,
100
+ * 「先查再占」中间的空档会让两个 tick 同时进同一个分组。
101
+ * 自定义适配器可以不实现(runScheduledTick 会退回不占位的行为,代价是慢
102
+ * 任务可能被下一跳重复触发);实现了但忽略第四个参数的,分组串行退化成只在
103
+ * 同一跳内生效。
104
+ */
105
+ claimTask?: (taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null) => Promise<boolean>;
106
+ /**
107
+ * List tasks for a user with optional filters and pagination.
108
+ */
109
+ listTasks: (userId: string, opts: {
110
+ status?: string;
111
+ limit?: number;
112
+ offset?: number;
113
+ }) => Promise<{
114
+ tasks: TaskRow[];
115
+ total: number;
116
+ }>;
117
+ /**
118
+ * Delete completed / failed tasks older than `days` (default 7).
119
+ */
120
+ cleanupOldTasks: (days?: number) => Promise<number>;
121
+ /**
122
+ * Return the status string of a task (used to distinguish 404 from 409).
123
+ */
124
+ getTaskStatus: (uuid: string, userId: string) => Promise<string | null>;
125
+ /**
126
+ * (optional; single-user/D1 only) Batch upsert of client state, last-write-wins on updatedAt.
127
+ * `cleanups` 先于 upsert 在同一事务里删旧行,两种形态:带 `keyPrefix` 的按 key 前缀删(清理大值
128
+ * 旧写入留下的切片行,见 lib/state-chunks.js),带 `key` 的删这一个 key(删整条状态;前缀会连带
129
+ * 删掉同前缀的兄弟 key)。两种都只删 `updated_at <= updatedAt` 的行。自定义 adapter 忽略前缀形态
130
+ * 只损失存储卫生;忽略精确 key 形态则删不掉状态,`ctx.writeState()` 的删除会失效。
131
+ * `outcomes` 逐条报告 entries[i] 是否真的写入(缺席时调用方按物理行计数兜底)。
132
+ */
133
+ upsertClientState?: (userId: string, entries: Array<{
134
+ namespace: string;
135
+ key: string;
136
+ value: string;
137
+ updatedAt: number;
138
+ }>, cleanups?: Array<{
139
+ namespace: string;
140
+ key?: string;
141
+ keyPrefix?: string;
142
+ updatedAt: number;
143
+ }>) => Promise<{
144
+ upserted: number;
145
+ skipped: number;
146
+ outcomes?: boolean[];
147
+ }>;
148
+ /**
149
+ * (optional; single-user/D1 only) All entries of one namespace; values still encrypted.
150
+ */
151
+ getClientState?: (userId: string, namespace: string) => Promise<Array<{
152
+ namespace: string;
153
+ key: string;
154
+ value: string;
155
+ updated_at: number;
156
+ }>>;
157
+ /**
158
+ * (optional; single-user/D1 only) Delete every entry of this user; returns rows deleted.
159
+ */
160
+ clearClientState?: (userId: string) => Promise<number>;
161
+ /**
162
+ * 这个用户当前登记的 Web Push 订阅(`subscription` 是密文,解密在上层)。没有登记过 → null。
163
+ * 一个用户一份:任务行不携带订阅,到点投递时读这里。
164
+ */
165
+ getPushSubscription?: (userId: string) => Promise<{
166
+ subscription: string;
167
+ updated_at: number;
168
+ } | null>;
169
+ /**
170
+ * 覆盖写这个用户的订阅(`updatedAt` 是 epoch 毫秒)。没有 last-write-wins 比较——
171
+ * 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
172
+ */
173
+ upsertPushSubscription?: (userId: string, encryptedSubscription: string, updatedAt: number) => Promise<boolean>;
174
+ /**
175
+ * 删掉这个用户的订阅;返回是否真的删掉了一行。
176
+ *
177
+ * 上面三个方法要么都实现、要么都不实现:缺任何一个,`PUT/GET/DELETE
178
+ * /push-subscription` 返回 501,`POST /schedule-message` 也会拒绝建任务
179
+ * (建了也永远发不出去)。内置的 D1 / pg / neon 适配器都实现了。
180
+ */
181
+ deletePushSubscription?: (userId: string) => Promise<boolean>;
182
+ /**
183
+ * 批量覆盖写这个用户的 LLM 凭据(`encryptedValue` 是密文,加密在上层)。
184
+ * 已存在的行覆盖 encrypted_value 并刷 updated_at。返回实际写入/覆盖的行数。
185
+ */
186
+ upsertLlmCredentials?: (userId: string, entries: Array<{
187
+ credId: string;
188
+ encryptedValue: string;
189
+ }>) => Promise<number>;
190
+ /**
191
+ * 按 cred_id 批量读凭据行(密文原样返回)。排程时的存在性检查和 fire 时的
192
+ * 解析共用这一个口。
193
+ */
194
+ getLlmCredentials?: (userId: string, credIds: string[]) => Promise<Array<{
195
+ cred_id: string;
196
+ encrypted_value: string;
197
+ updated_at: string;
198
+ }>>;
199
+ /**
200
+ * 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出这个
201
+ * 方法)。按 cred_id 排序。
202
+ */
203
+ listLlmCredentials?: (userId: string) => Promise<Array<{
204
+ cred_id: string;
205
+ updated_at: string;
206
+ }>>;
207
+ /**
208
+ * 删凭据:数组删指定那几行,null 删全部。返回删掉的行数。
209
+ *
210
+ * llm_credentials 四个方法要么都实现、要么都不实现:缺任何一个,
211
+ * `PUT/GET/DELETE /llm-credentials` 返回 501,带 `credRefs` 的
212
+ * `POST /schedule-message` 也会被拒。内置的 D1 / pg / neon 适配器都实现了。
213
+ */
214
+ deleteLlmCredentials?: (userId: string, credIds: string[] | null) => Promise<number>;
215
+ /**
216
+ * (可选)投递期间的租约续期(runScheduledTick 的心跳)。只在行仍是
217
+ * pending 且 lease_until 非空时生效——收尾放掉租约之后,迟到的心跳不会把
218
+ * 它复活。不实现 → 心跳自动关闭,退回一次性长租约(claimLeaseMs)。
219
+ */
220
+ renewTaskLease?: (taskId: number, leaseUntil: string | Date) => Promise<boolean>;
221
+ /**
222
+ * (可选)状态 + last_error 列(脱敏失败摘要的 JSON 串)。GET /message 用
223
+ * 它把「为什么失败」透给已失败的行;不实现时退回 getTaskStatus(409 里就
224
+ * 没有 lastError)。
225
+ */
226
+ getTaskStatusInfo?: (uuid: string, userId: string) => Promise<{
227
+ status: string;
228
+ last_error: string | null;
229
+ } | null>;
230
+ /**
231
+ * (可选)建新任务的同一事务里取消旧的那条(POST /schedule-message 的
232
+ * supersedesUuid)。不实现时 handler 退回「先删再建」两步(失去原子性)。
233
+ */
234
+ createTaskSuperseding?: (params: InsertTaskParams, supersedesUuid: string) => Promise<TaskRow & {
235
+ superseded: boolean;
236
+ }>;
237
+ /**
238
+ * (可选;单用户/D1)push 发送前把整批落进 message_outbox(密文 payload),
239
+ * (user_id, message_id) 冲突时更新未 ack 的行、不动已 ack 的。
240
+ */
241
+ appendOutboxMessages?: (userId: string, rows: Array<any>) => Promise<number>;
242
+ /**
243
+ * (可选;单用户/D1)把发出去的段标 delivered_at。
244
+ */
245
+ markOutboxDelivered?: (userId: string, messageIds: string[], deliveredAt: number) => Promise<number>;
246
+ /**
247
+ * (可选;单用户/D1)未 ack 的行,id 升序游标翻页(GET /outbox)。
248
+ */
249
+ listUnackedOutbox?: (userId: string, sinceId: number, limit: number) => Promise<Array<any>>;
250
+ /**
251
+ * (可选;单用户/D1)客户端确认收到(POST /outbox/ack,幂等)。
252
+ */
253
+ ackOutboxMessages?: (userId: string, messageIds: string[], ackedAt: number) => Promise<number>;
254
+ /**
255
+ * (可选;单用户/D1)outbox 例行清理(runScheduledTick 每跳顺手调)。
256
+ *
257
+ * outbox 五个方法要么都实现、要么都不实现:缺写入侧的(append / mark),
258
+ * 发送链路静默跳过落行;缺读取侧的(list / ack),`GET /outbox` 与
259
+ * `POST /outbox/ack` 返回 501。内置只有 D1 实现(与 client_state 同待遇)。
260
+ */
261
+ cleanupOutbox?: (opts: {
262
+ ackedBeforeMs?: number;
263
+ allBeforeMs?: number;
264
+ }) => Promise<number>;
265
+ };