@rei-standard/amsg-server 2.6.0-next.8 → 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.
- package/LICENSE +21 -0
- package/README.md +861 -35
- package/dist/adapters/d1.d.ts +693 -0
- package/dist/adapters/factory.d.ts +39 -0
- package/dist/adapters/interface.d.ts +384 -0
- package/dist/adapters/neon.d.ts +105 -0
- package/dist/adapters/pg-shared.d.ts +194 -0
- package/dist/adapters/pg.d.ts +101 -0
- package/dist/adapters/schema.d.ts +57 -0
- package/dist/adapters/schema.sqlite.d.ts +80 -0
- package/dist/chunk-7NMQFTDJ.cjs +148 -0
- package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
- package/dist/chunk-RL7V7VSS.mjs +8784 -0
- package/dist/chunk-TKDISKCF.cjs +163 -0
- package/dist/chunk-XKUWMYVF.mjs +163 -0
- package/dist/chunk-YBDQJVFQ.cjs +8784 -0
- package/dist/cloudflare/single-user-worker.d.ts +39 -0
- package/dist/cloudflare.cjs +35 -2
- package/dist/cloudflare.d.ts +11 -2
- package/dist/cloudflare.mjs +36 -3
- package/dist/handlers/cancel-message.d.ts +3 -0
- package/dist/handlers/capabilities.d.ts +4 -0
- package/dist/handlers/client-state-namespaces.d.ts +5 -0
- package/dist/handlers/client-state.d.ts +7 -0
- package/dist/handlers/cloud-data.d.ts +4 -0
- package/dist/handlers/get-message.d.ts +3 -0
- package/dist/handlers/get-user-key.d.ts +3 -0
- package/dist/handlers/init-tenant.d.ts +23 -0
- package/dist/handlers/llm-credentials.d.ts +5 -0
- package/dist/handlers/messages.d.ts +3 -0
- package/dist/handlers/outbox.d.ts +9 -0
- package/dist/handlers/push-subscription.d.ts +5 -0
- package/dist/handlers/schedule-message.d.ts +3 -0
- package/dist/handlers/send-notifications.d.ts +3 -0
- package/dist/handlers/single-user-init.d.ts +13 -0
- package/dist/handlers/update-message.d.ts +3 -0
- package/dist/handlers/vapid-public-key.d.ts +19 -0
- package/dist/index.cjs +133 -44
- package/dist/index.d.cts +145 -766
- package/dist/index.d.ts +145 -766
- package/dist/index.mjs +125 -36
- package/dist/lib/agentic-fire.d.ts +177 -0
- package/dist/lib/client-state-store.d.ts +88 -0
- package/dist/lib/cloud-data-cleanup.d.ts +36 -0
- package/dist/lib/cloud-data-guard.d.ts +14 -0
- package/dist/lib/cloud-data-inventory.d.ts +15 -0
- package/dist/lib/constant-time.d.ts +14 -0
- package/dist/lib/db-errors.d.ts +10 -0
- package/dist/lib/encryption.d.ts +48 -0
- package/dist/lib/errors.d.ts +273 -0
- package/dist/lib/llm-credentials-store.d.ts +147 -0
- package/dist/lib/llm.d.ts +12 -0
- package/dist/lib/message-processor.d.ts +82 -0
- package/dist/lib/outbox-store.d.ts +162 -0
- package/dist/lib/push-policy.d.ts +11 -0
- package/dist/lib/push-subscription-store.d.ts +94 -0
- package/dist/lib/recurrence.d.ts +51 -0
- package/dist/lib/request.d.ts +182 -0
- package/dist/lib/result-emitter.d.ts +60 -0
- package/dist/lib/retry-policy.d.ts +11 -0
- package/dist/lib/run-tick.d.ts +121 -0
- package/dist/lib/schema-version.d.ts +50 -0
- package/dist/lib/state-accessors.d.ts +70 -0
- package/dist/lib/state-chunks.d.ts +65 -0
- package/dist/lib/task-projection.d.ts +92 -0
- package/dist/lib/validation.d.ts +97 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/webcrypto-utils.d.ts +1 -0
- package/dist/lib/webpush-webcrypto.d.ts +78 -0
- package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
- package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
- package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
- package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
- package/dist/single-user.d.ts +106 -0
- package/dist/tenant/blob-store.d.ts +30 -0
- package/dist/tenant/context.d.ts +47 -0
- package/dist/tenant/single-user-context.d.ts +29 -0
- package/dist/tenant/token.d.ts +26 -0
- package/package.json +3 -3
- package/dist/chunk-DCKCAGFV.cjs +0 -81
- package/dist/chunk-HQHRXSQA.cjs +0 -2710
- package/dist/chunk-LEE5KQ7Y.mjs +0 -2710
- package/dist/cloudflare-CH3dBl5b.d.cts +0 -4235
- package/dist/cloudflare-CH3dBl5b.d.ts +0 -4235
- package/dist/cloudflare.d.cts +0 -2
- package/dist/neon-D8Z2Nn97.d.cts +0 -295
- package/dist/neon-PqNFJIz2.d.ts +0 -295
- package/dist/pg-C48FwCWW.d.cts +0 -289
- package/dist/pg-C5gtWUfU.d.ts +0 -289
- package/dist/schema-BHuyH_6Z.d.cts +0 -88
- package/dist/schema-BHuyH_6Z.d.ts +0 -88
package/dist/cloudflare.d.cts
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export { M as MAX_PUSH_PAYLOAD_BYTES, W as WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES, f as WEB_PUSH_MAX_BODY_BYTES, h as createD1Adapter, j as createSingleUserCloudflareWorker, k as createSingleUserServer, l as createWebCryptoWebPush, m as decryptFromStorage, n as decryptPayload, o as deriveUserEncryptionKey, q as encryptForStorage, v as measurePushPayload, r as runScheduledTick } from './cloudflare-CH3dBl5b.cjs';
|
|
2
|
-
import '@rei-standard/amsg-shared';
|
package/dist/neon-D8Z2Nn97.d.cts
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import { neon } from '@neondatabase/serverless';
|
|
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
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Neon Serverless Database Adapter
|
|
6
|
-
* ReiStandard SDK v2.0.1
|
|
7
|
-
*
|
|
8
|
-
* @implements {import('./interface.js').DbAdapter}
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class NeonAdapter {
|
|
13
|
-
/** @param {string} connectionString */
|
|
14
|
-
constructor(connectionString) {
|
|
15
|
-
/** @private */
|
|
16
|
-
this._connectionString = connectionString;
|
|
17
|
-
/** @private */
|
|
18
|
-
this._sql = null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** @private */
|
|
22
|
-
_getSql() {
|
|
23
|
-
if (!this._sql) {
|
|
24
|
-
this._sql = neon(this._connectionString);
|
|
25
|
-
}
|
|
26
|
-
return this._sql;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async initSchema() {
|
|
30
|
-
const sql = this._getSql();
|
|
31
|
-
|
|
32
|
-
await sql.query(TABLE_SQL);
|
|
33
|
-
for (const migration of MIGRATIONS) {
|
|
34
|
-
await sql.query(migration.sql);
|
|
35
|
-
}
|
|
36
|
-
const indexResults = [];
|
|
37
|
-
for (const index of INDEXES) {
|
|
38
|
-
try {
|
|
39
|
-
await sql.query(index.sql);
|
|
40
|
-
indexResults.push({
|
|
41
|
-
name: index.name,
|
|
42
|
-
status: 'success',
|
|
43
|
-
description: index.description,
|
|
44
|
-
critical: !!index.critical
|
|
45
|
-
});
|
|
46
|
-
} catch (error) {
|
|
47
|
-
indexResults.push({
|
|
48
|
-
name: index.name,
|
|
49
|
-
status: 'failed',
|
|
50
|
-
description: index.description,
|
|
51
|
-
critical: !!index.critical,
|
|
52
|
-
error: error.message
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const criticalFailures = indexResults.filter((index) => index.critical && index.status === 'failed');
|
|
58
|
-
if (criticalFailures.length > 0) {
|
|
59
|
-
const failedNames = criticalFailures.map((index) => index.name).join(', ');
|
|
60
|
-
throw new Error(
|
|
61
|
-
`Critical index creation failed (${failedNames}). ` +
|
|
62
|
-
'Please remove duplicate UUID rows and run initSchema again.'
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const tableCheck = await sql.query(VERIFY_TABLE_SQL);
|
|
67
|
-
if (tableCheck.length === 0) {
|
|
68
|
-
throw new Error('Table creation verification failed');
|
|
69
|
-
}
|
|
70
|
-
const columns = await sql.query(COLUMNS_SQL);
|
|
71
|
-
return {
|
|
72
|
-
columnsCreated: columns.length,
|
|
73
|
-
indexesCreated: indexResults.filter(r => r.status === 'success').length,
|
|
74
|
-
indexesFailed: indexResults.filter(r => r.status === 'failed').length,
|
|
75
|
-
columns: [
|
|
76
|
-
...columns.map(c => ({ table: 'scheduled_messages', name: c.column_name, type: c.data_type, nullable: c.is_nullable === 'YES' }))
|
|
77
|
-
],
|
|
78
|
-
indexes: indexResults
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async dropSchema() {
|
|
83
|
-
const sql = this._getSql();
|
|
84
|
-
await sql.query('DROP TABLE IF EXISTS scheduled_messages CASCADE');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async createTask(params) {
|
|
88
|
-
const sql = this._getSql();
|
|
89
|
-
const rows = await sql.query(
|
|
90
|
-
`INSERT INTO scheduled_messages
|
|
91
|
-
(user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
|
|
92
|
-
VALUES ($1, $2, $3, $4, $5, 'pending', 0, NOW(), NOW())
|
|
93
|
-
RETURNING id, uuid, next_send_at, status, created_at`,
|
|
94
|
-
[params.user_id, params.uuid, params.encrypted_payload, params.next_send_at, params.message_type]
|
|
95
|
-
);
|
|
96
|
-
return rows[0] || null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async getTaskByUuid(uuid, userId) {
|
|
100
|
-
const sql = this._getSql();
|
|
101
|
-
const rows = await sql.query(
|
|
102
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
103
|
-
FROM scheduled_messages
|
|
104
|
-
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
105
|
-
LIMIT 1`,
|
|
106
|
-
[uuid, userId]
|
|
107
|
-
);
|
|
108
|
-
return rows[0] || null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async getTaskByUuidOnly(uuid) {
|
|
112
|
-
const sql = this._getSql();
|
|
113
|
-
const rows = await sql.query(
|
|
114
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
115
|
-
FROM scheduled_messages
|
|
116
|
-
WHERE uuid = $1 AND status = 'pending'
|
|
117
|
-
LIMIT 1`,
|
|
118
|
-
[uuid]
|
|
119
|
-
);
|
|
120
|
-
return rows[0] || null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async updateTaskById(taskId, updates) {
|
|
124
|
-
const sql = this._getSql();
|
|
125
|
-
const sets = [];
|
|
126
|
-
const values = [];
|
|
127
|
-
let idx = 1;
|
|
128
|
-
|
|
129
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
130
|
-
sets.push(`${key} = $${idx}`);
|
|
131
|
-
values.push(value);
|
|
132
|
-
idx++;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
sets.push('updated_at = NOW()');
|
|
136
|
-
values.push(taskId);
|
|
137
|
-
|
|
138
|
-
const rows = await sql.query(
|
|
139
|
-
`UPDATE scheduled_messages SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
|
|
140
|
-
values
|
|
141
|
-
);
|
|
142
|
-
return rows[0] || null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
146
|
-
const sql = this._getSql();
|
|
147
|
-
const sets = ['encrypted_payload = $1', 'updated_at = NOW()'];
|
|
148
|
-
const values = [encryptedPayload];
|
|
149
|
-
let idx = 2;
|
|
150
|
-
|
|
151
|
-
if (extraFields) {
|
|
152
|
-
for (const [key, value] of Object.entries(extraFields)) {
|
|
153
|
-
sets.push(`${key} = $${idx}`);
|
|
154
|
-
values.push(value);
|
|
155
|
-
idx++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
values.push(uuid, userId);
|
|
160
|
-
const rows = await sql.query(
|
|
161
|
-
`UPDATE scheduled_messages SET ${sets.join(', ')}
|
|
162
|
-
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
163
|
-
RETURNING uuid, updated_at`,
|
|
164
|
-
values
|
|
165
|
-
);
|
|
166
|
-
return rows[0] || null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async deleteTaskById(taskId) {
|
|
170
|
-
const sql = this._getSql();
|
|
171
|
-
const rows = await sql.query('DELETE FROM scheduled_messages WHERE id = $1 RETURNING id', [taskId]);
|
|
172
|
-
return rows.length > 0;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async deleteTaskByUuid(uuid, userId) {
|
|
176
|
-
const sql = this._getSql();
|
|
177
|
-
const rows = await sql.query(
|
|
178
|
-
'DELETE FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 RETURNING id',
|
|
179
|
-
[uuid, userId]
|
|
180
|
-
);
|
|
181
|
-
return rows.length > 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async getPendingTasks(limit = 50) {
|
|
185
|
-
const sql = this._getSql();
|
|
186
|
-
return sql.query(
|
|
187
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
188
|
-
FROM scheduled_messages
|
|
189
|
-
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
190
|
-
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
191
|
-
ORDER BY next_send_at ASC
|
|
192
|
-
LIMIT $1`,
|
|
193
|
-
[limit]
|
|
194
|
-
);
|
|
195
|
-
}
|
|
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
|
-
|
|
237
|
-
async listTasks(userId, opts = {}) {
|
|
238
|
-
const sql = this._getSql();
|
|
239
|
-
const { status = 'all', limit = 20, offset = 0 } = opts;
|
|
240
|
-
|
|
241
|
-
const conditions = ['user_id = $1'];
|
|
242
|
-
const params = [userId];
|
|
243
|
-
let idx = 2;
|
|
244
|
-
|
|
245
|
-
if (status !== 'all') {
|
|
246
|
-
conditions.push(`status = $${idx}`);
|
|
247
|
-
params.push(status);
|
|
248
|
-
idx++;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const where = conditions.join(' AND ');
|
|
252
|
-
|
|
253
|
-
const countRows = await sql.query(
|
|
254
|
-
`SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`,
|
|
255
|
-
params
|
|
256
|
-
);
|
|
257
|
-
const total = parseInt(countRows[0].count, 10);
|
|
258
|
-
|
|
259
|
-
const taskParams = [...params, limit, offset];
|
|
260
|
-
const tasks = await sql.query(
|
|
261
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
262
|
-
FROM scheduled_messages
|
|
263
|
-
WHERE ${where}
|
|
264
|
-
ORDER BY next_send_at ASC
|
|
265
|
-
LIMIT $${idx} OFFSET $${idx + 1}`,
|
|
266
|
-
taskParams
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
return { tasks, total };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async cleanupOldTasks(days = 7) {
|
|
273
|
-
const sql = this._getSql();
|
|
274
|
-
const safeDays = Math.max(1, Math.floor(Number(days)));
|
|
275
|
-
const rows = await sql.query(
|
|
276
|
-
`DELETE FROM scheduled_messages
|
|
277
|
-
WHERE status IN ('sent', 'failed')
|
|
278
|
-
AND updated_at < NOW() - make_interval(days => $1)
|
|
279
|
-
RETURNING id`,
|
|
280
|
-
[safeDays]
|
|
281
|
-
);
|
|
282
|
-
return rows.length;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async getTaskStatus(uuid, userId) {
|
|
286
|
-
const sql = this._getSql();
|
|
287
|
-
const rows = await sql.query(
|
|
288
|
-
'SELECT status FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1',
|
|
289
|
-
[uuid, userId]
|
|
290
|
-
);
|
|
291
|
-
return rows.length > 0 ? rows[0].status : null;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export { NeonAdapter };
|
package/dist/neon-PqNFJIz2.d.ts
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import { neon } from '@neondatabase/serverless';
|
|
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
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Neon Serverless Database Adapter
|
|
6
|
-
* ReiStandard SDK v2.0.1
|
|
7
|
-
*
|
|
8
|
-
* @implements {import('./interface.js').DbAdapter}
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class NeonAdapter {
|
|
13
|
-
/** @param {string} connectionString */
|
|
14
|
-
constructor(connectionString) {
|
|
15
|
-
/** @private */
|
|
16
|
-
this._connectionString = connectionString;
|
|
17
|
-
/** @private */
|
|
18
|
-
this._sql = null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** @private */
|
|
22
|
-
_getSql() {
|
|
23
|
-
if (!this._sql) {
|
|
24
|
-
this._sql = neon(this._connectionString);
|
|
25
|
-
}
|
|
26
|
-
return this._sql;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async initSchema() {
|
|
30
|
-
const sql = this._getSql();
|
|
31
|
-
|
|
32
|
-
await sql.query(TABLE_SQL);
|
|
33
|
-
for (const migration of MIGRATIONS) {
|
|
34
|
-
await sql.query(migration.sql);
|
|
35
|
-
}
|
|
36
|
-
const indexResults = [];
|
|
37
|
-
for (const index of INDEXES) {
|
|
38
|
-
try {
|
|
39
|
-
await sql.query(index.sql);
|
|
40
|
-
indexResults.push({
|
|
41
|
-
name: index.name,
|
|
42
|
-
status: 'success',
|
|
43
|
-
description: index.description,
|
|
44
|
-
critical: !!index.critical
|
|
45
|
-
});
|
|
46
|
-
} catch (error) {
|
|
47
|
-
indexResults.push({
|
|
48
|
-
name: index.name,
|
|
49
|
-
status: 'failed',
|
|
50
|
-
description: index.description,
|
|
51
|
-
critical: !!index.critical,
|
|
52
|
-
error: error.message
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const criticalFailures = indexResults.filter((index) => index.critical && index.status === 'failed');
|
|
58
|
-
if (criticalFailures.length > 0) {
|
|
59
|
-
const failedNames = criticalFailures.map((index) => index.name).join(', ');
|
|
60
|
-
throw new Error(
|
|
61
|
-
`Critical index creation failed (${failedNames}). ` +
|
|
62
|
-
'Please remove duplicate UUID rows and run initSchema again.'
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const tableCheck = await sql.query(VERIFY_TABLE_SQL);
|
|
67
|
-
if (tableCheck.length === 0) {
|
|
68
|
-
throw new Error('Table creation verification failed');
|
|
69
|
-
}
|
|
70
|
-
const columns = await sql.query(COLUMNS_SQL);
|
|
71
|
-
return {
|
|
72
|
-
columnsCreated: columns.length,
|
|
73
|
-
indexesCreated: indexResults.filter(r => r.status === 'success').length,
|
|
74
|
-
indexesFailed: indexResults.filter(r => r.status === 'failed').length,
|
|
75
|
-
columns: [
|
|
76
|
-
...columns.map(c => ({ table: 'scheduled_messages', name: c.column_name, type: c.data_type, nullable: c.is_nullable === 'YES' }))
|
|
77
|
-
],
|
|
78
|
-
indexes: indexResults
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async dropSchema() {
|
|
83
|
-
const sql = this._getSql();
|
|
84
|
-
await sql.query('DROP TABLE IF EXISTS scheduled_messages CASCADE');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async createTask(params) {
|
|
88
|
-
const sql = this._getSql();
|
|
89
|
-
const rows = await sql.query(
|
|
90
|
-
`INSERT INTO scheduled_messages
|
|
91
|
-
(user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
|
|
92
|
-
VALUES ($1, $2, $3, $4, $5, 'pending', 0, NOW(), NOW())
|
|
93
|
-
RETURNING id, uuid, next_send_at, status, created_at`,
|
|
94
|
-
[params.user_id, params.uuid, params.encrypted_payload, params.next_send_at, params.message_type]
|
|
95
|
-
);
|
|
96
|
-
return rows[0] || null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async getTaskByUuid(uuid, userId) {
|
|
100
|
-
const sql = this._getSql();
|
|
101
|
-
const rows = await sql.query(
|
|
102
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
103
|
-
FROM scheduled_messages
|
|
104
|
-
WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
|
|
105
|
-
LIMIT 1`,
|
|
106
|
-
[uuid, userId]
|
|
107
|
-
);
|
|
108
|
-
return rows[0] || null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async getTaskByUuidOnly(uuid) {
|
|
112
|
-
const sql = this._getSql();
|
|
113
|
-
const rows = await sql.query(
|
|
114
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
115
|
-
FROM scheduled_messages
|
|
116
|
-
WHERE uuid = $1 AND status = 'pending'
|
|
117
|
-
LIMIT 1`,
|
|
118
|
-
[uuid]
|
|
119
|
-
);
|
|
120
|
-
return rows[0] || null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async updateTaskById(taskId, updates) {
|
|
124
|
-
const sql = this._getSql();
|
|
125
|
-
const sets = [];
|
|
126
|
-
const values = [];
|
|
127
|
-
let idx = 1;
|
|
128
|
-
|
|
129
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
130
|
-
sets.push(`${key} = $${idx}`);
|
|
131
|
-
values.push(value);
|
|
132
|
-
idx++;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
sets.push('updated_at = NOW()');
|
|
136
|
-
values.push(taskId);
|
|
137
|
-
|
|
138
|
-
const rows = await sql.query(
|
|
139
|
-
`UPDATE scheduled_messages SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
|
|
140
|
-
values
|
|
141
|
-
);
|
|
142
|
-
return rows[0] || null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
|
|
146
|
-
const sql = this._getSql();
|
|
147
|
-
const sets = ['encrypted_payload = $1', 'updated_at = NOW()'];
|
|
148
|
-
const values = [encryptedPayload];
|
|
149
|
-
let idx = 2;
|
|
150
|
-
|
|
151
|
-
if (extraFields) {
|
|
152
|
-
for (const [key, value] of Object.entries(extraFields)) {
|
|
153
|
-
sets.push(`${key} = $${idx}`);
|
|
154
|
-
values.push(value);
|
|
155
|
-
idx++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
values.push(uuid, userId);
|
|
160
|
-
const rows = await sql.query(
|
|
161
|
-
`UPDATE scheduled_messages SET ${sets.join(', ')}
|
|
162
|
-
WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
|
|
163
|
-
RETURNING uuid, updated_at`,
|
|
164
|
-
values
|
|
165
|
-
);
|
|
166
|
-
return rows[0] || null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async deleteTaskById(taskId) {
|
|
170
|
-
const sql = this._getSql();
|
|
171
|
-
const rows = await sql.query('DELETE FROM scheduled_messages WHERE id = $1 RETURNING id', [taskId]);
|
|
172
|
-
return rows.length > 0;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async deleteTaskByUuid(uuid, userId) {
|
|
176
|
-
const sql = this._getSql();
|
|
177
|
-
const rows = await sql.query(
|
|
178
|
-
'DELETE FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 RETURNING id',
|
|
179
|
-
[uuid, userId]
|
|
180
|
-
);
|
|
181
|
-
return rows.length > 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async getPendingTasks(limit = 50) {
|
|
185
|
-
const sql = this._getSql();
|
|
186
|
-
return sql.query(
|
|
187
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
|
|
188
|
-
FROM scheduled_messages
|
|
189
|
-
WHERE status = 'pending' AND next_send_at <= NOW()
|
|
190
|
-
AND (lease_until IS NULL OR lease_until <= NOW())
|
|
191
|
-
ORDER BY next_send_at ASC
|
|
192
|
-
LIMIT $1`,
|
|
193
|
-
[limit]
|
|
194
|
-
);
|
|
195
|
-
}
|
|
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
|
-
|
|
237
|
-
async listTasks(userId, opts = {}) {
|
|
238
|
-
const sql = this._getSql();
|
|
239
|
-
const { status = 'all', limit = 20, offset = 0 } = opts;
|
|
240
|
-
|
|
241
|
-
const conditions = ['user_id = $1'];
|
|
242
|
-
const params = [userId];
|
|
243
|
-
let idx = 2;
|
|
244
|
-
|
|
245
|
-
if (status !== 'all') {
|
|
246
|
-
conditions.push(`status = $${idx}`);
|
|
247
|
-
params.push(status);
|
|
248
|
-
idx++;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const where = conditions.join(' AND ');
|
|
252
|
-
|
|
253
|
-
const countRows = await sql.query(
|
|
254
|
-
`SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`,
|
|
255
|
-
params
|
|
256
|
-
);
|
|
257
|
-
const total = parseInt(countRows[0].count, 10);
|
|
258
|
-
|
|
259
|
-
const taskParams = [...params, limit, offset];
|
|
260
|
-
const tasks = await sql.query(
|
|
261
|
-
`SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
|
|
262
|
-
FROM scheduled_messages
|
|
263
|
-
WHERE ${where}
|
|
264
|
-
ORDER BY next_send_at ASC
|
|
265
|
-
LIMIT $${idx} OFFSET $${idx + 1}`,
|
|
266
|
-
taskParams
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
return { tasks, total };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async cleanupOldTasks(days = 7) {
|
|
273
|
-
const sql = this._getSql();
|
|
274
|
-
const safeDays = Math.max(1, Math.floor(Number(days)));
|
|
275
|
-
const rows = await sql.query(
|
|
276
|
-
`DELETE FROM scheduled_messages
|
|
277
|
-
WHERE status IN ('sent', 'failed')
|
|
278
|
-
AND updated_at < NOW() - make_interval(days => $1)
|
|
279
|
-
RETURNING id`,
|
|
280
|
-
[safeDays]
|
|
281
|
-
);
|
|
282
|
-
return rows.length;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async getTaskStatus(uuid, userId) {
|
|
286
|
-
const sql = this._getSql();
|
|
287
|
-
const rows = await sql.query(
|
|
288
|
-
'SELECT status FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1',
|
|
289
|
-
[uuid, userId]
|
|
290
|
-
);
|
|
291
|
-
return rows.length > 0 ? rows[0].status : null;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export { NeonAdapter };
|