@rei-standard/amsg-server 2.6.0-next.34 → 2.6.0-next.36
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/README.md +2 -2
- package/dist/adapters/d1.d.ts +34 -4
- package/dist/adapters/interface.d.ts +3 -1
- package/dist/adapters/schema.sqlite.d.ts +13 -1
- package/dist/{chunk-BACG4KJQ.mjs → chunk-67RM2E46.mjs} +280 -52
- package/dist/{chunk-5GRXWKQR.cjs → chunk-I4BGZXQA.cjs} +295 -67
- package/dist/cloudflare/single-user-worker.d.ts +4 -2
- 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/schema-version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -902,8 +902,8 @@ export default createSingleUserCloudflareWorker(buildConfig, {
|
|
|
902
902
|
});
|
|
903
903
|
```
|
|
904
904
|
|
|
905
|
-
- `onError` 放在工厂的第二个参数上,而不是 `buildConfig` 的返回值里:`buildConfig` 自己抛错时配置里的东西一个都读不到,而那恰恰是最需要被看见的一种故障。`stage` 在 cron 路径上是 `config`(配置构建失败 / VAPID
|
|
906
|
-
- `scheduled()` 现在有返回值:`{ ok: true, summary }` 或 `{ ok: false, cause }`。Cloudflare 不看它,是给「自己包一层再转调 `scheduled`」的宿主和测试用的。
|
|
905
|
+
- `onError` 放在工厂的第二个参数上,而不是 `buildConfig` 的返回值里:`buildConfig` 自己抛错时配置里的东西一个都读不到,而那恰恰是最需要被看见的一种故障。`stage` 在 cron 路径上是 `config`(配置构建失败 / VAPID 没配齐)、`tick`(那一跳抛错)或 `cloud-cleanup`(云端数据清理那一步抛错,这一跳的消息照常投递)。VAPID 没配齐这一支没有异常对象,`error` 为 `null`、`cause.name` 是 `VapidNotConfigured`。
|
|
906
|
+
- `scheduled()` 现在有返回值:`{ ok: true, summary }` 或 `{ ok: false, cause }`,`ok` 只看消息投递这一段。云端数据清理那一步出错时,两种返回值上都会多一个 `cloudCleanupCause`。Cloudflare 不看它,是给「自己包一层再转调 `scheduled`」的宿主和测试用的。
|
|
907
907
|
|
|
908
908
|
## 导出 API(Exports)
|
|
909
909
|
|
package/dist/adapters/d1.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export class D1Adapter {
|
|
|
67
67
|
data: any;
|
|
68
68
|
updatedAt: any;
|
|
69
69
|
}>;
|
|
70
|
-
putCloudDataRecord(userId: any, kind: any, id: any, data: any, { idempotencyKey, createOnly, leaseToken }?: {
|
|
70
|
+
putCloudDataRecord(userId: any, kind: any, id: any, data: any, { idempotencyKey, createOnly, leaseToken, work }?: {
|
|
71
71
|
idempotencyKey?: any;
|
|
72
72
|
createOnly?: boolean;
|
|
73
73
|
}): Promise<{
|
|
@@ -105,8 +105,37 @@ export class D1Adapter {
|
|
|
105
105
|
renewCloudDataRecordLease(userId: any, kind: any, id: any, leaseMs: any, leaseToken: any): Promise<boolean>;
|
|
106
106
|
deleteCloudDataRecord(userId: any, kind: any, id: any): Promise<boolean>;
|
|
107
107
|
cleanupCloudDataRecords(kind: any, beforeMs: any): Promise<any>;
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
listDueCloudDataOperations(now?: number, limit?: number): Promise<any[]>;
|
|
109
|
+
cleanupExpiredCloudDataRecords(now?: number, limit?: number): Promise<any>;
|
|
110
|
+
getCloudMaintenanceBatch(name: any, table: any, limit?: number): Promise<{
|
|
111
|
+
rows: any[];
|
|
112
|
+
completed: boolean;
|
|
113
|
+
upperBound?: undefined;
|
|
114
|
+
cursor?: undefined;
|
|
115
|
+
} | {
|
|
116
|
+
rows: any;
|
|
117
|
+
upperBound: any;
|
|
118
|
+
cursor: any;
|
|
119
|
+
completed: boolean;
|
|
120
|
+
}>;
|
|
121
|
+
finishCloudMaintenanceBatch(name: any, cursor: any, completed: any): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Index a whole repair batch in one statement. Never overwrites an existing schedule, and
|
|
124
|
+
* skips a record that was rewritten after the batch was read.
|
|
125
|
+
*/
|
|
126
|
+
indexCloudDataRecordWorkBatch(entries: any): Promise<void>;
|
|
127
|
+
/** Move an indexed record's schedule, unless the record was rewritten since the caller read it. */
|
|
128
|
+
rescheduleCloudDataRecordWork(userId: any, kind: any, id: any, work: any, expectedWriteToken?: any): Promise<boolean>;
|
|
129
|
+
readCloudMaintenanceRecord(row: any): Promise<{
|
|
130
|
+
id: any;
|
|
131
|
+
userId: any;
|
|
132
|
+
kind: any;
|
|
133
|
+
data: any;
|
|
134
|
+
updatedAt: any;
|
|
135
|
+
}>;
|
|
136
|
+
repairCloudResourceMetadata(limit?: number): Promise<void>;
|
|
137
|
+
/** One explicit repair, or a bounded upgrade batch; never called as a recurring sweep. */
|
|
138
|
+
cleanupCloudResourceMetadata(userId?: any, range?: any): Promise<any>;
|
|
110
139
|
listCloudOwners(userId: any): Promise<any>;
|
|
111
140
|
getCloudOwner(userId: any, owner: any): Promise<{
|
|
112
141
|
active: boolean;
|
|
@@ -151,11 +180,12 @@ export class D1Adapter {
|
|
|
151
180
|
* 构变了而老部署没跑过 initSchema 时,cron 会每分钟静默挂在缺的那一列上,
|
|
152
181
|
* 界面上一切正常——这个方法就是让宿主查得出来。
|
|
153
182
|
*
|
|
154
|
-
* @returns {Promise<{ tables: Record<string, string[]>, indexes: string[] }>}
|
|
183
|
+
* @returns {Promise<{ tables: Record<string, string[]>, indexes: string[], triggers: string[] }>}
|
|
155
184
|
*/
|
|
156
185
|
describeSchema(): Promise<{
|
|
157
186
|
tables: Record<string, string[]>;
|
|
158
187
|
indexes: string[];
|
|
188
|
+
triggers: string[];
|
|
159
189
|
}>;
|
|
160
190
|
dropSchema(): Promise<void>;
|
|
161
191
|
createTask(params: any, cloudGuard?: any): Promise<any>;
|
|
@@ -42,13 +42,15 @@ export type DbAdapter = {
|
|
|
42
42
|
*/
|
|
43
43
|
initSchema: () => Promise<InitSchemaResult>;
|
|
44
44
|
/**
|
|
45
|
-
* (可选)活库里现在实际有哪些表 / 列 /
|
|
45
|
+
* (可选)活库里现在实际有哪些表 / 列 / 索引 / 触发器,只读不写。`triggers`
|
|
46
|
+
* 可以不回,不回时触发器这一项不查。
|
|
46
47
|
* `getSchemaVersion` / `ensureSchema`(lib/schema-version.js)拿它跟这一版
|
|
47
48
|
* 需要的清单对照;不实现的适配器调那两个函数会抛错。内置只有 D1 实现。
|
|
48
49
|
*/
|
|
49
50
|
describeSchema?: () => Promise<{
|
|
50
51
|
tables: Record<string, string[]>;
|
|
51
52
|
indexes: string[];
|
|
53
|
+
triggers?: string[];
|
|
52
54
|
}>;
|
|
53
55
|
/**
|
|
54
56
|
* Drop the scheduled_messages table (CASCADE).
|
|
@@ -54,15 +54,27 @@ export const SQLITE_ALL_INDEXES: {
|
|
|
54
54
|
critical: boolean;
|
|
55
55
|
}[];
|
|
56
56
|
export const CLOUD_DATA_TABLES_SQL: string[];
|
|
57
|
+
export const CLOUD_DATA_WORK_TABLES_SQL: string[];
|
|
58
|
+
export const CLOUD_DATA_WORK_INDEXES: {
|
|
59
|
+
name: string;
|
|
60
|
+
sql: string;
|
|
61
|
+
description: string;
|
|
62
|
+
critical: boolean;
|
|
63
|
+
}[];
|
|
64
|
+
export const CLOUD_DATA_DELETE_TRIGGERS: {
|
|
65
|
+
name: string;
|
|
66
|
+
sql: string;
|
|
67
|
+
}[];
|
|
57
68
|
/**
|
|
58
69
|
* 这一版代码跑起来需要的表 / 列 / 索引。
|
|
59
70
|
*
|
|
60
71
|
* 索引只列 critical 的那几个(uidx_uuid 之类):其余索引缺了只是慢,缺了它则
|
|
61
72
|
* 是正确性问题。
|
|
62
73
|
*
|
|
63
|
-
* @type {{ tables: Record<string, string[]>, indexes: string[] }}
|
|
74
|
+
* @type {{ tables: Record<string, string[]>, indexes: string[], triggers: string[] }}
|
|
64
75
|
*/
|
|
65
76
|
export const SQLITE_REQUIRED_SCHEMA: {
|
|
66
77
|
tables: Record<string, string[]>;
|
|
67
78
|
indexes: string[];
|
|
79
|
+
triggers: string[];
|
|
68
80
|
};
|
|
@@ -5070,6 +5070,66 @@ var CLOUD_DATA_TABLES_SQL = [
|
|
|
5070
5070
|
lease_allowed INTEGER CONSTRAINT cloud_lease_guard CHECK (lease_allowed = 1)
|
|
5071
5071
|
)`
|
|
5072
5072
|
];
|
|
5073
|
+
var CLOUD_DATA_WORK_TABLES_SQL = [
|
|
5074
|
+
`CREATE TABLE IF NOT EXISTS cloud_data_work (
|
|
5075
|
+
user_id TEXT NOT NULL, kind TEXT NOT NULL, id TEXT NOT NULL,
|
|
5076
|
+
next_run_at INTEGER, expires_at INTEGER,
|
|
5077
|
+
PRIMARY KEY (user_id, kind, id)
|
|
5078
|
+
)`,
|
|
5079
|
+
`CREATE TABLE IF NOT EXISTS cloud_data_maintenance (
|
|
5080
|
+
name TEXT PRIMARY KEY, cursor INTEGER NOT NULL, upper_bound INTEGER NOT NULL,
|
|
5081
|
+
completed INTEGER NOT NULL DEFAULT 0
|
|
5082
|
+
)`
|
|
5083
|
+
];
|
|
5084
|
+
CLOUD_DATA_TABLES_SQL.push(...CLOUD_DATA_WORK_TABLES_SQL);
|
|
5085
|
+
var CLOUD_DATA_WORK_INDEXES = [
|
|
5086
|
+
{
|
|
5087
|
+
name: "idx_cloud_work_due",
|
|
5088
|
+
sql: `CREATE INDEX IF NOT EXISTS idx_cloud_work_due
|
|
5089
|
+
ON cloud_data_work (kind, next_run_at) WHERE next_run_at IS NOT NULL`,
|
|
5090
|
+
description: "Only due cleanup operations",
|
|
5091
|
+
critical: true
|
|
5092
|
+
},
|
|
5093
|
+
{
|
|
5094
|
+
name: "idx_cloud_work_expiry",
|
|
5095
|
+
sql: `CREATE INDEX IF NOT EXISTS idx_cloud_work_expiry
|
|
5096
|
+
ON cloud_data_work (expires_at) WHERE expires_at IS NOT NULL`,
|
|
5097
|
+
description: "Only expired management records",
|
|
5098
|
+
critical: true
|
|
5099
|
+
}
|
|
5100
|
+
];
|
|
5101
|
+
SQLITE_ALL_INDEXES.push(...CLOUD_DATA_WORK_INDEXES);
|
|
5102
|
+
var chunkPrefixSql = "char(31)||'amsg-chunks'||char(31)";
|
|
5103
|
+
var logicalNamespaceSql = `CASE WHEN substr(OLD.namespace,1,13)=${chunkPrefixSql}
|
|
5104
|
+
THEN substr(OLD.namespace,14) ELSE OLD.namespace END`;
|
|
5105
|
+
var logicalKeySql = `CASE WHEN substr(OLD.namespace,1,13)=${chunkPrefixSql} AND instr(OLD.key,char(31))>0
|
|
5106
|
+
THEN substr(OLD.key,1,instr(OLD.key,char(31))-1) ELSE OLD.key END`;
|
|
5107
|
+
var CLOUD_DATA_DELETE_TRIGGERS = [
|
|
5108
|
+
...[
|
|
5109
|
+
["scheduled_messages", "task", "uuid"],
|
|
5110
|
+
["llm_credentials", "credential", "cred_id"],
|
|
5111
|
+
["message_outbox", "outbox", "message_id"],
|
|
5112
|
+
["push_subscriptions", "subscription", "user_id"]
|
|
5113
|
+
].map(([table, type, column]) => ({
|
|
5114
|
+
name: `trg_cloud_metadata_${type}_delete`,
|
|
5115
|
+
sql: `CREATE TRIGGER IF NOT EXISTS trg_cloud_metadata_${type}_delete AFTER DELETE ON ${table}
|
|
5116
|
+
BEGIN DELETE FROM cloud_resource_metadata WHERE user_id=OLD.user_id
|
|
5117
|
+
AND resource_key=json_array('${type}'${column ? ",OLD." + column : ""}); END`
|
|
5118
|
+
})),
|
|
5119
|
+
{ name: "trg_cloud_metadata_state_delete", sql: `CREATE TRIGGER IF NOT EXISTS trg_cloud_metadata_state_delete
|
|
5120
|
+
AFTER DELETE ON client_state BEGIN
|
|
5121
|
+
DELETE FROM cloud_resource_metadata WHERE user_id=OLD.user_id
|
|
5122
|
+
AND resource_key=json_array('state',${logicalNamespaceSql},${logicalKeySql})
|
|
5123
|
+
AND NOT EXISTS (SELECT 1 FROM client_state r WHERE r.user_id=OLD.user_id
|
|
5124
|
+
AND r.namespace=(${logicalNamespaceSql}) AND r.key=(${logicalKeySql}))
|
|
5125
|
+
AND NOT EXISTS (SELECT 1 FROM client_state r WHERE r.user_id=OLD.user_id
|
|
5126
|
+
AND r.namespace=${chunkPrefixSql}||(${logicalNamespaceSql})
|
|
5127
|
+
AND r.key >= (${logicalKeySql})||char(31) AND r.key < (${logicalKeySql})||char(32)); END` },
|
|
5128
|
+
{ name: "trg_cloud_work_delete", sql: `CREATE TRIGGER IF NOT EXISTS trg_cloud_work_delete
|
|
5129
|
+
AFTER DELETE ON cloud_data_records BEGIN
|
|
5130
|
+
DELETE FROM cloud_data_work WHERE user_id=OLD.user_id AND kind=OLD.kind AND id=OLD.id;
|
|
5131
|
+
DELETE FROM cloud_data_record_chunks WHERE user_id=OLD.user_id AND kind=OLD.kind AND id=OLD.id; END` }
|
|
5132
|
+
];
|
|
5073
5133
|
function parseTableName(sql) {
|
|
5074
5134
|
const match = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_][A-Za-z0-9_]*)/i.exec(sql);
|
|
5075
5135
|
return match ? match[1] : "";
|
|
@@ -5105,7 +5165,8 @@ var SQLITE_REQUIRED_SCHEMA = Object.freeze({
|
|
|
5105
5165
|
describeTable(MESSAGE_OUTBOX_TABLE_SQL),
|
|
5106
5166
|
...CLOUD_DATA_TABLES_SQL.map(describeTable)
|
|
5107
5167
|
])),
|
|
5108
|
-
indexes: Object.freeze(SQLITE_ALL_INDEXES.filter((index) => index.critical).map((index) => index.name))
|
|
5168
|
+
indexes: Object.freeze(SQLITE_ALL_INDEXES.filter((index) => index.critical).map((index) => index.name)),
|
|
5169
|
+
triggers: Object.freeze(CLOUD_DATA_DELETE_TRIGGERS.map((trigger) => trigger.name))
|
|
5109
5170
|
});
|
|
5110
5171
|
|
|
5111
5172
|
// src/server/adapters/d1.js
|
|
@@ -5304,7 +5365,7 @@ var D1Adapter = class {
|
|
|
5304
5365
|
}
|
|
5305
5366
|
return { id: row.id, userId: row.user_id, kind: row.kind, data, updatedAt: row.updated_at };
|
|
5306
5367
|
}
|
|
5307
|
-
async putCloudDataRecord(userId, kind, id, data, { idempotencyKey = null, createOnly = false, leaseToken } = {}) {
|
|
5368
|
+
async putCloudDataRecord(userId, kind, id, data, { idempotencyKey = null, createOnly = false, leaseToken, work } = {}) {
|
|
5308
5369
|
const slices = [];
|
|
5309
5370
|
if (data.length > 200 * 1024) for (let offset = 0; offset < data.length; offset += 200 * 1024) slices.push(data.slice(offset, offset + 200 * 1024));
|
|
5310
5371
|
const rootValue = slices.length ? "amsg-record" + slices.length : data;
|
|
@@ -5331,6 +5392,12 @@ var D1Adapter = class {
|
|
|
5331
5392
|
SELECT ?,?,?,?,?,? WHERE ${rootGate}`
|
|
5332
5393
|
).bind(userId, kind, id, index, slices[index], writeToken, userId, kind, id, writeToken));
|
|
5333
5394
|
}
|
|
5395
|
+
const schedule = work ?? (["inventory", "plan"].includes(kind) ? { nextRunAt: null, expiresAt: now + 36e5 } : null);
|
|
5396
|
+
if (schedule) statements.push(this._db.prepare(
|
|
5397
|
+
`INSERT INTO cloud_data_work (user_id,kind,id,next_run_at,expires_at)
|
|
5398
|
+
SELECT ?,?,?,?,? WHERE ${rootGate}
|
|
5399
|
+
ON CONFLICT (user_id,kind,id) DO UPDATE SET next_run_at=excluded.next_run_at,expires_at=excluded.expires_at`
|
|
5400
|
+
).bind(userId, kind, id, schedule.nextRunAt ?? null, schedule.expiresAt ?? null, userId, kind, id, writeToken));
|
|
5334
5401
|
if (!this.cloudDataManagement) throw new Error("Cloud management requires transactional batches");
|
|
5335
5402
|
const results = await this._db.batch(statements);
|
|
5336
5403
|
if (leaseToken !== void 0 && !results[0].meta.changes) throw Object.assign(new Error("Cloud operation processing lease was lost"), { code: "CLOUD_LEASE_LOST" });
|
|
@@ -5408,23 +5475,102 @@ var D1Adapter = class {
|
|
|
5408
5475
|
]);
|
|
5409
5476
|
return results[1].meta.changes || 0;
|
|
5410
5477
|
}
|
|
5411
|
-
|
|
5412
|
-
|
|
5478
|
+
async listDueCloudDataOperations(now = Date.now(), limit = 25) {
|
|
5479
|
+
const result = await this._db.prepare(`SELECT r.* FROM cloud_data_work w JOIN cloud_data_records r
|
|
5480
|
+
ON r.user_id=w.user_id AND r.kind=w.kind AND r.id=w.id
|
|
5481
|
+
WHERE w.kind='operation' AND w.next_run_at IS NOT NULL AND w.next_run_at<=?
|
|
5482
|
+
AND (r.lease_until IS NULL OR r.lease_until<=?)
|
|
5483
|
+
ORDER BY w.next_run_at LIMIT ?`).bind(now, now, limit).all();
|
|
5484
|
+
return Promise.all((result.results || []).map(async (row) => ({ ...await this._cloudRecordForList(row), writeToken: row.write_token })));
|
|
5485
|
+
}
|
|
5486
|
+
async cleanupExpiredCloudDataRecords(now = Date.now(), limit = 100) {
|
|
5487
|
+
const result = await this._db.prepare(`DELETE FROM cloud_data_records WHERE rowid IN (
|
|
5488
|
+
SELECT r.rowid FROM cloud_data_work w JOIN cloud_data_records r
|
|
5489
|
+
ON r.user_id=w.user_id AND r.kind=w.kind AND r.id=w.id
|
|
5490
|
+
WHERE w.expires_at IS NOT NULL AND w.expires_at<=?
|
|
5491
|
+
AND (r.lease_until IS NULL OR r.lease_until<=?) ORDER BY w.expires_at LIMIT ?
|
|
5492
|
+
)`).bind(now, now, limit).run();
|
|
5493
|
+
return result.meta.changes || 0;
|
|
5494
|
+
}
|
|
5495
|
+
async getCloudMaintenanceBatch(name, table, limit = 100) {
|
|
5496
|
+
if (!["cloud_resource_metadata", "cloud_data_records"].includes(table)) throw new Error("Invalid maintenance source");
|
|
5497
|
+
let marker = await this._db.prepare("SELECT * FROM cloud_data_maintenance WHERE name=?").bind(name).first();
|
|
5498
|
+
if (!marker) {
|
|
5499
|
+
await this._db.prepare(`INSERT INTO cloud_data_maintenance (name,cursor,upper_bound,completed)
|
|
5500
|
+
SELECT ?,0,COALESCE(MAX(rowid),0),0 FROM ${table} WHERE 1 ON CONFLICT (name) DO NOTHING`).bind(name).run();
|
|
5501
|
+
marker = await this._db.prepare("SELECT * FROM cloud_data_maintenance WHERE name=?").bind(name).first();
|
|
5502
|
+
}
|
|
5503
|
+
if (marker.completed) return { rows: [], completed: true };
|
|
5504
|
+
const columns = table === "cloud_data_records" ? "rowid AS maintenance_rowid,user_id,kind,id,updated_at,write_token,CASE WHEN kind='operation' THEN data END AS data" : "rowid AS maintenance_rowid";
|
|
5505
|
+
const result = await this._db.prepare(`SELECT ${columns} FROM ${table}
|
|
5506
|
+
WHERE rowid>? AND rowid<=? ORDER BY rowid LIMIT ?`).bind(marker.cursor, marker.upper_bound, limit).all();
|
|
5507
|
+
return { rows: result.results || [], upperBound: marker.upper_bound, cursor: marker.cursor, completed: false };
|
|
5508
|
+
}
|
|
5509
|
+
async finishCloudMaintenanceBatch(name, cursor, completed) {
|
|
5510
|
+
await this._db.prepare(`UPDATE cloud_data_maintenance SET cursor=MAX(cursor,?),completed=MAX(completed,?) WHERE name=?`).bind(cursor, completed ? 1 : 0, name).run();
|
|
5511
|
+
}
|
|
5512
|
+
/**
|
|
5513
|
+
* Index a whole repair batch in one statement. Never overwrites an existing schedule, and
|
|
5514
|
+
* skips a record that was rewritten after the batch was read.
|
|
5515
|
+
*/
|
|
5516
|
+
async indexCloudDataRecordWorkBatch(entries) {
|
|
5517
|
+
if (!entries.length) return;
|
|
5518
|
+
const rows = entries.map(({ userId, kind, id, work, writeToken }) => [userId, kind, id, work.nextRunAt ?? null, work.expiresAt ?? null, writeToken ?? null]);
|
|
5519
|
+
await this._db.prepare(`INSERT INTO cloud_data_work (user_id,kind,id,next_run_at,expires_at)
|
|
5520
|
+
SELECT r.user_id,r.kind,r.id,json_extract(j.value,'$[3]'),json_extract(j.value,'$[4]')
|
|
5521
|
+
FROM json_each(?) j JOIN cloud_data_records r ON r.user_id=json_extract(j.value,'$[0]')
|
|
5522
|
+
AND r.kind=json_extract(j.value,'$[1]') AND r.id=json_extract(j.value,'$[2]')
|
|
5523
|
+
AND r.write_token IS json_extract(j.value,'$[5]')
|
|
5524
|
+
WHERE 1 ON CONFLICT (user_id,kind,id) DO NOTHING`).bind(JSON.stringify(rows)).run();
|
|
5525
|
+
}
|
|
5526
|
+
/** Move an indexed record's schedule, unless the record was rewritten since the caller read it. */
|
|
5527
|
+
async rescheduleCloudDataRecordWork(userId, kind, id, work, expectedWriteToken = null) {
|
|
5528
|
+
const result = await this._db.prepare(`UPDATE cloud_data_work SET next_run_at=?,expires_at=?
|
|
5529
|
+
WHERE user_id=? AND kind=? AND id=? AND EXISTS (SELECT 1 FROM cloud_data_records r
|
|
5530
|
+
WHERE r.user_id=cloud_data_work.user_id AND r.kind=cloud_data_work.kind AND r.id=cloud_data_work.id AND r.write_token IS ?)`).bind(work.nextRunAt ?? null, work.expiresAt ?? null, userId, kind, id, expectedWriteToken).run();
|
|
5531
|
+
return result.meta.changes > 0;
|
|
5532
|
+
}
|
|
5533
|
+
async readCloudMaintenanceRecord(row) {
|
|
5534
|
+
return this._cloudRecordForList(row);
|
|
5535
|
+
}
|
|
5536
|
+
async repairCloudResourceMetadata(limit = 100) {
|
|
5537
|
+
const name = "metadata-sidecars-v1";
|
|
5538
|
+
const batch = await this.getCloudMaintenanceBatch(name, "cloud_resource_metadata", limit);
|
|
5539
|
+
if (batch.completed) return;
|
|
5540
|
+
const cursor = batch.rows.at(-1)?.maintenance_rowid ?? batch.upperBound;
|
|
5541
|
+
if (batch.rows.length) await this.cleanupCloudResourceMetadata(null, { after: batch.cursor, through: cursor });
|
|
5542
|
+
await this.finishCloudMaintenanceBatch(name, cursor, batch.rows.length < limit || cursor >= batch.upperBound);
|
|
5543
|
+
}
|
|
5544
|
+
/** One explicit repair, or a bounded upgrade batch; never called as a recurring sweep. */
|
|
5545
|
+
async cleanupCloudResourceMetadata(userId = null, range = null) {
|
|
5413
5546
|
const targets = {
|
|
5414
|
-
task: `SELECT 1 FROM scheduled_messages r WHERE r.user_id=m.user_id AND r.uuid=json_extract(m.resource_key,'$[1]')
|
|
5415
|
-
state:
|
|
5416
|
-
|
|
5417
|
-
|
|
5547
|
+
task: [`SELECT 1 FROM scheduled_messages r WHERE r.user_id=m.user_id AND r.uuid=json_extract(m.resource_key,'$[1]')`],
|
|
5548
|
+
state: [
|
|
5549
|
+
`SELECT 1 FROM client_state r WHERE r.user_id=m.user_id
|
|
5550
|
+
AND r.namespace=json_extract(m.resource_key,'$[1]') AND r.key=json_extract(m.resource_key,'$[2]')`,
|
|
5551
|
+
`SELECT 1 FROM client_state r WHERE r.user_id=m.user_id
|
|
5552
|
+
AND r.namespace=char(31)||'amsg-chunks'||char(31)||json_extract(m.resource_key,'$[1]')
|
|
5418
5553
|
AND r.key >= json_extract(m.resource_key,'$[2]')||char(31)
|
|
5419
|
-
AND r.key < json_extract(m.resource_key,'$[2]')||char(32)
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5554
|
+
AND r.key < json_extract(m.resource_key,'$[2]')||char(32)`
|
|
5555
|
+
],
|
|
5556
|
+
credential: [`SELECT 1 FROM llm_credentials r WHERE r.user_id=m.user_id AND r.cred_id=json_extract(m.resource_key,'$[1]')`],
|
|
5557
|
+
outbox: [`SELECT 1 FROM message_outbox r WHERE r.user_id=m.user_id AND r.message_id=json_extract(m.resource_key,'$[1]')`],
|
|
5558
|
+
subscription: [`SELECT 1 FROM push_subscriptions r WHERE r.user_id=m.user_id`]
|
|
5423
5559
|
};
|
|
5424
|
-
const statements = Object.entries(targets).map(([type, exists]) =>
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5560
|
+
const statements = Object.entries(targets).map(([type, exists]) => {
|
|
5561
|
+
const conditions = ["json_valid(resource_key)", "json_extract(resource_key,'$[0]')=?"];
|
|
5562
|
+
const args = [type];
|
|
5563
|
+
if (userId !== null) {
|
|
5564
|
+
conditions.unshift("user_id=?");
|
|
5565
|
+
args.unshift(userId);
|
|
5566
|
+
}
|
|
5567
|
+
if (range) {
|
|
5568
|
+
conditions.unshift("m.rowid>? AND m.rowid<=?");
|
|
5569
|
+
args.unshift(range.after, range.through);
|
|
5570
|
+
}
|
|
5571
|
+
return this._db.prepare(`DELETE FROM cloud_resource_metadata AS m WHERE ${conditions.join(" AND ")}
|
|
5572
|
+
AND ${exists.map((sql) => `NOT EXISTS (${sql})`).join(" AND ")}`).bind(...args);
|
|
5573
|
+
});
|
|
5428
5574
|
const results = await this._cloudBatch(statements, userId);
|
|
5429
5575
|
return results.reduce((sum, result) => sum + (result.meta.changes || 0), 0);
|
|
5430
5576
|
}
|
|
@@ -5564,6 +5710,7 @@ var D1Adapter = class {
|
|
|
5564
5710
|
indexResults.push({ name: index.name, status: "failed", description: index.description, critical: !!index.critical, error: error.message });
|
|
5565
5711
|
}
|
|
5566
5712
|
}
|
|
5713
|
+
for (const trigger of CLOUD_DATA_DELETE_TRIGGERS) await this._db.prepare(trigger.sql).run();
|
|
5567
5714
|
const criticalFailures = indexResults.filter((i) => i.critical && i.status === "failed");
|
|
5568
5715
|
if (criticalFailures.length > 0) {
|
|
5569
5716
|
const names = criticalFailures.map((i) => i.name).join(", ");
|
|
@@ -5587,7 +5734,7 @@ var D1Adapter = class {
|
|
|
5587
5734
|
* 构变了而老部署没跑过 initSchema 时,cron 会每分钟静默挂在缺的那一列上,
|
|
5588
5735
|
* 界面上一切正常——这个方法就是让宿主查得出来。
|
|
5589
5736
|
*
|
|
5590
|
-
* @returns {Promise<{ tables: Record<string, string[]>, indexes: string[] }>}
|
|
5737
|
+
* @returns {Promise<{ tables: Record<string, string[]>, indexes: string[], triggers: string[] }>}
|
|
5591
5738
|
*/
|
|
5592
5739
|
async describeSchema() {
|
|
5593
5740
|
const tableRes = await this._db.prepare(
|
|
@@ -5604,7 +5751,8 @@ var D1Adapter = class {
|
|
|
5604
5751
|
const indexRes = await this._db.prepare(
|
|
5605
5752
|
`SELECT name FROM sqlite_master WHERE type = 'index' AND name IS NOT NULL`
|
|
5606
5753
|
).all();
|
|
5607
|
-
|
|
5754
|
+
const triggerRes = await this._db.prepare("SELECT name FROM sqlite_master WHERE type = 'trigger'").all();
|
|
5755
|
+
return { tables, indexes: (indexRes.results || []).map((row) => row.name), triggers: (triggerRes.results || []).map((row) => row.name) };
|
|
5608
5756
|
}
|
|
5609
5757
|
async dropSchema() {
|
|
5610
5758
|
await this._db.prepare("DROP TABLE IF EXISTS scheduled_messages").run();
|
|
@@ -5612,7 +5760,7 @@ var D1Adapter = class {
|
|
|
5612
5760
|
await this._db.prepare("DROP TABLE IF EXISTS push_subscriptions").run();
|
|
5613
5761
|
await this._db.prepare("DROP TABLE IF EXISTS llm_credentials").run();
|
|
5614
5762
|
await this._db.prepare("DROP TABLE IF EXISTS message_outbox").run();
|
|
5615
|
-
for (const table of ["cloud_data_record_chunks", "cloud_data_records", "cloud_data_owners", "cloud_resource_metadata", "cloud_guard_assertions"]) {
|
|
5763
|
+
for (const table of ["cloud_data_work", "cloud_data_maintenance", "cloud_data_record_chunks", "cloud_data_records", "cloud_data_owners", "cloud_resource_metadata", "cloud_guard_assertions"]) {
|
|
5616
5764
|
await this._db.prepare(`DROP TABLE IF EXISTS ${table}`).run();
|
|
5617
5765
|
}
|
|
5618
5766
|
}
|
|
@@ -5890,7 +6038,8 @@ var D1Adapter = class {
|
|
|
5890
6038
|
const cutoff = new Date(Date.now() - safeDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
5891
6039
|
const res = await this._db.prepare(
|
|
5892
6040
|
`DELETE FROM scheduled_messages
|
|
5893
|
-
WHERE
|
|
6041
|
+
WHERE rowid IN (SELECT rowid FROM scheduled_messages
|
|
6042
|
+
WHERE status IN ('sent', 'failed') AND updated_at < ? LIMIT 100)`
|
|
5894
6043
|
).bind(cutoff).run();
|
|
5895
6044
|
return res.meta.changes || 0;
|
|
5896
6045
|
}
|
|
@@ -6056,7 +6205,7 @@ var D1Adapter = class {
|
|
|
6056
6205
|
*/
|
|
6057
6206
|
async cleanupClientState(targets = []) {
|
|
6058
6207
|
if (!Array.isArray(targets) || targets.length === 0) return 0;
|
|
6059
|
-
const SQL = "DELETE FROM client_state WHERE namespace = ? AND updated_at < ?";
|
|
6208
|
+
const SQL = "DELETE FROM client_state WHERE rowid IN (SELECT rowid FROM client_state WHERE namespace = ? AND updated_at < ? LIMIT 100)";
|
|
6060
6209
|
const statements = targets.map(
|
|
6061
6210
|
(target) => this._db.prepare(SQL).bind(target.namespace, target.updatedBefore)
|
|
6062
6211
|
);
|
|
@@ -6552,13 +6701,13 @@ var D1Adapter = class {
|
|
|
6552
6701
|
let deleted = 0;
|
|
6553
6702
|
if (Number.isFinite(ackedBeforeMs)) {
|
|
6554
6703
|
const res = await this._db.prepare(
|
|
6555
|
-
"DELETE FROM message_outbox WHERE acked_at IS NOT NULL AND acked_at < ?"
|
|
6704
|
+
"DELETE FROM message_outbox WHERE rowid IN (SELECT rowid FROM message_outbox WHERE acked_at IS NOT NULL AND acked_at < ? LIMIT 100)"
|
|
6556
6705
|
).bind(ackedBeforeMs).run();
|
|
6557
6706
|
deleted += res.meta.changes || 0;
|
|
6558
6707
|
}
|
|
6559
6708
|
if (Number.isFinite(allBeforeMs)) {
|
|
6560
6709
|
const res = await this._db.prepare(
|
|
6561
|
-
"DELETE FROM message_outbox WHERE created_at < ?"
|
|
6710
|
+
"DELETE FROM message_outbox WHERE rowid IN (SELECT rowid FROM message_outbox WHERE created_at < ? LIMIT 100)"
|
|
6562
6711
|
).bind(allBeforeMs).run();
|
|
6563
6712
|
deleted += res.meta.changes || 0;
|
|
6564
6713
|
}
|
|
@@ -6879,7 +7028,7 @@ function createClientStateNamespacesHandler(ctx) {
|
|
|
6879
7028
|
}
|
|
6880
7029
|
|
|
6881
7030
|
// src/server/lib/version.js
|
|
6882
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
7031
|
+
var SERVER_VERSION = true ? "2.6.0-next.36" : "0.0.0-dev";
|
|
6883
7032
|
|
|
6884
7033
|
// src/server/handlers/capabilities.js
|
|
6885
7034
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -7301,6 +7450,15 @@ async function cloudInventory(ctx, db, userId, userKey) {
|
|
|
7301
7450
|
resolveOwner: ctx.cloudData?.resolveOwner
|
|
7302
7451
|
});
|
|
7303
7452
|
}
|
|
7453
|
+
function operationWork(operation) {
|
|
7454
|
+
if (["pending", "running"].includes(operation.status)) {
|
|
7455
|
+
return { nextRunAt: Number(operation.nextAttemptAt) || 0, expiresAt: null };
|
|
7456
|
+
}
|
|
7457
|
+
if (["completed", "failed"].includes(operation.status)) {
|
|
7458
|
+
return { nextRunAt: null, expiresAt: Number(operation.updatedAt) + 30 * 864e5 };
|
|
7459
|
+
}
|
|
7460
|
+
return { nextRunAt: null, expiresAt: null };
|
|
7461
|
+
}
|
|
7304
7462
|
async function save(db, userId, key, kind, value, options) {
|
|
7305
7463
|
return db.putCloudDataRecord(
|
|
7306
7464
|
userId,
|
|
@@ -7313,7 +7471,7 @@ async function save(db, userId, key, kind, value, options) {
|
|
|
7313
7471
|
}),
|
|
7314
7472
|
key
|
|
7315
7473
|
),
|
|
7316
|
-
options
|
|
7474
|
+
{ ...options, ...kind === "operation" ? { work: operationWork(value) } : {} }
|
|
7317
7475
|
);
|
|
7318
7476
|
}
|
|
7319
7477
|
async function readCloudRecord(db, userId, key, kind, id) {
|
|
@@ -7675,33 +7833,104 @@ async function advanceCleanup(ctx, db, userId, key, operation) {
|
|
|
7675
7833
|
}
|
|
7676
7834
|
return operation;
|
|
7677
7835
|
}
|
|
7836
|
+
var WORK_INDEX_METHODS = [
|
|
7837
|
+
"listDueCloudDataOperations",
|
|
7838
|
+
"cleanupExpiredCloudDataRecords",
|
|
7839
|
+
"rescheduleCloudDataRecordWork",
|
|
7840
|
+
"getCloudMaintenanceBatch",
|
|
7841
|
+
"finishCloudMaintenanceBatch",
|
|
7842
|
+
"readCloudMaintenanceRecord",
|
|
7843
|
+
"indexCloudDataRecordWorkBatch",
|
|
7844
|
+
"repairCloudResourceMetadata"
|
|
7845
|
+
];
|
|
7846
|
+
var UNREADABLE_RETRY_MS = 15 * 6e4;
|
|
7847
|
+
async function repairLegacyWorkIndex(ctx) {
|
|
7848
|
+
const db = ctx.db;
|
|
7849
|
+
const name = "management-work-v1";
|
|
7850
|
+
const batch = await db.getCloudMaintenanceBatch(name, "cloud_data_records", 100);
|
|
7851
|
+
if (batch.completed) return;
|
|
7852
|
+
const entries = [];
|
|
7853
|
+
for (const row of batch.rows) {
|
|
7854
|
+
let work;
|
|
7855
|
+
if (["inventory", "plan"].includes(row.kind)) {
|
|
7856
|
+
work = { nextRunAt: null, expiresAt: row.updated_at + 36e5 };
|
|
7857
|
+
} else if (row.kind === "operation") {
|
|
7858
|
+
try {
|
|
7859
|
+
const record = await db.readCloudMaintenanceRecord(row);
|
|
7860
|
+
const key = await deriveUserEncryptionKey(row.user_id, ctx.masterKey);
|
|
7861
|
+
work = operationWork(JSON.parse(await decryptFromStorage(record.data, key)));
|
|
7862
|
+
} catch {
|
|
7863
|
+
console.warn("[amsg] Legacy cleanup operation could not be read; queued for retry");
|
|
7864
|
+
work = { nextRunAt: 0, expiresAt: null };
|
|
7865
|
+
}
|
|
7866
|
+
} else continue;
|
|
7867
|
+
entries.push({ userId: row.user_id, kind: row.kind, id: row.id, work, writeToken: row.write_token });
|
|
7868
|
+
}
|
|
7869
|
+
await db.indexCloudDataRecordWorkBatch(entries);
|
|
7870
|
+
const cursor = batch.rows.at(-1)?.maintenance_rowid ?? batch.upperBound;
|
|
7871
|
+
await db.finishCloudMaintenanceBatch(name, cursor, batch.rows.length < 100 || cursor >= batch.upperBound);
|
|
7872
|
+
}
|
|
7678
7873
|
async function resumeCloudDataCleanups(ctx) {
|
|
7679
7874
|
const db = ctx.db;
|
|
7680
7875
|
if (!db?.cloudDataManagement) return;
|
|
7681
|
-
const
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7876
|
+
const indexed = WORK_INDEX_METHODS.every((method) => typeof db[method] === "function");
|
|
7877
|
+
let rows;
|
|
7878
|
+
if (indexed) {
|
|
7879
|
+
await repairLegacyWorkIndex(ctx);
|
|
7880
|
+
await db.repairCloudResourceMetadata(100);
|
|
7881
|
+
await db.cleanupExpiredCloudDataRecords(Date.now(), 100);
|
|
7882
|
+
rows = await db.listDueCloudDataOperations(Date.now(), 25);
|
|
7883
|
+
} else {
|
|
7884
|
+
await db.cleanupCloudDataRecords("inventory", Date.now() - 36e5);
|
|
7885
|
+
await db.cleanupCloudDataRecords("plan", Date.now() - 36e5);
|
|
7886
|
+
rows = await db.listCloudDataRecordsAcrossUsers("operation");
|
|
7887
|
+
}
|
|
7686
7888
|
for (const row of rows) {
|
|
7687
7889
|
try {
|
|
7688
7890
|
const key = await deriveUserEncryptionKey(row.userId, ctx.masterKey);
|
|
7689
7891
|
const operation = JSON.parse(await decryptFromStorage(row.data, key));
|
|
7690
|
-
|
|
7691
|
-
|
|
7892
|
+
const work = operationWork(operation);
|
|
7893
|
+
if (indexed) {
|
|
7894
|
+
if (work.nextRunAt === null || work.nextRunAt > Date.now()) {
|
|
7895
|
+
await db.rescheduleCloudDataRecordWork(row.userId, "operation", row.id, work, row.writeToken);
|
|
7896
|
+
continue;
|
|
7897
|
+
}
|
|
7898
|
+
} else if (work.expiresAt !== null && work.expiresAt <= Date.now()) {
|
|
7899
|
+
await db.deleteCloudDataRecord(row.userId, "operation", row.id);
|
|
7692
7900
|
continue;
|
|
7693
7901
|
}
|
|
7694
7902
|
if (["pending", "running"].includes(operation.status)) {
|
|
7695
7903
|
await advanceCleanup(ctx, db, row.userId, key, operation);
|
|
7696
|
-
if (++processed >= 100) break;
|
|
7697
7904
|
}
|
|
7698
|
-
} catch {
|
|
7905
|
+
} catch (error) {
|
|
7699
7906
|
console.warn("[amsg] A cloud cleanup operation could not be resumed");
|
|
7907
|
+
if (!indexed || error?.code === "CLOUD_LEASE_LOST") continue;
|
|
7908
|
+
await db.rescheduleCloudDataRecordWork(
|
|
7909
|
+
row.userId,
|
|
7910
|
+
"operation",
|
|
7911
|
+
row.id,
|
|
7912
|
+
{ nextRunAt: Date.now() + UNREADABLE_RETRY_MS, expiresAt: null },
|
|
7913
|
+
row.writeToken
|
|
7914
|
+
).catch(() => {
|
|
7915
|
+
});
|
|
7700
7916
|
}
|
|
7701
7917
|
}
|
|
7702
7918
|
}
|
|
7703
7919
|
|
|
7704
7920
|
// src/server/handlers/cloud-data.js
|
|
7921
|
+
function inventorySummary(inventory) {
|
|
7922
|
+
const resources = inventory.entries.map((entry) => entry.resource);
|
|
7923
|
+
return {
|
|
7924
|
+
total: resources.length,
|
|
7925
|
+
counts: CLOUD_RESOURCE_TYPES.map((type) => ({
|
|
7926
|
+
type,
|
|
7927
|
+
count: resources.filter((resource) => resource.type === type).length,
|
|
7928
|
+
byteSize: resources.filter((resource) => resource.type === type).reduce((sum, resource) => sum + (resource.byteSize || 0), 0)
|
|
7929
|
+
})),
|
|
7930
|
+
complete: inventory.complete,
|
|
7931
|
+
gaps: inventory.gaps
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7705
7934
|
function createCloudDataHandler(ctx) {
|
|
7706
7935
|
async function run(url, headers, body, method) {
|
|
7707
7936
|
const tenant = await ctx.tenantManager.resolveTenant(headers);
|
|
@@ -7748,17 +7977,7 @@ function createCloudDataHandler(ctx) {
|
|
|
7748
7977
|
const filter = { ownerType, ownerId, type };
|
|
7749
7978
|
if (path.endsWith("/summary")) {
|
|
7750
7979
|
const inventory = await cloudInventory(ctx, db, userId, key);
|
|
7751
|
-
|
|
7752
|
-
data = {
|
|
7753
|
-
total: resources.length,
|
|
7754
|
-
counts: CLOUD_RESOURCE_TYPES.map((type2) => ({
|
|
7755
|
-
type: type2,
|
|
7756
|
-
count: resources.filter((r) => r.type === type2).length,
|
|
7757
|
-
byteSize: resources.filter((r) => r.type === type2).reduce((sum, r) => sum + (r.byteSize || 0), 0)
|
|
7758
|
-
})),
|
|
7759
|
-
complete: inventory.complete,
|
|
7760
|
-
gaps: inventory.gaps
|
|
7761
|
-
};
|
|
7980
|
+
data = inventorySummary(inventory);
|
|
7762
7981
|
} else {
|
|
7763
7982
|
const limit = Number(params.get("limit") || 50);
|
|
7764
7983
|
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
@@ -7790,6 +8009,7 @@ function createCloudDataHandler(ctx) {
|
|
|
7790
8009
|
snapshot = {
|
|
7791
8010
|
id: crypto.randomUUID(),
|
|
7792
8011
|
filter,
|
|
8012
|
+
summary: inventorySummary(inventory),
|
|
7793
8013
|
expiresAt: Date.now() + 15 * 60 * 1e3,
|
|
7794
8014
|
resources: inventory.entries.map((e) => e.resource).filter(
|
|
7795
8015
|
(r) => (!type || r.type === type) && (!ownerId || r.owner?.type === ownerType && r.owner.id === ownerId)
|
|
@@ -7806,6 +8026,7 @@ function createCloudDataHandler(ctx) {
|
|
|
7806
8026
|
}
|
|
7807
8027
|
data = {
|
|
7808
8028
|
resources: snapshot.resources.slice(offset, offset + limit),
|
|
8029
|
+
...snapshot.summary ? { summary: snapshot.summary } : {},
|
|
7809
8030
|
nextCursor: offset + limit < snapshot.resources.length ? `${snapshot.id}:${offset + limit}` : null,
|
|
7810
8031
|
complete: snapshot.complete,
|
|
7811
8032
|
gaps: snapshot.gaps
|
|
@@ -8150,7 +8371,7 @@ function createSingleUserServer(config) {
|
|
|
8150
8371
|
}
|
|
8151
8372
|
|
|
8152
8373
|
// src/server/lib/schema-version.js
|
|
8153
|
-
var SCHEMA_VERSION = "2.6.0-cloud-data.
|
|
8374
|
+
var SCHEMA_VERSION = "2.6.0-cloud-data.2";
|
|
8154
8375
|
function requireIntrospection(db) {
|
|
8155
8376
|
if (!db || typeof db.describeSchema !== "function") {
|
|
8156
8377
|
throw new Error(
|
|
@@ -8178,6 +8399,12 @@ async function getSchemaVersion(db) {
|
|
|
8178
8399
|
for (const index of SQLITE_REQUIRED_SCHEMA.indexes) {
|
|
8179
8400
|
if (!liveIndexes.has(index)) missing.push(`index:${index}`);
|
|
8180
8401
|
}
|
|
8402
|
+
if (Array.isArray(live?.triggers)) {
|
|
8403
|
+
const liveTriggers = new Set(live.triggers);
|
|
8404
|
+
for (const name of SQLITE_REQUIRED_SCHEMA.triggers) {
|
|
8405
|
+
if (!liveTriggers.has(name)) missing.push(`trigger:${name}`);
|
|
8406
|
+
}
|
|
8407
|
+
}
|
|
8181
8408
|
const ok = missing.length === 0;
|
|
8182
8409
|
return { current: ok ? SCHEMA_VERSION : null, required: SCHEMA_VERSION, ok, missing };
|
|
8183
8410
|
}
|
|
@@ -8434,12 +8661,13 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
8434
8661
|
await reportError({ stage: "config", error, cause, path: null });
|
|
8435
8662
|
return { ok: false, cause };
|
|
8436
8663
|
}
|
|
8664
|
+
const cleanup = {};
|
|
8437
8665
|
try {
|
|
8438
8666
|
await resumeCloudDataCleanups(buildTickContext(cfg));
|
|
8439
8667
|
} catch (error) {
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8668
|
+
console.error("[amsg single-user] scheduled(): cloud cleanup failed:", error && error.message);
|
|
8669
|
+
cleanup.cloudCleanupCause = summarizeErrorCause(error, "cloud-cleanup");
|
|
8670
|
+
await reportError({ stage: "cloud-cleanup", error, cause: cleanup.cloudCleanupCause, path: null });
|
|
8443
8671
|
}
|
|
8444
8672
|
if (!pushConfigured(cfg)) {
|
|
8445
8673
|
console.error("[amsg single-user] scheduled(): VAPID/webpush not configured; skipping tick");
|
|
@@ -8448,16 +8676,16 @@ function createSingleUserCloudflareWorker(buildConfig, options = {}) {
|
|
|
8448
8676
|
"config"
|
|
8449
8677
|
);
|
|
8450
8678
|
await reportError({ stage: "config", error: null, cause, path: null });
|
|
8451
|
-
return { ok: false, cause };
|
|
8679
|
+
return { ok: false, cause, ...cleanup };
|
|
8452
8680
|
}
|
|
8453
8681
|
try {
|
|
8454
8682
|
const summary = await runScheduledTick(buildTickContext(cfg));
|
|
8455
|
-
return { ok: true, summary };
|
|
8683
|
+
return { ok: true, summary, ...cleanup };
|
|
8456
8684
|
} catch (error) {
|
|
8457
8685
|
console.error("[amsg single-user] scheduled(): tick failed:", error && error.message);
|
|
8458
8686
|
const cause = summarizeErrorCause(error, "tick");
|
|
8459
8687
|
await reportError({ stage: "tick", error, cause, path: null });
|
|
8460
|
-
return { ok: false, cause };
|
|
8688
|
+
return { ok: false, cause, ...cleanup };
|
|
8461
8689
|
}
|
|
8462
8690
|
}
|
|
8463
8691
|
async function runTask2(uuid, env) {
|