@rei-standard/amsg-server 2.6.0-next.26 → 2.6.0-next.27

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.
@@ -216,8 +216,11 @@ export class D1Adapter {
216
216
  * @param {number} [now] - 服务端当前时刻(epoch 毫秒),判定「这行来自未来」用的
217
217
  * 就是它。调用方(lib/client-state-store.js)传下来,测试可以钉住一个假时钟;
218
218
  * 自定义调用方不传时退回本机时钟。
219
- * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
219
+ * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[], cleanupOutcomes?: Array<boolean|null> }>}
220
220
  * `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
221
+ * `cleanupOutcomes[i]` 对应 cleanups[i](传了 cleanups 才有):精确 key 形态
222
+ * 回 `true` = 这个 key 的行已经不在(删掉了,或本来就没有)、`false` = 行还在
223
+ * (库里那行更新,删除被条件写拦下);前缀形态不探测,回 `null`。
221
224
  */
222
225
  upsertClientState(userId: string, entries: Array<{
223
226
  namespace: string;
@@ -233,6 +236,7 @@ export class D1Adapter {
233
236
  upserted: number;
234
237
  skipped: number;
235
238
  outcomes: boolean[];
239
+ cleanupOutcomes?: Array<boolean | null>;
236
240
  }>;
237
241
  /**
238
242
  * All entries of one namespace (values still encrypted).
@@ -147,6 +147,9 @@ export type DbAdapter = {
147
147
  * 写入与删除都对这种行放行,否则那一行会一直卡到真实时间追上去为止。忽略这个参数的自定义
148
148
  * adapter 功能不变,只是没有这道解锁。
149
149
  * `outcomes` 逐条报告 entries[i] 是否真的写入(缺席时调用方按物理行计数兜底)。
150
+ * `cleanupOutcomes`(可选)逐条报告 cleanups[i] 的结局:精确 key 形态回 `true`(这个 key 的行已不在——
151
+ * 删掉了或本来就没有)/ `false`(行还在,库里那行更新、删除被条件写拦下);前缀形态回 `null`。
152
+ * 调用方靠它把被拦下的删除计入 skippedEntries;缺席时删除一律按「已删」计。
150
153
  */
151
154
  upsertClientState?: (userId: string, entries: Array<{
152
155
  namespace: string;
@@ -162,6 +165,7 @@ export type DbAdapter = {
162
165
  upserted: number;
163
166
  skipped: number;
164
167
  outcomes?: boolean[];
168
+ cleanupOutcomes?: Array<boolean | null>;
165
169
  }>;
166
170
  /**
167
171
  * (optional; single-user/D1 only) All entries of one namespace; values still encrypted.
@@ -31,10 +31,28 @@ export const SQLITE_INDEXES: {
31
31
  critical: boolean;
32
32
  }[];
33
33
  export const CLIENT_STATE_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS client_state (\n user_id TEXT NOT NULL,\n namespace TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (user_id, namespace, key)\n )\n";
34
+ export const CLIENT_STATE_INDEXES: {
35
+ name: string;
36
+ sql: string;
37
+ description: string;
38
+ critical: boolean;
39
+ }[];
34
40
  export const PUSH_SUBSCRIPTION_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS push_subscriptions (\n user_id TEXT PRIMARY KEY,\n subscription TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n )\n";
35
41
  export const LLM_CREDENTIALS_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS llm_credentials (\n user_id TEXT NOT NULL,\n cred_id TEXT NOT NULL,\n encrypted_value TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, cred_id)\n )\n";
36
42
  export const MESSAGE_OUTBOX_TABLE_SQL: "\n CREATE TABLE IF NOT EXISTS message_outbox (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n user_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n task_uuid TEXT,\n session_id TEXT,\n message_index INTEGER,\n total_messages INTEGER,\n payload TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n delivered_at INTEGER,\n acked_at INTEGER,\n UNIQUE (user_id, message_id)\n )\n";
37
- export const MESSAGE_OUTBOX_INDEX_SQL: "\n CREATE INDEX IF NOT EXISTS idx_outbox_unacked\n ON message_outbox (user_id, id)\n WHERE acked_at IS NULL\n";
43
+ export const MESSAGE_OUTBOX_INDEXES: {
44
+ name: string;
45
+ sql: string;
46
+ description: string;
47
+ critical: boolean;
48
+ }[];
49
+ /** 三张表的索引合在一起,initSchema 按这份顺序逐条建。 */
50
+ export const SQLITE_ALL_INDEXES: {
51
+ name: string;
52
+ sql: string;
53
+ description: string;
54
+ critical: boolean;
55
+ }[];
38
56
  /**
39
57
  * 这一版代码跑起来需要的表 / 列 / 索引。
40
58
  *
@@ -1065,7 +1065,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1065
1065
  const cleanups = [];
1066
1066
  const rootRowIndexes = [];
1067
1067
  const rootRowEntries = [];
1068
- let deleted = 0;
1068
+ const deletions = [];
1069
1069
  for (const entry of entries) {
1070
1070
  const rawGuardAt = Number.isInteger(entry.version) && entry.version > 0 ? entry.version : entry.updatedAt;
1071
1071
  const guardAt = Math.min(rawGuardAt, at);
@@ -1075,12 +1075,12 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1075
1075
  updatedAt: guardAt
1076
1076
  });
1077
1077
  if (entry.value === null) {
1078
+ deletions.push({ cleanupIndex: cleanups.length, entry });
1078
1079
  cleanups.push({
1079
1080
  namespace: entry.namespace,
1080
1081
  key: entry.key,
1081
1082
  updatedAt: guardAt
1082
1083
  });
1083
- deleted++;
1084
1084
  continue;
1085
1085
  }
1086
1086
  rootRowIndexes.push(physicalRows.length);
@@ -1117,6 +1117,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1117
1117
  const result = await db.upsertClientState(userId, physicalRows, cleanups, at);
1118
1118
  let upserted = 0;
1119
1119
  let skipped = 0;
1120
+ let deleted = 0;
1120
1121
  const skippedEntries = [];
1121
1122
  if (Array.isArray(result.outcomes) && result.outcomes.length === physicalRows.length) {
1122
1123
  for (let i = 0; i < rootRowIndexes.length; i++) {
@@ -1132,6 +1133,15 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1132
1133
  upserted = result.upserted;
1133
1134
  skipped = result.skipped;
1134
1135
  }
1136
+ const cleanupOutcomes = Array.isArray(result.cleanupOutcomes) && result.cleanupOutcomes.length === cleanups.length ? result.cleanupOutcomes : null;
1137
+ for (const { cleanupIndex, entry } of deletions) {
1138
+ if (cleanupOutcomes && cleanupOutcomes[cleanupIndex] === false) {
1139
+ skipped++;
1140
+ skippedEntries.push({ namespace: entry.namespace, key: entry.key });
1141
+ } else {
1142
+ deleted++;
1143
+ }
1144
+ }
1135
1145
  return { upserted, skipped, deleted, skippedEntries };
1136
1146
  }
1137
1147
 
@@ -4128,6 +4138,19 @@ var CLIENT_STATE_TABLE_SQL = `
4128
4138
  PRIMARY KEY (user_id, namespace, key)
4129
4139
  )
4130
4140
  `;
4141
+ var CLIENT_STATE_INDEXES = [
4142
+ {
4143
+ name: "idx_client_state_cleanup",
4144
+ // 服务 cleanupClientState 的
4145
+ // DELETE FROM client_state WHERE namespace = ? AND updated_at < ?
4146
+ // 主键 (user_id, namespace, key) 最左列是 user_id,按 namespace 起头的条件
4147
+ // 吃不到它。
4148
+ sql: `CREATE INDEX IF NOT EXISTS idx_client_state_cleanup
4149
+ ON client_state (namespace, updated_at)`,
4150
+ description: "TTL cleanup index (cleanupClientState by namespace + updated_at)",
4151
+ critical: false
4152
+ }
4153
+ ];
4131
4154
  var PUSH_SUBSCRIPTION_TABLE_SQL = `
4132
4155
  CREATE TABLE IF NOT EXISTS push_subscriptions (
4133
4156
  user_id TEXT PRIMARY KEY,
@@ -4161,11 +4184,57 @@ var MESSAGE_OUTBOX_TABLE_SQL = `
4161
4184
  UNIQUE (user_id, message_id)
4162
4185
  )
4163
4186
  `;
4164
- var MESSAGE_OUTBOX_INDEX_SQL = `
4165
- CREATE INDEX IF NOT EXISTS idx_outbox_unacked
4166
- ON message_outbox (user_id, id)
4167
- WHERE acked_at IS NULL
4168
- `;
4187
+ var MESSAGE_OUTBOX_INDEXES = [
4188
+ {
4189
+ name: "idx_outbox_unacked",
4190
+ // 服务 listUnackedOutbox 的
4191
+ // SELECT … WHERE user_id = ? AND acked_at IS NULL AND id > ? ORDER BY id
4192
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_unacked
4193
+ ON message_outbox (user_id, id)
4194
+ WHERE acked_at IS NULL`,
4195
+ description: "Unacked outbox paging index (GET /outbox)",
4196
+ critical: false
4197
+ },
4198
+ {
4199
+ name: "idx_outbox_created",
4200
+ // 服务 cleanupOutbox 的
4201
+ // DELETE FROM message_outbox WHERE created_at < ?
4202
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_created
4203
+ ON message_outbox (created_at)`,
4204
+ description: "Outbox retention cleanup index (cleanupOutbox by created_at)",
4205
+ critical: false
4206
+ },
4207
+ {
4208
+ name: "idx_outbox_acked",
4209
+ // 服务 cleanupOutbox 的
4210
+ // DELETE FROM message_outbox WHERE acked_at IS NOT NULL AND acked_at < ?
4211
+ // 部分索引只收已 ack 的行:未 ack 的那部分本来就不是这条语句的目标,
4212
+ // idx_outbox_unacked 的 WHERE 条件与它正好互补。
4213
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_acked
4214
+ ON message_outbox (acked_at)
4215
+ WHERE acked_at IS NOT NULL`,
4216
+ description: "Acked outbox cleanup index (cleanupOutbox by acked_at)",
4217
+ critical: false
4218
+ },
4219
+ {
4220
+ name: "idx_outbox_task_undelivered",
4221
+ // 服务 discardUndeliveredOutboxForTask 的
4222
+ // DELETE FROM message_outbox
4223
+ // WHERE user_id = ? AND task_uuid = ? AND delivered_at IS NULL AND acked_at IS NULL
4224
+ // 没有它这条只能靶着 user_id 走 (user_id, message_id) 或 idx_outbox_unacked,
4225
+ // 单用户部署下 user_id 对每一行都成立,等于把整个未 ack 积压扫一遍。
4226
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_task_undelivered
4227
+ ON message_outbox (user_id, task_uuid)
4228
+ WHERE delivered_at IS NULL AND acked_at IS NULL`,
4229
+ description: "Undelivered-by-task discard index (cancel / supersede)",
4230
+ critical: false
4231
+ }
4232
+ ];
4233
+ var SQLITE_ALL_INDEXES = [
4234
+ ...SQLITE_INDEXES,
4235
+ ...CLIENT_STATE_INDEXES,
4236
+ ...MESSAGE_OUTBOX_INDEXES
4237
+ ];
4169
4238
  function parseTableName(sql) {
4170
4239
  const match = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_][A-Za-z0-9_]*)/i.exec(sql);
4171
4240
  return match ? match[1] : "";
@@ -4200,7 +4269,7 @@ var SQLITE_REQUIRED_SCHEMA = Object.freeze({
4200
4269
  describeTable(LLM_CREDENTIALS_TABLE_SQL),
4201
4270
  describeTable(MESSAGE_OUTBOX_TABLE_SQL)
4202
4271
  ])),
4203
- indexes: Object.freeze(SQLITE_INDEXES.filter((index) => index.critical).map((index) => index.name))
4272
+ indexes: Object.freeze(SQLITE_ALL_INDEXES.filter((index) => index.critical).map((index) => index.name))
4204
4273
  });
4205
4274
 
4206
4275
  // src/server/adapters/d1.js
@@ -4305,7 +4374,6 @@ var D1Adapter = class {
4305
4374
  await this._db.prepare(PUSH_SUBSCRIPTION_TABLE_SQL).run();
4306
4375
  await this._db.prepare(LLM_CREDENTIALS_TABLE_SQL).run();
4307
4376
  await this._db.prepare(MESSAGE_OUTBOX_TABLE_SQL).run();
4308
- await this._db.prepare(MESSAGE_OUTBOX_INDEX_SQL).run();
4309
4377
  for (const migration of SQLITE_MIGRATIONS) {
4310
4378
  try {
4311
4379
  await this._db.prepare(migration.sql).run();
@@ -4314,7 +4382,7 @@ var D1Adapter = class {
4314
4382
  }
4315
4383
  }
4316
4384
  const indexResults = [];
4317
- for (const index of SQLITE_INDEXES) {
4385
+ for (const index of SQLITE_ALL_INDEXES) {
4318
4386
  try {
4319
4387
  await this._db.prepare(index.sql).run();
4320
4388
  indexResults.push({ name: index.name, status: "success", description: index.description, critical: !!index.critical });
@@ -4683,8 +4751,11 @@ var D1Adapter = class {
4683
4751
  * @param {number} [now] - 服务端当前时刻(epoch 毫秒),判定「这行来自未来」用的
4684
4752
  * 就是它。调用方(lib/client-state-store.js)传下来,测试可以钉住一个假时钟;
4685
4753
  * 自定义调用方不传时退回本机时钟。
4686
- * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
4754
+ * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[], cleanupOutcomes?: Array<boolean|null> }>}
4687
4755
  * `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
4756
+ * `cleanupOutcomes[i]` 对应 cleanups[i](传了 cleanups 才有):精确 key 形态
4757
+ * 回 `true` = 这个 key 的行已经不在(删掉了,或本来就没有)、`false` = 行还在
4758
+ * (库里那行更新,删除被条件写拦下);前缀形态不探测,回 `null`。
4688
4759
  */
4689
4760
  async upsertClientState(userId, entries, cleanups = [], now = Date.now()) {
4690
4761
  const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
@@ -4700,29 +4771,53 @@ var D1Adapter = class {
4700
4771
  const CLEANUP_KEY_SQL = `DELETE FROM client_state
4701
4772
  WHERE user_id = ? AND namespace = ? AND key = ?
4702
4773
  AND (updated_at <= ? OR updated_at > ?)`;
4703
- const buildStatements = () => [
4704
- ...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt, now) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt, now)),
4705
- ...entries.map(
4706
- (entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt, now)
4707
- )
4708
- ];
4774
+ const PROBE_KEY_SQL = `SELECT COUNT(*) AS n FROM client_state
4775
+ WHERE user_id = ? AND namespace = ? AND key = ?`;
4776
+ const statements = [];
4777
+ const probeIndexes = [];
4778
+ for (const c of cleanups) {
4779
+ if (typeof c.key === "string") {
4780
+ statements.push(this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt, now));
4781
+ probeIndexes.push(statements.length);
4782
+ statements.push(this._db.prepare(PROBE_KEY_SQL).bind(userId, c.namespace, c.key));
4783
+ } else {
4784
+ statements.push(
4785
+ this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt, now)
4786
+ );
4787
+ probeIndexes.push(null);
4788
+ }
4789
+ }
4790
+ const upsertStart = statements.length;
4791
+ for (const entry of entries) {
4792
+ statements.push(
4793
+ this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt, now)
4794
+ );
4795
+ }
4709
4796
  let results;
4710
4797
  if (typeof this._db.batch === "function") {
4711
- results = await this._db.batch(buildStatements());
4798
+ results = await this._db.batch(statements);
4712
4799
  } else {
4713
4800
  results = [];
4714
- for (const stmt of buildStatements()) {
4801
+ for (const stmt of statements) {
4715
4802
  results.push(await stmt.run());
4716
4803
  }
4717
4804
  }
4718
- const outcomes = results.slice(cleanups.length).map((res) => res.meta.changes > 0);
4805
+ const outcomes = results.slice(upsertStart).map((res) => res.meta.changes > 0);
4719
4806
  let upserted = 0;
4720
4807
  let skipped = 0;
4721
4808
  for (const wrote of outcomes) {
4722
4809
  if (wrote) upserted++;
4723
4810
  else skipped++;
4724
4811
  }
4725
- return { upserted, skipped, outcomes };
4812
+ const result = { upserted, skipped, outcomes };
4813
+ if (cleanups.length > 0) {
4814
+ result.cleanupOutcomes = probeIndexes.map((probeAt) => {
4815
+ if (probeAt === null) return null;
4816
+ const row = results[probeAt] && Array.isArray(results[probeAt].results) ? results[probeAt].results[0] : null;
4817
+ return Number(row?.n ?? 0) === 0;
4818
+ });
4819
+ }
4820
+ return result;
4726
4821
  }
4727
4822
  /**
4728
4823
  * All entries of one namespace (values still encrypted).
@@ -5211,12 +5306,14 @@ function validateEntry(entry, index, maxValueBytes) {
5211
5306
  if (INTERNAL_STATE_CHAR_RE.test(entry.key)) {
5212
5307
  return rejectEntry(entry, index, "INVALID_STATE_KEY", `entries[${index}].key \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
5213
5308
  }
5214
- if (typeof entry.value !== "string") {
5215
- return rejectEntry(entry, index, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09`);
5309
+ if (entry.value !== null && typeof entry.value !== "string") {
5310
+ return rejectEntry(entry, index, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09\uFF0C\u6216 null \u8868\u793A\u5220\u9664`);
5216
5311
  }
5217
- const bytes = stateValueBytes(entry.value);
5218
- if (bytes > maxValueBytes) {
5219
- return rejectEntry(entry, index, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u603B\u4E0A\u9650`, { bytes, maxBytes: maxValueBytes });
5312
+ if (typeof entry.value === "string") {
5313
+ const bytes = stateValueBytes(entry.value);
5314
+ if (bytes > maxValueBytes) {
5315
+ return rejectEntry(entry, index, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u603B\u4E0A\u9650`, { bytes, maxBytes: maxValueBytes });
5316
+ }
5220
5317
  }
5221
5318
  if (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0) {
5222
5319
  return rejectEntry(entry, index, "INVALID_STATE_UPDATED_AT", `entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
@@ -5277,8 +5374,9 @@ function createClientStateHandler(ctx) {
5277
5374
  if (typeof db.upsertClientState !== "function") {
5278
5375
  return err3(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
5279
5376
  }
5280
- const { upserted, skipped, skippedEntries } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
5377
+ const { upserted, skipped, deleted, skippedEntries } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
5281
5378
  const data = { upserted, skipped };
5379
+ if (deleted > 0) data.deleted = deleted;
5282
5380
  if (skippedEntries.length > 0) data.skippedEntries = skippedEntries;
5283
5381
  if (rejected.length > 0) data.rejected = rejected;
5284
5382
  return { status: 200, body: { success: true, data } };
@@ -5325,7 +5423,7 @@ function createClientStateHandler(ctx) {
5325
5423
  }
5326
5424
 
5327
5425
  // src/server/lib/version.js
5328
- var SERVER_VERSION = true ? "2.6.0-next.26" : "0.0.0-dev";
5426
+ var SERVER_VERSION = true ? "2.6.0-next.27" : "0.0.0-dev";
5329
5427
 
5330
5428
  // src/server/handlers/capabilities.js
5331
5429
  var SERVER_FEATURES = Object.freeze([
@@ -5411,7 +5509,10 @@ var SERVER_FEATURES = Object.freeze([
5411
5509
  // client_state 按命名空间过期清理(config 的 clientStateTtl,cron 每跳顺手做)。
5412
5510
  "client-state-ttl",
5413
5511
  // hook ctx 带 emitResult(payload):往客户端补一条自定义结果(落收件箱 + 推送)。
5414
- "emit-result"
5512
+ "emit-result",
5513
+ // PUT /client-state 的 entry 认 value: null(删掉这个 key,连切片行一起;同一套
5514
+ // last-write-wins,被拦下的进 skippedEntries;删掉的条数在 data.deleted)。
5515
+ "client-state-delete"
5415
5516
  ]);
5416
5517
  function createCapabilitiesHandler(ctx) {
5417
5518
  async function GET(url, headers) {
@@ -1065,7 +1065,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1065
1065
  const cleanups = [];
1066
1066
  const rootRowIndexes = [];
1067
1067
  const rootRowEntries = [];
1068
- let deleted = 0;
1068
+ const deletions = [];
1069
1069
  for (const entry of entries) {
1070
1070
  const rawGuardAt = Number.isInteger(entry.version) && entry.version > 0 ? entry.version : entry.updatedAt;
1071
1071
  const guardAt = Math.min(rawGuardAt, at);
@@ -1075,12 +1075,12 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1075
1075
  updatedAt: guardAt
1076
1076
  });
1077
1077
  if (entry.value === null) {
1078
+ deletions.push({ cleanupIndex: cleanups.length, entry });
1078
1079
  cleanups.push({
1079
1080
  namespace: entry.namespace,
1080
1081
  key: entry.key,
1081
1082
  updatedAt: guardAt
1082
1083
  });
1083
- deleted++;
1084
1084
  continue;
1085
1085
  }
1086
1086
  rootRowIndexes.push(physicalRows.length);
@@ -1117,6 +1117,7 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1117
1117
  const result = await db.upsertClientState(userId, physicalRows, cleanups, at);
1118
1118
  let upserted = 0;
1119
1119
  let skipped = 0;
1120
+ let deleted = 0;
1120
1121
  const skippedEntries = [];
1121
1122
  if (Array.isArray(result.outcomes) && result.outcomes.length === physicalRows.length) {
1122
1123
  for (let i = 0; i < rootRowIndexes.length; i++) {
@@ -1132,6 +1133,15 @@ async function writeClientStateEntries({ db, userId, userKey, entries, now }) {
1132
1133
  upserted = result.upserted;
1133
1134
  skipped = result.skipped;
1134
1135
  }
1136
+ const cleanupOutcomes = Array.isArray(result.cleanupOutcomes) && result.cleanupOutcomes.length === cleanups.length ? result.cleanupOutcomes : null;
1137
+ for (const { cleanupIndex, entry } of deletions) {
1138
+ if (cleanupOutcomes && cleanupOutcomes[cleanupIndex] === false) {
1139
+ skipped++;
1140
+ skippedEntries.push({ namespace: entry.namespace, key: entry.key });
1141
+ } else {
1142
+ deleted++;
1143
+ }
1144
+ }
1135
1145
  return { upserted, skipped, deleted, skippedEntries };
1136
1146
  }
1137
1147
 
@@ -4128,6 +4138,19 @@ var CLIENT_STATE_TABLE_SQL = `
4128
4138
  PRIMARY KEY (user_id, namespace, key)
4129
4139
  )
4130
4140
  `;
4141
+ var CLIENT_STATE_INDEXES = [
4142
+ {
4143
+ name: "idx_client_state_cleanup",
4144
+ // 服务 cleanupClientState 的
4145
+ // DELETE FROM client_state WHERE namespace = ? AND updated_at < ?
4146
+ // 主键 (user_id, namespace, key) 最左列是 user_id,按 namespace 起头的条件
4147
+ // 吃不到它。
4148
+ sql: `CREATE INDEX IF NOT EXISTS idx_client_state_cleanup
4149
+ ON client_state (namespace, updated_at)`,
4150
+ description: "TTL cleanup index (cleanupClientState by namespace + updated_at)",
4151
+ critical: false
4152
+ }
4153
+ ];
4131
4154
  var PUSH_SUBSCRIPTION_TABLE_SQL = `
4132
4155
  CREATE TABLE IF NOT EXISTS push_subscriptions (
4133
4156
  user_id TEXT PRIMARY KEY,
@@ -4161,11 +4184,57 @@ var MESSAGE_OUTBOX_TABLE_SQL = `
4161
4184
  UNIQUE (user_id, message_id)
4162
4185
  )
4163
4186
  `;
4164
- var MESSAGE_OUTBOX_INDEX_SQL = `
4165
- CREATE INDEX IF NOT EXISTS idx_outbox_unacked
4166
- ON message_outbox (user_id, id)
4167
- WHERE acked_at IS NULL
4168
- `;
4187
+ var MESSAGE_OUTBOX_INDEXES = [
4188
+ {
4189
+ name: "idx_outbox_unacked",
4190
+ // 服务 listUnackedOutbox 的
4191
+ // SELECT … WHERE user_id = ? AND acked_at IS NULL AND id > ? ORDER BY id
4192
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_unacked
4193
+ ON message_outbox (user_id, id)
4194
+ WHERE acked_at IS NULL`,
4195
+ description: "Unacked outbox paging index (GET /outbox)",
4196
+ critical: false
4197
+ },
4198
+ {
4199
+ name: "idx_outbox_created",
4200
+ // 服务 cleanupOutbox 的
4201
+ // DELETE FROM message_outbox WHERE created_at < ?
4202
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_created
4203
+ ON message_outbox (created_at)`,
4204
+ description: "Outbox retention cleanup index (cleanupOutbox by created_at)",
4205
+ critical: false
4206
+ },
4207
+ {
4208
+ name: "idx_outbox_acked",
4209
+ // 服务 cleanupOutbox 的
4210
+ // DELETE FROM message_outbox WHERE acked_at IS NOT NULL AND acked_at < ?
4211
+ // 部分索引只收已 ack 的行:未 ack 的那部分本来就不是这条语句的目标,
4212
+ // idx_outbox_unacked 的 WHERE 条件与它正好互补。
4213
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_acked
4214
+ ON message_outbox (acked_at)
4215
+ WHERE acked_at IS NOT NULL`,
4216
+ description: "Acked outbox cleanup index (cleanupOutbox by acked_at)",
4217
+ critical: false
4218
+ },
4219
+ {
4220
+ name: "idx_outbox_task_undelivered",
4221
+ // 服务 discardUndeliveredOutboxForTask 的
4222
+ // DELETE FROM message_outbox
4223
+ // WHERE user_id = ? AND task_uuid = ? AND delivered_at IS NULL AND acked_at IS NULL
4224
+ // 没有它这条只能靶着 user_id 走 (user_id, message_id) 或 idx_outbox_unacked,
4225
+ // 单用户部署下 user_id 对每一行都成立,等于把整个未 ack 积压扫一遍。
4226
+ sql: `CREATE INDEX IF NOT EXISTS idx_outbox_task_undelivered
4227
+ ON message_outbox (user_id, task_uuid)
4228
+ WHERE delivered_at IS NULL AND acked_at IS NULL`,
4229
+ description: "Undelivered-by-task discard index (cancel / supersede)",
4230
+ critical: false
4231
+ }
4232
+ ];
4233
+ var SQLITE_ALL_INDEXES = [
4234
+ ...SQLITE_INDEXES,
4235
+ ...CLIENT_STATE_INDEXES,
4236
+ ...MESSAGE_OUTBOX_INDEXES
4237
+ ];
4169
4238
  function parseTableName(sql) {
4170
4239
  const match = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_][A-Za-z0-9_]*)/i.exec(sql);
4171
4240
  return match ? match[1] : "";
@@ -4200,7 +4269,7 @@ var SQLITE_REQUIRED_SCHEMA = Object.freeze({
4200
4269
  describeTable(LLM_CREDENTIALS_TABLE_SQL),
4201
4270
  describeTable(MESSAGE_OUTBOX_TABLE_SQL)
4202
4271
  ])),
4203
- indexes: Object.freeze(SQLITE_INDEXES.filter((index) => index.critical).map((index) => index.name))
4272
+ indexes: Object.freeze(SQLITE_ALL_INDEXES.filter((index) => index.critical).map((index) => index.name))
4204
4273
  });
4205
4274
 
4206
4275
  // src/server/adapters/d1.js
@@ -4305,7 +4374,6 @@ var D1Adapter = class {
4305
4374
  await this._db.prepare(PUSH_SUBSCRIPTION_TABLE_SQL).run();
4306
4375
  await this._db.prepare(LLM_CREDENTIALS_TABLE_SQL).run();
4307
4376
  await this._db.prepare(MESSAGE_OUTBOX_TABLE_SQL).run();
4308
- await this._db.prepare(MESSAGE_OUTBOX_INDEX_SQL).run();
4309
4377
  for (const migration of SQLITE_MIGRATIONS) {
4310
4378
  try {
4311
4379
  await this._db.prepare(migration.sql).run();
@@ -4314,7 +4382,7 @@ var D1Adapter = class {
4314
4382
  }
4315
4383
  }
4316
4384
  const indexResults = [];
4317
- for (const index of SQLITE_INDEXES) {
4385
+ for (const index of SQLITE_ALL_INDEXES) {
4318
4386
  try {
4319
4387
  await this._db.prepare(index.sql).run();
4320
4388
  indexResults.push({ name: index.name, status: "success", description: index.description, critical: !!index.critical });
@@ -4683,8 +4751,11 @@ var D1Adapter = class {
4683
4751
  * @param {number} [now] - 服务端当前时刻(epoch 毫秒),判定「这行来自未来」用的
4684
4752
  * 就是它。调用方(lib/client-state-store.js)传下来,测试可以钉住一个假时钟;
4685
4753
  * 自定义调用方不传时退回本机时钟。
4686
- * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[] }>}
4754
+ * @returns {Promise<{ upserted: number, skipped: number, outcomes: boolean[], cleanupOutcomes?: Array<boolean|null> }>}
4687
4755
  * `outcomes[i]` 对应 entries[i] 是否真的写入(changes > 0)。
4756
+ * `cleanupOutcomes[i]` 对应 cleanups[i](传了 cleanups 才有):精确 key 形态
4757
+ * 回 `true` = 这个 key 的行已经不在(删掉了,或本来就没有)、`false` = 行还在
4758
+ * (库里那行更新,删除被条件写拦下);前缀形态不探测,回 `null`。
4688
4759
  */
4689
4760
  async upsertClientState(userId, entries, cleanups = [], now = Date.now()) {
4690
4761
  const UPSERT_SQL = `INSERT INTO client_state (user_id, namespace, key, value, updated_at)
@@ -4700,29 +4771,53 @@ var D1Adapter = class {
4700
4771
  const CLEANUP_KEY_SQL = `DELETE FROM client_state
4701
4772
  WHERE user_id = ? AND namespace = ? AND key = ?
4702
4773
  AND (updated_at <= ? OR updated_at > ?)`;
4703
- const buildStatements = () => [
4704
- ...cleanups.map((c) => typeof c.key === "string" ? this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt, now) : this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt, now)),
4705
- ...entries.map(
4706
- (entry) => this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt, now)
4707
- )
4708
- ];
4774
+ const PROBE_KEY_SQL = `SELECT COUNT(*) AS n FROM client_state
4775
+ WHERE user_id = ? AND namespace = ? AND key = ?`;
4776
+ const statements = [];
4777
+ const probeIndexes = [];
4778
+ for (const c of cleanups) {
4779
+ if (typeof c.key === "string") {
4780
+ statements.push(this._db.prepare(CLEANUP_KEY_SQL).bind(userId, c.namespace, c.key, c.updatedAt, now));
4781
+ probeIndexes.push(statements.length);
4782
+ statements.push(this._db.prepare(PROBE_KEY_SQL).bind(userId, c.namespace, c.key));
4783
+ } else {
4784
+ statements.push(
4785
+ this._db.prepare(CLEANUP_PREFIX_SQL).bind(userId, c.namespace, c.keyPrefix, prefixRangeEnd(c.keyPrefix), c.updatedAt, now)
4786
+ );
4787
+ probeIndexes.push(null);
4788
+ }
4789
+ }
4790
+ const upsertStart = statements.length;
4791
+ for (const entry of entries) {
4792
+ statements.push(
4793
+ this._db.prepare(UPSERT_SQL).bind(userId, entry.namespace, entry.key, entry.value, entry.updatedAt, now)
4794
+ );
4795
+ }
4709
4796
  let results;
4710
4797
  if (typeof this._db.batch === "function") {
4711
- results = await this._db.batch(buildStatements());
4798
+ results = await this._db.batch(statements);
4712
4799
  } else {
4713
4800
  results = [];
4714
- for (const stmt of buildStatements()) {
4801
+ for (const stmt of statements) {
4715
4802
  results.push(await stmt.run());
4716
4803
  }
4717
4804
  }
4718
- const outcomes = results.slice(cleanups.length).map((res) => res.meta.changes > 0);
4805
+ const outcomes = results.slice(upsertStart).map((res) => res.meta.changes > 0);
4719
4806
  let upserted = 0;
4720
4807
  let skipped = 0;
4721
4808
  for (const wrote of outcomes) {
4722
4809
  if (wrote) upserted++;
4723
4810
  else skipped++;
4724
4811
  }
4725
- return { upserted, skipped, outcomes };
4812
+ const result = { upserted, skipped, outcomes };
4813
+ if (cleanups.length > 0) {
4814
+ result.cleanupOutcomes = probeIndexes.map((probeAt) => {
4815
+ if (probeAt === null) return null;
4816
+ const row = results[probeAt] && Array.isArray(results[probeAt].results) ? results[probeAt].results[0] : null;
4817
+ return Number(_nullishCoalesce(_optionalChain([row, 'optionalAccess', _3 => _3.n]), () => ( 0))) === 0;
4818
+ });
4819
+ }
4820
+ return result;
4726
4821
  }
4727
4822
  /**
4728
4823
  * All entries of one namespace (values still encrypted).
@@ -5211,12 +5306,14 @@ function validateEntry(entry, index, maxValueBytes) {
5211
5306
  if (INTERNAL_STATE_CHAR_RE.test(entry.key)) {
5212
5307
  return rejectEntry(entry, index, "INVALID_STATE_KEY", `entries[${index}].key \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26\uFF08\\u0000-\\u001f \u4E3A\u5E93\u5185\u90E8\u4FDD\u7559\uFF09`);
5213
5308
  }
5214
- if (typeof entry.value !== "string") {
5215
- return rejectEntry(entry, index, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09`);
5309
+ if (entry.value !== null && typeof entry.value !== "string") {
5310
+ return rejectEntry(entry, index, "INVALID_STATE_VALUE", `entries[${index}].value \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\uFF08\u5BBF\u4E3B\u81EA\u884C\u5E8F\u5217\u5316\uFF09\uFF0C\u6216 null \u8868\u793A\u5220\u9664`);
5216
5311
  }
5217
- const bytes = stateValueBytes(entry.value);
5218
- if (bytes > maxValueBytes) {
5219
- return rejectEntry(entry, index, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u603B\u4E0A\u9650`, { bytes, maxBytes: maxValueBytes });
5312
+ if (typeof entry.value === "string") {
5313
+ const bytes = stateValueBytes(entry.value);
5314
+ if (bytes > maxValueBytes) {
5315
+ return rejectEntry(entry, index, "STATE_VALUE_TOO_LARGE", `entries[${index}].value \u8D85\u8FC7\u5355\u6761\u603B\u4E0A\u9650`, { bytes, maxBytes: maxValueBytes });
5316
+ }
5220
5317
  }
5221
5318
  if (!Number.isInteger(entry.updatedAt) || entry.updatedAt <= 0) {
5222
5319
  return rejectEntry(entry, index, "INVALID_STATE_UPDATED_AT", `entries[${index}].updatedAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08epoch \u6BEB\u79D2\uFF09`);
@@ -5277,8 +5374,9 @@ function createClientStateHandler(ctx) {
5277
5374
  if (typeof db.upsertClientState !== "function") {
5278
5375
  return err3(501, "CLIENT_STATE_NOT_SUPPORTED", "\u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301 client_state");
5279
5376
  }
5280
- const { upserted, skipped, skippedEntries } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
5377
+ const { upserted, skipped, deleted, skippedEntries } = await writeClientStateEntries({ db, userId, userKey, entries: accepted });
5281
5378
  const data = { upserted, skipped };
5379
+ if (deleted > 0) data.deleted = deleted;
5282
5380
  if (skippedEntries.length > 0) data.skippedEntries = skippedEntries;
5283
5381
  if (rejected.length > 0) data.rejected = rejected;
5284
5382
  return { status: 200, body: { success: true, data } };
@@ -5325,7 +5423,7 @@ function createClientStateHandler(ctx) {
5325
5423
  }
5326
5424
 
5327
5425
  // src/server/lib/version.js
5328
- var SERVER_VERSION = true ? "2.6.0-next.26" : "0.0.0-dev";
5426
+ var SERVER_VERSION = true ? "2.6.0-next.27" : "0.0.0-dev";
5329
5427
 
5330
5428
  // src/server/handlers/capabilities.js
5331
5429
  var SERVER_FEATURES = Object.freeze([
@@ -5411,7 +5509,10 @@ var SERVER_FEATURES = Object.freeze([
5411
5509
  // client_state 按命名空间过期清理(config 的 clientStateTtl,cron 每跳顺手做)。
5412
5510
  "client-state-ttl",
5413
5511
  // hook ctx 带 emitResult(payload):往客户端补一条自定义结果(落收件箱 + 推送)。
5414
- "emit-result"
5512
+ "emit-result",
5513
+ // PUT /client-state 的 entry 认 value: null(删掉这个 key,连切片行一起;同一套
5514
+ // last-write-wins,被拦下的进 skippedEntries;删掉的条数在 data.deleted)。
5515
+ "client-state-delete"
5415
5516
  ]);
5416
5517
  function createCapabilitiesHandler(ctx) {
5417
5518
  async function GET(url, headers) {
@@ -26,7 +26,7 @@
26
26
 
27
27
 
28
28
 
29
- var _chunkGXYVOEF3cjs = require('./chunk-GXYVOEF3.cjs');
29
+ var _chunkWCXN5AWJcjs = require('./chunk-WCXN5AWJ.cjs');
30
30
  require('./chunk-7NMQFTDJ.cjs');
31
31
 
32
32
 
@@ -56,4 +56,4 @@ require('./chunk-7NMQFTDJ.cjs');
56
56
 
57
57
 
58
58
 
59
- exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkGXYVOEF3cjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkGXYVOEF3cjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunkGXYVOEF3cjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkGXYVOEF3cjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkGXYVOEF3cjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkGXYVOEF3cjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkGXYVOEF3cjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkGXYVOEF3cjs.advanceOccurrence; exports.createD1Adapter = _chunkGXYVOEF3cjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkGXYVOEF3cjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkGXYVOEF3cjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkGXYVOEF3cjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkGXYVOEF3cjs.decryptFromStorage; exports.decryptPayload = _chunkGXYVOEF3cjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkGXYVOEF3cjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkGXYVOEF3cjs.encryptForStorage; exports.ensureSchema = _chunkGXYVOEF3cjs.ensureSchema; exports.getSchemaVersion = _chunkGXYVOEF3cjs.getSchemaVersion; exports.isNonRetryableError = _chunkGXYVOEF3cjs.isNonRetryableError; exports.isValidTimeZoneId = _chunkGXYVOEF3cjs.isValidTimeZoneId; exports.measurePushPayload = _chunkGXYVOEF3cjs.measurePushPayload; exports.nextFutureOccurrence = _chunkGXYVOEF3cjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkGXYVOEF3cjs.planNextOccurrence; exports.readRequestBody = _chunkGXYVOEF3cjs.readRequestBody; exports.runScheduledTick = _chunkGXYVOEF3cjs.runScheduledTick; exports.runTask = _chunkGXYVOEF3cjs.runTask; exports.summarizeErrorCause = _chunkGXYVOEF3cjs.summarizeErrorCause;
59
+ exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkWCXN5AWJcjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkWCXN5AWJcjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunkWCXN5AWJcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkWCXN5AWJcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkWCXN5AWJcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkWCXN5AWJcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkWCXN5AWJcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkWCXN5AWJcjs.advanceOccurrence; exports.createD1Adapter = _chunkWCXN5AWJcjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkWCXN5AWJcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkWCXN5AWJcjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkWCXN5AWJcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkWCXN5AWJcjs.decryptFromStorage; exports.decryptPayload = _chunkWCXN5AWJcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkWCXN5AWJcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkWCXN5AWJcjs.encryptForStorage; exports.ensureSchema = _chunkWCXN5AWJcjs.ensureSchema; exports.getSchemaVersion = _chunkWCXN5AWJcjs.getSchemaVersion; exports.isNonRetryableError = _chunkWCXN5AWJcjs.isNonRetryableError; exports.isValidTimeZoneId = _chunkWCXN5AWJcjs.isValidTimeZoneId; exports.measurePushPayload = _chunkWCXN5AWJcjs.measurePushPayload; exports.nextFutureOccurrence = _chunkWCXN5AWJcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkWCXN5AWJcjs.planNextOccurrence; exports.readRequestBody = _chunkWCXN5AWJcjs.readRequestBody; exports.runScheduledTick = _chunkWCXN5AWJcjs.runScheduledTick; exports.runTask = _chunkWCXN5AWJcjs.runTask; exports.summarizeErrorCause = _chunkWCXN5AWJcjs.summarizeErrorCause;
@@ -26,7 +26,7 @@ import {
26
26
  runScheduledTick,
27
27
  runTask,
28
28
  summarizeErrorCause
29
- } from "./chunk-4YH3TS4W.mjs";
29
+ } from "./chunk-FPVXATA4.mjs";
30
30
  import "./chunk-GN44PST5.mjs";
31
31
  export {
32
32
  DEFAULT_MAX_REQUEST_BODY_BYTES,
package/dist/index.cjs CHANGED
@@ -71,7 +71,7 @@
71
71
 
72
72
 
73
73
 
74
- var _chunkGXYVOEF3cjs = require('./chunk-GXYVOEF3.cjs');
74
+ var _chunkWCXN5AWJcjs = require('./chunk-WCXN5AWJ.cjs');
75
75
  require('./chunk-7NMQFTDJ.cjs');
76
76
 
77
77
  // src/server/adapters/factory.js
@@ -105,7 +105,7 @@ async function createAdapter(config) {
105
105
  // src/server/handlers/init-tenant.js
106
106
  function createInitTenantHandler(ctx) {
107
107
  async function POST(headers, body) {
108
- const initSecret = _chunkGXYVOEF3cjs.getHeader.call(void 0, headers, "x-init-secret");
108
+ const initSecret = _chunkWCXN5AWJcjs.getHeader.call(void 0, headers, "x-init-secret");
109
109
  if (ctx.tenant.initSecret && initSecret !== ctx.tenant.initSecret) {
110
110
  return {
111
111
  status: 401,
@@ -118,7 +118,7 @@ function createInitTenantHandler(ctx) {
118
118
  }
119
119
  };
120
120
  }
121
- const parsed = _chunkGXYVOEF3cjs.parseJsonBody.call(void 0, body);
121
+ const parsed = _chunkWCXN5AWJcjs.parseJsonBody.call(void 0, body);
122
122
  if (!parsed.ok) {
123
123
  return {
124
124
  status: 400,
@@ -132,7 +132,7 @@ function createInitTenantHandler(ctx) {
132
132
  const tenantId = payload.tenantId || void 0;
133
133
  const driver = payload.driver;
134
134
  const databaseUrl = payload.databaseUrl || payload.connectionString;
135
- if (tenantId && !_chunkGXYVOEF3cjs.isValidUUIDv4.call(void 0, tenantId)) {
135
+ if (tenantId && !_chunkWCXN5AWJcjs.isValidUUIDv4.call(void 0, tenantId)) {
136
136
  return {
137
137
  status: 400,
138
138
  body: {
@@ -252,7 +252,7 @@ function createSendNotificationsHandler(ctx) {
252
252
  }
253
253
  };
254
254
  }
255
- const data = await _chunkGXYVOEF3cjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
255
+ const data = await _chunkWCXN5AWJcjs.runScheduledTick.call(void 0, { ...ctx, db, masterKey });
256
256
  return { status: 200, body: { success: true, data } };
257
257
  }
258
258
  return { POST };
@@ -274,16 +274,16 @@ function encryptConfig(config, kekBuffer) {
274
274
  const plaintext = Buffer.from(JSON.stringify(config), "utf8");
275
275
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
276
276
  const authTag = cipher.getAuthTag();
277
- return `v1.${_chunkGXYVOEF3cjs.bytesToBase64Url.call(void 0, iv)}.${_chunkGXYVOEF3cjs.bytesToBase64Url.call(void 0, authTag)}.${_chunkGXYVOEF3cjs.bytesToBase64Url.call(void 0, ciphertext)}`;
277
+ return `v1.${_chunkWCXN5AWJcjs.bytesToBase64Url.call(void 0, iv)}.${_chunkWCXN5AWJcjs.bytesToBase64Url.call(void 0, authTag)}.${_chunkWCXN5AWJcjs.bytesToBase64Url.call(void 0, ciphertext)}`;
278
278
  }
279
279
  function decryptConfig(encrypted, kekBuffer) {
280
280
  const parts = String(encrypted || "").split(".");
281
281
  if (parts.length !== 4 || parts[0] !== "v1") {
282
282
  throw new Error("INVALID_TENANT_CONFIG");
283
283
  }
284
- const iv = _chunkGXYVOEF3cjs.base64UrlToBytes.call(void 0, parts[1]);
285
- const authTag = _chunkGXYVOEF3cjs.base64UrlToBytes.call(void 0, parts[2]);
286
- const ciphertext = _chunkGXYVOEF3cjs.base64UrlToBytes.call(void 0, parts[3]);
284
+ const iv = _chunkWCXN5AWJcjs.base64UrlToBytes.call(void 0, parts[1]);
285
+ const authTag = _chunkWCXN5AWJcjs.base64UrlToBytes.call(void 0, parts[2]);
286
+ const ciphertext = _chunkWCXN5AWJcjs.base64UrlToBytes.call(void 0, parts[3]);
287
287
  const decipher = _crypto.createDecipheriv.call(void 0, "aes-256-gcm", kekBuffer, iv);
288
288
  decipher.setAuthTag(authTag);
289
289
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
@@ -351,7 +351,7 @@ function createTenantBlobStore(options) {
351
351
  // src/server/tenant/token.js
352
352
 
353
353
  function sign(input, secret) {
354
- return _chunkGXYVOEF3cjs.bytesToBase64Url.call(void 0, _crypto.createHmac.call(void 0, "sha256", secret).update(input).digest());
354
+ return _chunkWCXN5AWJcjs.bytesToBase64Url.call(void 0, _crypto.createHmac.call(void 0, "sha256", secret).update(input).digest());
355
355
  }
356
356
  function nowEpochSeconds() {
357
357
  return Math.floor(Date.now() / 1e3);
@@ -367,8 +367,8 @@ function createTenantToken(params, secret) {
367
367
  v: 1
368
368
  };
369
369
  const header = { alg: "HS256", typ: "JWT" };
370
- const encodedHeader = _chunkGXYVOEF3cjs.bytesToBase64Url.call(void 0, _chunkGXYVOEF3cjs.utf8.call(void 0, JSON.stringify(header)));
371
- const encodedPayload = _chunkGXYVOEF3cjs.bytesToBase64Url.call(void 0, _chunkGXYVOEF3cjs.utf8.call(void 0, JSON.stringify(payload)));
370
+ const encodedHeader = _chunkWCXN5AWJcjs.bytesToBase64Url.call(void 0, _chunkWCXN5AWJcjs.utf8.call(void 0, JSON.stringify(header)));
371
+ const encodedPayload = _chunkWCXN5AWJcjs.bytesToBase64Url.call(void 0, _chunkWCXN5AWJcjs.utf8.call(void 0, JSON.stringify(payload)));
372
372
  const signingInput = `${encodedHeader}.${encodedPayload}`;
373
373
  const signature = sign(signingInput, secret);
374
374
  return `${signingInput}.${signature}`;
@@ -387,17 +387,17 @@ function verifyTenantToken(token, secret, options = {}) {
387
387
  let receivedBytes;
388
388
  let expectedBytes;
389
389
  try {
390
- receivedBytes = _chunkGXYVOEF3cjs.base64UrlToBytes.call(void 0, receivedSignature);
391
- expectedBytes = _chunkGXYVOEF3cjs.base64UrlToBytes.call(void 0, expectedSignature);
390
+ receivedBytes = _chunkWCXN5AWJcjs.base64UrlToBytes.call(void 0, receivedSignature);
391
+ expectedBytes = _chunkWCXN5AWJcjs.base64UrlToBytes.call(void 0, expectedSignature);
392
392
  } catch (e) {
393
393
  throw new Error("INVALID_TENANT_AUTH");
394
394
  }
395
- if (!_chunkGXYVOEF3cjs.timingSafeEqualBytes.call(void 0, receivedBytes, expectedBytes)) {
395
+ if (!_chunkWCXN5AWJcjs.timingSafeEqualBytes.call(void 0, receivedBytes, expectedBytes)) {
396
396
  throw new Error("INVALID_TENANT_AUTH");
397
397
  }
398
398
  let payload;
399
399
  try {
400
- payload = JSON.parse(_chunkGXYVOEF3cjs.utf8Decode.call(void 0, _chunkGXYVOEF3cjs.base64UrlToBytes.call(void 0, encodedPayload)));
400
+ payload = JSON.parse(_chunkWCXN5AWJcjs.utf8Decode.call(void 0, _chunkWCXN5AWJcjs.base64UrlToBytes.call(void 0, encodedPayload)));
401
401
  } catch (e2) {
402
402
  throw new Error("INVALID_TENANT_AUTH");
403
403
  }
@@ -621,15 +621,15 @@ async function createReiServer(config) {
621
621
  return {
622
622
  handlers: {
623
623
  initTenant: createInitTenantHandler(ctx),
624
- getUserKey: _chunkGXYVOEF3cjs.createGetUserKeyHandler.call(void 0, ctx),
625
- scheduleMessage: _chunkGXYVOEF3cjs.createScheduleMessageHandler.call(void 0, ctx),
624
+ getUserKey: _chunkWCXN5AWJcjs.createGetUserKeyHandler.call(void 0, ctx),
625
+ scheduleMessage: _chunkWCXN5AWJcjs.createScheduleMessageHandler.call(void 0, ctx),
626
626
  sendNotifications: createSendNotificationsHandler(ctx),
627
- updateMessage: _chunkGXYVOEF3cjs.createUpdateMessageHandler.call(void 0, ctx),
628
- cancelMessage: _chunkGXYVOEF3cjs.createCancelMessageHandler.call(void 0, ctx),
629
- messages: _chunkGXYVOEF3cjs.createMessagesHandler.call(void 0, ctx),
630
- getMessage: _chunkGXYVOEF3cjs.createGetMessageHandler.call(void 0, ctx),
631
- pushSubscription: _chunkGXYVOEF3cjs.createPushSubscriptionHandler.call(void 0, ctx),
632
- llmCredentials: _chunkGXYVOEF3cjs.createLlmCredentialsHandler.call(void 0, ctx)
627
+ updateMessage: _chunkWCXN5AWJcjs.createUpdateMessageHandler.call(void 0, ctx),
628
+ cancelMessage: _chunkWCXN5AWJcjs.createCancelMessageHandler.call(void 0, ctx),
629
+ messages: _chunkWCXN5AWJcjs.createMessagesHandler.call(void 0, ctx),
630
+ getMessage: _chunkWCXN5AWJcjs.createGetMessageHandler.call(void 0, ctx),
631
+ pushSubscription: _chunkWCXN5AWJcjs.createPushSubscriptionHandler.call(void 0, ctx),
632
+ llmCredentials: _chunkWCXN5AWJcjs.createLlmCredentialsHandler.call(void 0, ctx)
633
633
  }
634
634
  };
635
635
  }
@@ -695,4 +695,4 @@ async function createReiServer(config) {
695
695
 
696
696
 
697
697
 
698
- exports.CORS_ALLOW_HEADERS = _chunkGXYVOEF3cjs.CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = _chunkGXYVOEF3cjs.CORS_ALLOW_METHODS; exports.CRED_ID_MAX_LENGTH = _chunkGXYVOEF3cjs.CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = _chunkGXYVOEF3cjs.CRED_PUT_BATCH_MAX; exports.CRED_REFS_MAX_ENTRIES = _chunkGXYVOEF3cjs.CRED_REFS_MAX_ENTRIES; exports.CRED_ROWS_PER_USER_MAX = _chunkGXYVOEF3cjs.CRED_ROWS_PER_USER_MAX; exports.DEFAULT_CLAIM_LEASE_MS = _chunkGXYVOEF3cjs.DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = _chunkGXYVOEF3cjs.DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = _chunkGXYVOEF3cjs.DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkGXYVOEF3cjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = _chunkGXYVOEF3cjs.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.DEFAULT_MAX_TOOL_ITERATIONS = _chunkGXYVOEF3cjs.DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = _chunkGXYVOEF3cjs.DEFAULT_TOTAL_TIMEOUT_MS; exports.DeploymentConfigError = _chunkGXYVOEF3cjs.DeploymentConfigError; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkGXYVOEF3cjs.MAX_PUSH_PAYLOAD_BYTES; exports.MAX_TASK_PAYLOAD_BYTES = _chunkGXYVOEF3cjs.MAX_TASK_PAYLOAD_BYTES; exports.MIN_SCHEDULE_LEAD_MS = _chunkGXYVOEF3cjs.MIN_SCHEDULE_LEAD_MS; exports.NonRetryableError = _chunkGXYVOEF3cjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkGXYVOEF3cjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkGXYVOEF3cjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkGXYVOEF3cjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkGXYVOEF3cjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkGXYVOEF3cjs.advanceOccurrence; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunkGXYVOEF3cjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunkGXYVOEF3cjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkGXYVOEF3cjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunkGXYVOEF3cjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkGXYVOEF3cjs.decryptFromStorage; exports.decryptPayload = _chunkGXYVOEF3cjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkGXYVOEF3cjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkGXYVOEF3cjs.encryptForStorage; exports.ensureSchema = _chunkGXYVOEF3cjs.ensureSchema; exports.getSchemaVersion = _chunkGXYVOEF3cjs.getSchemaVersion; exports.hasChatCredRef = _chunkGXYVOEF3cjs.hasChatCredRef; exports.isNonRetryableError = _chunkGXYVOEF3cjs.isNonRetryableError; exports.isValidCredId = _chunkGXYVOEF3cjs.isValidCredId; exports.isValidISO8601 = _chunkGXYVOEF3cjs.isValidISO8601; exports.isValidTimeZoneId = _chunkGXYVOEF3cjs.isValidTimeZoneId; exports.isValidUUID = _chunkGXYVOEF3cjs.isValidUUID; exports.isValidUUIDv4 = _chunkGXYVOEF3cjs.isValidUUIDv4; exports.isValidUrl = _chunkGXYVOEF3cjs.isValidUrl; exports.measurePushPayload = _chunkGXYVOEF3cjs.measurePushPayload; exports.nextFutureOccurrence = _chunkGXYVOEF3cjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkGXYVOEF3cjs.planNextOccurrence; exports.readRequestBody = _chunkGXYVOEF3cjs.readRequestBody; exports.resolveLlmCredential = _chunkGXYVOEF3cjs.resolveLlmCredential; exports.runScheduledTick = _chunkGXYVOEF3cjs.runScheduledTick; exports.runTask = _chunkGXYVOEF3cjs.runTask; exports.sanitizeErrorSummary = _chunkGXYVOEF3cjs.sanitizeErrorSummary; exports.summarizeErrorCause = _chunkGXYVOEF3cjs.summarizeErrorCause; exports.supportsLlmCredentialsStore = _chunkGXYVOEF3cjs.supportsLlmCredentialsStore; exports.validateAvatarUrl = _chunkGXYVOEF3cjs.validateAvatarUrl; exports.validateCredRefs = _chunkGXYVOEF3cjs.validateCredRefs; exports.validateCredValue = _chunkGXYVOEF3cjs.validateCredValue; exports.validateLlmMessagesArray = _chunkGXYVOEF3cjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunkGXYVOEF3cjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunkGXYVOEF3cjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
698
+ exports.CORS_ALLOW_HEADERS = _chunkWCXN5AWJcjs.CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = _chunkWCXN5AWJcjs.CORS_ALLOW_METHODS; exports.CRED_ID_MAX_LENGTH = _chunkWCXN5AWJcjs.CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = _chunkWCXN5AWJcjs.CRED_PUT_BATCH_MAX; exports.CRED_REFS_MAX_ENTRIES = _chunkWCXN5AWJcjs.CRED_REFS_MAX_ENTRIES; exports.CRED_ROWS_PER_USER_MAX = _chunkWCXN5AWJcjs.CRED_ROWS_PER_USER_MAX; exports.DEFAULT_CLAIM_LEASE_MS = _chunkWCXN5AWJcjs.DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = _chunkWCXN5AWJcjs.DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = _chunkWCXN5AWJcjs.DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkWCXN5AWJcjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = _chunkWCXN5AWJcjs.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.DEFAULT_MAX_TOOL_ITERATIONS = _chunkWCXN5AWJcjs.DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = _chunkWCXN5AWJcjs.DEFAULT_TOTAL_TIMEOUT_MS; exports.DeploymentConfigError = _chunkWCXN5AWJcjs.DeploymentConfigError; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkWCXN5AWJcjs.MAX_PUSH_PAYLOAD_BYTES; exports.MAX_TASK_PAYLOAD_BYTES = _chunkWCXN5AWJcjs.MAX_TASK_PAYLOAD_BYTES; exports.MIN_SCHEDULE_LEAD_MS = _chunkWCXN5AWJcjs.MIN_SCHEDULE_LEAD_MS; exports.NonRetryableError = _chunkWCXN5AWJcjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkWCXN5AWJcjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkWCXN5AWJcjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkWCXN5AWJcjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkWCXN5AWJcjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkWCXN5AWJcjs.advanceOccurrence; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunkWCXN5AWJcjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunkWCXN5AWJcjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkWCXN5AWJcjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunkWCXN5AWJcjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkWCXN5AWJcjs.decryptFromStorage; exports.decryptPayload = _chunkWCXN5AWJcjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkWCXN5AWJcjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkWCXN5AWJcjs.encryptForStorage; exports.ensureSchema = _chunkWCXN5AWJcjs.ensureSchema; exports.getSchemaVersion = _chunkWCXN5AWJcjs.getSchemaVersion; exports.hasChatCredRef = _chunkWCXN5AWJcjs.hasChatCredRef; exports.isNonRetryableError = _chunkWCXN5AWJcjs.isNonRetryableError; exports.isValidCredId = _chunkWCXN5AWJcjs.isValidCredId; exports.isValidISO8601 = _chunkWCXN5AWJcjs.isValidISO8601; exports.isValidTimeZoneId = _chunkWCXN5AWJcjs.isValidTimeZoneId; exports.isValidUUID = _chunkWCXN5AWJcjs.isValidUUID; exports.isValidUUIDv4 = _chunkWCXN5AWJcjs.isValidUUIDv4; exports.isValidUrl = _chunkWCXN5AWJcjs.isValidUrl; exports.measurePushPayload = _chunkWCXN5AWJcjs.measurePushPayload; exports.nextFutureOccurrence = _chunkWCXN5AWJcjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkWCXN5AWJcjs.planNextOccurrence; exports.readRequestBody = _chunkWCXN5AWJcjs.readRequestBody; exports.resolveLlmCredential = _chunkWCXN5AWJcjs.resolveLlmCredential; exports.runScheduledTick = _chunkWCXN5AWJcjs.runScheduledTick; exports.runTask = _chunkWCXN5AWJcjs.runTask; exports.sanitizeErrorSummary = _chunkWCXN5AWJcjs.sanitizeErrorSummary; exports.summarizeErrorCause = _chunkWCXN5AWJcjs.summarizeErrorCause; exports.supportsLlmCredentialsStore = _chunkWCXN5AWJcjs.supportsLlmCredentialsStore; exports.validateAvatarUrl = _chunkWCXN5AWJcjs.validateAvatarUrl; exports.validateCredRefs = _chunkWCXN5AWJcjs.validateCredRefs; exports.validateCredValue = _chunkWCXN5AWJcjs.validateCredValue; exports.validateLlmMessagesArray = _chunkWCXN5AWJcjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunkWCXN5AWJcjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunkWCXN5AWJcjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
package/dist/index.mjs CHANGED
@@ -71,7 +71,7 @@ import {
71
71
  validateLlmMessagesArray,
72
72
  validateScheduleMessagePayload,
73
73
  validateSplitPattern
74
- } from "./chunk-4YH3TS4W.mjs";
74
+ } from "./chunk-FPVXATA4.mjs";
75
75
  import "./chunk-GN44PST5.mjs";
76
76
 
77
77
  // src/server/adapters/factory.js
@@ -53,9 +53,12 @@ export function planClientStateCleanup(ttl: Record<string, number> | null | unde
53
53
  * @param {() => number} [args.now] - 取服务端当前时刻(测试可注入假时钟)。适配器
54
54
  * 拿它认出库里「来自未来」的脏行。
55
55
  * @returns {Promise<{ upserted: number, skipped: number, deleted: number, skippedEntries: Array<{ namespace: string, key: string }> }>}
56
- * `upserted` / `skipped` 按逻辑条目计(切片行不计);`deleted` 是请求删除的
57
- * key 数,不代表这些 key 原本一定存在。`skippedEntries` 逐条列出被
58
- * last-write-wins 拦下的 key(适配器不回 outcomes 时为空数组——分不清是哪条)。
56
+ * `upserted` / `skipped` / `deleted` 按逻辑条目计(切片行不计),三者之和等于
57
+ * 条目数。`deleted` 是删除之后 key 已不在库里的条数——删掉了、或本来就没有,
58
+ * 两种都算;被 last-write-wins 拦下的删除(库里那行更新)计入 `skipped`。
59
+ * `skippedEntries` 逐条列出被拦下的 key,写入与删除都在里面(适配器不回
60
+ * outcomes / cleanupOutcomes 时分不清是哪条:写入按物理行计数兜底,删除一律
61
+ * 按「已删」计)。
59
62
  */
60
63
  export function writeClientStateEntries({ db, userId, userKey, entries, now }: {
61
64
  db: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-server",
3
- "version": "2.6.0-next.26",
3
+ "version": "2.6.0-next.27",
4
4
  "description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
5
5
  "repository": {
6
6
  "type": "git",