@rei-standard/amsg-server 2.6.0-next.26 → 2.6.0-next.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/d1.d.ts +5 -1
- package/dist/adapters/interface.d.ts +4 -0
- package/dist/adapters/schema.sqlite.d.ts +19 -1
- package/dist/{chunk-GXYVOEF3.cjs → chunk-PBOY4LG7.cjs} +130 -29
- package/dist/{chunk-4YH3TS4W.mjs → chunk-YBBXKK7U.mjs} +130 -29
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +25 -25
- package/dist/index.mjs +1 -1
- package/dist/lib/client-state-store.d.ts +6 -3
- package/package.json +2 -2
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
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(
|
|
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
|
|
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
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
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(
|
|
4798
|
+
results = await this._db.batch(statements);
|
|
4712
4799
|
} else {
|
|
4713
4800
|
results = [];
|
|
4714
|
-
for (const stmt of
|
|
4801
|
+
for (const stmt of statements) {
|
|
4715
4802
|
results.push(await stmt.run());
|
|
4716
4803
|
}
|
|
4717
4804
|
}
|
|
4718
|
-
const outcomes = results.slice(
|
|
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
|
-
|
|
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
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
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.
|
|
5426
|
+
var SERVER_VERSION = true ? "2.6.0-next.28" : "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
|
-
|
|
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
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
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(
|
|
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
|
|
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
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
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(
|
|
4798
|
+
results = await this._db.batch(statements);
|
|
4712
4799
|
} else {
|
|
4713
4800
|
results = [];
|
|
4714
|
-
for (const stmt of
|
|
4801
|
+
for (const stmt of statements) {
|
|
4715
4802
|
results.push(await stmt.run());
|
|
4716
4803
|
}
|
|
4717
4804
|
}
|
|
4718
|
-
const outcomes = results.slice(
|
|
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
|
-
|
|
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
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
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.
|
|
5426
|
+
var SERVER_VERSION = true ? "2.6.0-next.28" : "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) {
|
package/dist/cloudflare.cjs
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
var
|
|
29
|
+
var _chunkPBOY4LG7cjs = require('./chunk-PBOY4LG7.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 =
|
|
59
|
+
exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkPBOY4LG7cjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkPBOY4LG7cjs.MAX_PUSH_PAYLOAD_BYTES; exports.NonRetryableError = _chunkPBOY4LG7cjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkPBOY4LG7cjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkPBOY4LG7cjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkPBOY4LG7cjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkPBOY4LG7cjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkPBOY4LG7cjs.advanceOccurrence; exports.createD1Adapter = _chunkPBOY4LG7cjs.createD1Adapter; exports.createSingleUserCloudflareWorker = _chunkPBOY4LG7cjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkPBOY4LG7cjs.createSingleUserServer; exports.createWebCryptoWebPush = _chunkPBOY4LG7cjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkPBOY4LG7cjs.decryptFromStorage; exports.decryptPayload = _chunkPBOY4LG7cjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkPBOY4LG7cjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkPBOY4LG7cjs.encryptForStorage; exports.ensureSchema = _chunkPBOY4LG7cjs.ensureSchema; exports.getSchemaVersion = _chunkPBOY4LG7cjs.getSchemaVersion; exports.isNonRetryableError = _chunkPBOY4LG7cjs.isNonRetryableError; exports.isValidTimeZoneId = _chunkPBOY4LG7cjs.isValidTimeZoneId; exports.measurePushPayload = _chunkPBOY4LG7cjs.measurePushPayload; exports.nextFutureOccurrence = _chunkPBOY4LG7cjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkPBOY4LG7cjs.planNextOccurrence; exports.readRequestBody = _chunkPBOY4LG7cjs.readRequestBody; exports.runScheduledTick = _chunkPBOY4LG7cjs.runScheduledTick; exports.runTask = _chunkPBOY4LG7cjs.runTask; exports.summarizeErrorCause = _chunkPBOY4LG7cjs.summarizeErrorCause;
|
package/dist/cloudflare.mjs
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
var
|
|
74
|
+
var _chunkPBOY4LG7cjs = require('./chunk-PBOY4LG7.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 =
|
|
108
|
+
const initSecret = _chunkPBOY4LG7cjs.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 =
|
|
121
|
+
const parsed = _chunkPBOY4LG7cjs.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 && !
|
|
135
|
+
if (tenantId && !_chunkPBOY4LG7cjs.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
|
|
255
|
+
const data = await _chunkPBOY4LG7cjs.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.${
|
|
277
|
+
return `v1.${_chunkPBOY4LG7cjs.bytesToBase64Url.call(void 0, iv)}.${_chunkPBOY4LG7cjs.bytesToBase64Url.call(void 0, authTag)}.${_chunkPBOY4LG7cjs.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 =
|
|
285
|
-
const authTag =
|
|
286
|
-
const ciphertext =
|
|
284
|
+
const iv = _chunkPBOY4LG7cjs.base64UrlToBytes.call(void 0, parts[1]);
|
|
285
|
+
const authTag = _chunkPBOY4LG7cjs.base64UrlToBytes.call(void 0, parts[2]);
|
|
286
|
+
const ciphertext = _chunkPBOY4LG7cjs.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
|
|
354
|
+
return _chunkPBOY4LG7cjs.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 =
|
|
371
|
-
const encodedPayload =
|
|
370
|
+
const encodedHeader = _chunkPBOY4LG7cjs.bytesToBase64Url.call(void 0, _chunkPBOY4LG7cjs.utf8.call(void 0, JSON.stringify(header)));
|
|
371
|
+
const encodedPayload = _chunkPBOY4LG7cjs.bytesToBase64Url.call(void 0, _chunkPBOY4LG7cjs.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 =
|
|
391
|
-
expectedBytes =
|
|
390
|
+
receivedBytes = _chunkPBOY4LG7cjs.base64UrlToBytes.call(void 0, receivedSignature);
|
|
391
|
+
expectedBytes = _chunkPBOY4LG7cjs.base64UrlToBytes.call(void 0, expectedSignature);
|
|
392
392
|
} catch (e) {
|
|
393
393
|
throw new Error("INVALID_TENANT_AUTH");
|
|
394
394
|
}
|
|
395
|
-
if (!
|
|
395
|
+
if (!_chunkPBOY4LG7cjs.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(
|
|
400
|
+
payload = JSON.parse(_chunkPBOY4LG7cjs.utf8Decode.call(void 0, _chunkPBOY4LG7cjs.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:
|
|
625
|
-
scheduleMessage:
|
|
624
|
+
getUserKey: _chunkPBOY4LG7cjs.createGetUserKeyHandler.call(void 0, ctx),
|
|
625
|
+
scheduleMessage: _chunkPBOY4LG7cjs.createScheduleMessageHandler.call(void 0, ctx),
|
|
626
626
|
sendNotifications: createSendNotificationsHandler(ctx),
|
|
627
|
-
updateMessage:
|
|
628
|
-
cancelMessage:
|
|
629
|
-
messages:
|
|
630
|
-
getMessage:
|
|
631
|
-
pushSubscription:
|
|
632
|
-
llmCredentials:
|
|
627
|
+
updateMessage: _chunkPBOY4LG7cjs.createUpdateMessageHandler.call(void 0, ctx),
|
|
628
|
+
cancelMessage: _chunkPBOY4LG7cjs.createCancelMessageHandler.call(void 0, ctx),
|
|
629
|
+
messages: _chunkPBOY4LG7cjs.createMessagesHandler.call(void 0, ctx),
|
|
630
|
+
getMessage: _chunkPBOY4LG7cjs.createGetMessageHandler.call(void 0, ctx),
|
|
631
|
+
pushSubscription: _chunkPBOY4LG7cjs.createPushSubscriptionHandler.call(void 0, ctx),
|
|
632
|
+
llmCredentials: _chunkPBOY4LG7cjs.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 =
|
|
698
|
+
exports.CORS_ALLOW_HEADERS = _chunkPBOY4LG7cjs.CORS_ALLOW_HEADERS; exports.CORS_ALLOW_METHODS = _chunkPBOY4LG7cjs.CORS_ALLOW_METHODS; exports.CRED_ID_MAX_LENGTH = _chunkPBOY4LG7cjs.CRED_ID_MAX_LENGTH; exports.CRED_PUT_BATCH_MAX = _chunkPBOY4LG7cjs.CRED_PUT_BATCH_MAX; exports.CRED_REFS_MAX_ENTRIES = _chunkPBOY4LG7cjs.CRED_REFS_MAX_ENTRIES; exports.CRED_ROWS_PER_USER_MAX = _chunkPBOY4LG7cjs.CRED_ROWS_PER_USER_MAX; exports.DEFAULT_CLAIM_LEASE_MS = _chunkPBOY4LG7cjs.DEFAULT_CLAIM_LEASE_MS; exports.DEFAULT_HEARTBEAT_LEASE_TTL_MS = _chunkPBOY4LG7cjs.DEFAULT_HEARTBEAT_LEASE_TTL_MS; exports.DEFAULT_LEASE_HEARTBEAT_MS = _chunkPBOY4LG7cjs.DEFAULT_LEASE_HEARTBEAT_MS; exports.DEFAULT_MAX_REQUEST_BODY_BYTES = _chunkPBOY4LG7cjs.DEFAULT_MAX_REQUEST_BODY_BYTES; exports.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = _chunkPBOY4LG7cjs.DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE; exports.DEFAULT_MAX_TOOL_ITERATIONS = _chunkPBOY4LG7cjs.DEFAULT_MAX_TOOL_ITERATIONS; exports.DEFAULT_TOTAL_TIMEOUT_MS = _chunkPBOY4LG7cjs.DEFAULT_TOTAL_TIMEOUT_MS; exports.DeploymentConfigError = _chunkPBOY4LG7cjs.DeploymentConfigError; exports.MAX_PUSH_PAYLOAD_BYTES = _chunkPBOY4LG7cjs.MAX_PUSH_PAYLOAD_BYTES; exports.MAX_TASK_PAYLOAD_BYTES = _chunkPBOY4LG7cjs.MAX_TASK_PAYLOAD_BYTES; exports.MIN_SCHEDULE_LEAD_MS = _chunkPBOY4LG7cjs.MIN_SCHEDULE_LEAD_MS; exports.NonRetryableError = _chunkPBOY4LG7cjs.NonRetryableError; exports.PUSH_ENVELOPE_RESERVED_BYTES = _chunkPBOY4LG7cjs.PUSH_ENVELOPE_RESERVED_BYTES; exports.SCHEMA_VERSION = _chunkPBOY4LG7cjs.SCHEMA_VERSION; exports.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES = _chunkPBOY4LG7cjs.WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES; exports.WEB_PUSH_MAX_BODY_BYTES = _chunkPBOY4LG7cjs.WEB_PUSH_MAX_BODY_BYTES; exports.advanceOccurrence = _chunkPBOY4LG7cjs.advanceOccurrence; exports.createAdapter = createAdapter; exports.createD1Adapter = _chunkPBOY4LG7cjs.createD1Adapter; exports.createReiServer = createReiServer; exports.createSingleUserCloudflareWorker = _chunkPBOY4LG7cjs.createSingleUserCloudflareWorker; exports.createSingleUserServer = _chunkPBOY4LG7cjs.createSingleUserServer; exports.createTenantToken = createTenantToken; exports.createWebCryptoWebPush = _chunkPBOY4LG7cjs.createWebCryptoWebPush; exports.decryptFromStorage = _chunkPBOY4LG7cjs.decryptFromStorage; exports.decryptPayload = _chunkPBOY4LG7cjs.decryptPayload; exports.deriveUserEncryptionKey = _chunkPBOY4LG7cjs.deriveUserEncryptionKey; exports.encryptForStorage = _chunkPBOY4LG7cjs.encryptForStorage; exports.ensureSchema = _chunkPBOY4LG7cjs.ensureSchema; exports.getSchemaVersion = _chunkPBOY4LG7cjs.getSchemaVersion; exports.hasChatCredRef = _chunkPBOY4LG7cjs.hasChatCredRef; exports.isNonRetryableError = _chunkPBOY4LG7cjs.isNonRetryableError; exports.isValidCredId = _chunkPBOY4LG7cjs.isValidCredId; exports.isValidISO8601 = _chunkPBOY4LG7cjs.isValidISO8601; exports.isValidTimeZoneId = _chunkPBOY4LG7cjs.isValidTimeZoneId; exports.isValidUUID = _chunkPBOY4LG7cjs.isValidUUID; exports.isValidUUIDv4 = _chunkPBOY4LG7cjs.isValidUUIDv4; exports.isValidUrl = _chunkPBOY4LG7cjs.isValidUrl; exports.measurePushPayload = _chunkPBOY4LG7cjs.measurePushPayload; exports.nextFutureOccurrence = _chunkPBOY4LG7cjs.nextFutureOccurrence; exports.planNextOccurrence = _chunkPBOY4LG7cjs.planNextOccurrence; exports.readRequestBody = _chunkPBOY4LG7cjs.readRequestBody; exports.resolveLlmCredential = _chunkPBOY4LG7cjs.resolveLlmCredential; exports.runScheduledTick = _chunkPBOY4LG7cjs.runScheduledTick; exports.runTask = _chunkPBOY4LG7cjs.runTask; exports.sanitizeErrorSummary = _chunkPBOY4LG7cjs.sanitizeErrorSummary; exports.summarizeErrorCause = _chunkPBOY4LG7cjs.summarizeErrorCause; exports.supportsLlmCredentialsStore = _chunkPBOY4LG7cjs.supportsLlmCredentialsStore; exports.validateAvatarUrl = _chunkPBOY4LG7cjs.validateAvatarUrl; exports.validateCredRefs = _chunkPBOY4LG7cjs.validateCredRefs; exports.validateCredValue = _chunkPBOY4LG7cjs.validateCredValue; exports.validateLlmMessagesArray = _chunkPBOY4LG7cjs.validateLlmMessagesArray; exports.validateScheduleMessagePayload = _chunkPBOY4LG7cjs.validateScheduleMessagePayload; exports.validateSplitPattern = _chunkPBOY4LG7cjs.validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
|
package/dist/index.mjs
CHANGED
|
@@ -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`
|
|
57
|
-
*
|
|
58
|
-
* last-write-wins
|
|
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.
|
|
3
|
+
"version": "2.6.0-next.28",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@netlify/blobs": "^8.1.0",
|
|
42
|
-
"@rei-standard/amsg-shared": "^0.4.0-next.
|
|
42
|
+
"@rei-standard/amsg-shared": "^0.4.0-next.10",
|
|
43
43
|
"web-push": "^3.6.7"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|