@rei-standard/amsg-server 2.6.0-next.13 → 2.6.0-next.15
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/dist/adapters/d1.d.ts +106 -0
- package/dist/adapters/interface.d.ts +50 -0
- package/dist/adapters/neon.d.ts +10 -58
- package/dist/adapters/pg-shared.d.ts +114 -0
- package/dist/adapters/pg.d.ts +10 -58
- package/dist/adapters/schema.d.ts +1 -1
- package/dist/adapters/schema.sqlite.d.ts +3 -1
- package/dist/{chunk-B2FDV7YK.mjs → chunk-34I2YSWE.mjs} +10 -0
- package/dist/{chunk-TV7N3RTH.mjs → chunk-D3DQF647.mjs} +1086 -348
- package/dist/{chunk-V2SDWGUB.cjs → chunk-OUPM4HAV.cjs} +10 -0
- package/dist/{chunk-5IOYCCFD.cjs → chunk-TCKPLM3P.cjs} +1086 -348
- package/dist/chunk-TFFBMYA2.cjs +95 -0
- package/dist/chunk-UYROU4L3.mjs +95 -0
- package/dist/cloudflare/single-user-worker.d.ts +2 -0
- package/dist/cloudflare.cjs +3 -2
- package/dist/cloudflare.mjs +2 -1
- package/dist/handlers/outbox.d.ts +7 -0
- package/dist/index.cjs +64 -36
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.mjs +45 -17
- package/dist/lib/client-state-store.d.ts +18 -3
- package/dist/lib/errors.d.ts +50 -0
- package/dist/lib/message-processor.d.ts +8 -2
- package/dist/lib/outbox-store.d.ts +33 -0
- package/dist/lib/push-subscription-store.d.ts +10 -2
- package/dist/lib/request.d.ts +36 -0
- package/dist/lib/run-tick.d.ts +28 -0
- package/dist/lib/validation.d.ts +1 -7
- package/dist/{neon-CUIGAB2T.mjs → neon-XLWK27RL.mjs} +29 -104
- package/dist/{neon-3ASBLLCM.cjs → neon-ZWA2U7IZ.cjs} +37 -112
- package/dist/{pg-IEZTAR43.cjs → pg-Q4VU6D75.cjs} +35 -112
- package/dist/{pg-KMJPL6QV.mjs → pg-X63HFEV7.mjs} +27 -104
- package/dist/single-user.d.ts +4 -0
- package/package.json +2 -2
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -38,6 +38,19 @@ export class D1Adapter {
|
|
|
38
38
|
}>;
|
|
39
39
|
dropSchema(): Promise<void>;
|
|
40
40
|
createTask(params: any): Promise<any>;
|
|
41
|
+
/**
|
|
42
|
+
* 建新任务的同时取消旧的那条(`POST /schedule-message` 的 supersedesUuid)。
|
|
43
|
+
*
|
|
44
|
+
* 两条语句走一次 batch(D1 的隐式事务 + 单次网络往返):不会出现「旧的删了、
|
|
45
|
+
* 新的没建成」的中间态——INSERT 撞 uuid 唯一索引时整个 batch 回滚,旧行原样
|
|
46
|
+
* 留着,调用方按既有的 409 冲突路径处理。
|
|
47
|
+
*
|
|
48
|
+
* @param {import('./interface.js').InsertTaskParams} params
|
|
49
|
+
* @param {string} supersedesUuid - 要取消的旧任务 uuid(同一 user_id 下)
|
|
50
|
+
* @returns {Promise<Object>} createTask 的返回行 + `superseded`(旧行是否
|
|
51
|
+
* 真的被删掉;false = 旧行本就不存在)
|
|
52
|
+
*/
|
|
53
|
+
createTaskSuperseding(params: import("./interface.js").InsertTaskParams, supersedesUuid: string): Promise<any>;
|
|
41
54
|
getTaskByUuid(uuid: any, userId: any): Promise<any>;
|
|
42
55
|
getTaskByUuidOnly(uuid: any): Promise<any>;
|
|
43
56
|
updateTaskById(taskId: any, updates: any): Promise<any>;
|
|
@@ -83,12 +96,34 @@ export class D1Adapter {
|
|
|
83
96
|
* 有任务正在跑、排期被改过、或行已不是 pending
|
|
84
97
|
*/
|
|
85
98
|
claimTask(taskId: number, expectedNextSendAt: string, leaseUntil: string | Date, serializeGroup?: string | null): Promise<boolean>;
|
|
99
|
+
/**
|
|
100
|
+
* 投递期间的租约续期(run-tick 的心跳用)。只在行仍是 pending 且确实持有
|
|
101
|
+
* 租约(lease_until 非空)时生效——收尾把租约放掉之后,迟到的心跳不会把
|
|
102
|
+
* 租约复活。
|
|
103
|
+
*
|
|
104
|
+
* @param {number} taskId
|
|
105
|
+
* @param {string|Date} leaseUntil - 新的租期末尾
|
|
106
|
+
* @returns {Promise<boolean>} true = 续上了
|
|
107
|
+
*/
|
|
108
|
+
renewTaskLease(taskId: number, leaseUntil: string | Date): Promise<boolean>;
|
|
86
109
|
listTasks(userId: any, opts?: {}): Promise<{
|
|
87
110
|
tasks: any;
|
|
88
111
|
total: number;
|
|
89
112
|
}>;
|
|
90
113
|
cleanupOldTasks(days?: number): Promise<any>;
|
|
91
114
|
getTaskStatus(uuid: any, userId: any): Promise<any>;
|
|
115
|
+
/**
|
|
116
|
+
* 状态 + 失败摘要(GET /message 用它把「为什么失败」透给已失败的行——那些
|
|
117
|
+
* 行 getTaskByUuid 读不到,payload 里的 lastError 也就够不着了)。
|
|
118
|
+
*
|
|
119
|
+
* @param {string} uuid
|
|
120
|
+
* @param {string} userId
|
|
121
|
+
* @returns {Promise<{ status: string, last_error: string|null }|null>}
|
|
122
|
+
*/
|
|
123
|
+
getTaskStatusInfo(uuid: string, userId: string): Promise<{
|
|
124
|
+
status: string;
|
|
125
|
+
last_error: string | null;
|
|
126
|
+
} | null>;
|
|
92
127
|
/**
|
|
93
128
|
* Batch upsert. Last-write-wins per (namespace, key): an entry older
|
|
94
129
|
* than the stored row (updatedAt strictly lower) is skipped; equal or
|
|
@@ -178,4 +213,75 @@ export class D1Adapter {
|
|
|
178
213
|
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
179
214
|
*/
|
|
180
215
|
deletePushSubscription(userId: string): Promise<boolean>;
|
|
216
|
+
/**
|
|
217
|
+
* 发送前把这一批 push 落进 outbox(一次 batch)。(user_id, message_id)
|
|
218
|
+
* 唯一:重试同一 occurrence 带着同一批 messageId 再来时更新 payload、不加
|
|
219
|
+
* 第二行;客户端已经 ack 过的行不动(ack 是终态,重试不该把它拉回未读)。
|
|
220
|
+
*
|
|
221
|
+
* @param {string} userId
|
|
222
|
+
* @param {Array<{ message_id: string, task_uuid?: string|null, session_id?: string|null,
|
|
223
|
+
* message_index?: number|null, total_messages?: number|null, payload: string, created_at: number }>} rows
|
|
224
|
+
* `payload` 是整条 push JSON 的 encryptForStorage 密文。
|
|
225
|
+
* @returns {Promise<number>} 实际写入/更新的行数
|
|
226
|
+
*/
|
|
227
|
+
appendOutboxMessages(userId: string, rows: Array<{
|
|
228
|
+
message_id: string;
|
|
229
|
+
task_uuid?: string | null;
|
|
230
|
+
session_id?: string | null;
|
|
231
|
+
message_index?: number | null;
|
|
232
|
+
total_messages?: number | null;
|
|
233
|
+
payload: string;
|
|
234
|
+
created_at: number;
|
|
235
|
+
}>): Promise<number>;
|
|
236
|
+
/**
|
|
237
|
+
* 把这一批标记为「Web Push 已发出」。发送失败的段不标——delivered_at 为
|
|
238
|
+
* null 的行正是客户端最需要拉的那部分。
|
|
239
|
+
*
|
|
240
|
+
* @param {string} userId
|
|
241
|
+
* @param {string[]} messageIds
|
|
242
|
+
* @param {number} deliveredAt - epoch 毫秒
|
|
243
|
+
* @returns {Promise<number>}
|
|
244
|
+
*/
|
|
245
|
+
markOutboxDelivered(userId: string, messageIds: string[], deliveredAt: number): Promise<number>;
|
|
246
|
+
/**
|
|
247
|
+
* 未 ack 的行(id 升序,游标翻页)。payload 仍是密文,解密在 handler。
|
|
248
|
+
*
|
|
249
|
+
* @param {string} userId
|
|
250
|
+
* @param {number} sinceId - 上一页游标(0 = 从头)
|
|
251
|
+
* @param {number} limit
|
|
252
|
+
* @returns {Promise<Array<{ id: number, message_id: string, task_uuid: string|null,
|
|
253
|
+
* session_id: string|null, message_index: number|null, total_messages: number|null,
|
|
254
|
+
* payload: string, created_at: number, delivered_at: number|null }>>}
|
|
255
|
+
*/
|
|
256
|
+
listUnackedOutbox(userId: string, sinceId?: number, limit?: number): Promise<Array<{
|
|
257
|
+
id: number;
|
|
258
|
+
message_id: string;
|
|
259
|
+
task_uuid: string | null;
|
|
260
|
+
session_id: string | null;
|
|
261
|
+
message_index: number | null;
|
|
262
|
+
total_messages: number | null;
|
|
263
|
+
payload: string;
|
|
264
|
+
created_at: number;
|
|
265
|
+
delivered_at: number | null;
|
|
266
|
+
}>>;
|
|
267
|
+
/**
|
|
268
|
+
* 客户端确认收到这一批(幂等:已 ack 的行再 ack 不动)。
|
|
269
|
+
*
|
|
270
|
+
* @param {string} userId
|
|
271
|
+
* @param {string[]} messageIds
|
|
272
|
+
* @param {number} ackedAt - epoch 毫秒
|
|
273
|
+
* @returns {Promise<number>} 本次真正被 ack 的行数
|
|
274
|
+
*/
|
|
275
|
+
ackOutboxMessages(userId: string, messageIds: string[], ackedAt: number): Promise<number>;
|
|
276
|
+
/**
|
|
277
|
+
* outbox 的例行清理(run-tick 每跳顺手调):已 ack 的行留短一些,未 ack 的
|
|
278
|
+
* 也不无限留(Web Push TTL 上限四周,比它更老的推送谁也收不到了)。
|
|
279
|
+
*
|
|
280
|
+
* @param {{ ackedBeforeMs?: number, allBeforeMs?: number }} opts - epoch 毫秒阈值
|
|
281
|
+
* @returns {Promise<number>} 删掉的行数
|
|
282
|
+
*/
|
|
283
|
+
cleanupOutbox({ ackedBeforeMs, allBeforeMs }?: {
|
|
284
|
+
ackedBeforeMs?: number;
|
|
285
|
+
allBeforeMs?: number;
|
|
286
|
+
}): Promise<number>;
|
|
181
287
|
}
|
|
@@ -162,4 +162,54 @@ export type DbAdapter = {
|
|
|
162
162
|
* (建了也永远发不出去)。内置的 D1 / pg / neon 适配器都实现了。
|
|
163
163
|
*/
|
|
164
164
|
deletePushSubscription?: (userId: string) => Promise<boolean>;
|
|
165
|
+
/**
|
|
166
|
+
* (可选)投递期间的租约续期(runScheduledTick 的心跳)。只在行仍是
|
|
167
|
+
* pending 且 lease_until 非空时生效——收尾放掉租约之后,迟到的心跳不会把
|
|
168
|
+
* 它复活。不实现 → 心跳自动关闭,退回一次性长租约(claimLeaseMs)。
|
|
169
|
+
*/
|
|
170
|
+
renewTaskLease?: (taskId: number, leaseUntil: string | Date) => Promise<boolean>;
|
|
171
|
+
/**
|
|
172
|
+
* (可选)状态 + last_error 列(脱敏失败摘要的 JSON 串)。GET /message 用
|
|
173
|
+
* 它把「为什么失败」透给已失败的行;不实现时退回 getTaskStatus(409 里就
|
|
174
|
+
* 没有 lastError)。
|
|
175
|
+
*/
|
|
176
|
+
getTaskStatusInfo?: (uuid: string, userId: string) => Promise<{
|
|
177
|
+
status: string;
|
|
178
|
+
last_error: string | null;
|
|
179
|
+
} | null>;
|
|
180
|
+
/**
|
|
181
|
+
* (可选)建新任务的同一事务里取消旧的那条(POST /schedule-message 的
|
|
182
|
+
* supersedesUuid)。不实现时 handler 退回「先删再建」两步(失去原子性)。
|
|
183
|
+
*/
|
|
184
|
+
createTaskSuperseding?: (params: InsertTaskParams, supersedesUuid: string) => Promise<TaskRow & {
|
|
185
|
+
superseded: boolean;
|
|
186
|
+
}>;
|
|
187
|
+
/**
|
|
188
|
+
* (可选;单用户/D1)push 发送前把整批落进 message_outbox(密文 payload),
|
|
189
|
+
* (user_id, message_id) 冲突时更新未 ack 的行、不动已 ack 的。
|
|
190
|
+
*/
|
|
191
|
+
appendOutboxMessages?: (userId: string, rows: Array<any>) => Promise<number>;
|
|
192
|
+
/**
|
|
193
|
+
* (可选;单用户/D1)把发出去的段标 delivered_at。
|
|
194
|
+
*/
|
|
195
|
+
markOutboxDelivered?: (userId: string, messageIds: string[], deliveredAt: number) => Promise<number>;
|
|
196
|
+
/**
|
|
197
|
+
* (可选;单用户/D1)未 ack 的行,id 升序游标翻页(GET /outbox)。
|
|
198
|
+
*/
|
|
199
|
+
listUnackedOutbox?: (userId: string, sinceId: number, limit: number) => Promise<Array<any>>;
|
|
200
|
+
/**
|
|
201
|
+
* (可选;单用户/D1)客户端确认收到(POST /outbox/ack,幂等)。
|
|
202
|
+
*/
|
|
203
|
+
ackOutboxMessages?: (userId: string, messageIds: string[], ackedAt: number) => Promise<number>;
|
|
204
|
+
/**
|
|
205
|
+
* (可选;单用户/D1)outbox 例行清理(runScheduledTick 每跳顺手调)。
|
|
206
|
+
*
|
|
207
|
+
* outbox 五个方法要么都实现、要么都不实现:缺写入侧的(append / mark),
|
|
208
|
+
* 发送链路静默跳过落行;缺读取侧的(list / ack),`GET /outbox` 与
|
|
209
|
+
* `POST /outbox/ack` 返回 501。内置只有 D1 实现(与 client_state 同待遇)。
|
|
210
|
+
*/
|
|
211
|
+
cleanupOutbox?: (opts: {
|
|
212
|
+
ackedBeforeMs?: number;
|
|
213
|
+
allBeforeMs?: number;
|
|
214
|
+
}) => Promise<number>;
|
|
165
215
|
};
|
package/dist/adapters/neon.d.ts
CHANGED
|
@@ -40,70 +40,22 @@ export class NeonAdapter {
|
|
|
40
40
|
deleteTaskById(taskId: any): Promise<boolean>;
|
|
41
41
|
deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
|
|
42
42
|
getPendingTasks(limit?: number): Promise<Record<string, any>[]>;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* 本次投递期间别的 tick 领不走它。
|
|
46
|
-
*
|
|
47
|
-
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
48
|
-
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
49
|
-
*
|
|
50
|
-
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
51
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
52
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
53
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
54
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
55
|
-
* 这一跳就不该再按旧时刻发。
|
|
56
|
-
*
|
|
57
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
58
|
-
* 要改表。
|
|
59
|
-
*
|
|
60
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
61
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
62
|
-
*
|
|
63
|
-
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
64
|
-
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
65
|
-
* 里完成,「先查再占」的空档天然不存在。分组门只看租约,不看
|
|
66
|
-
* `retry_after`:等着重试的任务其实闲着,不该把同分组的其他任务一起堵住。
|
|
67
|
-
*
|
|
68
|
-
* @param {number} taskId
|
|
69
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
70
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
71
|
-
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
72
|
-
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、同分组
|
|
73
|
-
* 有任务正在跑、排期被改过、或行已不是 pending
|
|
74
|
-
*/
|
|
75
|
-
claimTask(taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null): Promise<boolean>;
|
|
43
|
+
claimTask(taskId: any, expectedNextSendAt: any, leaseUntil: any, serializeGroup?: any): Promise<boolean>;
|
|
44
|
+
renewTaskLease(taskId: any, leaseUntil: any): Promise<boolean>;
|
|
76
45
|
listTasks(userId: any, opts?: {}): Promise<{
|
|
77
46
|
tasks: Record<string, any>[];
|
|
78
47
|
total: number;
|
|
79
48
|
}>;
|
|
80
49
|
cleanupOldTasks(days?: number): Promise<number>;
|
|
81
50
|
getTaskStatus(uuid: any, userId: any): Promise<any>;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
getPushSubscription(userId: string): Promise<{
|
|
51
|
+
getTaskStatusInfo(uuid: any, userId: any): Promise<{
|
|
52
|
+
status: string;
|
|
53
|
+
last_error: string | null;
|
|
54
|
+
}>;
|
|
55
|
+
getPushSubscription(userId: any): Promise<{
|
|
89
56
|
subscription: string;
|
|
90
57
|
updated_at: number;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
|
|
95
|
-
*
|
|
96
|
-
* @param {string} userId
|
|
97
|
-
* @param {string} encryptedSubscription
|
|
98
|
-
* @param {number} updatedAt - epoch 毫秒
|
|
99
|
-
* @returns {Promise<boolean>}
|
|
100
|
-
*/
|
|
101
|
-
upsertPushSubscription(userId: string, encryptedSubscription: string, updatedAt: number): Promise<boolean>;
|
|
102
|
-
/**
|
|
103
|
-
* 删掉这个用户的订阅(设置页「停止接收推送」)。
|
|
104
|
-
*
|
|
105
|
-
* @param {string} userId
|
|
106
|
-
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
107
|
-
*/
|
|
108
|
-
deletePushSubscription(userId: string): Promise<boolean>;
|
|
58
|
+
}>;
|
|
59
|
+
upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
|
|
60
|
+
deletePushSubscription(userId: any): Promise<boolean>;
|
|
109
61
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pg / neon 共用的 Postgres 查询实现。
|
|
3
|
+
*
|
|
4
|
+
* 两个适配器只在「怎么把 SQL 发出去」上不同(pg 的连接池 vs neon 的 HTTP
|
|
5
|
+
* 驱动),SQL 与并发语义必须逐字一致——各自复制一份的话,修一边漏一边就会让
|
|
6
|
+
* 两种 Postgres 部署的串行化行为静默分歧,而各自的测试还都是绿的。所以这里
|
|
7
|
+
* 按执行器参数化:适配器只递一个 `query(text, params) → rows`。
|
|
8
|
+
*
|
|
9
|
+
* D1(SQLite 方言、单写者、ISO TEXT 时间戳)不走这份实现。
|
|
10
|
+
*
|
|
11
|
+
* @typedef {(text: string, params?: any[]) => Promise<any[]>} PgQuery
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
15
|
+
* 本次投递期间别的 tick 领不走它。
|
|
16
|
+
*
|
|
17
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
18
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
19
|
+
*
|
|
20
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
21
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
22
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
23
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
24
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
25
|
+
* 这一跳就不该再按旧时刻发。
|
|
26
|
+
*
|
|
27
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
28
|
+
* 要改表。
|
|
29
|
+
*
|
|
30
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
31
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
32
|
+
*
|
|
33
|
+
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
34
|
+
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
35
|
+
* 里完成——但那只对「抢同一行」成立;READ COMMITTED 下两个并发 tick 各领
|
|
36
|
+
* 同组的**不同**行时,各自的 NOT EXISTS 子查询都看不到对方尚未提交的租约,
|
|
37
|
+
* 也没有行锁冲突逼它重查(写偏斜)。所以占位成功后再回头查一次:真撞上了
|
|
38
|
+
* 就把自己刚写的租约放掉、这一跳不跑——两边都让也没事,行保持 pending,
|
|
39
|
+
* 下一跳重试。分组门只看租约,不看 `retry_after`:等着重试的任务其实闲着,
|
|
40
|
+
* 不该把同分组的其他任务一起堵住。
|
|
41
|
+
*
|
|
42
|
+
* @param {PgQuery} query
|
|
43
|
+
* @param {number} taskId
|
|
44
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
45
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
46
|
+
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
47
|
+
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走、同分组有
|
|
48
|
+
* 任务正在跑、排期被改过、或行已不是 pending
|
|
49
|
+
*/
|
|
50
|
+
export function claimTask(query: PgQuery, taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* 投递期间的租约续期(run-tick 的心跳用)。只在行仍是 pending 且确实持有
|
|
53
|
+
* 租约(lease_until 非空)时生效——收尾把租约放掉之后,迟到的心跳不会把
|
|
54
|
+
* 租约复活。
|
|
55
|
+
*
|
|
56
|
+
* @param {PgQuery} query
|
|
57
|
+
* @param {number} taskId
|
|
58
|
+
* @param {string|Date} leaseUntil - 新的租期末尾
|
|
59
|
+
* @returns {Promise<boolean>} true = 续上了
|
|
60
|
+
*/
|
|
61
|
+
export function renewTaskLease(query: PgQuery, taskId: number, leaseUntil: string | Date): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* 状态 + 失败摘要(GET /message 用它把「为什么失败」透给已失败的行)。
|
|
64
|
+
*
|
|
65
|
+
* @param {PgQuery} query
|
|
66
|
+
* @param {string} uuid
|
|
67
|
+
* @param {string} userId
|
|
68
|
+
* @returns {Promise<{ status: string, last_error: string|null }|null>}
|
|
69
|
+
*/
|
|
70
|
+
export function getTaskStatusInfo(query: PgQuery, uuid: string, userId: string): Promise<{
|
|
71
|
+
status: string;
|
|
72
|
+
last_error: string | null;
|
|
73
|
+
} | null>;
|
|
74
|
+
/**
|
|
75
|
+
* 这个用户当前登记的推送订阅(密文原样返回,解密在上层)。
|
|
76
|
+
*
|
|
77
|
+
* @param {PgQuery} query
|
|
78
|
+
* @param {string} userId
|
|
79
|
+
* @returns {Promise<{ subscription: string, updated_at: number }|null>}
|
|
80
|
+
*/
|
|
81
|
+
export function getPushSubscription(query: PgQuery, userId: string): Promise<{
|
|
82
|
+
subscription: string;
|
|
83
|
+
updated_at: number;
|
|
84
|
+
} | null>;
|
|
85
|
+
/**
|
|
86
|
+
* 覆盖写这个用户的订阅。一个用户一行,没有 last-write-wins 之类的比较——
|
|
87
|
+
* 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
|
|
88
|
+
*
|
|
89
|
+
* @param {PgQuery} query
|
|
90
|
+
* @param {string} userId
|
|
91
|
+
* @param {string} encryptedSubscription
|
|
92
|
+
* @param {number} updatedAt - epoch 毫秒
|
|
93
|
+
* @returns {Promise<boolean>}
|
|
94
|
+
*/
|
|
95
|
+
export function upsertPushSubscription(query: PgQuery, userId: string, encryptedSubscription: string, updatedAt: number): Promise<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* 删掉这个用户的订阅(设置页「停止接收推送」)。
|
|
98
|
+
*
|
|
99
|
+
* @param {PgQuery} query
|
|
100
|
+
* @param {string} userId
|
|
101
|
+
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
102
|
+
*/
|
|
103
|
+
export function deletePushSubscription(query: PgQuery, userId: string): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* pg / neon 共用的 Postgres 查询实现。
|
|
106
|
+
*
|
|
107
|
+
* 两个适配器只在「怎么把 SQL 发出去」上不同(pg 的连接池 vs neon 的 HTTP
|
|
108
|
+
* 驱动),SQL 与并发语义必须逐字一致——各自复制一份的话,修一边漏一边就会让
|
|
109
|
+
* 两种 Postgres 部署的串行化行为静默分歧,而各自的测试还都是绿的。所以这里
|
|
110
|
+
* 按执行器参数化:适配器只递一个 `query(text, params) → rows`。
|
|
111
|
+
*
|
|
112
|
+
* D1(SQLite 方言、单写者、ISO TEXT 时间戳)不走这份实现。
|
|
113
|
+
*/
|
|
114
|
+
export type PgQuery = (text: string, params?: any[]) => Promise<any[]>;
|
package/dist/adapters/pg.d.ts
CHANGED
|
@@ -42,70 +42,22 @@ export class PgAdapter {
|
|
|
42
42
|
deleteTaskById(taskId: any): Promise<boolean>;
|
|
43
43
|
deleteTaskByUuid(uuid: any, userId: any): Promise<boolean>;
|
|
44
44
|
getPendingTasks(limit?: number): Promise<any[][]>;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* 本次投递期间别的 tick 领不走它。
|
|
48
|
-
*
|
|
49
|
-
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
50
|
-
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
51
|
-
*
|
|
52
|
-
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
53
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
54
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
55
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
56
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
57
|
-
* 这一跳就不该再按旧时刻发。
|
|
58
|
-
*
|
|
59
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
60
|
-
* 要改表。
|
|
61
|
-
*
|
|
62
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
63
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
64
|
-
*
|
|
65
|
-
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
66
|
-
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
67
|
-
* 里完成,「先查再占」的空档天然不存在。分组门只看租约,不看
|
|
68
|
-
* `retry_after`:等着重试的任务其实闲着,不该把同分组的其他任务一起堵住。
|
|
69
|
-
*
|
|
70
|
-
* @param {number} taskId
|
|
71
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
72
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
73
|
-
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
74
|
-
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走、同分组有
|
|
75
|
-
* 任务正在跑、或行已不是 pending
|
|
76
|
-
*/
|
|
77
|
-
claimTask(taskId: number, expectedNextSendAt: string | Date, leaseUntil: string | Date, serializeGroup?: string | null): Promise<boolean>;
|
|
45
|
+
claimTask(taskId: any, expectedNextSendAt: any, leaseUntil: any, serializeGroup?: any): Promise<boolean>;
|
|
46
|
+
renewTaskLease(taskId: any, leaseUntil: any): Promise<boolean>;
|
|
78
47
|
listTasks(userId: any, opts?: {}): Promise<{
|
|
79
48
|
tasks: any[][];
|
|
80
49
|
total: number;
|
|
81
50
|
}>;
|
|
82
51
|
cleanupOldTasks(days?: number): Promise<number>;
|
|
83
52
|
getTaskStatus(uuid: any, userId: any): Promise<any>;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*/
|
|
90
|
-
getPushSubscription(userId: string): Promise<{
|
|
53
|
+
getTaskStatusInfo(uuid: any, userId: any): Promise<{
|
|
54
|
+
status: string;
|
|
55
|
+
last_error: string | null;
|
|
56
|
+
}>;
|
|
57
|
+
getPushSubscription(userId: any): Promise<{
|
|
91
58
|
subscription: string;
|
|
92
59
|
updated_at: number;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
|
|
97
|
-
*
|
|
98
|
-
* @param {string} userId
|
|
99
|
-
* @param {string} encryptedSubscription
|
|
100
|
-
* @param {number} updatedAt - epoch 毫秒
|
|
101
|
-
* @returns {Promise<boolean>}
|
|
102
|
-
*/
|
|
103
|
-
upsertPushSubscription(userId: string, encryptedSubscription: string, updatedAt: number): Promise<boolean>;
|
|
104
|
-
/**
|
|
105
|
-
* 删掉这个用户的订阅(设置页「停止接收推送」)。
|
|
106
|
-
*
|
|
107
|
-
* @param {string} userId
|
|
108
|
-
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
109
|
-
*/
|
|
110
|
-
deletePushSubscription(userId: string): Promise<boolean>;
|
|
60
|
+
}>;
|
|
61
|
+
upsertPushSubscription(userId: any, encryptedSubscription: any, updatedAt: any): Promise<boolean>;
|
|
62
|
+
deletePushSubscription(userId: any): Promise<boolean>;
|
|
111
63
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 派生值而不是宿主给的原始 key —— 原始 key 往往是角色 id 之类的宿主数据,
|
|
13
13
|
* 任务内容都是密文落库的,这一列不该成为它的明文出口。
|
|
14
14
|
*/
|
|
15
|
-
export const TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS scheduled_messages (\n id SERIAL PRIMARY KEY,\n user_id VARCHAR(255) NOT NULL,\n uuid VARCHAR(36),\n encrypted_payload TEXT NOT NULL,\n message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),\n next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,\n lease_until TIMESTAMP WITH TIME ZONE,\n retry_after TIMESTAMP WITH TIME ZONE,\n serialize_group VARCHAR(64),\n status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),\n retry_count INTEGER DEFAULT 0,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n";
|
|
15
|
+
export const TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS scheduled_messages (\n id SERIAL PRIMARY KEY,\n user_id VARCHAR(255) NOT NULL,\n uuid VARCHAR(36),\n encrypted_payload TEXT NOT NULL,\n message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),\n next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,\n lease_until TIMESTAMP WITH TIME ZONE,\n retry_after TIMESTAMP WITH TIME ZONE,\n serialize_group VARCHAR(64),\n status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),\n retry_count INTEGER DEFAULT 0,\n last_error TEXT,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n";
|
|
16
16
|
/**
|
|
17
17
|
* 建表语句用的是 CREATE TABLE IF NOT EXISTS,已经存在的表不会被改动,所以
|
|
18
18
|
* 后加的列要单独补。initSchema 每次都会跑一遍,Postgres 的 IF NOT EXISTS
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Index entries mirror the Postgres INDEXES shape ({ name, sql, description,
|
|
15
15
|
* critical }) so both adapters' initSchema() return the same index metadata.
|
|
16
16
|
*/
|
|
17
|
-
export const SQLITE_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS scheduled_messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n uuid TEXT,\n encrypted_payload TEXT NOT NULL,\n message_type TEXT NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),\n next_send_at TEXT NOT NULL,\n lease_until TEXT,\n retry_after TEXT,\n serialize_group TEXT,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),\n retry_count INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n";
|
|
17
|
+
export const SQLITE_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS scheduled_messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n uuid TEXT,\n encrypted_payload TEXT NOT NULL,\n message_type TEXT NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),\n next_send_at TEXT NOT NULL,\n lease_until TEXT,\n retry_after TEXT,\n serialize_group TEXT,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),\n retry_count INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n";
|
|
18
18
|
/**
|
|
19
19
|
* 建表语句用的是 CREATE TABLE IF NOT EXISTS,已经存在的表不会被改动,所以
|
|
20
20
|
* 后加的列要单独补。initSchema 每次都会跑一遍,列已经在了就跳过。
|
|
@@ -32,3 +32,5 @@ export const SQLITE_INDEXES: {
|
|
|
32
32
|
}[];
|
|
33
33
|
export const CLIENT_STATE_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS client_state (\n user_id TEXT NOT NULL,\n namespace TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (user_id, namespace, key)\n )\n";
|
|
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
|
+
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
|
+
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";
|
|
@@ -12,6 +12,7 @@ var TABLE_SQL = `
|
|
|
12
12
|
serialize_group VARCHAR(64),
|
|
13
13
|
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
14
14
|
retry_count INTEGER DEFAULT 0,
|
|
15
|
+
last_error TEXT,
|
|
15
16
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
16
17
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
17
18
|
)
|
|
@@ -31,6 +32,14 @@ var MIGRATIONS = [
|
|
|
31
32
|
name: "add_serialize_group",
|
|
32
33
|
sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS serialize_group VARCHAR(64)",
|
|
33
34
|
description: "Serialization group for runScheduledTick serializeBy (2.6.0)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "add_last_error",
|
|
38
|
+
// 上一次投递失败的脱敏摘要(JSON:{ at, occurrence, reason })。payload 里
|
|
39
|
+
// 的 lastError 是密文里的完整记录,这一列是它的明文出口——payload 本身
|
|
40
|
+
// 解密失败时也写得进去,GET /message 对已失败的行也读得出来。
|
|
41
|
+
sql: "ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS last_error TEXT",
|
|
42
|
+
description: "Sanitized last delivery-failure summary (2.6.0)"
|
|
34
43
|
}
|
|
35
44
|
];
|
|
36
45
|
var INDEXES = [
|
|
@@ -108,6 +117,7 @@ var UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
|
|
|
108
117
|
"serialize_group",
|
|
109
118
|
"status",
|
|
110
119
|
"retry_count",
|
|
120
|
+
"last_error",
|
|
111
121
|
"created_at",
|
|
112
122
|
"updated_at"
|
|
113
123
|
]);
|