@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
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
var _chunkTFFBMYA2cjs = require('./chunk-TFFBMYA2.cjs');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
var _chunkOUPM4HAVcjs = require('./chunk-OUPM4HAV.cjs');
|
|
10
18
|
|
|
11
19
|
// src/server/adapters/neon.js
|
|
12
20
|
var _serverless = require('@neondatabase/serverless');
|
|
@@ -25,13 +33,13 @@ var NeonAdapter = class {
|
|
|
25
33
|
}
|
|
26
34
|
async initSchema() {
|
|
27
35
|
const sql = this._getSql();
|
|
28
|
-
await sql.query(
|
|
29
|
-
await sql.query(
|
|
30
|
-
for (const migration of
|
|
36
|
+
await sql.query(_chunkOUPM4HAVcjs.TABLE_SQL);
|
|
37
|
+
await sql.query(_chunkOUPM4HAVcjs.PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
38
|
+
for (const migration of _chunkOUPM4HAVcjs.MIGRATIONS) {
|
|
31
39
|
await sql.query(migration.sql);
|
|
32
40
|
}
|
|
33
41
|
const indexResults = [];
|
|
34
|
-
for (const index of
|
|
42
|
+
for (const index of _chunkOUPM4HAVcjs.INDEXES) {
|
|
35
43
|
try {
|
|
36
44
|
await sql.query(index.sql);
|
|
37
45
|
indexResults.push({
|
|
@@ -57,11 +65,11 @@ var NeonAdapter = class {
|
|
|
57
65
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
58
66
|
);
|
|
59
67
|
}
|
|
60
|
-
const tableCheck = await sql.query(
|
|
68
|
+
const tableCheck = await sql.query(_chunkOUPM4HAVcjs.VERIFY_TABLE_SQL);
|
|
61
69
|
if (tableCheck.length === 0) {
|
|
62
70
|
throw new Error("Table creation verification failed");
|
|
63
71
|
}
|
|
64
|
-
const columns = await sql.query(
|
|
72
|
+
const columns = await sql.query(_chunkOUPM4HAVcjs.COLUMNS_SQL);
|
|
65
73
|
return {
|
|
66
74
|
columnsCreated: columns.length,
|
|
67
75
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -91,7 +99,7 @@ var NeonAdapter = class {
|
|
|
91
99
|
async getTaskByUuid(uuid, userId) {
|
|
92
100
|
const sql = this._getSql();
|
|
93
101
|
const rows = await sql.query(
|
|
94
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
102
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
|
|
95
103
|
FROM scheduled_messages
|
|
96
104
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
97
105
|
LIMIT 1`,
|
|
@@ -116,7 +124,7 @@ var NeonAdapter = class {
|
|
|
116
124
|
const values = [];
|
|
117
125
|
let idx = 1;
|
|
118
126
|
for (const [key, value] of Object.entries(updates)) {
|
|
119
|
-
if (!
|
|
127
|
+
if (!_chunkOUPM4HAVcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
120
128
|
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
121
129
|
}
|
|
122
130
|
sets.push(`${key} = $${idx}`);
|
|
@@ -138,7 +146,7 @@ var NeonAdapter = class {
|
|
|
138
146
|
let idx = 2;
|
|
139
147
|
if (extraFields) {
|
|
140
148
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
141
|
-
if (!
|
|
149
|
+
if (!_chunkOUPM4HAVcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
142
150
|
throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
|
|
143
151
|
}
|
|
144
152
|
sets.push(`${key} = $${idx}`);
|
|
@@ -171,7 +179,7 @@ var NeonAdapter = class {
|
|
|
171
179
|
async getPendingTasks(limit = 50) {
|
|
172
180
|
const sql = this._getSql();
|
|
173
181
|
return sql.query(
|
|
174
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
182
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count
|
|
175
183
|
FROM scheduled_messages
|
|
176
184
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
177
185
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
@@ -181,65 +189,16 @@ var NeonAdapter = class {
|
|
|
181
189
|
[limit]
|
|
182
190
|
);
|
|
183
191
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
* 本次投递期间别的 tick 领不走它。
|
|
187
|
-
*
|
|
188
|
-
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
189
|
-
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
190
|
-
*
|
|
191
|
-
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
192
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
193
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
194
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
195
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
196
|
-
* 这一跳就不该再按旧时刻发。
|
|
197
|
-
*
|
|
198
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
199
|
-
* 要改表。
|
|
200
|
-
*
|
|
201
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
202
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
203
|
-
*
|
|
204
|
-
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
205
|
-
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
206
|
-
* 里完成,「先查再占」的空档天然不存在。分组门只看租约,不看
|
|
207
|
-
* `retry_after`:等着重试的任务其实闲着,不该把同分组的其他任务一起堵住。
|
|
208
|
-
*
|
|
209
|
-
* @param {number} taskId
|
|
210
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
211
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
212
|
-
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
213
|
-
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、同分组
|
|
214
|
-
* 有任务正在跑、排期被改过、或行已不是 pending
|
|
215
|
-
*/
|
|
192
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
193
|
+
// 语义说明也在那里)。
|
|
216
194
|
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
217
195
|
const sql = this._getSql();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
setClause = "lease_until = $1, serialize_group = $4, updated_at = NOW()";
|
|
225
|
-
groupGuard = `
|
|
226
|
-
AND NOT EXISTS (
|
|
227
|
-
SELECT 1 FROM scheduled_messages busy
|
|
228
|
-
WHERE busy.serialize_group = $4 AND busy.id <> $2
|
|
229
|
-
AND busy.status = 'pending' AND busy.lease_until > NOW()
|
|
230
|
-
)`;
|
|
231
|
-
}
|
|
232
|
-
const rows = await sql.query(
|
|
233
|
-
`UPDATE scheduled_messages
|
|
234
|
-
SET ${setClause}
|
|
235
|
-
WHERE id = $2 AND status = 'pending'
|
|
236
|
-
AND date_trunc('milliseconds', next_send_at)
|
|
237
|
-
= date_trunc('milliseconds', $3::timestamptz)
|
|
238
|
-
AND (lease_until IS NULL OR lease_until <= NOW())${groupGuard}
|
|
239
|
-
RETURNING id`,
|
|
240
|
-
params
|
|
241
|
-
);
|
|
242
|
-
return rows.length > 0;
|
|
196
|
+
return _chunkTFFBMYA2cjs.claimTask.call(void 0, (text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
197
|
+
}
|
|
198
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
199
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
200
|
+
const sql = this._getSql();
|
|
201
|
+
return _chunkTFFBMYA2cjs.renewTaskLease.call(void 0, (text, params) => sql.query(text, params), taskId, leaseUntil);
|
|
243
202
|
}
|
|
244
203
|
async listTasks(userId, opts = {}) {
|
|
245
204
|
const sql = this._getSql();
|
|
@@ -260,7 +219,7 @@ var NeonAdapter = class {
|
|
|
260
219
|
const total = parseInt(countRows[0].count, 10);
|
|
261
220
|
const taskParams = [...params, limit, offset];
|
|
262
221
|
const tasks = await sql.query(
|
|
263
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
222
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
|
|
264
223
|
FROM scheduled_messages
|
|
265
224
|
WHERE ${where}
|
|
266
225
|
ORDER BY next_send_at ASC
|
|
@@ -289,57 +248,23 @@ var NeonAdapter = class {
|
|
|
289
248
|
);
|
|
290
249
|
return rows.length > 0 ? rows[0].status : null;
|
|
291
250
|
}
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
*/
|
|
251
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
252
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
253
|
+
const sql = this._getSql();
|
|
254
|
+
return _chunkTFFBMYA2cjs.getTaskStatusInfo.call(void 0, (text, params) => sql.query(text, params), uuid, userId);
|
|
255
|
+
}
|
|
256
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
299
257
|
async getPushSubscription(userId) {
|
|
300
258
|
const sql = this._getSql();
|
|
301
|
-
|
|
302
|
-
"SELECT subscription, updated_at FROM push_subscriptions WHERE user_id = $1 LIMIT 1",
|
|
303
|
-
[userId]
|
|
304
|
-
);
|
|
305
|
-
if (rows.length === 0) return null;
|
|
306
|
-
return { subscription: rows[0].subscription, updated_at: Number(rows[0].updated_at) };
|
|
259
|
+
return _chunkTFFBMYA2cjs.getPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
307
260
|
}
|
|
308
|
-
/**
|
|
309
|
-
* 覆盖写这个用户的订阅。一个用户一行,没有 last-write-wins 之类的比较——
|
|
310
|
-
* 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
|
|
311
|
-
*
|
|
312
|
-
* @param {string} userId
|
|
313
|
-
* @param {string} encryptedSubscription
|
|
314
|
-
* @param {number} updatedAt - epoch 毫秒
|
|
315
|
-
* @returns {Promise<boolean>}
|
|
316
|
-
*/
|
|
317
261
|
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
318
262
|
const sql = this._getSql();
|
|
319
|
-
|
|
320
|
-
`INSERT INTO push_subscriptions (user_id, subscription, updated_at)
|
|
321
|
-
VALUES ($1, $2, $3)
|
|
322
|
-
ON CONFLICT (user_id) DO UPDATE SET
|
|
323
|
-
subscription = EXCLUDED.subscription,
|
|
324
|
-
updated_at = EXCLUDED.updated_at
|
|
325
|
-
RETURNING user_id`,
|
|
326
|
-
[userId, encryptedSubscription, updatedAt]
|
|
327
|
-
);
|
|
328
|
-
return rows.length > 0;
|
|
263
|
+
return _chunkTFFBMYA2cjs.upsertPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
|
|
329
264
|
}
|
|
330
|
-
/**
|
|
331
|
-
* 删掉这个用户的订阅(设置页「停止接收推送」)。
|
|
332
|
-
*
|
|
333
|
-
* @param {string} userId
|
|
334
|
-
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
335
|
-
*/
|
|
336
265
|
async deletePushSubscription(userId) {
|
|
337
266
|
const sql = this._getSql();
|
|
338
|
-
|
|
339
|
-
"DELETE FROM push_subscriptions WHERE user_id = $1 RETURNING user_id",
|
|
340
|
-
[userId]
|
|
341
|
-
);
|
|
342
|
-
return rows.length > 0;
|
|
267
|
+
return _chunkTFFBMYA2cjs.deletePushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
|
|
343
268
|
}
|
|
344
269
|
};
|
|
345
270
|
|
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
var _chunkTFFBMYA2cjs = require('./chunk-TFFBMYA2.cjs');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
var _chunkOUPM4HAVcjs = require('./chunk-OUPM4HAV.cjs');
|
|
10
18
|
|
|
11
19
|
// src/server/adapters/pg.js
|
|
12
20
|
var _pg = require('pg');
|
|
@@ -30,13 +38,13 @@ var PgAdapter = class {
|
|
|
30
38
|
return result.rows;
|
|
31
39
|
}
|
|
32
40
|
async initSchema() {
|
|
33
|
-
await this._query(
|
|
34
|
-
await this._query(
|
|
35
|
-
for (const migration of
|
|
41
|
+
await this._query(_chunkOUPM4HAVcjs.TABLE_SQL);
|
|
42
|
+
await this._query(_chunkOUPM4HAVcjs.PUSH_SUBSCRIPTION_TABLE_SQL);
|
|
43
|
+
for (const migration of _chunkOUPM4HAVcjs.MIGRATIONS) {
|
|
36
44
|
await this._query(migration.sql);
|
|
37
45
|
}
|
|
38
46
|
const indexResults = [];
|
|
39
|
-
for (const index of
|
|
47
|
+
for (const index of _chunkOUPM4HAVcjs.INDEXES) {
|
|
40
48
|
try {
|
|
41
49
|
await this._query(index.sql);
|
|
42
50
|
indexResults.push({
|
|
@@ -62,11 +70,11 @@ var PgAdapter = class {
|
|
|
62
70
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
63
71
|
);
|
|
64
72
|
}
|
|
65
|
-
const tableCheck = await this._query(
|
|
73
|
+
const tableCheck = await this._query(_chunkOUPM4HAVcjs.VERIFY_TABLE_SQL);
|
|
66
74
|
if (tableCheck.length === 0) {
|
|
67
75
|
throw new Error("Table creation verification failed");
|
|
68
76
|
}
|
|
69
|
-
const columns = await this._query(
|
|
77
|
+
const columns = await this._query(_chunkOUPM4HAVcjs.COLUMNS_SQL);
|
|
70
78
|
return {
|
|
71
79
|
columnsCreated: columns.length,
|
|
72
80
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -93,7 +101,7 @@ var PgAdapter = class {
|
|
|
93
101
|
}
|
|
94
102
|
async getTaskByUuid(uuid, userId) {
|
|
95
103
|
const rows = await this._query(
|
|
96
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
104
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
|
|
97
105
|
FROM scheduled_messages
|
|
98
106
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
99
107
|
LIMIT 1`,
|
|
@@ -116,7 +124,7 @@ var PgAdapter = class {
|
|
|
116
124
|
const values = [];
|
|
117
125
|
let idx = 1;
|
|
118
126
|
for (const [key, value] of Object.entries(updates)) {
|
|
119
|
-
if (!
|
|
127
|
+
if (!_chunkOUPM4HAVcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
120
128
|
throw new Error(`[amsg-server pg] rejected unknown update column: ${key}`);
|
|
121
129
|
}
|
|
122
130
|
sets.push(`${key} = $${idx}`);
|
|
@@ -137,7 +145,7 @@ var PgAdapter = class {
|
|
|
137
145
|
let idx = 2;
|
|
138
146
|
if (extraFields) {
|
|
139
147
|
for (const [key, value] of Object.entries(extraFields)) {
|
|
140
|
-
if (!
|
|
148
|
+
if (!_chunkOUPM4HAVcjs.UPDATABLE_COLUMNS.has(key)) {
|
|
141
149
|
throw new Error(`[amsg-server pg] rejected unknown update column: ${key}`);
|
|
142
150
|
}
|
|
143
151
|
sets.push(`${key} = $${idx}`);
|
|
@@ -167,7 +175,7 @@ var PgAdapter = class {
|
|
|
167
175
|
}
|
|
168
176
|
async getPendingTasks(limit = 50) {
|
|
169
177
|
return this._query(
|
|
170
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
178
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count
|
|
171
179
|
FROM scheduled_messages
|
|
172
180
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
173
181
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
@@ -177,64 +185,14 @@ var PgAdapter = class {
|
|
|
177
185
|
[limit]
|
|
178
186
|
);
|
|
179
187
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
* 本次投递期间别的 tick 领不走它。
|
|
183
|
-
*
|
|
184
|
-
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
185
|
-
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
186
|
-
*
|
|
187
|
-
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
188
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
189
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
190
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
191
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
192
|
-
* 这一跳就不该再按旧时刻发。
|
|
193
|
-
*
|
|
194
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
195
|
-
* 要改表。
|
|
196
|
-
*
|
|
197
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
198
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
199
|
-
*
|
|
200
|
-
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
201
|
-
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
202
|
-
* 里完成,「先查再占」的空档天然不存在。分组门只看租约,不看
|
|
203
|
-
* `retry_after`:等着重试的任务其实闲着,不该把同分组的其他任务一起堵住。
|
|
204
|
-
*
|
|
205
|
-
* @param {number} taskId
|
|
206
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
207
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
208
|
-
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
209
|
-
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走、同分组有
|
|
210
|
-
* 任务正在跑、或行已不是 pending
|
|
211
|
-
*/
|
|
188
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
189
|
+
// 语义说明也在那里)。
|
|
212
190
|
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
params.push(serializeGroup);
|
|
219
|
-
setClause = "lease_until = $1, serialize_group = $4, updated_at = NOW()";
|
|
220
|
-
groupGuard = `
|
|
221
|
-
AND NOT EXISTS (
|
|
222
|
-
SELECT 1 FROM scheduled_messages busy
|
|
223
|
-
WHERE busy.serialize_group = $4 AND busy.id <> $2
|
|
224
|
-
AND busy.status = 'pending' AND busy.lease_until > NOW()
|
|
225
|
-
)`;
|
|
226
|
-
}
|
|
227
|
-
const rows = await this._query(
|
|
228
|
-
`UPDATE scheduled_messages
|
|
229
|
-
SET ${setClause}
|
|
230
|
-
WHERE id = $2 AND status = 'pending'
|
|
231
|
-
AND date_trunc('milliseconds', next_send_at)
|
|
232
|
-
= date_trunc('milliseconds', $3::timestamptz)
|
|
233
|
-
AND (lease_until IS NULL OR lease_until <= NOW())${groupGuard}
|
|
234
|
-
RETURNING id`,
|
|
235
|
-
params
|
|
236
|
-
);
|
|
237
|
-
return rows.length > 0;
|
|
191
|
+
return _chunkTFFBMYA2cjs.claimTask.call(void 0, (text, params) => this._query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
192
|
+
}
|
|
193
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
194
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
195
|
+
return _chunkTFFBMYA2cjs.renewTaskLease.call(void 0, (text, params) => this._query(text, params), taskId, leaseUntil);
|
|
238
196
|
}
|
|
239
197
|
async listTasks(userId, opts = {}) {
|
|
240
198
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -254,7 +212,7 @@ var PgAdapter = class {
|
|
|
254
212
|
const total = parseInt(countRows[0].count, 10);
|
|
255
213
|
const taskParams = [...params, limit, offset];
|
|
256
214
|
const tasks = await this._query(
|
|
257
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
215
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
|
|
258
216
|
FROM scheduled_messages
|
|
259
217
|
WHERE ${where}
|
|
260
218
|
ORDER BY next_send_at ASC
|
|
@@ -281,54 +239,19 @@ var PgAdapter = class {
|
|
|
281
239
|
);
|
|
282
240
|
return rows.length > 0 ? rows[0].status : null;
|
|
283
241
|
}
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
* @returns {Promise<{ subscription: string, updated_at: number }|null>}
|
|
290
|
-
*/
|
|
242
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
243
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
244
|
+
return _chunkTFFBMYA2cjs.getTaskStatusInfo.call(void 0, (text, params) => this._query(text, params), uuid, userId);
|
|
245
|
+
}
|
|
246
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
291
247
|
async getPushSubscription(userId) {
|
|
292
|
-
|
|
293
|
-
"SELECT subscription, updated_at FROM push_subscriptions WHERE user_id = $1 LIMIT 1",
|
|
294
|
-
[userId]
|
|
295
|
-
);
|
|
296
|
-
if (rows.length === 0) return null;
|
|
297
|
-
return { subscription: rows[0].subscription, updated_at: Number(rows[0].updated_at) };
|
|
248
|
+
return _chunkTFFBMYA2cjs.getPushSubscription.call(void 0, (text, params) => this._query(text, params), userId);
|
|
298
249
|
}
|
|
299
|
-
/**
|
|
300
|
-
* 覆盖写这个用户的订阅。一个用户一行,没有 last-write-wins 之类的比较——
|
|
301
|
-
* 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
|
|
302
|
-
*
|
|
303
|
-
* @param {string} userId
|
|
304
|
-
* @param {string} encryptedSubscription
|
|
305
|
-
* @param {number} updatedAt - epoch 毫秒
|
|
306
|
-
* @returns {Promise<boolean>}
|
|
307
|
-
*/
|
|
308
250
|
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
309
|
-
|
|
310
|
-
`INSERT INTO push_subscriptions (user_id, subscription, updated_at)
|
|
311
|
-
VALUES ($1, $2, $3)
|
|
312
|
-
ON CONFLICT (user_id) DO UPDATE SET
|
|
313
|
-
subscription = EXCLUDED.subscription,
|
|
314
|
-
updated_at = EXCLUDED.updated_at
|
|
315
|
-
RETURNING user_id`,
|
|
316
|
-
[userId, encryptedSubscription, updatedAt]
|
|
317
|
-
);
|
|
318
|
-
return rows.length > 0;
|
|
251
|
+
return _chunkTFFBMYA2cjs.upsertPushSubscription.call(void 0, (text, params) => this._query(text, params), userId, encryptedSubscription, updatedAt);
|
|
319
252
|
}
|
|
320
|
-
/**
|
|
321
|
-
* 删掉这个用户的订阅(设置页「停止接收推送」)。
|
|
322
|
-
*
|
|
323
|
-
* @param {string} userId
|
|
324
|
-
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
325
|
-
*/
|
|
326
253
|
async deletePushSubscription(userId) {
|
|
327
|
-
|
|
328
|
-
"DELETE FROM push_subscriptions WHERE user_id = $1 RETURNING user_id",
|
|
329
|
-
[userId]
|
|
330
|
-
);
|
|
331
|
-
return rows.length > 0;
|
|
254
|
+
return _chunkTFFBMYA2cjs.deletePushSubscription.call(void 0, (text, params) => this._query(text, params), userId);
|
|
332
255
|
}
|
|
333
256
|
};
|
|
334
257
|
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
claimTask,
|
|
3
|
+
deletePushSubscription,
|
|
4
|
+
getPushSubscription,
|
|
5
|
+
getTaskStatusInfo,
|
|
6
|
+
renewTaskLease,
|
|
7
|
+
upsertPushSubscription
|
|
8
|
+
} from "./chunk-UYROU4L3.mjs";
|
|
1
9
|
import {
|
|
2
10
|
COLUMNS_SQL,
|
|
3
11
|
INDEXES,
|
|
@@ -6,7 +14,7 @@ import {
|
|
|
6
14
|
TABLE_SQL,
|
|
7
15
|
UPDATABLE_COLUMNS,
|
|
8
16
|
VERIFY_TABLE_SQL
|
|
9
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-34I2YSWE.mjs";
|
|
10
18
|
|
|
11
19
|
// src/server/adapters/pg.js
|
|
12
20
|
import { Pool } from "pg";
|
|
@@ -93,7 +101,7 @@ var PgAdapter = class {
|
|
|
93
101
|
}
|
|
94
102
|
async getTaskByUuid(uuid, userId) {
|
|
95
103
|
const rows = await this._query(
|
|
96
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
104
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
|
|
97
105
|
FROM scheduled_messages
|
|
98
106
|
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
99
107
|
LIMIT 1`,
|
|
@@ -167,7 +175,7 @@ var PgAdapter = class {
|
|
|
167
175
|
}
|
|
168
176
|
async getPendingTasks(limit = 50) {
|
|
169
177
|
return this._query(
|
|
170
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
178
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, retry_after, status, retry_count
|
|
171
179
|
FROM scheduled_messages
|
|
172
180
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
173
181
|
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
@@ -177,64 +185,14 @@ var PgAdapter = class {
|
|
|
177
185
|
[limit]
|
|
178
186
|
);
|
|
179
187
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
* 本次投递期间别的 tick 领不走它。
|
|
183
|
-
*
|
|
184
|
-
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
185
|
-
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
186
|
-
*
|
|
187
|
-
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
188
|
-
* 过。WHERE 里的两个条件各管一件事:
|
|
189
|
-
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
190
|
-
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
191
|
-
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
192
|
-
* 这一跳就不该再按旧时刻发。
|
|
193
|
-
*
|
|
194
|
-
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
195
|
-
* 要改表。
|
|
196
|
-
*
|
|
197
|
-
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
198
|
-
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
199
|
-
*
|
|
200
|
-
* 带 serializeGroup 时多一道分组门:同一分组里已经有别的行拿着未到期的租
|
|
201
|
-
* 约,这条就领不走(同一分组同时只跑一条)。判定和写租约在同一条 UPDATE
|
|
202
|
-
* 里完成,「先查再占」的空档天然不存在。分组门只看租约,不看
|
|
203
|
-
* `retry_after`:等着重试的任务其实闲着,不该把同分组的其他任务一起堵住。
|
|
204
|
-
*
|
|
205
|
-
* @param {number} taskId
|
|
206
|
-
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
207
|
-
* @param {string|Date} leaseUntil - 租期末尾
|
|
208
|
-
* @param {string|null} [serializeGroup] - 串行分组标识;空表示不参与分组串行
|
|
209
|
-
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走、同分组有
|
|
210
|
-
* 任务正在跑、或行已不是 pending
|
|
211
|
-
*/
|
|
188
|
+
// 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
|
|
189
|
+
// 语义说明也在那里)。
|
|
212
190
|
async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
params.push(serializeGroup);
|
|
219
|
-
setClause = "lease_until = $1, serialize_group = $4, updated_at = NOW()";
|
|
220
|
-
groupGuard = `
|
|
221
|
-
AND NOT EXISTS (
|
|
222
|
-
SELECT 1 FROM scheduled_messages busy
|
|
223
|
-
WHERE busy.serialize_group = $4 AND busy.id <> $2
|
|
224
|
-
AND busy.status = 'pending' AND busy.lease_until > NOW()
|
|
225
|
-
)`;
|
|
226
|
-
}
|
|
227
|
-
const rows = await this._query(
|
|
228
|
-
`UPDATE scheduled_messages
|
|
229
|
-
SET ${setClause}
|
|
230
|
-
WHERE id = $2 AND status = 'pending'
|
|
231
|
-
AND date_trunc('milliseconds', next_send_at)
|
|
232
|
-
= date_trunc('milliseconds', $3::timestamptz)
|
|
233
|
-
AND (lease_until IS NULL OR lease_until <= NOW())${groupGuard}
|
|
234
|
-
RETURNING id`,
|
|
235
|
-
params
|
|
236
|
-
);
|
|
237
|
-
return rows.length > 0;
|
|
191
|
+
return claimTask((text, params) => this._query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
|
|
192
|
+
}
|
|
193
|
+
// 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
|
|
194
|
+
async renewTaskLease(taskId, leaseUntil) {
|
|
195
|
+
return renewTaskLease((text, params) => this._query(text, params), taskId, leaseUntil);
|
|
238
196
|
}
|
|
239
197
|
async listTasks(userId, opts = {}) {
|
|
240
198
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -254,7 +212,7 @@ var PgAdapter = class {
|
|
|
254
212
|
const total = parseInt(countRows[0].count, 10);
|
|
255
213
|
const taskParams = [...params, limit, offset];
|
|
256
214
|
const tasks = await this._query(
|
|
257
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
215
|
+
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, last_error, created_at, updated_at
|
|
258
216
|
FROM scheduled_messages
|
|
259
217
|
WHERE ${where}
|
|
260
218
|
ORDER BY next_send_at ASC
|
|
@@ -281,54 +239,19 @@ var PgAdapter = class {
|
|
|
281
239
|
);
|
|
282
240
|
return rows.length > 0 ? rows[0].status : null;
|
|
283
241
|
}
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
* @returns {Promise<{ subscription: string, updated_at: number }|null>}
|
|
290
|
-
*/
|
|
242
|
+
// 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
|
|
243
|
+
async getTaskStatusInfo(uuid, userId) {
|
|
244
|
+
return getTaskStatusInfo((text, params) => this._query(text, params), uuid, userId);
|
|
245
|
+
}
|
|
246
|
+
// ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
|
|
291
247
|
async getPushSubscription(userId) {
|
|
292
|
-
|
|
293
|
-
"SELECT subscription, updated_at FROM push_subscriptions WHERE user_id = $1 LIMIT 1",
|
|
294
|
-
[userId]
|
|
295
|
-
);
|
|
296
|
-
if (rows.length === 0) return null;
|
|
297
|
-
return { subscription: rows[0].subscription, updated_at: Number(rows[0].updated_at) };
|
|
248
|
+
return getPushSubscription((text, params) => this._query(text, params), userId);
|
|
298
249
|
}
|
|
299
|
-
/**
|
|
300
|
-
* 覆盖写这个用户的订阅。一个用户一行,没有 last-write-wins 之类的比较——
|
|
301
|
-
* 客户端拿到的新订阅永远比旧的有效,旧的那份只会 410。
|
|
302
|
-
*
|
|
303
|
-
* @param {string} userId
|
|
304
|
-
* @param {string} encryptedSubscription
|
|
305
|
-
* @param {number} updatedAt - epoch 毫秒
|
|
306
|
-
* @returns {Promise<boolean>}
|
|
307
|
-
*/
|
|
308
250
|
async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
|
|
309
|
-
|
|
310
|
-
`INSERT INTO push_subscriptions (user_id, subscription, updated_at)
|
|
311
|
-
VALUES ($1, $2, $3)
|
|
312
|
-
ON CONFLICT (user_id) DO UPDATE SET
|
|
313
|
-
subscription = EXCLUDED.subscription,
|
|
314
|
-
updated_at = EXCLUDED.updated_at
|
|
315
|
-
RETURNING user_id`,
|
|
316
|
-
[userId, encryptedSubscription, updatedAt]
|
|
317
|
-
);
|
|
318
|
-
return rows.length > 0;
|
|
251
|
+
return upsertPushSubscription((text, params) => this._query(text, params), userId, encryptedSubscription, updatedAt);
|
|
319
252
|
}
|
|
320
|
-
/**
|
|
321
|
-
* 删掉这个用户的订阅(设置页「停止接收推送」)。
|
|
322
|
-
*
|
|
323
|
-
* @param {string} userId
|
|
324
|
-
* @returns {Promise<boolean>} true = 确实删掉了一行
|
|
325
|
-
*/
|
|
326
253
|
async deletePushSubscription(userId) {
|
|
327
|
-
|
|
328
|
-
"DELETE FROM push_subscriptions WHERE user_id = $1 RETURNING user_id",
|
|
329
|
-
[userId]
|
|
330
|
-
);
|
|
331
|
-
return rows.length > 0;
|
|
254
|
+
return deletePushSubscription((text, params) => this._query(text, params), userId);
|
|
332
255
|
}
|
|
333
256
|
};
|
|
334
257
|
export {
|
package/dist/single-user.d.ts
CHANGED
|
@@ -81,5 +81,9 @@ export function createSingleUserServer(config: any): {
|
|
|
81
81
|
capabilities: {
|
|
82
82
|
GET: (url: any, headers: any) => Promise<any>;
|
|
83
83
|
};
|
|
84
|
+
outbox: {
|
|
85
|
+
GET: (url: any, headers: any) => Promise<any>;
|
|
86
|
+
POST: (headers: any, body: any) => Promise<any>;
|
|
87
|
+
};
|
|
84
88
|
};
|
|
85
89
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-server",
|
|
3
|
-
"version": "2.6.0-next.
|
|
3
|
+
"version": "2.6.0-next.15",
|
|
4
4
|
"description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@netlify/blobs": "^8.1.0",
|
|
42
|
-
"@rei-standard/amsg-shared": "^0.4.0-next.
|
|
42
|
+
"@rei-standard/amsg-shared": "^0.4.0-next.5",
|
|
43
43
|
"web-push": "^3.6.7"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|