@rei-standard/amsg-server 2.6.0-next.4 → 2.6.0-next.6
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/README.md +16 -0
- package/dist/{chunk-RGECD4OH.cjs → chunk-DCKCAGFV.cjs} +10 -1
- package/dist/{chunk-5ENJGVZX.mjs → chunk-GOBCWCJP.mjs} +9 -0
- package/dist/{chunk-OVGQHTSE.mjs → chunk-JODQV7MD.mjs} +108 -10
- package/dist/{chunk-REX5MCEZ.cjs → chunk-WFFIWXM5.cjs} +108 -10
- package/dist/{cloudflare-sVIPkSsf.d.cts → cloudflare-qy1YWQcp.d.cts} +186 -15
- package/dist/{cloudflare-sVIPkSsf.d.ts → cloudflare-qy1YWQcp.d.ts} +186 -15
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/{neon-DYvGnCzx.d.ts → neon-D8Z2Nn97.d.cts} +45 -1
- package/dist/{neon-CJl66EGy.d.cts → neon-PqNFJIz2.d.ts} +45 -1
- package/dist/{neon-CU5N3CSW.mjs → neon-QJ7KYHEX.mjs} +45 -1
- package/dist/{neon-BFUS25UX.cjs → neon-V4VOH55N.cjs} +49 -5
- package/dist/{pg-IIH3M4OM.cjs → pg-6X74HKMX.cjs} +48 -5
- package/dist/{pg-Du-pN_UT.d.ts → pg-C48FwCWW.d.cts} +44 -1
- package/dist/{pg-07S-u_H4.d.cts → pg-C5gtWUfU.d.ts} +44 -1
- package/dist/{pg-QO6NKTGL.mjs → pg-DFJSFOJQ.mjs} +44 -1
- package/dist/{schema-C8OnYk6j.d.cts → schema-BHuyH_6Z.d.cts} +15 -1
- package/dist/{schema-C8OnYk6j.d.ts → schema-BHuyH_6Z.d.ts} +15 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { neon } from '@neondatabase/serverless';
|
|
2
|
-
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-
|
|
2
|
+
import { T as TABLE_SQL, M as MIGRATIONS, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-BHuyH_6Z.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Neon Serverless Database Adapter
|
|
@@ -30,6 +30,9 @@ class NeonAdapter {
|
|
|
30
30
|
const sql = this._getSql();
|
|
31
31
|
|
|
32
32
|
await sql.query(TABLE_SQL);
|
|
33
|
+
for (const migration of MIGRATIONS) {
|
|
34
|
+
await sql.query(migration.sql);
|
|
35
|
+
}
|
|
33
36
|
const indexResults = [];
|
|
34
37
|
for (const index of INDEXES) {
|
|
35
38
|
try {
|
|
@@ -184,12 +187,53 @@ class NeonAdapter {
|
|
|
184
187
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
185
188
|
FROM scheduled_messages
|
|
186
189
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
190
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
187
191
|
ORDER BY next_send_at ASC
|
|
188
192
|
LIMIT $1`,
|
|
189
193
|
[limit]
|
|
190
194
|
);
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
/**
|
|
198
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
199
|
+
* 本次投递期间别的 tick 领不走它。
|
|
200
|
+
*
|
|
201
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
202
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
203
|
+
*
|
|
204
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
205
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
206
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
207
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
208
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
209
|
+
* 这一跳就不该再按旧时刻发。
|
|
210
|
+
*
|
|
211
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
212
|
+
* 要改表。
|
|
213
|
+
*
|
|
214
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
215
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
216
|
+
*
|
|
217
|
+
* @param {number} taskId
|
|
218
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
219
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
220
|
+
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、排期被改过、或行已不是 pending
|
|
221
|
+
*/
|
|
222
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
223
|
+
const sql = this._getSql();
|
|
224
|
+
const rows = await sql.query(
|
|
225
|
+
`UPDATE scheduled_messages
|
|
226
|
+
SET lease_until = $1, updated_at = NOW()
|
|
227
|
+
WHERE id = $2 AND status = 'pending'
|
|
228
|
+
AND date_trunc('milliseconds', next_send_at)
|
|
229
|
+
= date_trunc('milliseconds', $3::timestamptz)
|
|
230
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
231
|
+
RETURNING id`,
|
|
232
|
+
[leaseUntil, taskId, expectedNextSendAt]
|
|
233
|
+
);
|
|
234
|
+
return rows.length > 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
193
237
|
async listTasks(userId, opts = {}) {
|
|
194
238
|
const sql = this._getSql();
|
|
195
239
|
const { status = 'all', limit = 20, offset = 0 } = opts;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
COLUMNS_SQL,
|
|
3
3
|
INDEXES,
|
|
4
|
+
MIGRATIONS,
|
|
4
5
|
TABLE_SQL,
|
|
5
6
|
VERIFY_TABLE_SQL
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GOBCWCJP.mjs";
|
|
7
8
|
|
|
8
9
|
// src/server/adapters/neon.js
|
|
9
10
|
import { neon } from "@neondatabase/serverless";
|
|
@@ -23,6 +24,9 @@ var NeonAdapter = class {
|
|
|
23
24
|
async initSchema() {
|
|
24
25
|
const sql = this._getSql();
|
|
25
26
|
await sql.query(TABLE_SQL);
|
|
27
|
+
for (const migration of MIGRATIONS) {
|
|
28
|
+
await sql.query(migration.sql);
|
|
29
|
+
}
|
|
26
30
|
const indexResults = [];
|
|
27
31
|
for (const index of INDEXES) {
|
|
28
32
|
try {
|
|
@@ -160,11 +164,51 @@ var NeonAdapter = class {
|
|
|
160
164
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
161
165
|
FROM scheduled_messages
|
|
162
166
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
167
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
163
168
|
ORDER BY next_send_at ASC
|
|
164
169
|
LIMIT $1`,
|
|
165
170
|
[limit]
|
|
166
171
|
);
|
|
167
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
175
|
+
* 本次投递期间别的 tick 领不走它。
|
|
176
|
+
*
|
|
177
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
178
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
179
|
+
*
|
|
180
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
181
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
182
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
183
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
184
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
185
|
+
* 这一跳就不该再按旧时刻发。
|
|
186
|
+
*
|
|
187
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
188
|
+
* 要改表。
|
|
189
|
+
*
|
|
190
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
191
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
192
|
+
*
|
|
193
|
+
* @param {number} taskId
|
|
194
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
195
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
196
|
+
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、排期被改过、或行已不是 pending
|
|
197
|
+
*/
|
|
198
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
199
|
+
const sql = this._getSql();
|
|
200
|
+
const rows = await sql.query(
|
|
201
|
+
`UPDATE scheduled_messages
|
|
202
|
+
SET lease_until = $1, updated_at = NOW()
|
|
203
|
+
WHERE id = $2 AND status = 'pending'
|
|
204
|
+
AND date_trunc('milliseconds', next_send_at)
|
|
205
|
+
= date_trunc('milliseconds', $3::timestamptz)
|
|
206
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
207
|
+
RETURNING id`,
|
|
208
|
+
[leaseUntil, taskId, expectedNextSendAt]
|
|
209
|
+
);
|
|
210
|
+
return rows.length > 0;
|
|
211
|
+
}
|
|
168
212
|
async listTasks(userId, opts = {}) {
|
|
169
213
|
const sql = this._getSql();
|
|
170
214
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
var _chunkDCKCAGFVcjs = require('./chunk-DCKCAGFV.cjs');
|
|
7
8
|
|
|
8
9
|
// src/server/adapters/neon.js
|
|
9
10
|
var _serverless = require('@neondatabase/serverless');
|
|
@@ -22,9 +23,12 @@ var NeonAdapter = class {
|
|
|
22
23
|
}
|
|
23
24
|
async initSchema() {
|
|
24
25
|
const sql = this._getSql();
|
|
25
|
-
await sql.query(
|
|
26
|
+
await sql.query(_chunkDCKCAGFVcjs.TABLE_SQL);
|
|
27
|
+
for (const migration of _chunkDCKCAGFVcjs.MIGRATIONS) {
|
|
28
|
+
await sql.query(migration.sql);
|
|
29
|
+
}
|
|
26
30
|
const indexResults = [];
|
|
27
|
-
for (const index of
|
|
31
|
+
for (const index of _chunkDCKCAGFVcjs.INDEXES) {
|
|
28
32
|
try {
|
|
29
33
|
await sql.query(index.sql);
|
|
30
34
|
indexResults.push({
|
|
@@ -50,11 +54,11 @@ var NeonAdapter = class {
|
|
|
50
54
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
|
-
const tableCheck = await sql.query(
|
|
57
|
+
const tableCheck = await sql.query(_chunkDCKCAGFVcjs.VERIFY_TABLE_SQL);
|
|
54
58
|
if (tableCheck.length === 0) {
|
|
55
59
|
throw new Error("Table creation verification failed");
|
|
56
60
|
}
|
|
57
|
-
const columns = await sql.query(
|
|
61
|
+
const columns = await sql.query(_chunkDCKCAGFVcjs.COLUMNS_SQL);
|
|
58
62
|
return {
|
|
59
63
|
columnsCreated: columns.length,
|
|
60
64
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -160,11 +164,51 @@ var NeonAdapter = class {
|
|
|
160
164
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
161
165
|
FROM scheduled_messages
|
|
162
166
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
167
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
163
168
|
ORDER BY next_send_at ASC
|
|
164
169
|
LIMIT $1`,
|
|
165
170
|
[limit]
|
|
166
171
|
);
|
|
167
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
175
|
+
* 本次投递期间别的 tick 领不走它。
|
|
176
|
+
*
|
|
177
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
178
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
179
|
+
*
|
|
180
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
181
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
182
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
183
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
184
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
185
|
+
* 这一跳就不该再按旧时刻发。
|
|
186
|
+
*
|
|
187
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
188
|
+
* 要改表。
|
|
189
|
+
*
|
|
190
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
191
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
192
|
+
*
|
|
193
|
+
* @param {number} taskId
|
|
194
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
195
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
196
|
+
* @returns {Promise<boolean>} true = 领到了;false = 别人正拿着租约、排期被改过、或行已不是 pending
|
|
197
|
+
*/
|
|
198
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
199
|
+
const sql = this._getSql();
|
|
200
|
+
const rows = await sql.query(
|
|
201
|
+
`UPDATE scheduled_messages
|
|
202
|
+
SET lease_until = $1, updated_at = NOW()
|
|
203
|
+
WHERE id = $2 AND status = 'pending'
|
|
204
|
+
AND date_trunc('milliseconds', next_send_at)
|
|
205
|
+
= date_trunc('milliseconds', $3::timestamptz)
|
|
206
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
207
|
+
RETURNING id`,
|
|
208
|
+
[leaseUntil, taskId, expectedNextSendAt]
|
|
209
|
+
);
|
|
210
|
+
return rows.length > 0;
|
|
211
|
+
}
|
|
168
212
|
async listTasks(userId, opts = {}) {
|
|
169
213
|
const sql = this._getSql();
|
|
170
214
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
var _chunkDCKCAGFVcjs = require('./chunk-DCKCAGFV.cjs');
|
|
7
8
|
|
|
8
9
|
// src/server/adapters/pg.js
|
|
9
10
|
var _pg = require('pg');
|
|
@@ -27,9 +28,12 @@ var PgAdapter = class {
|
|
|
27
28
|
return result.rows;
|
|
28
29
|
}
|
|
29
30
|
async initSchema() {
|
|
30
|
-
await this._query(
|
|
31
|
+
await this._query(_chunkDCKCAGFVcjs.TABLE_SQL);
|
|
32
|
+
for (const migration of _chunkDCKCAGFVcjs.MIGRATIONS) {
|
|
33
|
+
await this._query(migration.sql);
|
|
34
|
+
}
|
|
31
35
|
const indexResults = [];
|
|
32
|
-
for (const index of
|
|
36
|
+
for (const index of _chunkDCKCAGFVcjs.INDEXES) {
|
|
33
37
|
try {
|
|
34
38
|
await this._query(index.sql);
|
|
35
39
|
indexResults.push({
|
|
@@ -55,11 +59,11 @@ var PgAdapter = class {
|
|
|
55
59
|
`Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
|
|
56
60
|
);
|
|
57
61
|
}
|
|
58
|
-
const tableCheck = await this._query(
|
|
62
|
+
const tableCheck = await this._query(_chunkDCKCAGFVcjs.VERIFY_TABLE_SQL);
|
|
59
63
|
if (tableCheck.length === 0) {
|
|
60
64
|
throw new Error("Table creation verification failed");
|
|
61
65
|
}
|
|
62
|
-
const columns = await this._query(
|
|
66
|
+
const columns = await this._query(_chunkDCKCAGFVcjs.COLUMNS_SQL);
|
|
63
67
|
return {
|
|
64
68
|
columnsCreated: columns.length,
|
|
65
69
|
indexesCreated: indexResults.filter((r) => r.status === "success").length,
|
|
@@ -156,11 +160,50 @@ var PgAdapter = class {
|
|
|
156
160
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
157
161
|
FROM scheduled_messages
|
|
158
162
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
163
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
159
164
|
ORDER BY next_send_at ASC
|
|
160
165
|
LIMIT $1`,
|
|
161
166
|
[limit]
|
|
162
167
|
);
|
|
163
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
171
|
+
* 本次投递期间别的 tick 领不走它。
|
|
172
|
+
*
|
|
173
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
174
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
175
|
+
*
|
|
176
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
177
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
178
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
179
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
180
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
181
|
+
* 这一跳就不该再按旧时刻发。
|
|
182
|
+
*
|
|
183
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
184
|
+
* 要改表。
|
|
185
|
+
*
|
|
186
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
187
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
188
|
+
*
|
|
189
|
+
* @param {number} taskId
|
|
190
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
191
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
192
|
+
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走或行已不是 pending
|
|
193
|
+
*/
|
|
194
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
195
|
+
const rows = await this._query(
|
|
196
|
+
`UPDATE scheduled_messages
|
|
197
|
+
SET lease_until = $1, updated_at = NOW()
|
|
198
|
+
WHERE id = $2 AND status = 'pending'
|
|
199
|
+
AND date_trunc('milliseconds', next_send_at)
|
|
200
|
+
= date_trunc('milliseconds', $3::timestamptz)
|
|
201
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
202
|
+
RETURNING id`,
|
|
203
|
+
[leaseUntil, taskId, expectedNextSendAt]
|
|
204
|
+
);
|
|
205
|
+
return rows.length > 0;
|
|
206
|
+
}
|
|
164
207
|
async listTasks(userId, opts = {}) {
|
|
165
208
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
166
209
|
const conditions = ["user_id = $1"];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
|
-
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-
|
|
2
|
+
import { T as TABLE_SQL, M as MIGRATIONS, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-BHuyH_6Z.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* PostgreSQL (pg) Database Adapter
|
|
@@ -37,6 +37,9 @@ class PgAdapter {
|
|
|
37
37
|
|
|
38
38
|
async initSchema() {
|
|
39
39
|
await this._query(TABLE_SQL);
|
|
40
|
+
for (const migration of MIGRATIONS) {
|
|
41
|
+
await this._query(migration.sql);
|
|
42
|
+
}
|
|
40
43
|
const indexResults = [];
|
|
41
44
|
for (const index of INDEXES) {
|
|
42
45
|
try {
|
|
@@ -182,12 +185,52 @@ class PgAdapter {
|
|
|
182
185
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
183
186
|
FROM scheduled_messages
|
|
184
187
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
188
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
185
189
|
ORDER BY next_send_at ASC
|
|
186
190
|
LIMIT $1`,
|
|
187
191
|
[limit]
|
|
188
192
|
);
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
197
|
+
* 本次投递期间别的 tick 领不走它。
|
|
198
|
+
*
|
|
199
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
200
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
201
|
+
*
|
|
202
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
203
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
204
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
205
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
206
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
207
|
+
* 这一跳就不该再按旧时刻发。
|
|
208
|
+
*
|
|
209
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
210
|
+
* 要改表。
|
|
211
|
+
*
|
|
212
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
213
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
214
|
+
*
|
|
215
|
+
* @param {number} taskId
|
|
216
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
217
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
218
|
+
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走或行已不是 pending
|
|
219
|
+
*/
|
|
220
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
221
|
+
const rows = await this._query(
|
|
222
|
+
`UPDATE scheduled_messages
|
|
223
|
+
SET lease_until = $1, updated_at = NOW()
|
|
224
|
+
WHERE id = $2 AND status = 'pending'
|
|
225
|
+
AND date_trunc('milliseconds', next_send_at)
|
|
226
|
+
= date_trunc('milliseconds', $3::timestamptz)
|
|
227
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
228
|
+
RETURNING id`,
|
|
229
|
+
[leaseUntil, taskId, expectedNextSendAt]
|
|
230
|
+
);
|
|
231
|
+
return rows.length > 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
191
234
|
async listTasks(userId, opts = {}) {
|
|
192
235
|
const { status = 'all', limit = 20, offset = 0 } = opts;
|
|
193
236
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
|
-
import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-
|
|
2
|
+
import { T as TABLE_SQL, M as MIGRATIONS, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-BHuyH_6Z.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* PostgreSQL (pg) Database Adapter
|
|
@@ -37,6 +37,9 @@ class PgAdapter {
|
|
|
37
37
|
|
|
38
38
|
async initSchema() {
|
|
39
39
|
await this._query(TABLE_SQL);
|
|
40
|
+
for (const migration of MIGRATIONS) {
|
|
41
|
+
await this._query(migration.sql);
|
|
42
|
+
}
|
|
40
43
|
const indexResults = [];
|
|
41
44
|
for (const index of INDEXES) {
|
|
42
45
|
try {
|
|
@@ -182,12 +185,52 @@ class PgAdapter {
|
|
|
182
185
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
183
186
|
FROM scheduled_messages
|
|
184
187
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
188
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
185
189
|
ORDER BY next_send_at ASC
|
|
186
190
|
LIMIT $1`,
|
|
187
191
|
[limit]
|
|
188
192
|
);
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
197
|
+
* 本次投递期间别的 tick 领不走它。
|
|
198
|
+
*
|
|
199
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
200
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
201
|
+
*
|
|
202
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
203
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
204
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
205
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
206
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
207
|
+
* 这一跳就不该再按旧时刻发。
|
|
208
|
+
*
|
|
209
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
210
|
+
* 要改表。
|
|
211
|
+
*
|
|
212
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
213
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
214
|
+
*
|
|
215
|
+
* @param {number} taskId
|
|
216
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
217
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
218
|
+
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走或行已不是 pending
|
|
219
|
+
*/
|
|
220
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
221
|
+
const rows = await this._query(
|
|
222
|
+
`UPDATE scheduled_messages
|
|
223
|
+
SET lease_until = $1, updated_at = NOW()
|
|
224
|
+
WHERE id = $2 AND status = 'pending'
|
|
225
|
+
AND date_trunc('milliseconds', next_send_at)
|
|
226
|
+
= date_trunc('milliseconds', $3::timestamptz)
|
|
227
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
228
|
+
RETURNING id`,
|
|
229
|
+
[leaseUntil, taskId, expectedNextSendAt]
|
|
230
|
+
);
|
|
231
|
+
return rows.length > 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
191
234
|
async listTasks(userId, opts = {}) {
|
|
192
235
|
const { status = 'all', limit = 20, offset = 0 } = opts;
|
|
193
236
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
COLUMNS_SQL,
|
|
3
3
|
INDEXES,
|
|
4
|
+
MIGRATIONS,
|
|
4
5
|
TABLE_SQL,
|
|
5
6
|
VERIFY_TABLE_SQL
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GOBCWCJP.mjs";
|
|
7
8
|
|
|
8
9
|
// src/server/adapters/pg.js
|
|
9
10
|
import { Pool } from "pg";
|
|
@@ -28,6 +29,9 @@ var PgAdapter = class {
|
|
|
28
29
|
}
|
|
29
30
|
async initSchema() {
|
|
30
31
|
await this._query(TABLE_SQL);
|
|
32
|
+
for (const migration of MIGRATIONS) {
|
|
33
|
+
await this._query(migration.sql);
|
|
34
|
+
}
|
|
31
35
|
const indexResults = [];
|
|
32
36
|
for (const index of INDEXES) {
|
|
33
37
|
try {
|
|
@@ -156,11 +160,50 @@ var PgAdapter = class {
|
|
|
156
160
|
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
157
161
|
FROM scheduled_messages
|
|
158
162
|
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
163
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
159
164
|
ORDER BY next_send_at ASC
|
|
160
165
|
LIMIT $1`,
|
|
161
166
|
[limit]
|
|
162
167
|
);
|
|
163
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* 领取一条到点的任务:在 lease_until 上写下「这条归我管到什么时候」,
|
|
171
|
+
* 本次投递期间别的 tick 领不走它。
|
|
172
|
+
*
|
|
173
|
+
* 租约写在自己的列上,next_send_at 全程不动——那一列是用户设的触发时刻,
|
|
174
|
+
* 任务列表要读它、循环任务推进下一次也要拿它当基准。
|
|
175
|
+
*
|
|
176
|
+
* 两个 tick 抢同一行时只有一个改得动,另一个拿不到 RETURNING 行,据此跳
|
|
177
|
+
* 过。WHERE 里的两个条件各管一件事:
|
|
178
|
+
* - lease_until 为空或已过期:没人正在跑这条。领了任务的 tick 中途没了
|
|
179
|
+
* 也不会把行焊死,租约到期后自然可以被接手。
|
|
180
|
+
* - next_send_at 等于读这行时看到的值:读出来之后用户又改了排期的话,
|
|
181
|
+
* 这一跳就不该再按旧时刻发。
|
|
182
|
+
*
|
|
183
|
+
* 不加一个 'sending' 状态来表达「正在跑」:status 上有 CHECK 约束,加值
|
|
184
|
+
* 要改表。
|
|
185
|
+
*
|
|
186
|
+
* 比 next_send_at 时两边都截到毫秒:列是 timestamptz(微秒精度),驱动读
|
|
187
|
+
* 出来是 JS Date(毫秒精度),原值送回去可能因为亚毫秒差对不上。
|
|
188
|
+
*
|
|
189
|
+
* @param {number} taskId
|
|
190
|
+
* @param {string|Date} expectedNextSendAt - 读这行时拿到的 next_send_at 原值
|
|
191
|
+
* @param {string|Date} leaseUntil - 租期末尾
|
|
192
|
+
* @returns {Promise<boolean>} true = 领到了;false = 已被别人领走或行已不是 pending
|
|
193
|
+
*/
|
|
194
|
+
async claimTask(taskId, expectedNextSendAt, leaseUntil) {
|
|
195
|
+
const rows = await this._query(
|
|
196
|
+
`UPDATE scheduled_messages
|
|
197
|
+
SET lease_until = $1, updated_at = NOW()
|
|
198
|
+
WHERE id = $2 AND status = 'pending'
|
|
199
|
+
AND date_trunc('milliseconds', next_send_at)
|
|
200
|
+
= date_trunc('milliseconds', $3::timestamptz)
|
|
201
|
+
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
202
|
+
RETURNING id`,
|
|
203
|
+
[leaseUntil, taskId, expectedNextSendAt]
|
|
204
|
+
);
|
|
205
|
+
return rows.length > 0;
|
|
206
|
+
}
|
|
164
207
|
async listTasks(userId, opts = {}) {
|
|
165
208
|
const { status = "all", limit = 20, offset = 0 } = opts;
|
|
166
209
|
const conditions = ["user_id = $1"];
|
|
@@ -11,6 +11,7 @@ const TABLE_SQL = `
|
|
|
11
11
|
encrypted_payload TEXT NOT NULL,
|
|
12
12
|
message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
|
|
13
13
|
next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
14
|
+
lease_until TIMESTAMP WITH TIME ZONE,
|
|
14
15
|
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
15
16
|
retry_count INTEGER DEFAULT 0,
|
|
16
17
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
@@ -18,6 +19,19 @@ const TABLE_SQL = `
|
|
|
18
19
|
)
|
|
19
20
|
`;
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 建表语句用的是 CREATE TABLE IF NOT EXISTS,已经存在的表不会被改动,所以
|
|
24
|
+
* 后加的列要单独补。initSchema 每次都会跑一遍,Postgres 的 IF NOT EXISTS
|
|
25
|
+
* 让重复执行也没事。
|
|
26
|
+
*/
|
|
27
|
+
const MIGRATIONS = [
|
|
28
|
+
{
|
|
29
|
+
name: 'add_lease_until',
|
|
30
|
+
sql: 'ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS lease_until TIMESTAMP WITH TIME ZONE',
|
|
31
|
+
description: 'Task claim lease (2.6.0)'
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
|
|
21
35
|
const INDEXES = [
|
|
22
36
|
{
|
|
23
37
|
name: 'idx_pending_tasks_optimized',
|
|
@@ -71,4 +85,4 @@ const COLUMNS_SQL = `
|
|
|
71
85
|
ORDER BY ordinal_position
|
|
72
86
|
`;
|
|
73
87
|
|
|
74
|
-
export { COLUMNS_SQL as C, INDEXES as I, TABLE_SQL as T, VERIFY_TABLE_SQL as V };
|
|
88
|
+
export { COLUMNS_SQL as C, INDEXES as I, MIGRATIONS as M, TABLE_SQL as T, VERIFY_TABLE_SQL as V };
|
|
@@ -11,6 +11,7 @@ const TABLE_SQL = `
|
|
|
11
11
|
encrypted_payload TEXT NOT NULL,
|
|
12
12
|
message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
|
|
13
13
|
next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
14
|
+
lease_until TIMESTAMP WITH TIME ZONE,
|
|
14
15
|
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
|
|
15
16
|
retry_count INTEGER DEFAULT 0,
|
|
16
17
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
@@ -18,6 +19,19 @@ const TABLE_SQL = `
|
|
|
18
19
|
)
|
|
19
20
|
`;
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 建表语句用的是 CREATE TABLE IF NOT EXISTS,已经存在的表不会被改动,所以
|
|
24
|
+
* 后加的列要单独补。initSchema 每次都会跑一遍,Postgres 的 IF NOT EXISTS
|
|
25
|
+
* 让重复执行也没事。
|
|
26
|
+
*/
|
|
27
|
+
const MIGRATIONS = [
|
|
28
|
+
{
|
|
29
|
+
name: 'add_lease_until',
|
|
30
|
+
sql: 'ALTER TABLE scheduled_messages ADD COLUMN IF NOT EXISTS lease_until TIMESTAMP WITH TIME ZONE',
|
|
31
|
+
description: 'Task claim lease (2.6.0)'
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
|
|
21
35
|
const INDEXES = [
|
|
22
36
|
{
|
|
23
37
|
name: 'idx_pending_tasks_optimized',
|
|
@@ -71,4 +85,4 @@ const COLUMNS_SQL = `
|
|
|
71
85
|
ORDER BY ordinal_position
|
|
72
86
|
`;
|
|
73
87
|
|
|
74
|
-
export { COLUMNS_SQL as C, INDEXES as I, TABLE_SQL as T, VERIFY_TABLE_SQL as V };
|
|
88
|
+
export { COLUMNS_SQL as C, INDEXES as I, MIGRATIONS as M, TABLE_SQL as T, VERIFY_TABLE_SQL as V };
|
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.6",
|
|
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",
|