@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,289 +0,0 @@
1
- import { Pool } from 'pg';
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
- * PostgreSQL (pg) Database Adapter
6
- * ReiStandard SDK v2.0.1
7
- *
8
- * Uses the standard 'pg' npm package.
9
- *
10
- * @implements {import('./interface.js').DbAdapter}
11
- */
12
-
13
-
14
- class PgAdapter {
15
- /** @param {string} connectionString */
16
- constructor(connectionString) {
17
- /** @private */
18
- this._connectionString = connectionString;
19
- /** @private */
20
- this._pool = null;
21
- }
22
-
23
- /** @private */
24
- _getPool() {
25
- if (!this._pool) {
26
- this._pool = new Pool({ connectionString: this._connectionString });
27
- }
28
- return this._pool;
29
- }
30
-
31
- /** @private */
32
- async _query(text, params) {
33
- const pool = this._getPool();
34
- const result = await pool.query(text, params);
35
- return result.rows;
36
- }
37
-
38
- async initSchema() {
39
- await this._query(TABLE_SQL);
40
- for (const migration of MIGRATIONS) {
41
- await this._query(migration.sql);
42
- }
43
- const indexResults = [];
44
- for (const index of INDEXES) {
45
- try {
46
- await this._query(index.sql);
47
- indexResults.push({
48
- name: index.name,
49
- status: 'success',
50
- description: index.description,
51
- critical: !!index.critical
52
- });
53
- } catch (error) {
54
- indexResults.push({
55
- name: index.name,
56
- status: 'failed',
57
- description: index.description,
58
- critical: !!index.critical,
59
- error: error.message
60
- });
61
- }
62
- }
63
-
64
- const criticalFailures = indexResults.filter((index) => index.critical && index.status === 'failed');
65
- if (criticalFailures.length > 0) {
66
- const failedNames = criticalFailures.map((index) => index.name).join(', ');
67
- throw new Error(
68
- `Critical index creation failed (${failedNames}). ` +
69
- 'Please remove duplicate UUID rows and run initSchema again.'
70
- );
71
- }
72
-
73
- const tableCheck = await this._query(VERIFY_TABLE_SQL);
74
- if (tableCheck.length === 0) {
75
- throw new Error('Table creation verification failed');
76
- }
77
- const columns = await this._query(COLUMNS_SQL);
78
- return {
79
- columnsCreated: columns.length,
80
- indexesCreated: indexResults.filter(r => r.status === 'success').length,
81
- indexesFailed: indexResults.filter(r => r.status === 'failed').length,
82
- columns: [
83
- ...columns.map(c => ({ table: 'scheduled_messages', name: c.column_name, type: c.data_type, nullable: c.is_nullable === 'YES' }))
84
- ],
85
- indexes: indexResults
86
- };
87
- }
88
-
89
- async dropSchema() {
90
- await this._query('DROP TABLE IF EXISTS scheduled_messages CASCADE');
91
- }
92
-
93
- async createTask(params) {
94
- const rows = await this._query(
95
- `INSERT INTO scheduled_messages
96
- (user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
97
- VALUES ($1, $2, $3, $4, $5, 'pending', 0, NOW(), NOW())
98
- RETURNING id, uuid, next_send_at, status, created_at`,
99
- [params.user_id, params.uuid, params.encrypted_payload, params.next_send_at, params.message_type]
100
- );
101
- return rows[0] || null;
102
- }
103
-
104
- async getTaskByUuid(uuid, userId) {
105
- const rows = await this._query(
106
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
107
- FROM scheduled_messages
108
- WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
109
- LIMIT 1`,
110
- [uuid, userId]
111
- );
112
- return rows[0] || null;
113
- }
114
-
115
- async getTaskByUuidOnly(uuid) {
116
- const rows = await this._query(
117
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
118
- FROM scheduled_messages
119
- WHERE uuid = $1 AND status = 'pending'
120
- LIMIT 1`,
121
- [uuid]
122
- );
123
- return rows[0] || null;
124
- }
125
-
126
- async updateTaskById(taskId, updates) {
127
- const sets = [];
128
- const values = [];
129
- let idx = 1;
130
-
131
- for (const [key, value] of Object.entries(updates)) {
132
- sets.push(`${key} = $${idx}`);
133
- values.push(value);
134
- idx++;
135
- }
136
-
137
- sets.push('updated_at = NOW()');
138
- values.push(taskId);
139
-
140
- const rows = await this._query(
141
- `UPDATE scheduled_messages SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
142
- values
143
- );
144
- return rows[0] || null;
145
- }
146
-
147
- async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
148
- const sets = ['encrypted_payload = $1', 'updated_at = NOW()'];
149
- const values = [encryptedPayload];
150
- let idx = 2;
151
-
152
- if (extraFields) {
153
- for (const [key, value] of Object.entries(extraFields)) {
154
- sets.push(`${key} = $${idx}`);
155
- values.push(value);
156
- idx++;
157
- }
158
- }
159
-
160
- values.push(uuid, userId);
161
- const rows = await this._query(
162
- `UPDATE scheduled_messages SET ${sets.join(', ')}
163
- WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
164
- RETURNING uuid, updated_at`,
165
- values
166
- );
167
- return rows[0] || null;
168
- }
169
-
170
- async deleteTaskById(taskId) {
171
- const rows = await this._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 rows = await this._query(
177
- 'DELETE FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 RETURNING id',
178
- [uuid, userId]
179
- );
180
- return rows.length > 0;
181
- }
182
-
183
- async getPendingTasks(limit = 50) {
184
- return this._query(
185
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
186
- FROM scheduled_messages
187
- WHERE status = 'pending' AND next_send_at <= NOW()
188
- AND (lease_until IS NULL OR lease_until <= NOW())
189
- ORDER BY next_send_at ASC
190
- LIMIT $1`,
191
- [limit]
192
- );
193
- }
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
-
234
- async listTasks(userId, opts = {}) {
235
- const { status = 'all', limit = 20, offset = 0 } = opts;
236
-
237
- const conditions = ['user_id = $1'];
238
- const params = [userId];
239
- let idx = 2;
240
-
241
- if (status !== 'all') {
242
- conditions.push(`status = $${idx}`);
243
- params.push(status);
244
- idx++;
245
- }
246
-
247
- const where = conditions.join(' AND ');
248
-
249
- const countRows = await this._query(
250
- `SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`,
251
- params
252
- );
253
- const total = parseInt(countRows[0].count, 10);
254
-
255
- const taskParams = [...params, limit, offset];
256
- 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
258
- FROM scheduled_messages
259
- WHERE ${where}
260
- ORDER BY next_send_at ASC
261
- LIMIT $${idx} OFFSET $${idx + 1}`,
262
- taskParams
263
- );
264
-
265
- return { tasks, total };
266
- }
267
-
268
- async cleanupOldTasks(days = 7) {
269
- const safeDays = Math.max(1, Math.floor(Number(days)));
270
- const rows = await this._query(
271
- `DELETE FROM scheduled_messages
272
- WHERE status IN ('sent', 'failed')
273
- AND updated_at < NOW() - make_interval(days => $1)
274
- RETURNING id`,
275
- [safeDays]
276
- );
277
- return rows.length;
278
- }
279
-
280
- async getTaskStatus(uuid, userId) {
281
- const rows = await this._query(
282
- 'SELECT status FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1',
283
- [uuid, userId]
284
- );
285
- return rows.length > 0 ? rows[0].status : null;
286
- }
287
- }
288
-
289
- export { PgAdapter };
@@ -1,289 +0,0 @@
1
- import { Pool } from 'pg';
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
- * PostgreSQL (pg) Database Adapter
6
- * ReiStandard SDK v2.0.1
7
- *
8
- * Uses the standard 'pg' npm package.
9
- *
10
- * @implements {import('./interface.js').DbAdapter}
11
- */
12
-
13
-
14
- class PgAdapter {
15
- /** @param {string} connectionString */
16
- constructor(connectionString) {
17
- /** @private */
18
- this._connectionString = connectionString;
19
- /** @private */
20
- this._pool = null;
21
- }
22
-
23
- /** @private */
24
- _getPool() {
25
- if (!this._pool) {
26
- this._pool = new Pool({ connectionString: this._connectionString });
27
- }
28
- return this._pool;
29
- }
30
-
31
- /** @private */
32
- async _query(text, params) {
33
- const pool = this._getPool();
34
- const result = await pool.query(text, params);
35
- return result.rows;
36
- }
37
-
38
- async initSchema() {
39
- await this._query(TABLE_SQL);
40
- for (const migration of MIGRATIONS) {
41
- await this._query(migration.sql);
42
- }
43
- const indexResults = [];
44
- for (const index of INDEXES) {
45
- try {
46
- await this._query(index.sql);
47
- indexResults.push({
48
- name: index.name,
49
- status: 'success',
50
- description: index.description,
51
- critical: !!index.critical
52
- });
53
- } catch (error) {
54
- indexResults.push({
55
- name: index.name,
56
- status: 'failed',
57
- description: index.description,
58
- critical: !!index.critical,
59
- error: error.message
60
- });
61
- }
62
- }
63
-
64
- const criticalFailures = indexResults.filter((index) => index.critical && index.status === 'failed');
65
- if (criticalFailures.length > 0) {
66
- const failedNames = criticalFailures.map((index) => index.name).join(', ');
67
- throw new Error(
68
- `Critical index creation failed (${failedNames}). ` +
69
- 'Please remove duplicate UUID rows and run initSchema again.'
70
- );
71
- }
72
-
73
- const tableCheck = await this._query(VERIFY_TABLE_SQL);
74
- if (tableCheck.length === 0) {
75
- throw new Error('Table creation verification failed');
76
- }
77
- const columns = await this._query(COLUMNS_SQL);
78
- return {
79
- columnsCreated: columns.length,
80
- indexesCreated: indexResults.filter(r => r.status === 'success').length,
81
- indexesFailed: indexResults.filter(r => r.status === 'failed').length,
82
- columns: [
83
- ...columns.map(c => ({ table: 'scheduled_messages', name: c.column_name, type: c.data_type, nullable: c.is_nullable === 'YES' }))
84
- ],
85
- indexes: indexResults
86
- };
87
- }
88
-
89
- async dropSchema() {
90
- await this._query('DROP TABLE IF EXISTS scheduled_messages CASCADE');
91
- }
92
-
93
- async createTask(params) {
94
- const rows = await this._query(
95
- `INSERT INTO scheduled_messages
96
- (user_id, uuid, encrypted_payload, next_send_at, message_type, status, retry_count, created_at, updated_at)
97
- VALUES ($1, $2, $3, $4, $5, 'pending', 0, NOW(), NOW())
98
- RETURNING id, uuid, next_send_at, status, created_at`,
99
- [params.user_id, params.uuid, params.encrypted_payload, params.next_send_at, params.message_type]
100
- );
101
- return rows[0] || null;
102
- }
103
-
104
- async getTaskByUuid(uuid, userId) {
105
- const rows = await this._query(
106
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
107
- FROM scheduled_messages
108
- WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
109
- LIMIT 1`,
110
- [uuid, userId]
111
- );
112
- return rows[0] || null;
113
- }
114
-
115
- async getTaskByUuidOnly(uuid) {
116
- const rows = await this._query(
117
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
118
- FROM scheduled_messages
119
- WHERE uuid = $1 AND status = 'pending'
120
- LIMIT 1`,
121
- [uuid]
122
- );
123
- return rows[0] || null;
124
- }
125
-
126
- async updateTaskById(taskId, updates) {
127
- const sets = [];
128
- const values = [];
129
- let idx = 1;
130
-
131
- for (const [key, value] of Object.entries(updates)) {
132
- sets.push(`${key} = $${idx}`);
133
- values.push(value);
134
- idx++;
135
- }
136
-
137
- sets.push('updated_at = NOW()');
138
- values.push(taskId);
139
-
140
- const rows = await this._query(
141
- `UPDATE scheduled_messages SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`,
142
- values
143
- );
144
- return rows[0] || null;
145
- }
146
-
147
- async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
148
- const sets = ['encrypted_payload = $1', 'updated_at = NOW()'];
149
- const values = [encryptedPayload];
150
- let idx = 2;
151
-
152
- if (extraFields) {
153
- for (const [key, value] of Object.entries(extraFields)) {
154
- sets.push(`${key} = $${idx}`);
155
- values.push(value);
156
- idx++;
157
- }
158
- }
159
-
160
- values.push(uuid, userId);
161
- const rows = await this._query(
162
- `UPDATE scheduled_messages SET ${sets.join(', ')}
163
- WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
164
- RETURNING uuid, updated_at`,
165
- values
166
- );
167
- return rows[0] || null;
168
- }
169
-
170
- async deleteTaskById(taskId) {
171
- const rows = await this._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 rows = await this._query(
177
- 'DELETE FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 RETURNING id',
178
- [uuid, userId]
179
- );
180
- return rows.length > 0;
181
- }
182
-
183
- async getPendingTasks(limit = 50) {
184
- return this._query(
185
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
186
- FROM scheduled_messages
187
- WHERE status = 'pending' AND next_send_at <= NOW()
188
- AND (lease_until IS NULL OR lease_until <= NOW())
189
- ORDER BY next_send_at ASC
190
- LIMIT $1`,
191
- [limit]
192
- );
193
- }
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
-
234
- async listTasks(userId, opts = {}) {
235
- const { status = 'all', limit = 20, offset = 0 } = opts;
236
-
237
- const conditions = ['user_id = $1'];
238
- const params = [userId];
239
- let idx = 2;
240
-
241
- if (status !== 'all') {
242
- conditions.push(`status = $${idx}`);
243
- params.push(status);
244
- idx++;
245
- }
246
-
247
- const where = conditions.join(' AND ');
248
-
249
- const countRows = await this._query(
250
- `SELECT COUNT(*) as count FROM scheduled_messages WHERE ${where}`,
251
- params
252
- );
253
- const total = parseInt(countRows[0].count, 10);
254
-
255
- const taskParams = [...params, limit, offset];
256
- 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
258
- FROM scheduled_messages
259
- WHERE ${where}
260
- ORDER BY next_send_at ASC
261
- LIMIT $${idx} OFFSET $${idx + 1}`,
262
- taskParams
263
- );
264
-
265
- return { tasks, total };
266
- }
267
-
268
- async cleanupOldTasks(days = 7) {
269
- const safeDays = Math.max(1, Math.floor(Number(days)));
270
- const rows = await this._query(
271
- `DELETE FROM scheduled_messages
272
- WHERE status IN ('sent', 'failed')
273
- AND updated_at < NOW() - make_interval(days => $1)
274
- RETURNING id`,
275
- [safeDays]
276
- );
277
- return rows.length;
278
- }
279
-
280
- async getTaskStatus(uuid, userId) {
281
- const rows = await this._query(
282
- 'SELECT status FROM scheduled_messages WHERE uuid = $1 AND user_id = $2 LIMIT 1',
283
- [uuid, userId]
284
- );
285
- return rows.length > 0 ? rows[0].status : null;
286
- }
287
- }
288
-
289
- export { PgAdapter };
@@ -1,88 +0,0 @@
1
- /**
2
- * Shared SQL schema constants
3
- * ReiStandard SDK v2.0.1
4
- */
5
-
6
- const TABLE_SQL = `
7
- CREATE TABLE IF NOT EXISTS scheduled_messages (
8
- id SERIAL PRIMARY KEY,
9
- user_id VARCHAR(255) NOT NULL,
10
- uuid VARCHAR(36),
11
- encrypted_payload TEXT NOT NULL,
12
- message_type VARCHAR(50) NOT NULL CHECK (message_type IN ('fixed', 'prompted', 'auto', 'instant')),
13
- next_send_at TIMESTAMP WITH TIME ZONE NOT NULL,
14
- lease_until TIMESTAMP WITH TIME ZONE,
15
- status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
16
- retry_count INTEGER DEFAULT 0,
17
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
18
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
19
- )
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
-
35
- const INDEXES = [
36
- {
37
- name: 'idx_pending_tasks_optimized',
38
- sql: `CREATE INDEX IF NOT EXISTS idx_pending_tasks_optimized
39
- ON scheduled_messages (status, next_send_at, id, retry_count)
40
- WHERE status = 'pending'`,
41
- description: 'Main query index (Cron Job finds pending tasks)'
42
- },
43
- {
44
- name: 'idx_cleanup_completed',
45
- sql: `CREATE INDEX IF NOT EXISTS idx_cleanup_completed
46
- ON scheduled_messages (status, updated_at)
47
- WHERE status IN ('sent', 'failed')`,
48
- description: 'Cleanup query index'
49
- },
50
- {
51
- name: 'idx_failed_retry',
52
- sql: `CREATE INDEX IF NOT EXISTS idx_failed_retry
53
- ON scheduled_messages (status, retry_count, next_send_at)
54
- WHERE status = 'failed' AND retry_count < 3`,
55
- description: 'Failed retry index'
56
- },
57
- {
58
- name: 'idx_user_id',
59
- sql: `CREATE INDEX IF NOT EXISTS idx_user_id
60
- ON scheduled_messages (user_id)`,
61
- description: 'User task query index'
62
- },
63
- {
64
- name: 'uidx_uuid',
65
- sql: `CREATE UNIQUE INDEX IF NOT EXISTS uidx_uuid
66
- ON scheduled_messages (uuid)
67
- WHERE uuid IS NOT NULL`,
68
- description: 'UUID uniqueness guard',
69
- critical: true
70
- }
71
- ];
72
-
73
- const VERIFY_TABLE_SQL = `
74
- SELECT table_name
75
- FROM information_schema.tables
76
- WHERE table_schema = 'public'
77
- AND table_name = 'scheduled_messages'
78
- `;
79
-
80
- const COLUMNS_SQL = `
81
- SELECT column_name, data_type, is_nullable
82
- FROM information_schema.columns
83
- WHERE table_schema = 'public'
84
- AND table_name = 'scheduled_messages'
85
- ORDER BY ordinal_position
86
- `;
87
-
88
- export { COLUMNS_SQL as C, INDEXES as I, MIGRATIONS as M, TABLE_SQL as T, VERIFY_TABLE_SQL as V };