@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,10 +1,28 @@
1
+ import {
2
+ claimTask,
3
+ createTaskSuperseding,
4
+ deleteLlmCredentials,
5
+ deletePushSubscription,
6
+ getLlmCredentials,
7
+ getPushSubscription,
8
+ getTaskStatusInfo,
9
+ listLlmCredentials,
10
+ renewTaskLease,
11
+ upsertLlmCredentials,
12
+ upsertPushSubscription
13
+ } from "./chunk-XKUWMYVF.mjs";
1
14
  import {
2
15
  COLUMNS_SQL,
3
16
  INDEXES,
17
+ LLM_CREDENTIALS_TABLE_SQL,
4
18
  MIGRATIONS,
19
+ PUSH_SUBSCRIPTION_TABLE_SQL,
5
20
  TABLE_SQL,
21
+ TASK_DELIVERY_COLUMNS,
22
+ TASK_DETAIL_COLUMNS,
23
+ UPDATABLE_COLUMNS,
6
24
  VERIFY_TABLE_SQL
7
- } from "./chunk-GOBCWCJP.mjs";
25
+ } from "./chunk-GN44PST5.mjs";
8
26
 
9
27
  // src/server/adapters/neon.js
10
28
  import { neon } from "@neondatabase/serverless";
@@ -14,7 +32,13 @@ var NeonAdapter = class {
14
32
  this._connectionString = connectionString;
15
33
  this._sql = null;
16
34
  }
17
- /** @private */
35
+ /**
36
+ * neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
37
+ * pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
38
+ * Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
39
+ *
40
+ * @private
41
+ */
18
42
  _getSql() {
19
43
  if (!this._sql) {
20
44
  this._sql = neon(this._connectionString);
@@ -24,6 +48,8 @@ var NeonAdapter = class {
24
48
  async initSchema() {
25
49
  const sql = this._getSql();
26
50
  await sql.query(TABLE_SQL);
51
+ await sql.query(PUSH_SUBSCRIPTION_TABLE_SQL);
52
+ await sql.query(LLM_CREDENTIALS_TABLE_SQL);
27
53
  for (const migration of MIGRATIONS) {
28
54
  await sql.query(migration.sql);
29
55
  }
@@ -72,6 +98,8 @@ var NeonAdapter = class {
72
98
  async dropSchema() {
73
99
  const sql = this._getSql();
74
100
  await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
101
+ await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
102
+ await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
75
103
  }
76
104
  async createTask(params) {
77
105
  const sql = this._getSql();
@@ -84,10 +112,15 @@ var NeonAdapter = class {
84
112
  );
85
113
  return rows[0] || null;
86
114
  }
115
+ // 建新任务 + 删掉被顶替的旧任务,一条语句一起成败(SQL 与语义见 pg-shared.js)。
116
+ async createTaskSuperseding(params, supersedesUuid) {
117
+ const sql = this._getSql();
118
+ return createTaskSuperseding((text, args) => sql.query(text, args), params, supersedesUuid);
119
+ }
87
120
  async getTaskByUuid(uuid, userId) {
88
121
  const sql = this._getSql();
89
122
  const rows = await sql.query(
90
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
123
+ `SELECT ${TASK_DETAIL_COLUMNS}
91
124
  FROM scheduled_messages
92
125
  WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
93
126
  LIMIT 1`,
@@ -98,7 +131,7 @@ var NeonAdapter = class {
98
131
  async getTaskByUuidOnly(uuid) {
99
132
  const sql = this._getSql();
100
133
  const rows = await sql.query(
101
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
134
+ `SELECT ${TASK_DELIVERY_COLUMNS}
102
135
  FROM scheduled_messages
103
136
  WHERE uuid = $1 AND status = 'pending'
104
137
  LIMIT 1`,
@@ -106,12 +139,30 @@ var NeonAdapter = class {
106
139
  );
107
140
  return rows[0] || null;
108
141
  }
142
+ /**
143
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
144
+ * 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
145
+ *
146
+ * @param {string} uuid
147
+ * @returns {Promise<{ status: string }|null>}
148
+ */
149
+ async getTaskStatusByUuidOnly(uuid) {
150
+ const sql = this._getSql();
151
+ const rows = await sql.query(
152
+ `SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
153
+ [uuid]
154
+ );
155
+ return rows[0] ? { status: rows[0].status } : null;
156
+ }
109
157
  async updateTaskById(taskId, updates) {
110
158
  const sql = this._getSql();
111
159
  const sets = [];
112
160
  const values = [];
113
161
  let idx = 1;
114
162
  for (const [key, value] of Object.entries(updates)) {
163
+ if (!UPDATABLE_COLUMNS.has(key)) {
164
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
165
+ }
115
166
  sets.push(`${key} = $${idx}`);
116
167
  values.push(value);
117
168
  idx++;
@@ -124,6 +175,15 @@ var NeonAdapter = class {
124
175
  );
125
176
  return rows[0] || null;
126
177
  }
178
+ /**
179
+ * 按 uuid 改一条 pending 任务(PUT /update-message、fire hook 的 renewTask)。
180
+ *
181
+ * 改排期(extraFields 里带 next_send_at)时多一道租约门:这条任务正被一次投递
182
+ * 占着的话不改,返回 null。投递收尾会按自己领取时看到的排期推进下一次(一次性
183
+ * 任务干脆标成已发送),这期间写进去的新时刻随后就被盖掉,接口却已经回了成功
184
+ * ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
185
+ * 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
186
+ */
127
187
  async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
128
188
  const sql = this._getSql();
129
189
  const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
@@ -131,15 +191,19 @@ var NeonAdapter = class {
131
191
  let idx = 2;
132
192
  if (extraFields) {
133
193
  for (const [key, value] of Object.entries(extraFields)) {
194
+ if (!UPDATABLE_COLUMNS.has(key)) {
195
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
196
+ }
134
197
  sets.push(`${key} = $${idx}`);
135
198
  values.push(value);
136
199
  idx++;
137
200
  }
138
201
  }
139
202
  values.push(uuid, userId);
203
+ const leaseGate = extraFields && Object.prototype.hasOwnProperty.call(extraFields, "next_send_at") ? " AND (lease_until IS NULL OR lease_until <= NOW())" : "";
140
204
  const rows = await sql.query(
141
205
  `UPDATE scheduled_messages SET ${sets.join(", ")}
142
- WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
206
+ WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'${leaseGate}
143
207
  RETURNING uuid, updated_at`,
144
208
  values
145
209
  );
@@ -161,53 +225,26 @@ var NeonAdapter = class {
161
225
  async getPendingTasks(limit = 50) {
162
226
  const sql = this._getSql();
163
227
  return sql.query(
164
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
228
+ `SELECT ${TASK_DELIVERY_COLUMNS}
165
229
  FROM scheduled_messages
166
230
  WHERE status = 'pending' AND next_send_at <= NOW()
167
231
  AND (lease_until IS NULL OR lease_until <= NOW())
232
+ AND (retry_after IS NULL OR retry_after <= NOW())
168
233
  ORDER BY next_send_at ASC
169
234
  LIMIT $1`,
170
235
  [limit]
171
236
  );
172
237
  }
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) {
238
+ // 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
239
+ // 语义说明也在那里)。
240
+ async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
199
241
  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;
242
+ return claimTask((text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
243
+ }
244
+ // 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
245
+ async renewTaskLease(taskId, leaseUntil) {
246
+ const sql = this._getSql();
247
+ return renewTaskLease((text, params) => sql.query(text, params), taskId, leaseUntil);
211
248
  }
212
249
  async listTasks(userId, opts = {}) {
213
250
  const sql = this._getSql();
@@ -228,7 +265,7 @@ var NeonAdapter = class {
228
265
  const total = parseInt(countRows[0].count, 10);
229
266
  const taskParams = [...params, limit, offset];
230
267
  const tasks = await sql.query(
231
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
268
+ `SELECT ${TASK_DETAIL_COLUMNS}
232
269
  FROM scheduled_messages
233
270
  WHERE ${where}
234
271
  ORDER BY next_send_at ASC
@@ -257,6 +294,41 @@ var NeonAdapter = class {
257
294
  );
258
295
  return rows.length > 0 ? rows[0].status : null;
259
296
  }
297
+ // 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
298
+ async getTaskStatusInfo(uuid, userId) {
299
+ const sql = this._getSql();
300
+ return getTaskStatusInfo((text, params) => sql.query(text, params), uuid, userId);
301
+ }
302
+ // ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
303
+ async getPushSubscription(userId) {
304
+ const sql = this._getSql();
305
+ return getPushSubscription((text, params) => sql.query(text, params), userId);
306
+ }
307
+ async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
308
+ const sql = this._getSql();
309
+ return upsertPushSubscription((text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
310
+ }
311
+ async deletePushSubscription(userId) {
312
+ const sql = this._getSql();
313
+ return deletePushSubscription((text, params) => sql.query(text, params), userId);
314
+ }
315
+ // ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
316
+ async upsertLlmCredentials(userId, entries) {
317
+ const sql = this._getSql();
318
+ return upsertLlmCredentials((text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
319
+ }
320
+ async getLlmCredentials(userId, credIds) {
321
+ const sql = this._getSql();
322
+ return getLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
323
+ }
324
+ async listLlmCredentials(userId) {
325
+ const sql = this._getSql();
326
+ return listLlmCredentials((text, params) => sql.query(text, params), userId);
327
+ }
328
+ async deleteLlmCredentials(userId, credIds = null) {
329
+ const sql = this._getSql();
330
+ return deleteLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
331
+ }
260
332
  };
261
333
  export {
262
334
  NeonAdapter
@@ -4,7 +4,25 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkDCKCAGFVcjs = require('./chunk-DCKCAGFV.cjs');
7
+
8
+
9
+
10
+
11
+
12
+
13
+ var _chunkTKDISKCFcjs = require('./chunk-TKDISKCF.cjs');
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+ var _chunk7NMQFTDJcjs = require('./chunk-7NMQFTDJ.cjs');
8
26
 
9
27
  // src/server/adapters/neon.js
10
28
  var _serverless = require('@neondatabase/serverless');
@@ -14,7 +32,13 @@ var NeonAdapter = class {
14
32
  this._connectionString = connectionString;
15
33
  this._sql = null;
16
34
  }
17
- /** @private */
35
+ /**
36
+ * neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
37
+ * pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
38
+ * Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
39
+ *
40
+ * @private
41
+ */
18
42
  _getSql() {
19
43
  if (!this._sql) {
20
44
  this._sql = _serverless.neon.call(void 0, this._connectionString);
@@ -23,12 +47,14 @@ var NeonAdapter = class {
23
47
  }
24
48
  async initSchema() {
25
49
  const sql = this._getSql();
26
- await sql.query(_chunkDCKCAGFVcjs.TABLE_SQL);
27
- for (const migration of _chunkDCKCAGFVcjs.MIGRATIONS) {
50
+ await sql.query(_chunk7NMQFTDJcjs.TABLE_SQL);
51
+ await sql.query(_chunk7NMQFTDJcjs.PUSH_SUBSCRIPTION_TABLE_SQL);
52
+ await sql.query(_chunk7NMQFTDJcjs.LLM_CREDENTIALS_TABLE_SQL);
53
+ for (const migration of _chunk7NMQFTDJcjs.MIGRATIONS) {
28
54
  await sql.query(migration.sql);
29
55
  }
30
56
  const indexResults = [];
31
- for (const index of _chunkDCKCAGFVcjs.INDEXES) {
57
+ for (const index of _chunk7NMQFTDJcjs.INDEXES) {
32
58
  try {
33
59
  await sql.query(index.sql);
34
60
  indexResults.push({
@@ -54,11 +80,11 @@ var NeonAdapter = class {
54
80
  `Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
55
81
  );
56
82
  }
57
- const tableCheck = await sql.query(_chunkDCKCAGFVcjs.VERIFY_TABLE_SQL);
83
+ const tableCheck = await sql.query(_chunk7NMQFTDJcjs.VERIFY_TABLE_SQL);
58
84
  if (tableCheck.length === 0) {
59
85
  throw new Error("Table creation verification failed");
60
86
  }
61
- const columns = await sql.query(_chunkDCKCAGFVcjs.COLUMNS_SQL);
87
+ const columns = await sql.query(_chunk7NMQFTDJcjs.COLUMNS_SQL);
62
88
  return {
63
89
  columnsCreated: columns.length,
64
90
  indexesCreated: indexResults.filter((r) => r.status === "success").length,
@@ -72,6 +98,8 @@ var NeonAdapter = class {
72
98
  async dropSchema() {
73
99
  const sql = this._getSql();
74
100
  await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
101
+ await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
102
+ await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
75
103
  }
76
104
  async createTask(params) {
77
105
  const sql = this._getSql();
@@ -84,10 +112,15 @@ var NeonAdapter = class {
84
112
  );
85
113
  return rows[0] || null;
86
114
  }
115
+ // 建新任务 + 删掉被顶替的旧任务,一条语句一起成败(SQL 与语义见 pg-shared.js)。
116
+ async createTaskSuperseding(params, supersedesUuid) {
117
+ const sql = this._getSql();
118
+ return _chunkTKDISKCFcjs.createTaskSuperseding.call(void 0, (text, args) => sql.query(text, args), params, supersedesUuid);
119
+ }
87
120
  async getTaskByUuid(uuid, userId) {
88
121
  const sql = this._getSql();
89
122
  const rows = await sql.query(
90
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
123
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
91
124
  FROM scheduled_messages
92
125
  WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
93
126
  LIMIT 1`,
@@ -98,7 +131,7 @@ var NeonAdapter = class {
98
131
  async getTaskByUuidOnly(uuid) {
99
132
  const sql = this._getSql();
100
133
  const rows = await sql.query(
101
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
134
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
102
135
  FROM scheduled_messages
103
136
  WHERE uuid = $1 AND status = 'pending'
104
137
  LIMIT 1`,
@@ -106,12 +139,30 @@ var NeonAdapter = class {
106
139
  );
107
140
  return rows[0] || null;
108
141
  }
142
+ /**
143
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
144
+ * 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
145
+ *
146
+ * @param {string} uuid
147
+ * @returns {Promise<{ status: string }|null>}
148
+ */
149
+ async getTaskStatusByUuidOnly(uuid) {
150
+ const sql = this._getSql();
151
+ const rows = await sql.query(
152
+ `SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
153
+ [uuid]
154
+ );
155
+ return rows[0] ? { status: rows[0].status } : null;
156
+ }
109
157
  async updateTaskById(taskId, updates) {
110
158
  const sql = this._getSql();
111
159
  const sets = [];
112
160
  const values = [];
113
161
  let idx = 1;
114
162
  for (const [key, value] of Object.entries(updates)) {
163
+ if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
164
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
165
+ }
115
166
  sets.push(`${key} = $${idx}`);
116
167
  values.push(value);
117
168
  idx++;
@@ -124,6 +175,15 @@ var NeonAdapter = class {
124
175
  );
125
176
  return rows[0] || null;
126
177
  }
178
+ /**
179
+ * 按 uuid 改一条 pending 任务(PUT /update-message、fire hook 的 renewTask)。
180
+ *
181
+ * 改排期(extraFields 里带 next_send_at)时多一道租约门:这条任务正被一次投递
182
+ * 占着的话不改,返回 null。投递收尾会按自己领取时看到的排期推进下一次(一次性
183
+ * 任务干脆标成已发送),这期间写进去的新时刻随后就被盖掉,接口却已经回了成功
184
+ * ——用户以为改期生效了,实际什么都没留下。正文这类字段不受这道门约束:它们
185
+ * 只影响以后的触发,收尾那边本来就不会覆盖(见 run-tick 的收尾守卫)。
186
+ */
127
187
  async updateTaskByUuid(uuid, userId, encryptedPayload, extraFields) {
128
188
  const sql = this._getSql();
129
189
  const sets = ["encrypted_payload = $1", "updated_at = NOW()"];
@@ -131,15 +191,19 @@ var NeonAdapter = class {
131
191
  let idx = 2;
132
192
  if (extraFields) {
133
193
  for (const [key, value] of Object.entries(extraFields)) {
194
+ if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
195
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
196
+ }
134
197
  sets.push(`${key} = $${idx}`);
135
198
  values.push(value);
136
199
  idx++;
137
200
  }
138
201
  }
139
202
  values.push(uuid, userId);
203
+ const leaseGate = extraFields && Object.prototype.hasOwnProperty.call(extraFields, "next_send_at") ? " AND (lease_until IS NULL OR lease_until <= NOW())" : "";
140
204
  const rows = await sql.query(
141
205
  `UPDATE scheduled_messages SET ${sets.join(", ")}
142
- WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'
206
+ WHERE uuid = $${idx} AND user_id = $${idx + 1} AND status = 'pending'${leaseGate}
143
207
  RETURNING uuid, updated_at`,
144
208
  values
145
209
  );
@@ -161,53 +225,26 @@ var NeonAdapter = class {
161
225
  async getPendingTasks(limit = 50) {
162
226
  const sql = this._getSql();
163
227
  return sql.query(
164
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
228
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
165
229
  FROM scheduled_messages
166
230
  WHERE status = 'pending' AND next_send_at <= NOW()
167
231
  AND (lease_until IS NULL OR lease_until <= NOW())
232
+ AND (retry_after IS NULL OR retry_after <= NOW())
168
233
  ORDER BY next_send_at ASC
169
234
  LIMIT $1`,
170
235
  [limit]
171
236
  );
172
237
  }
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) {
238
+ // 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
239
+ // 语义说明也在那里)。
240
+ async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
199
241
  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;
242
+ return _chunkTKDISKCFcjs.claimTask.call(void 0, (text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
243
+ }
244
+ // 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
245
+ async renewTaskLease(taskId, leaseUntil) {
246
+ const sql = this._getSql();
247
+ return _chunkTKDISKCFcjs.renewTaskLease.call(void 0, (text, params) => sql.query(text, params), taskId, leaseUntil);
211
248
  }
212
249
  async listTasks(userId, opts = {}) {
213
250
  const sql = this._getSql();
@@ -228,7 +265,7 @@ var NeonAdapter = class {
228
265
  const total = parseInt(countRows[0].count, 10);
229
266
  const taskParams = [...params, limit, offset];
230
267
  const tasks = await sql.query(
231
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
268
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
232
269
  FROM scheduled_messages
233
270
  WHERE ${where}
234
271
  ORDER BY next_send_at ASC
@@ -257,6 +294,41 @@ var NeonAdapter = class {
257
294
  );
258
295
  return rows.length > 0 ? rows[0].status : null;
259
296
  }
297
+ // 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
298
+ async getTaskStatusInfo(uuid, userId) {
299
+ const sql = this._getSql();
300
+ return _chunkTKDISKCFcjs.getTaskStatusInfo.call(void 0, (text, params) => sql.query(text, params), uuid, userId);
301
+ }
302
+ // ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
303
+ async getPushSubscription(userId) {
304
+ const sql = this._getSql();
305
+ return _chunkTKDISKCFcjs.getPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
306
+ }
307
+ async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
308
+ const sql = this._getSql();
309
+ return _chunkTKDISKCFcjs.upsertPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
310
+ }
311
+ async deletePushSubscription(userId) {
312
+ const sql = this._getSql();
313
+ return _chunkTKDISKCFcjs.deletePushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
314
+ }
315
+ // ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
316
+ async upsertLlmCredentials(userId, entries) {
317
+ const sql = this._getSql();
318
+ return _chunkTKDISKCFcjs.upsertLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
319
+ }
320
+ async getLlmCredentials(userId, credIds) {
321
+ const sql = this._getSql();
322
+ return _chunkTKDISKCFcjs.getLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
323
+ }
324
+ async listLlmCredentials(userId) {
325
+ const sql = this._getSql();
326
+ return _chunkTKDISKCFcjs.listLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId);
327
+ }
328
+ async deleteLlmCredentials(userId, credIds = null) {
329
+ const sql = this._getSql();
330
+ return _chunkTKDISKCFcjs.deleteLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
331
+ }
260
332
  };
261
333
 
262
334