@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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +861 -35
  3. package/dist/adapters/d1.d.ts +693 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +384 -0
  6. package/dist/adapters/neon.d.ts +105 -0
  7. package/dist/adapters/pg-shared.d.ts +194 -0
  8. package/dist/adapters/pg.d.ts +101 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +80 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/{chunk-GOBCWCJP.mjs → chunk-GN44PST5.mjs} +68 -1
  13. package/dist/chunk-RL7V7VSS.mjs +8784 -0
  14. package/dist/chunk-TKDISKCF.cjs +163 -0
  15. package/dist/chunk-XKUWMYVF.mjs +163 -0
  16. package/dist/chunk-YBDQJVFQ.cjs +8784 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +39 -0
  18. package/dist/cloudflare.cjs +35 -2
  19. package/dist/cloudflare.d.ts +11 -2
  20. package/dist/cloudflare.mjs +36 -3
  21. package/dist/handlers/cancel-message.d.ts +3 -0
  22. package/dist/handlers/capabilities.d.ts +4 -0
  23. package/dist/handlers/client-state-namespaces.d.ts +5 -0
  24. package/dist/handlers/client-state.d.ts +7 -0
  25. package/dist/handlers/cloud-data.d.ts +4 -0
  26. package/dist/handlers/get-message.d.ts +3 -0
  27. package/dist/handlers/get-user-key.d.ts +3 -0
  28. package/dist/handlers/init-tenant.d.ts +23 -0
  29. package/dist/handlers/llm-credentials.d.ts +5 -0
  30. package/dist/handlers/messages.d.ts +3 -0
  31. package/dist/handlers/outbox.d.ts +9 -0
  32. package/dist/handlers/push-subscription.d.ts +5 -0
  33. package/dist/handlers/schedule-message.d.ts +3 -0
  34. package/dist/handlers/send-notifications.d.ts +3 -0
  35. package/dist/handlers/single-user-init.d.ts +13 -0
  36. package/dist/handlers/update-message.d.ts +3 -0
  37. package/dist/handlers/vapid-public-key.d.ts +19 -0
  38. package/dist/index.cjs +133 -44
  39. package/dist/index.d.cts +145 -766
  40. package/dist/index.d.ts +145 -766
  41. package/dist/index.mjs +125 -36
  42. package/dist/lib/agentic-fire.d.ts +177 -0
  43. package/dist/lib/client-state-store.d.ts +88 -0
  44. package/dist/lib/cloud-data-cleanup.d.ts +36 -0
  45. package/dist/lib/cloud-data-guard.d.ts +14 -0
  46. package/dist/lib/cloud-data-inventory.d.ts +15 -0
  47. package/dist/lib/constant-time.d.ts +14 -0
  48. package/dist/lib/db-errors.d.ts +10 -0
  49. package/dist/lib/encryption.d.ts +48 -0
  50. package/dist/lib/errors.d.ts +273 -0
  51. package/dist/lib/llm-credentials-store.d.ts +147 -0
  52. package/dist/lib/llm.d.ts +12 -0
  53. package/dist/lib/message-processor.d.ts +82 -0
  54. package/dist/lib/outbox-store.d.ts +162 -0
  55. package/dist/lib/push-policy.d.ts +11 -0
  56. package/dist/lib/push-subscription-store.d.ts +94 -0
  57. package/dist/lib/recurrence.d.ts +51 -0
  58. package/dist/lib/request.d.ts +182 -0
  59. package/dist/lib/result-emitter.d.ts +60 -0
  60. package/dist/lib/retry-policy.d.ts +11 -0
  61. package/dist/lib/run-tick.d.ts +121 -0
  62. package/dist/lib/schema-version.d.ts +50 -0
  63. package/dist/lib/state-accessors.d.ts +70 -0
  64. package/dist/lib/state-chunks.d.ts +65 -0
  65. package/dist/lib/task-projection.d.ts +92 -0
  66. package/dist/lib/validation.d.ts +97 -0
  67. package/dist/lib/version.d.ts +7 -0
  68. package/dist/lib/webcrypto-utils.d.ts +1 -0
  69. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  70. package/dist/{neon-QJ7KYHEX.mjs → neon-GKLT4UY3.mjs} +116 -44
  71. package/dist/{neon-V4VOH55N.cjs → neon-XHPJZESV.cjs} +121 -49
  72. package/dist/{pg-6X74HKMX.cjs → pg-FHXQ6BJP.cjs} +109 -48
  73. package/dist/{pg-DFJSFOJQ.mjs → pg-J4YXRQP5.mjs} +104 -43
  74. package/dist/single-user.d.ts +106 -0
  75. package/dist/tenant/blob-store.d.ts +30 -0
  76. package/dist/tenant/context.d.ts +47 -0
  77. package/dist/tenant/single-user-context.d.ts +29 -0
  78. package/dist/tenant/token.d.ts +26 -0
  79. package/package.json +3 -3
  80. package/dist/chunk-DCKCAGFV.cjs +0 -81
  81. package/dist/chunk-HQHRXSQA.cjs +0 -2710
  82. package/dist/chunk-LEE5KQ7Y.mjs +0 -2710
  83. package/dist/cloudflare-CH3dBl5b.d.cts +0 -4235
  84. package/dist/cloudflare-CH3dBl5b.d.ts +0 -4235
  85. package/dist/cloudflare.d.cts +0 -2
  86. package/dist/neon-D8Z2Nn97.d.cts +0 -295
  87. package/dist/neon-PqNFJIz2.d.ts +0 -295
  88. package/dist/pg-C48FwCWW.d.cts +0 -289
  89. package/dist/pg-C5gtWUfU.d.ts +0 -289
  90. package/dist/schema-BHuyH_6Z.d.cts +0 -88
  91. package/dist/schema-BHuyH_6Z.d.ts +0 -88
@@ -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';
@@ -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 };
@@ -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 };