@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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +822 -41
  3. package/dist/adapters/d1.d.ts +693 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +384 -0
  6. package/dist/adapters/neon.d.ts +105 -0
  7. package/dist/adapters/pg-shared.d.ts +194 -0
  8. package/dist/adapters/pg.d.ts +101 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +80 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
  13. package/dist/chunk-RL7V7VSS.mjs +8784 -0
  14. package/dist/chunk-TKDISKCF.cjs +163 -0
  15. package/dist/chunk-XKUWMYVF.mjs +163 -0
  16. package/dist/chunk-YBDQJVFQ.cjs +8784 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +39 -0
  18. package/dist/cloudflare.cjs +35 -2
  19. package/dist/cloudflare.d.ts +11 -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-namespaces.d.ts +5 -0
  24. package/dist/handlers/client-state.d.ts +7 -0
  25. package/dist/handlers/cloud-data.d.ts +4 -0
  26. package/dist/handlers/get-message.d.ts +3 -0
  27. package/dist/handlers/get-user-key.d.ts +3 -0
  28. package/dist/handlers/init-tenant.d.ts +23 -0
  29. package/dist/handlers/llm-credentials.d.ts +5 -0
  30. package/dist/handlers/messages.d.ts +3 -0
  31. package/dist/handlers/outbox.d.ts +9 -0
  32. package/dist/handlers/push-subscription.d.ts +5 -0
  33. package/dist/handlers/schedule-message.d.ts +3 -0
  34. package/dist/handlers/send-notifications.d.ts +3 -0
  35. package/dist/handlers/single-user-init.d.ts +13 -0
  36. package/dist/handlers/update-message.d.ts +3 -0
  37. package/dist/handlers/vapid-public-key.d.ts +19 -0
  38. package/dist/index.cjs +133 -44
  39. package/dist/index.d.cts +145 -766
  40. package/dist/index.d.ts +145 -766
  41. package/dist/index.mjs +125 -36
  42. package/dist/lib/agentic-fire.d.ts +177 -0
  43. package/dist/lib/client-state-store.d.ts +88 -0
  44. package/dist/lib/cloud-data-cleanup.d.ts +36 -0
  45. package/dist/lib/cloud-data-guard.d.ts +14 -0
  46. package/dist/lib/cloud-data-inventory.d.ts +15 -0
  47. package/dist/lib/constant-time.d.ts +14 -0
  48. package/dist/lib/db-errors.d.ts +10 -0
  49. package/dist/lib/encryption.d.ts +48 -0
  50. package/dist/lib/errors.d.ts +273 -0
  51. package/dist/lib/llm-credentials-store.d.ts +147 -0
  52. package/dist/lib/llm.d.ts +12 -0
  53. package/dist/lib/message-processor.d.ts +82 -0
  54. package/dist/lib/outbox-store.d.ts +162 -0
  55. package/dist/lib/push-policy.d.ts +11 -0
  56. package/dist/lib/push-subscription-store.d.ts +94 -0
  57. package/dist/lib/recurrence.d.ts +51 -0
  58. package/dist/lib/request.d.ts +182 -0
  59. package/dist/lib/result-emitter.d.ts +60 -0
  60. package/dist/lib/retry-policy.d.ts +11 -0
  61. package/dist/lib/run-tick.d.ts +121 -0
  62. package/dist/lib/schema-version.d.ts +50 -0
  63. package/dist/lib/state-accessors.d.ts +70 -0
  64. package/dist/lib/state-chunks.d.ts +65 -0
  65. package/dist/lib/task-projection.d.ts +92 -0
  66. package/dist/lib/validation.d.ts +97 -0
  67. package/dist/lib/version.d.ts +7 -0
  68. package/dist/lib/webcrypto-utils.d.ts +1 -0
  69. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  70. package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
  71. package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
  72. package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
  73. package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
  74. package/dist/single-user.d.ts +106 -0
  75. package/dist/tenant/blob-store.d.ts +30 -0
  76. package/dist/tenant/context.d.ts +47 -0
  77. package/dist/tenant/single-user-context.d.ts +29 -0
  78. package/dist/tenant/token.d.ts +26 -0
  79. package/package.json +3 -3
  80. package/dist/chunk-3QRVP4JE.cjs +0 -2834
  81. package/dist/chunk-DCKCAGFV.cjs +0 -81
  82. package/dist/chunk-S6O3MXQO.mjs +0 -2834
  83. package/dist/cloudflare-kUY6z-k5.d.cts +0 -4460
  84. package/dist/cloudflare-kUY6z-k5.d.ts +0 -4460
  85. package/dist/cloudflare.d.cts +0 -2
  86. package/dist/neon-D8Z2Nn97.d.cts +0 -295
  87. package/dist/neon-PqNFJIz2.d.ts +0 -295
  88. package/dist/pg-C48FwCWW.d.cts +0 -289
  89. package/dist/pg-C5gtWUfU.d.ts +0 -289
  90. package/dist/schema-BHuyH_6Z.d.cts +0 -88
  91. package/dist/schema-BHuyH_6Z.d.ts +0 -88
@@ -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,384 @@
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
+ * 退避时刻。投递链路读的行(getPendingTasks / getTaskByUuidOnly)必须带上
14
+ * 它,读接口返回的行(getTaskByUuid / listTasks)不带。
15
+ */
16
+ retry_after?: string | null;
17
+ /**
18
+ * 上一次投递失败的脱敏摘要(JSON 串)。读接口返回的行才带。
19
+ *
20
+ * 两条链路各要一套列,内置适配器统一从 adapters/schema.js 的
21
+ * `TASK_DELIVERY_COLUMNS` / `TASK_DETAIL_COLUMNS` 取,三种方言共用一份。
22
+ */
23
+ last_error?: string | null;
24
+ };
25
+ export type InsertTaskParams = {
26
+ user_id: string;
27
+ uuid: string;
28
+ encrypted_payload: string;
29
+ next_send_at: string;
30
+ message_type: string;
31
+ };
32
+ export type InitSchemaResult = {
33
+ columnsCreated: number;
34
+ indexesCreated: number;
35
+ indexesFailed: number;
36
+ columns: any[];
37
+ indexes: any[];
38
+ };
39
+ export type DbAdapter = {
40
+ /**
41
+ * Create the scheduled_messages table and all indexes.
42
+ */
43
+ initSchema: () => Promise<InitSchemaResult>;
44
+ /**
45
+ * (可选)活库里现在实际有哪些表 / 列 / 索引 / 触发器,只读不写。`triggers`
46
+ * 可以不回,不回时触发器这一项不查。
47
+ * `getSchemaVersion` / `ensureSchema`(lib/schema-version.js)拿它跟这一版
48
+ * 需要的清单对照;不实现的适配器调那两个函数会抛错。内置只有 D1 实现。
49
+ */
50
+ describeSchema?: () => Promise<{
51
+ tables: Record<string, string[]>;
52
+ indexes: string[];
53
+ triggers?: string[];
54
+ }>;
55
+ /**
56
+ * Drop the scheduled_messages table (CASCADE).
57
+ */
58
+ dropSchema: () => Promise<void>;
59
+ /**
60
+ * Insert a new task row and return the created record.
61
+ */
62
+ createTask: (params: InsertTaskParams) => Promise<TaskRow>;
63
+ /**
64
+ * Fetch a single pending task by uuid + user_id.
65
+ */
66
+ getTaskByUuid: (uuid: string, userId: string) => Promise<TaskRow | null>;
67
+ /**
68
+ * Fetch a single pending task by uuid only (used by instant processing).
69
+ * 返回的行要和 `getPendingTasks` 是同一套列(`TASK_DELIVERY_COLUMNS`):
70
+ * `runTask` 拿这一行走同一条投递链,少了 `retry_after`,退避守卫读到的永远
71
+ * 是 undefined,还在等重试的任务会被当场再跑一遍。
72
+ */
73
+ getTaskByUuidOnly: (uuid: string) => Promise<TaskRow | null>;
74
+ /**
75
+ * (可选)这条 uuid 现在是什么状态——不限用户,也不限状态。`runTask` 用它
76
+ * 把「这条已经跑完进终态了」和「压根没这条」分开回报;不实现时两种都归
77
+ * `not_found`。
78
+ */
79
+ getTaskStatusByUuidOnly?: (uuid: string) => Promise<{
80
+ status: string;
81
+ } | null>;
82
+ /**
83
+ * Partially update a task row by its numeric id.
84
+ * 实现了 `claimTask` 的适配器还要认 `lease_until` 和 `retry_after`(含写
85
+ * null):投递收尾时 runScheduledTick 用前者把租约放掉,用后者写/清投递失败
86
+ * 的退避时刻。
87
+ */
88
+ updateTaskById: (taskId: number, updates: any) => Promise<TaskRow | null>;
89
+ /**
90
+ * Update a pending task's encrypted_payload (and optional index fields) by uuid + user_id.
91
+ */
92
+ updateTaskByUuid: (uuid: string, userId: string, encryptedPayload: string, extraFields?: any) => Promise<TaskRow | null>;
93
+ /**
94
+ * Delete a task by numeric id. Returns true if a row was affected.
95
+ */
96
+ deleteTaskById: (taskId: number) => Promise<boolean>;
97
+ /**
98
+ * Delete a task by uuid + user_id. Returns true if a row was affected.
99
+ */
100
+ deleteTaskByUuid: (uuid: string, userId: string) => Promise<boolean>;
101
+ /**
102
+ * Fetch pending tasks whose next_send_at <= NOW(), ordered ASC.
103
+ * 跳过两种还不该动的行:租约没到期的(`lease_until`,有人正在跑)、退避没到
104
+ * 点的(`retry_after`,上次投递失败在等重试)。两列都是空或已过期才算待发。
105
+ */
106
+ getPendingTasks: (limit?: number) => Promise<TaskRow[]>;
107
+ /**
108
+ * 领取一条到点的任务,返回是否领到。cron 每分钟一跳而一次投递可能跑几分
109
+ * 钟,runScheduledTick 靠它保证同一行同时只被一个 tick 跑。
110
+ * 三个条件同时成立才领得走:行仍是 pending;`lease_until` 为空或已过期
111
+ * (没别人正在跑);`next_send_at` 还等于 `expectedNextSendAt`(读这行时
112
+ * 拿到的值,用户中途改了排期就不发了)。领到就把 `lease_until` 写成
113
+ * `leaseUntil`,`next_send_at` 不动。
114
+ * 传了非空 `serializeGroup` 时还多一个条件:同一分组里没有别的行拿着未到期
115
+ * 的租约(同一分组同时只跑一条,`runScheduledTick` 的 `serializeBy` 用)。
116
+ * 领到时把这个值写进 `serialize_group` 列。判定和占位必须在同一条语句里,
117
+ * 「先查再占」中间的空档会让两个 tick 同时进同一个分组。
118
+ * 自定义适配器可以不实现(runScheduledTick 会退回不占位的行为,代价是慢
119
+ * 任务可能被下一跳重复触发);实现了但忽略第四个参数的,分组串行退化成只在
120
+ * 同一跳内生效。
121
+ */
122
+ claimTask?: (taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null) => Promise<boolean>;
123
+ /**
124
+ * List tasks for a user with optional filters and pagination.
125
+ */
126
+ listTasks: (userId: string, opts: {
127
+ status?: string;
128
+ limit?: number;
129
+ offset?: number;
130
+ }) => Promise<{
131
+ tasks: TaskRow[];
132
+ total: number;
133
+ }>;
134
+ /**
135
+ * Delete completed / failed tasks older than `days` (default 7).
136
+ */
137
+ cleanupOldTasks: (days?: number) => Promise<number>;
138
+ /**
139
+ * Return the status string of a task (used to distinguish 404 from 409).
140
+ */
141
+ getTaskStatus: (uuid: string, userId: string) => Promise<string | null>;
142
+ /**
143
+ * (optional; single-user/D1 only) Batch upsert of client state, last-write-wins on updatedAt.
144
+ * `cleanups` 先于 upsert 在同一事务里删旧行,两种形态:带 `keyPrefix` 的按 key 前缀删(清理大值
145
+ * 旧写入留下的切片行,见 lib/state-chunks.js),带 `key` 的删这一个 key(删整条状态;前缀会连带
146
+ * 删掉同前缀的兄弟 key)。两种都只删 `updated_at <= updatedAt` 的行。自定义 adapter 忽略前缀形态
147
+ * 只损失存储卫生;忽略精确 key 形态则删不掉状态,`ctx.writeState()` 的删除会失效。
148
+ * `now` 是服务端当前时刻(epoch 毫秒):库里 `updated_at` 晚于它的行只可能来自跑偏的设备时钟,
149
+ * 写入与删除都对这种行放行,否则那一行会一直卡到真实时间追上去为止。忽略这个参数的自定义
150
+ * adapter 功能不变,只是没有这道解锁。
151
+ * `outcomes` 逐条报告 entries[i] 是否真的写入(缺席时调用方按物理行计数兜底)。
152
+ * `cleanupOutcomes`(可选)逐条报告 cleanups[i] 的结局:精确 key 形态回 `true`(这个 key 的行已不在——
153
+ * 删掉了或本来就没有)/ `false`(行还在,库里那行更新、删除被条件写拦下);前缀形态回 `null`。
154
+ * 调用方靠它把被拦下的删除计入 skippedEntries;缺席时删除一律按「已删」计。
155
+ */
156
+ upsertClientState?: (userId: string, entries: Array<{
157
+ namespace: string;
158
+ key: string;
159
+ value: string;
160
+ updatedAt: number;
161
+ }>, cleanups?: Array<{
162
+ namespace: string;
163
+ key?: string;
164
+ keyPrefix?: string;
165
+ updatedAt: number;
166
+ }>, now?: number) => Promise<{
167
+ upserted: number;
168
+ skipped: number;
169
+ outcomes?: boolean[];
170
+ cleanupOutcomes?: Array<boolean | null>;
171
+ }>;
172
+ /**
173
+ * (optional; single-user/D1 only) All entries of one namespace; values still encrypted.
174
+ */
175
+ getClientState?: (userId: string, namespace: string) => Promise<Array<{
176
+ namespace: string;
177
+ key: string;
178
+ value: string;
179
+ updated_at: number;
180
+ }>>;
181
+ /**
182
+ * (optional; single-user/D1 only) Delete every entry of this user; returns rows deleted.
183
+ */
184
+ clearClientState?: (userId: string) => Promise<number>;
185
+ /**
186
+ * (可选;单用户/D1)这个用户名下有哪些命名空间,各自几条 / 占多少字节 / 最后
187
+ * 更新是什么时候。按 namespace 升序,最多 `limit` 条(调用方自己判断有没有被截断:
188
+ * 多要一条,回来的比 limit 多就说明还有)。
189
+ * `foldPrefix` 是大值分块那个保留命名空间的前缀(见 lib/state-chunks.js):以它
190
+ * 开头的行不单独列,字节数与最后更新时刻折算进去掉前缀后的原命名空间,条目数不
191
+ * 折算(切片是一个逻辑条目的几段)。折算必须在取 `limit` 之前做——保留命名空间以
192
+ * \u001f 开头,排在所有正常命名空间前面,先截断再折算会把额度全吃掉。
193
+ * 不实现 → `GET /client-state/namespaces` 返回 501。
194
+ */
195
+ listClientStateNamespaces?: (userId: string, opts?: {
196
+ limit?: number;
197
+ foldPrefix?: string | null;
198
+ }) => Promise<Array<{
199
+ namespace: string;
200
+ entry_count: number;
201
+ byte_size: number;
202
+ updated_at: number;
203
+ }>>;
204
+ /**
205
+ * (可选;单用户/D1)把这几个命名空间下这个用户的行一次删光,要在同一个事务里。
206
+ * 调用方传的是「原命名空间 + 它的切片保留命名空间」两个:只删前者会把大值的切片
207
+ * 行留成孤儿。返回删掉的行数合计。不实现 → `DELETE /client-state?namespace=` 返回
208
+ * 501(不带 namespace 的整表全清仍走 clearClientState,不受影响)。
209
+ */
210
+ deleteClientStateNamespaces?: (userId: string, namespaces: string[]) => Promise<number>;
211
+ /**
212
+ * (可选;单用户/D1)按命名空间清掉 `updated_at` 早于 `updatedBefore`(epoch 毫秒)
213
+ * 的行,不限用户。宿主配了 `clientStateTtl` 时 runScheduledTick 每跳顺手调;
214
+ * 指令由 lib/client-state-store.js 的 `planClientStateCleanup` 算好(含大值切片
215
+ * 所在的保留命名空间)。不实现 → 不清理,与不配 TTL 时行为一致。
216
+ */
217
+ cleanupClientState?: (targets: Array<{
218
+ namespace: string;
219
+ updatedBefore: number;
220
+ }>) => Promise<number>;
221
+ /**
222
+ * 这个用户当前登记的 Web Push 订阅(`subscription` 是密文,解密在上层)。没有登记过 → null。
223
+ * 一个用户一份:任务行不携带订阅,到点投递时读这里。
224
+ */
225
+ getPushSubscription?: (userId: string) => Promise<{
226
+ subscription: string;
227
+ updated_at: number;
228
+ } | null>;
229
+ /**
230
+ * 覆盖写这个用户的订阅(`updatedAt` 是 epoch 毫秒)。没有 last-write-wins 比较——
231
+ * 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
232
+ */
233
+ upsertPushSubscription?: (userId: string, encryptedSubscription: string, updatedAt: number) => Promise<boolean>;
234
+ /**
235
+ * 删掉这个用户的订阅;返回是否真的删掉了一行。
236
+ *
237
+ * 上面三个方法要么都实现、要么都不实现:缺任何一个,`PUT/GET/DELETE
238
+ * /push-subscription` 返回 501,`POST /schedule-message` 也会拒绝建任务
239
+ * (建了也永远发不出去)。内置的 D1 / pg / neon 适配器都实现了。
240
+ */
241
+ deletePushSubscription?: (userId: string) => Promise<boolean>;
242
+ /**
243
+ * 批量覆盖写这个用户的 LLM 凭据(`encryptedValue` 是密文,加密在上层)。
244
+ * 已存在的行覆盖 encrypted_value 并刷 updated_at。返回实际写入/覆盖的行数。
245
+ */
246
+ upsertLlmCredentials?: (userId: string, entries: Array<{
247
+ credId: string;
248
+ encryptedValue: string;
249
+ }>) => Promise<number>;
250
+ /**
251
+ * 按 cred_id 批量读凭据行(密文原样返回)。排程时的存在性检查和 fire 时的
252
+ * 解析共用这一个口。
253
+ */
254
+ getLlmCredentials?: (userId: string, credIds: string[]) => Promise<Array<{
255
+ cred_id: string;
256
+ encrypted_value: string;
257
+ updated_at: string;
258
+ }>>;
259
+ /**
260
+ * 这个用户名下所有凭据的对账清单(只有 cred_id 和 updated_at,密文不出这个
261
+ * 方法)。按 cred_id 排序。
262
+ */
263
+ listLlmCredentials?: (userId: string) => Promise<Array<{
264
+ cred_id: string;
265
+ updated_at: string;
266
+ }>>;
267
+ /**
268
+ * 删凭据:数组删指定那几行,null 删全部。返回删掉的行数。
269
+ */
270
+ deleteLlmCredentials?: (userId: string, credIds: string[] | null) => Promise<number>;
271
+ /**
272
+ * (可选)按 cred_id 前缀删(宿主按角色清理:`char:<charId>/` 一把清掉该角色
273
+ * 名下的几行)。返回删掉的行数。这一个单独可选,不在下面那组「四个要么都实现」
274
+ * 里——不实现时 `DELETE /llm-credentials` 的 `credIdPrefix` 入参返回 501,另外
275
+ * 两种入参(`credIds` / `all`)照常。
276
+ * 实现时别用 LIKE:D1 把 LIKE / GLOB 的 pattern 压到 50 字节,`char:<uuid>/`
277
+ * 就已经 42 字节了,稍长一点整条语句报 `pattern too complex`。走字典序范围
278
+ * (`cred_id >= 前缀 AND cred_id < 上界`)没有长度上限,见 adapters/d1.js。
279
+ *
280
+ * llm_credentials 四个方法要么都实现、要么都不实现:缺任何一个,
281
+ * `PUT/GET/DELETE /llm-credentials` 返回 501,带 `credRefs` 的
282
+ * `POST /schedule-message` 也会被拒。内置的 D1 / pg / neon 适配器都实现了。
283
+ */
284
+ deleteLlmCredentialsByPrefix?: (userId: string, credIdPrefix: string) => Promise<number>;
285
+ /**
286
+ * (可选)投递期间的租约续期(runScheduledTick 的心跳)。只在行仍是
287
+ * pending 且 lease_until 非空时生效——收尾放掉租约之后,迟到的心跳不会把
288
+ * 它复活。不实现 → 心跳自动关闭,退回一次性长租约(claimLeaseMs)。
289
+ *
290
+ * 返回值现在还是「这条任务是不是被取消/顶替了」的信号:明确返回 `false`
291
+ * 会中止这次投递剩下的推送(行已经不在了,再发就是「取消接口回了成功,
292
+ * 消息照样送达」)。所以匹配不到行时必须返回 `false`,不能返回 undefined
293
+ * ——什么都不返回等于关掉这条信号,取消撞上投递时又会照发。抛错不算行没
294
+ * 了,只当作这次没续上,下个心跳再试。
295
+ */
296
+ renewTaskLease?: (taskId: number, leaseUntil: string | Date) => Promise<boolean>;
297
+ /**
298
+ * (可选)状态 + last_error 列(脱敏失败摘要的 JSON 串)。GET /message 用
299
+ * 它把「为什么失败」透给已失败的行;不实现时退回 getTaskStatus(409 里就
300
+ * 没有 lastError)。
301
+ */
302
+ getTaskStatusInfo?: (uuid: string, userId: string) => Promise<{
303
+ status: string;
304
+ last_error: string | null;
305
+ } | null>;
306
+ /**
307
+ * (可选)建新任务的同时取消旧的那条,两件事一起成败(POST /schedule-message
308
+ * 的 supersedesUuid)。内置的 D1 / pg / neon 都实现了。不实现时 handler 退回
309
+ * 两步:先建新、后删旧——删旧失败最坏是两条都留着,客户端重试一次就能收拾,
310
+ * 而反过来(先删后建)建新失败就把旧任务白删了,找不回来。
311
+ */
312
+ createTaskSuperseding?: (params: InsertTaskParams, supersedesUuid: string) => Promise<TaskRow & {
313
+ superseded: boolean;
314
+ }>;
315
+ /**
316
+ * (可选;单用户/D1)push 发送前把整批落进 message_outbox(密文 payload),
317
+ * (user_id, message_id) 冲突时更新未 ack 的行、不动已 ack 的。
318
+ */
319
+ appendOutboxMessages?: (userId: string, rows: Array<any>) => Promise<number>;
320
+ /**
321
+ * (可选;单用户/D1)把发出去的段标 delivered_at。
322
+ */
323
+ markOutboxDelivered?: (userId: string, messageIds: string[], deliveredAt: number) => Promise<number>;
324
+ /**
325
+ * (可选;单用户/D1)把还没发出去的行删掉。任务投递到一半被取消 / 顶替时,
326
+ * 剩下那几条 push 已经落了行却不会再发,不撤掉的话客户端会从 `GET /outbox`
327
+ * 把它们补收回去。不实现 → 取消只挡住 Web Push 这一路。
328
+ */
329
+ discardOutboxMessages?: (userId: string, messageIds: string[]) => Promise<number>;
330
+ /**
331
+ * (可选;单用户/D1)按 task_uuid 一次删掉该任务名下还没发出去的行
332
+ * (delivered_at 与 acked_at 均为 NULL)。取消 / 顶替的 outbox 清理优先走
333
+ * 它:一个来回,且不受未 ack 积压量的影响。不实现 → 退回
334
+ * listUnackedOutbox + discardOutboxMessages 的翻页扫描,扫描有行数上限
335
+ * (见 lib/outbox-store.js),积压超过上限时会漏撤并打日志。
336
+ */
337
+ discardUndeliveredOutboxForTask?: (userId: string, taskUuid: string) => Promise<number>;
338
+ /**
339
+ * (可选;单用户/D1)未 ack 的行,id 升序游标翻页(GET /outbox)。行上要带
340
+ * `task_uuid` 和 `delivered_at`:适配器没实现 discardUndeliveredOutboxForTask
341
+ * 时,`DELETE /message` 和 supersedesUuid 顶替这两条路靠翻这份名单找出该任务
342
+ * 名下还没发出去的行,缺任一字段就挑不出来,那两条路上的 outbox 清理会静默
343
+ * 跳过。
344
+ */
345
+ listUnackedOutbox?: (userId: string, sinceId: number, limit: number) => Promise<Array<any>>;
346
+ /**
347
+ * (可选;单用户/D1)某条任务名下 `created_at >= sinceMs` 的行,**不论投递 /
348
+ * ack 状态**,行上要带 `message_index`、`total_messages`、`payload`、
349
+ * `created_at`、`delivered_at`、`acked_at`。生成成功之后推送失败、任务走重试
350
+ * 时,重试那一跳靠它找到这次触发已经落定的那一批,只补推送、不重新生成(见
351
+ * lib/outbox-store.js 的 findCommittedBatch)。不实现 → 退回
352
+ * listUnackedOutbox 的翻页扫描,读不到已 ack 的行:客户端恰好在两次重试之间
353
+ * 把整批 ack 了的话,那一跳会重新生成一份。
354
+ */
355
+ listOutboxForTask?: (userId: string, taskUuid: string, options?: {
356
+ sinceMs?: number;
357
+ limit?: number;
358
+ }) => Promise<Array<any>>;
359
+ /**
360
+ * (可选;单用户/D1)客户端确认收到(POST /outbox/ack,幂等)。
361
+ */
362
+ ackOutboxMessages?: (userId: string, messageIds: string[], ackedAt: number) => Promise<number>;
363
+ /**
364
+ * (可选;单用户/D1)主动删行:数组删指定那几条,null 删这个用户的全部
365
+ * (`DELETE /outbox`)。与 `discardOutboxMessages` 分工不同——那个只撤还没发出
366
+ * 去的行(取消 / 顶替的收尾),这个不看 delivered_at / acked_at,是宿主对完账
367
+ * 之后的清理口。不实现 → `DELETE /outbox` 返回 501。
368
+ * 数组形态要按 D1 的 100 个绑定参数上限切批(`user_id` 占 1 个 → 一批最多 99
369
+ * 个 id),切开的几条仍要在一个事务里。
370
+ */
371
+ deleteOutboxMessages?: (userId: string, messageIds: string[] | null) => Promise<number>;
372
+ /**
373
+ * (可选;单用户/D1)outbox 例行清理(runScheduledTick 每跳顺手调)。
374
+ *
375
+ * outbox 这几个方法要么都实现、要么都不实现:缺写入侧的(append / mark),
376
+ * 发送链路静默跳过落行;缺读取侧的(list / ack),`GET /outbox` 与
377
+ * `POST /outbox/ack` 返回 501;缺 discard(或取消那条路上缺 list),取消只挡
378
+ * 住 Web Push。内置只有 D1 实现(与 client_state 同待遇)。
379
+ */
380
+ cleanupOutbox?: (opts: {
381
+ ackedBeforeMs?: number;
382
+ allBeforeMs?: number;
383
+ }) => Promise<number>;
384
+ };
@@ -0,0 +1,105 @@
1
+ export class NeonAdapter {
2
+ /** @param {string} connectionString */
3
+ constructor(connectionString: string);
4
+ /** @private */
5
+ private _connectionString;
6
+ /** @private */
7
+ private _sql;
8
+ /**
9
+ * neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
10
+ * pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
11
+ * Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
12
+ *
13
+ * @private
14
+ */
15
+ private _getSql;
16
+ initSchema(): Promise<{
17
+ columnsCreated: number;
18
+ indexesCreated: number;
19
+ indexesFailed: number;
20
+ columns: {
21
+ table: string;
22
+ name: any;
23
+ type: any;
24
+ nullable: boolean;
25
+ }[];
26
+ indexes: ({
27
+ name: string;
28
+ status: string;
29
+ description: string;
30
+ critical: boolean;
31
+ error?: undefined;
32
+ } | {
33
+ name: string;
34
+ status: string;
35
+ description: string;
36
+ critical: boolean;
37
+ error: any;
38
+ })[];
39
+ }>;
40
+ dropSchema(): Promise<void>;
41
+ createTask(params: any): Promise<Record<string, any>>;
42
+ createTaskSuperseding(params: any, supersedesUuid: any): Promise<{
43
+ id: number;
44
+ uuid: string;
45
+ next_send_at: any;
46
+ status: string;
47
+ created_at: any;
48
+ superseded: boolean;
49
+ }>;
50
+ getTaskByUuid(uuid: any, userId: any): Promise<Record<string, any>>;
51
+ getTaskByUuidOnly(uuid: any): Promise<Record<string, any>>;
52
+ /**
53
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
54
+ * 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
55
+ *
56
+ * @param {string} uuid
57
+ * @returns {Promise<{ status: string }|null>}
58
+ */
59
+ getTaskStatusByUuidOnly(uuid: string): Promise<{
60
+ status: string;
61
+ } | null>;
62
+ updateTaskById(taskId: any, updates: any): Promise<Record<string, any>>;
63
+ /**
64
+ * 按 uuid 改一条 pending 任务(PUT /update-message、fire hook 的 renewTask)。
65
+ *
66
+ * 改排期(extraFields 里带 next_send_at)时多一道租约门:这条任务正被一次投递
67
+ * 占着的话不改,返回 null。投递收尾会按自己领取时看到的排期推进下一次(一次性
68
+ * 任务干脆标成已发送),这期间写进去的新时刻随后就被盖掉,接口却已经回了成功
69
+ * ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
70
+ * 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
71
+ */
72
+ updateTaskByUuid(uuid: any, userId: any, encryptedPayload: any, extraFields: any): Promise<Record<string, any>>;
73
+ deleteTaskById(taskId: any): Promise<boolean>;
74
+ deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
75
+ getPendingTasks(limit?: number): Promise<Record<string, any>[]>;
76
+ claimTask(taskId: any, expectedNextSendAt: any, leaseUntil: any, serializeGroup?: any): Promise<boolean>;
77
+ renewTaskLease(taskId: any, leaseUntil: any): Promise<boolean>;
78
+ listTasks(userId: any, opts?: {}): Promise<{
79
+ tasks: Record<string, any>[];
80
+ total: number;
81
+ }>;
82
+ cleanupOldTasks(days?: number): Promise<number>;
83
+ getTaskStatus(uuid: any, userId: any): Promise<any>;
84
+ getTaskStatusInfo(uuid: any, userId: any): Promise<{
85
+ status: string;
86
+ last_error: string | null;
87
+ }>;
88
+ getPushSubscription(userId: any): Promise<{
89
+ subscription: string;
90
+ updated_at: number;
91
+ }>;
92
+ upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
93
+ deletePushSubscription(userId: any): Promise<boolean>;
94
+ upsertLlmCredentials(userId: any, entries: any): Promise<number>;
95
+ getLlmCredentials(userId: any, credIds: any): Promise<{
96
+ cred_id: string;
97
+ encrypted_value: string;
98
+ updated_at: string;
99
+ }[]>;
100
+ listLlmCredentials(userId: any): Promise<{
101
+ cred_id: string;
102
+ updated_at: string;
103
+ }[]>;
104
+ deleteLlmCredentials(userId: any, credIds?: any): Promise<number>;
105
+ }