@morlay/ui-conversation-message-actions 0.0.14 → 0.0.16
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 +28 -24
- package/dist/client.js +72 -109
- package/dist/index.d.mts +14 -2
- package/dist/index.mjs +1 -1
- package/dist/{plan-BdU_mnTv.d.mts → plan-Cn3TPTa0.d.mts} +7 -2
- package/dist/plan.d.mts +1 -1
- package/dist/{src-ks167MhO.mjs → src-Dq1X3rOO.mjs} +52 -1
- package/dist/testing.d.mts +2581 -41
- package/dist/testing.mjs +1060 -6
- package/package.json +21 -20
- package/src/client/chat-node/MessageItem.tsx +27 -9
- package/src/client/controller.ts +30 -2
- package/src/index.ts +72 -1
- package/src/shared.ts +7 -0
- package/src/types.ts +8 -0
- package/src/client/chat-node/MessageEditDialog.module.css +0 -24
- package/src/client/chat-node/MessageEditDialog.tsx +0 -71
package/dist/testing.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as SessionEditor } from "./src-
|
|
1
|
+
import { n as SessionEditor } from "./src-Dq1X3rOO.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
4
|
import { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, Session as Session$1, SessionId, SessionId as SessionIdBrand, SessionLogOffset, SessionSeq, SessionSeq as SessionSeq$1, SessionStore } from "@deepseek-ai/dsh-session";
|
|
@@ -19191,6 +19191,21 @@ function toJsonlArtifact(meta, inheritedEventCount, events) {
|
|
|
19191
19191
|
for (const event of events) lines.push(JSON.stringify(sessionFormatCatalog.encodeCurrentEvent(event)));
|
|
19192
19192
|
return lines.join("\n");
|
|
19193
19193
|
}
|
|
19194
|
+
/**
|
|
19195
|
+
* 事件行 JSON → `session/title` 的标题文本。存储层写路径保证 `f_data` 是完整
|
|
19196
|
+
* 事件 JSON;形状不符(或损坏)按「无标题」处理——标题是派生显示数据。
|
|
19197
|
+
* @param data - `t_events.f_data` 原文。
|
|
19198
|
+
* @returns 标题文本,或 `undefined`。
|
|
19199
|
+
*/
|
|
19200
|
+
function titleOfEventData(data) {
|
|
19201
|
+
try {
|
|
19202
|
+
const value = JSON.parse(data);
|
|
19203
|
+
const title = value.data?.title ?? value.title;
|
|
19204
|
+
return typeof title === "string" ? title : void 0;
|
|
19205
|
+
} catch {
|
|
19206
|
+
return;
|
|
19207
|
+
}
|
|
19208
|
+
}
|
|
19194
19209
|
//#endregion
|
|
19195
19210
|
//#region ../../node_modules/.pnpm/drizzle-orm@1.0.0-rc.5-ab785fc_@opentelemetry+api@1.9.1_@types+pg@8.23.1_pg@8.23.0_typebox@1.3.7_zod@4.6.1/node_modules/drizzle-orm/column-builder.js
|
|
19196
19211
|
var ColumnBuilder = class {
|
|
@@ -22867,7 +22882,13 @@ const sessions = {
|
|
|
22867
22882
|
f_revision: {
|
|
22868
22883
|
type: "integer",
|
|
22869
22884
|
notNull: true
|
|
22870
|
-
}
|
|
22885
|
+
},
|
|
22886
|
+
/** 归档标记:非空即该会话已归档(毫秒时间戳),NULL 表示未归档。 */
|
|
22887
|
+
f_archived_at: { type: "bigint" },
|
|
22888
|
+
/** 最新会话标题(最后一条 `session/title` 事件的 title),NULL 表示尚无标题。 */
|
|
22889
|
+
f_title: { type: "text" },
|
|
22890
|
+
/** `f_title` 来源事件的稠密 seq(列表 hint 的水位)。 */
|
|
22891
|
+
f_title_seq: { type: "integer" }
|
|
22871
22892
|
}
|
|
22872
22893
|
};
|
|
22873
22894
|
//#endregion
|
|
@@ -22973,20 +22994,193 @@ const sessionEvents$1 = {
|
|
|
22973
22994
|
indexes: { idx_session_events_event_id: { columns: ["f_event_id"] } }
|
|
22974
22995
|
};
|
|
22975
22996
|
//#endregion
|
|
22997
|
+
//#region ../session-rdb/src/entities/v3/storage-units.ts
|
|
22998
|
+
/** storage 域版本账本:域首次打开时写入 descriptor version,其后按 accepted 集合校验。 */
|
|
22999
|
+
const storageUnits = {
|
|
23000
|
+
name: "t_storage_units",
|
|
23001
|
+
columns: {
|
|
23002
|
+
f_name: {
|
|
23003
|
+
type: "text",
|
|
23004
|
+
primaryKey: true
|
|
23005
|
+
},
|
|
23006
|
+
f_version: {
|
|
23007
|
+
type: "integer",
|
|
23008
|
+
notNull: true
|
|
23009
|
+
}
|
|
23010
|
+
}
|
|
23011
|
+
};
|
|
23012
|
+
//#endregion
|
|
23013
|
+
//#region ../session-rdb/src/entities/v3/workspaces.ts
|
|
23014
|
+
/**
|
|
23015
|
+
* workspace 域记录表(上游 `workspace` 域 v2 的 `workspaces` 表):
|
|
23016
|
+
* 记录本身拆成语义列;会话归属在 {@link workspaceSessions}(带顺序),
|
|
23017
|
+
* 显示顺序由 `f_position` 承载(未进入 `workspaceIds` 的记录为 -1)。
|
|
23018
|
+
*/
|
|
23019
|
+
const workspaces = {
|
|
23020
|
+
name: "t_workspaces",
|
|
23021
|
+
columns: {
|
|
23022
|
+
f_id: {
|
|
23023
|
+
type: "serial",
|
|
23024
|
+
primaryKey: true
|
|
23025
|
+
},
|
|
23026
|
+
f_workspace_id: {
|
|
23027
|
+
type: "text",
|
|
23028
|
+
notNull: true,
|
|
23029
|
+
unique: true
|
|
23030
|
+
},
|
|
23031
|
+
f_path: {
|
|
23032
|
+
type: "text",
|
|
23033
|
+
notNull: true
|
|
23034
|
+
},
|
|
23035
|
+
f_title: {
|
|
23036
|
+
type: "text",
|
|
23037
|
+
notNull: true
|
|
23038
|
+
},
|
|
23039
|
+
f_created_at: {
|
|
23040
|
+
type: "text",
|
|
23041
|
+
notNull: true
|
|
23042
|
+
},
|
|
23043
|
+
f_updated_at: {
|
|
23044
|
+
type: "text",
|
|
23045
|
+
notNull: true
|
|
23046
|
+
},
|
|
23047
|
+
f_position: {
|
|
23048
|
+
type: "integer",
|
|
23049
|
+
notNull: true,
|
|
23050
|
+
default: -1
|
|
23051
|
+
}
|
|
23052
|
+
}
|
|
23053
|
+
};
|
|
23054
|
+
//#endregion
|
|
23055
|
+
//#region ../session-rdb/src/entities/v3/workspace-sessions.ts
|
|
23056
|
+
/**
|
|
23057
|
+
* workspace 的会话归属(上游 `workspaceRecord.sessionIds` 的数组顺序):
|
|
23058
|
+
* 一个会话一行,`f_position` 是归属显示顺序。会话 id 有独立索引,可按
|
|
23059
|
+
* session 反查归属 workspace。
|
|
23060
|
+
*/
|
|
23061
|
+
const workspaceSessions = {
|
|
23062
|
+
name: "t_workspace_sessions",
|
|
23063
|
+
columns: {
|
|
23064
|
+
f_id: {
|
|
23065
|
+
type: "serial",
|
|
23066
|
+
primaryKey: true
|
|
23067
|
+
},
|
|
23068
|
+
f_workspace_id: {
|
|
23069
|
+
type: "text",
|
|
23070
|
+
notNull: true,
|
|
23071
|
+
references: {
|
|
23072
|
+
table: "t_workspaces",
|
|
23073
|
+
column: "f_workspace_id",
|
|
23074
|
+
onDelete: "cascade"
|
|
23075
|
+
}
|
|
23076
|
+
},
|
|
23077
|
+
f_session_id: {
|
|
23078
|
+
type: "text",
|
|
23079
|
+
notNull: true
|
|
23080
|
+
},
|
|
23081
|
+
f_position: {
|
|
23082
|
+
type: "integer",
|
|
23083
|
+
notNull: true
|
|
23084
|
+
}
|
|
23085
|
+
},
|
|
23086
|
+
uniques: { uq_workspace_sessions_workspace_session: { columns: ["f_workspace_id", "f_session_id"] } },
|
|
23087
|
+
indexes: { idx_workspace_sessions_session_id: { columns: ["f_session_id"] } }
|
|
23088
|
+
};
|
|
23089
|
+
//#endregion
|
|
23090
|
+
//#region ../session-rdb/src/entities/v3/workspace-state.ts
|
|
23091
|
+
/**
|
|
23092
|
+
* workspace 域 global 单例(上游 `workspaceDomainState`):`initialized` 是
|
|
23093
|
+
* 引导标记,`pendingMutation` 拆成操作与目标两列(create / delete 的可恢复
|
|
23094
|
+
* 两写标记)。显示顺序(`workspaceIds`)在 `t_workspaces.f_position`,
|
|
23095
|
+
* 归档集在 `t_session_archives`。
|
|
23096
|
+
*/
|
|
23097
|
+
const workspaceState = {
|
|
23098
|
+
name: "t_workspace_state",
|
|
23099
|
+
columns: {
|
|
23100
|
+
f_singleton: {
|
|
23101
|
+
type: "integer",
|
|
23102
|
+
primaryKey: true
|
|
23103
|
+
},
|
|
23104
|
+
f_initialized: {
|
|
23105
|
+
type: "integer",
|
|
23106
|
+
notNull: true
|
|
23107
|
+
},
|
|
23108
|
+
f_pending_operation: { type: "text" },
|
|
23109
|
+
f_pending_workspace_id: { type: "text" }
|
|
23110
|
+
},
|
|
23111
|
+
checks: { ck_workspace_state_singleton: { expression: "f_singleton = 1" } }
|
|
23112
|
+
};
|
|
23113
|
+
//#endregion
|
|
23114
|
+
//#region ../session-rdb/src/entities/v3/session-projcache-rows.ts
|
|
23115
|
+
/**
|
|
23116
|
+
* 投影 checkpoint 行(上游 checkpoint 的 `(sessionId, key, ver, seq, val)`
|
|
23117
|
+
* 逐行落库):`f_seq` 是该行的日志水位、`f_ver` 是投影单元的 stateVersion。
|
|
23118
|
+
* 行是折出捷径、非权威,版本不匹配时整行丢弃。
|
|
23119
|
+
*
|
|
23120
|
+
* checkpoint 的日志 identity 不另存一份:它与 `t_sessions` 的行是 1:1,直接
|
|
23121
|
+
* 复用该行的 `f_version` / `f_created_at` / `f_cwd` / `f_seed_length` 做校验。
|
|
23122
|
+
*/
|
|
23123
|
+
const sessionProjcacheRows = {
|
|
23124
|
+
name: "t_session_projcache_row",
|
|
23125
|
+
columns: {
|
|
23126
|
+
f_id: {
|
|
23127
|
+
type: "serial",
|
|
23128
|
+
primaryKey: true
|
|
23129
|
+
},
|
|
23130
|
+
f_session_id: {
|
|
23131
|
+
type: "text",
|
|
23132
|
+
notNull: true,
|
|
23133
|
+
references: {
|
|
23134
|
+
table: "t_sessions",
|
|
23135
|
+
column: "f_session_id",
|
|
23136
|
+
onDelete: "cascade"
|
|
23137
|
+
}
|
|
23138
|
+
},
|
|
23139
|
+
f_key: {
|
|
23140
|
+
type: "text",
|
|
23141
|
+
notNull: true
|
|
23142
|
+
},
|
|
23143
|
+
f_ver: {
|
|
23144
|
+
type: "integer",
|
|
23145
|
+
notNull: true
|
|
23146
|
+
},
|
|
23147
|
+
f_seq: {
|
|
23148
|
+
type: "integer",
|
|
23149
|
+
notNull: true
|
|
23150
|
+
},
|
|
23151
|
+
f_val: {
|
|
23152
|
+
type: "text",
|
|
23153
|
+
notNull: true
|
|
23154
|
+
}
|
|
23155
|
+
},
|
|
23156
|
+
uniques: { uq_session_projcache_row_session_key: { columns: ["f_session_id", "f_key"] } },
|
|
23157
|
+
indexes: { idx_session_projcache_row_key: { columns: ["f_key"] } }
|
|
23158
|
+
};
|
|
23159
|
+
//#endregion
|
|
22976
23160
|
//#region ../session-rdb/src/entities/v3/index.ts
|
|
22977
23161
|
const sqliteTableDefs = [
|
|
22978
23162
|
persistenceState,
|
|
22979
23163
|
schemaMeta,
|
|
22980
23164
|
sessions,
|
|
22981
23165
|
events,
|
|
22982
|
-
sessionEvents$1
|
|
23166
|
+
sessionEvents$1,
|
|
23167
|
+
storageUnits,
|
|
23168
|
+
workspaces,
|
|
23169
|
+
workspaceSessions,
|
|
23170
|
+
workspaceState,
|
|
23171
|
+
sessionProjcacheRows
|
|
22983
23172
|
];
|
|
22984
23173
|
const postgresTableDefs = [
|
|
22985
23174
|
persistenceState,
|
|
22986
23175
|
schemaMeta,
|
|
22987
23176
|
sessions,
|
|
22988
23177
|
events,
|
|
22989
|
-
sessionEvents$1
|
|
23178
|
+
sessionEvents$1,
|
|
23179
|
+
storageUnits,
|
|
23180
|
+
workspaces,
|
|
23181
|
+
workspaceSessions,
|
|
23182
|
+
workspaceState,
|
|
23183
|
+
sessionProjcacheRows
|
|
22990
23184
|
];
|
|
22991
23185
|
({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
|
|
22992
23186
|
//#endregion
|
|
@@ -22999,6 +23193,11 @@ sqliteTables["t_schema_meta"];
|
|
|
22999
23193
|
const tSessions = sqliteTables["t_sessions"];
|
|
23000
23194
|
const tEvents = sqliteTables["t_events"];
|
|
23001
23195
|
const tSessionEvents = sqliteTables["t_session_events"];
|
|
23196
|
+
const tStorageUnits = sqliteTables["t_storage_units"];
|
|
23197
|
+
const tWorkspaces = sqliteTables["t_workspaces"];
|
|
23198
|
+
const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
|
|
23199
|
+
const tWorkspaceState = sqliteTables["t_workspace_state"];
|
|
23200
|
+
const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
|
|
23002
23201
|
const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
23003
23202
|
function eventKind(event) {
|
|
23004
23203
|
switch (event.type) {
|
|
@@ -23013,6 +23212,8 @@ function eventKind(event) {
|
|
|
23013
23212
|
case "session/end-seed": return "turn";
|
|
23014
23213
|
case "tool/call":
|
|
23015
23214
|
case "tool/result":
|
|
23215
|
+
case "tool/ptc-dispatch-start":
|
|
23216
|
+
case "tool/ptc-dispatch":
|
|
23016
23217
|
case "tool/code-dispatch-start":
|
|
23017
23218
|
case "tool/code-dispatch": return "tool";
|
|
23018
23219
|
case "request/header":
|
|
@@ -23086,6 +23287,8 @@ function eventDimensions(event) {
|
|
|
23086
23287
|
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
23087
23288
|
actionId: typeof data["callId"] === "string" ? data["callId"] : ""
|
|
23088
23289
|
};
|
|
23290
|
+
case "tool/ptc-dispatch-start":
|
|
23291
|
+
case "tool/ptc-dispatch":
|
|
23089
23292
|
case "tool/code-dispatch-start":
|
|
23090
23293
|
case "tool/code-dispatch": return {
|
|
23091
23294
|
kind,
|
|
@@ -23288,6 +23491,254 @@ function migrate$1(db, config) {
|
|
|
23288
23491
|
return migrateSync(readMigrationFiles(config), db.session, config);
|
|
23289
23492
|
}
|
|
23290
23493
|
//#endregion
|
|
23494
|
+
//#region ../session-rdb/src/storage-takeover/repository.ts
|
|
23495
|
+
/**
|
|
23496
|
+
* storages 接管表的方言无关实现:SQLite 与 PostgreSQL 的 drizzle 查询构建器
|
|
23497
|
+
* 共享同一套调用形状(SQLite 走同步 `.all()`/`.get()`/`.run()`,PostgreSQL
|
|
23498
|
+
* 走 thenable),差别由本模块的执行 helper 吸收。
|
|
23499
|
+
*
|
|
23500
|
+
* workspace 域是权威数据:记录、归属、显示顺序与归档集都拆成语义表,每次写入
|
|
23501
|
+
* 在介质事务内完成(`host.writeAtomically`),不留部分应用的中间态。
|
|
23502
|
+
*/
|
|
23503
|
+
/** 多行查询:SQLite 同步 `.all()`,PostgreSQL await。 */
|
|
23504
|
+
async function allRows(query) {
|
|
23505
|
+
const q = query;
|
|
23506
|
+
if (typeof q.all === "function") return q.all();
|
|
23507
|
+
return await query;
|
|
23508
|
+
}
|
|
23509
|
+
/** 单行查询:SQLite 同步 `.get()`,PostgreSQL await 后取首行。 */
|
|
23510
|
+
async function oneRow(query) {
|
|
23511
|
+
const q = query;
|
|
23512
|
+
if (typeof q.get === "function") return q.get();
|
|
23513
|
+
return (await query)[0];
|
|
23514
|
+
}
|
|
23515
|
+
/** 写入语句:SQLite 同步 `.run()`,PostgreSQL await。 */
|
|
23516
|
+
async function runQuery(query) {
|
|
23517
|
+
const q = query;
|
|
23518
|
+
if (typeof q.run === "function") {
|
|
23519
|
+
q.run();
|
|
23520
|
+
return;
|
|
23521
|
+
}
|
|
23522
|
+
await query;
|
|
23523
|
+
}
|
|
23524
|
+
/** 一行会话元数据 → 上游 `CheckpointIdentity`。 */
|
|
23525
|
+
function identityOfSession(row) {
|
|
23526
|
+
return {
|
|
23527
|
+
formatVersion: row.fVersion,
|
|
23528
|
+
createdAt: row.fCreatedAt,
|
|
23529
|
+
...row.fCwd === null ? {} : { cwd: row.fCwd },
|
|
23530
|
+
isSeeded: row.fSeedLength !== null,
|
|
23531
|
+
...row.fSeedLength === null ? {} : { inheritedEventCount: SessionLogOffset(row.fSeedLength) }
|
|
23532
|
+
};
|
|
23533
|
+
}
|
|
23534
|
+
/** 存储的整数水位 → 上游 seq cursor(-1 是空日志哨兵)。 */
|
|
23535
|
+
function seqCursor(seq) {
|
|
23536
|
+
return seq === -1 ? -1 : SessionSeq$1(seq);
|
|
23537
|
+
}
|
|
23538
|
+
/** 两写标记的两列 → 上游 `pendingMutation`(未知操作按缺失处理)。 */
|
|
23539
|
+
function pendingMutationOf(row) {
|
|
23540
|
+
const operation = row.fPendingOperation;
|
|
23541
|
+
if (operation !== "create" && operation !== "delete") return void 0;
|
|
23542
|
+
return {
|
|
23543
|
+
operation,
|
|
23544
|
+
workspaceId: row.fPendingWorkspaceId ?? ""
|
|
23545
|
+
};
|
|
23546
|
+
}
|
|
23547
|
+
/**
|
|
23548
|
+
* 构建一个介质的 storages 接管访问层。
|
|
23549
|
+
* @param host - 介质句柄、方言表对象与事务执行器。
|
|
23550
|
+
* @returns 方言无关的访问层。
|
|
23551
|
+
*/
|
|
23552
|
+
function createStorageRepository(host) {
|
|
23553
|
+
const tables = host.tables;
|
|
23554
|
+
const tUnits = tables["t_storage_units"];
|
|
23555
|
+
const tSessions = tables["t_sessions"];
|
|
23556
|
+
const tWorkspaces = tables["t_workspaces"];
|
|
23557
|
+
const tWorkspaceSessions = tables["t_workspace_sessions"];
|
|
23558
|
+
const tWorkspaceState = tables["t_workspace_state"];
|
|
23559
|
+
const tProjcacheRows = tables["t_session_projcache_row"];
|
|
23560
|
+
/** checkpoint identity 的列投影:直接复用会话行,不再单存记录头。 */
|
|
23561
|
+
const sessionIdentity = {
|
|
23562
|
+
fSessionId: tSessions.fSessionId,
|
|
23563
|
+
fVersion: tSessions.fVersion,
|
|
23564
|
+
fCreatedAt: tSessions.fCreatedAt,
|
|
23565
|
+
fCwd: tSessions.fCwd,
|
|
23566
|
+
fSeedLength: tSessions.fSeedLength
|
|
23567
|
+
};
|
|
23568
|
+
const dbx = async () => await host.db();
|
|
23569
|
+
return {
|
|
23570
|
+
async readUnitVersion(name) {
|
|
23571
|
+
return (await oneRow((await dbx()).select().from(tUnits).where(eq(tUnits.fName, name))))?.fVersion;
|
|
23572
|
+
},
|
|
23573
|
+
async insertUnitVersion(name, version) {
|
|
23574
|
+
await runQuery((await dbx()).insert(tUnits).values({
|
|
23575
|
+
fName: name,
|
|
23576
|
+
fVersion: version
|
|
23577
|
+
}).onConflictDoNothing());
|
|
23578
|
+
},
|
|
23579
|
+
async listWorkspaces() {
|
|
23580
|
+
const db = await dbx();
|
|
23581
|
+
const rows = await allRows(db.select().from(tWorkspaces));
|
|
23582
|
+
const links = await allRows(db.select().from(tWorkspaceSessions));
|
|
23583
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
23584
|
+
for (const link of links) {
|
|
23585
|
+
const owned = sessions.get(link.fWorkspaceId) ?? [];
|
|
23586
|
+
owned.push({
|
|
23587
|
+
id: link.fSessionId,
|
|
23588
|
+
position: link.fPosition
|
|
23589
|
+
});
|
|
23590
|
+
sessions.set(link.fWorkspaceId, owned);
|
|
23591
|
+
}
|
|
23592
|
+
return rows.map((row) => ({
|
|
23593
|
+
id: row.fWorkspaceId,
|
|
23594
|
+
record: {
|
|
23595
|
+
path: row.fPath,
|
|
23596
|
+
title: row.fTitle,
|
|
23597
|
+
sessionIds: (sessions.get(row.fWorkspaceId) ?? []).sort((left, right) => left.position - right.position).map((entry) => SessionId(entry.id)),
|
|
23598
|
+
createdAt: row.fCreatedAt,
|
|
23599
|
+
updatedAt: row.fUpdatedAt
|
|
23600
|
+
}
|
|
23601
|
+
}));
|
|
23602
|
+
},
|
|
23603
|
+
async putWorkspace(id, record) {
|
|
23604
|
+
await host.writeAtomically(async () => {
|
|
23605
|
+
const db = await dbx();
|
|
23606
|
+
const values = {
|
|
23607
|
+
fWorkspaceId: id,
|
|
23608
|
+
fPath: record.path,
|
|
23609
|
+
fTitle: record.title,
|
|
23610
|
+
fCreatedAt: record.createdAt,
|
|
23611
|
+
fUpdatedAt: record.updatedAt
|
|
23612
|
+
};
|
|
23613
|
+
await runQuery(db.insert(tWorkspaces).values(values).onConflictDoUpdate({
|
|
23614
|
+
target: tWorkspaces.fWorkspaceId,
|
|
23615
|
+
set: values
|
|
23616
|
+
}));
|
|
23617
|
+
await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
|
|
23618
|
+
const links = record.sessionIds.map((sessionId, position) => ({
|
|
23619
|
+
fWorkspaceId: id,
|
|
23620
|
+
fSessionId: sessionId,
|
|
23621
|
+
fPosition: position
|
|
23622
|
+
}));
|
|
23623
|
+
if (links.length > 0) await runQuery(db.insert(tWorkspaceSessions).values(links));
|
|
23624
|
+
});
|
|
23625
|
+
},
|
|
23626
|
+
async deleteWorkspace(id) {
|
|
23627
|
+
await host.writeAtomically(async () => {
|
|
23628
|
+
const db = await dbx();
|
|
23629
|
+
await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
|
|
23630
|
+
await runQuery(db.delete(tWorkspaces).where(eq(tWorkspaces.fWorkspaceId, id)));
|
|
23631
|
+
});
|
|
23632
|
+
},
|
|
23633
|
+
async readWorkspaceState() {
|
|
23634
|
+
const db = await dbx();
|
|
23635
|
+
const row = await oneRow(db.select().from(tWorkspaceState).where(eq(tWorkspaceState.fSingleton, 1)));
|
|
23636
|
+
if (row === void 0) return null;
|
|
23637
|
+
const ordered = await allRows(db.select().from(tWorkspaces).where(gte(tWorkspaces.fPosition, 0)).orderBy(tWorkspaces.fPosition));
|
|
23638
|
+
const archives = await allRows(db.select({ fSessionId: tSessions.fSessionId }).from(tSessions).where(isNotNull(tSessions.fArchivedAt)).orderBy(tSessions.fArchivedAt));
|
|
23639
|
+
const pendingMutation = pendingMutationOf(row);
|
|
23640
|
+
return {
|
|
23641
|
+
initialized: row.fInitialized !== 0,
|
|
23642
|
+
workspaceIds: ordered.map((entry) => entry.fWorkspaceId),
|
|
23643
|
+
archivedSessionIds: archives.map((entry) => entry.fSessionId),
|
|
23644
|
+
...pendingMutation === void 0 ? {} : { pendingMutation }
|
|
23645
|
+
};
|
|
23646
|
+
},
|
|
23647
|
+
async writeWorkspaceState(state) {
|
|
23648
|
+
await host.writeAtomically(async () => {
|
|
23649
|
+
const db = await dbx();
|
|
23650
|
+
const pending = state.pendingMutation;
|
|
23651
|
+
const values = {
|
|
23652
|
+
fSingleton: 1,
|
|
23653
|
+
fInitialized: state.initialized ? 1 : 0,
|
|
23654
|
+
fPendingOperation: pending === void 0 || typeof pending.operation !== "string" ? null : pending.operation,
|
|
23655
|
+
fPendingWorkspaceId: pending === void 0 || typeof pending.workspaceId !== "string" ? null : pending.workspaceId
|
|
23656
|
+
};
|
|
23657
|
+
await runQuery(db.insert(tWorkspaceState).values(values).onConflictDoUpdate({
|
|
23658
|
+
target: tWorkspaceState.fSingleton,
|
|
23659
|
+
set: values
|
|
23660
|
+
}));
|
|
23661
|
+
await runQuery(db.update(tWorkspaces).set({ fPosition: -1 }));
|
|
23662
|
+
for (const [position, workspaceId] of state.workspaceIds.entries()) await runQuery(db.update(tWorkspaces).set({ fPosition: position }).where(eq(tWorkspaces.fWorkspaceId, workspaceId)));
|
|
23663
|
+
const archived = state.archivedSessionIds;
|
|
23664
|
+
await runQuery(archived.length === 0 ? db.update(tSessions).set({ fArchivedAt: null }).where(isNotNull(tSessions.fArchivedAt)) : db.update(tSessions).set({ fArchivedAt: null }).where(and(isNotNull(tSessions.fArchivedAt), notInArray(tSessions.fSessionId, archived))));
|
|
23665
|
+
const stamp = Date.now();
|
|
23666
|
+
for (const sessionId of archived) await runQuery(db.update(tSessions).set({ fArchivedAt: sql`COALESCE(${tSessions.fArchivedAt}, ${stamp})` }).where(eq(tSessions.fSessionId, sessionId)));
|
|
23667
|
+
});
|
|
23668
|
+
},
|
|
23669
|
+
async loadProjcache() {
|
|
23670
|
+
const db = await dbx();
|
|
23671
|
+
const rows = await allRows(db.select().from(tProjcacheRows));
|
|
23672
|
+
if (rows.length === 0) return [];
|
|
23673
|
+
const sessions = await allRows(db.select(sessionIdentity).from(tSessions));
|
|
23674
|
+
const entries = /* @__PURE__ */ new Map();
|
|
23675
|
+
for (const session of sessions) entries.set(session.fSessionId, {
|
|
23676
|
+
sessionId: SessionId(session.fSessionId),
|
|
23677
|
+
identity: identityOfSession(session),
|
|
23678
|
+
rows: {}
|
|
23679
|
+
});
|
|
23680
|
+
for (const row of rows) {
|
|
23681
|
+
const entry = entries.get(row.fSessionId);
|
|
23682
|
+
if (entry === void 0) continue;
|
|
23683
|
+
entry.rows[row.fKey] = {
|
|
23684
|
+
ver: row.fVer,
|
|
23685
|
+
seq: seqCursor(row.fSeq),
|
|
23686
|
+
val: JSON.parse(row.fVal)
|
|
23687
|
+
};
|
|
23688
|
+
}
|
|
23689
|
+
return [...entries.values()].filter((entry) => Object.keys(entry.rows).length > 0);
|
|
23690
|
+
},
|
|
23691
|
+
async putProjcache(sessionId, rows) {
|
|
23692
|
+
await host.writeAtomically(async () => {
|
|
23693
|
+
const db = await dbx();
|
|
23694
|
+
await runQuery(db.delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
|
|
23695
|
+
const values = Object.entries(rows).map(([key, row]) => ({
|
|
23696
|
+
fSessionId: sessionId,
|
|
23697
|
+
fKey: key,
|
|
23698
|
+
fVer: row.ver,
|
|
23699
|
+
fSeq: row.seq,
|
|
23700
|
+
fVal: JSON.stringify(row.val)
|
|
23701
|
+
}));
|
|
23702
|
+
if (values.length > 0) await runQuery(db.insert(tProjcacheRows).values(values));
|
|
23703
|
+
});
|
|
23704
|
+
},
|
|
23705
|
+
async deleteProjcache(sessionId) {
|
|
23706
|
+
await runQuery((await dbx()).delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
|
|
23707
|
+
},
|
|
23708
|
+
...host.dbSync === void 0 ? {} : {
|
|
23709
|
+
readProjcacheSync: (sessionId) => {
|
|
23710
|
+
const db = host.dbSync();
|
|
23711
|
+
const stored = db.select().from(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)).all();
|
|
23712
|
+
if (stored.length === 0) return void 0;
|
|
23713
|
+
const session = db.select(sessionIdentity).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
|
|
23714
|
+
if (session === void 0) return void 0;
|
|
23715
|
+
const rows = {};
|
|
23716
|
+
for (const row of stored) rows[row.fKey] = {
|
|
23717
|
+
ver: row.fVer,
|
|
23718
|
+
seq: seqCursor(row.fSeq),
|
|
23719
|
+
val: JSON.parse(row.fVal)
|
|
23720
|
+
};
|
|
23721
|
+
return {
|
|
23722
|
+
sessionId: SessionId(sessionId),
|
|
23723
|
+
identity: identityOfSession(session),
|
|
23724
|
+
rows
|
|
23725
|
+
};
|
|
23726
|
+
},
|
|
23727
|
+
readSessionTitleSync: (sessionId) => {
|
|
23728
|
+
const row = host.dbSync().select({
|
|
23729
|
+
fTitle: tSessions.fTitle,
|
|
23730
|
+
fTitleSeq: tSessions.fTitleSeq
|
|
23731
|
+
}).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
|
|
23732
|
+
if (row === void 0 || row.fTitle === null || row.fTitleSeq === null) return void 0;
|
|
23733
|
+
return {
|
|
23734
|
+
title: row.fTitle,
|
|
23735
|
+
seq: row.fTitleSeq
|
|
23736
|
+
};
|
|
23737
|
+
}
|
|
23738
|
+
}
|
|
23739
|
+
};
|
|
23740
|
+
}
|
|
23741
|
+
//#endregion
|
|
23291
23742
|
//#region ../session-rdb/src/sqlite.ts
|
|
23292
23743
|
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
23293
23744
|
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
@@ -23362,12 +23813,60 @@ var SqliteBackend = class SqliteBackend {
|
|
|
23362
23813
|
options;
|
|
23363
23814
|
kind = "sqlite";
|
|
23364
23815
|
storeIdentity;
|
|
23816
|
+
/** storages 接管表访问层;句柄在 open() 完成后解析。 */
|
|
23817
|
+
storage;
|
|
23365
23818
|
dbPath = "";
|
|
23366
23819
|
db;
|
|
23820
|
+
dbReady;
|
|
23821
|
+
resolveDb;
|
|
23822
|
+
rejectDb;
|
|
23367
23823
|
constructor(options) {
|
|
23368
23824
|
this.options = options;
|
|
23825
|
+
this.dbReady = new Promise((resolve, reject) => {
|
|
23826
|
+
this.resolveDb = resolve;
|
|
23827
|
+
this.rejectDb = reject;
|
|
23828
|
+
});
|
|
23829
|
+
this.dbReady.catch(() => {});
|
|
23830
|
+
this.storage = createStorageRepository({
|
|
23831
|
+
db: () => this.dbReady,
|
|
23832
|
+
dbSync: () => {
|
|
23833
|
+
if (this.db === void 0) throw new Error("sqlite session database is not open");
|
|
23834
|
+
return this.db;
|
|
23835
|
+
},
|
|
23836
|
+
writeAtomically: (fn) => enqueueSqliteTx(this.dbPath, async () => {
|
|
23837
|
+
const db = await this.dbReady;
|
|
23838
|
+
db.$client.exec("BEGIN IMMEDIATE");
|
|
23839
|
+
try {
|
|
23840
|
+
const result = await fn();
|
|
23841
|
+
db.$client.exec("COMMIT");
|
|
23842
|
+
return result;
|
|
23843
|
+
} catch (error) {
|
|
23844
|
+
try {
|
|
23845
|
+
db.$client.exec("ROLLBACK");
|
|
23846
|
+
} catch {}
|
|
23847
|
+
throw error;
|
|
23848
|
+
}
|
|
23849
|
+
}),
|
|
23850
|
+
tables: {
|
|
23851
|
+
t_sessions: tSessions,
|
|
23852
|
+
t_storage_units: tStorageUnits,
|
|
23853
|
+
t_workspaces: tWorkspaces,
|
|
23854
|
+
t_workspace_sessions: tWorkspaceSessions,
|
|
23855
|
+
t_workspace_state: tWorkspaceState,
|
|
23856
|
+
t_session_projcache_row: tSessionProjcacheRows
|
|
23857
|
+
}
|
|
23858
|
+
});
|
|
23369
23859
|
}
|
|
23370
23860
|
async open() {
|
|
23861
|
+
try {
|
|
23862
|
+
await this.doOpen();
|
|
23863
|
+
this.resolveDb(this.db);
|
|
23864
|
+
} catch (error) {
|
|
23865
|
+
this.rejectDb(error);
|
|
23866
|
+
throw error;
|
|
23867
|
+
}
|
|
23868
|
+
}
|
|
23869
|
+
async doOpen() {
|
|
23371
23870
|
const actual = this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
|
|
23372
23871
|
this.dbPath = actual;
|
|
23373
23872
|
if (actual !== ":memory:") {
|
|
@@ -23433,6 +23932,7 @@ var SqliteBackend = class SqliteBackend {
|
|
|
23433
23932
|
insertBridges: (rows) => this.insertBridges(rows),
|
|
23434
23933
|
updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
|
|
23435
23934
|
bumpRevision: (id) => this.bumpRevision(id),
|
|
23935
|
+
refreshTitle: (id) => this.refreshTitle(id),
|
|
23436
23936
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
|
|
23437
23937
|
getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence)
|
|
23438
23938
|
};
|
|
@@ -23478,6 +23978,18 @@ var SqliteBackend = class SqliteBackend {
|
|
|
23478
23978
|
async bumpRevision(id) {
|
|
23479
23979
|
this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
|
|
23480
23980
|
}
|
|
23981
|
+
/** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
|
|
23982
|
+
async refreshTitle(id) {
|
|
23983
|
+
const row = this.db.select({
|
|
23984
|
+
fSequence: tSessionEvents.fSequence,
|
|
23985
|
+
fData: tEvents.fData
|
|
23986
|
+
}).from(tSessionEvents).innerJoin(tEvents, eq(tEvents.fEventId, tSessionEvents.fEventId)).where(and(eq(tSessionEvents.fSessionId, id), eq(tEvents.fType, "session/title"))).orderBy(desc(tSessionEvents.fSequence)).limit(1).get();
|
|
23987
|
+
const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
|
|
23988
|
+
this.db.update(tSessions).set({
|
|
23989
|
+
fTitle: title ?? null,
|
|
23990
|
+
fTitleSeq: title === void 0 ? null : row.fSequence
|
|
23991
|
+
}).where(eq(tSessions.fSessionId, id)).run();
|
|
23992
|
+
}
|
|
23481
23993
|
async deleteBridgeTail(id, fromSequence) {
|
|
23482
23994
|
this.db.delete(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence))).run();
|
|
23483
23995
|
}
|
|
@@ -23517,12 +24029,46 @@ var PostgresBackend = class PostgresBackend {
|
|
|
23517
24029
|
kind = "postgres";
|
|
23518
24030
|
storeIdentity;
|
|
23519
24031
|
tables;
|
|
24032
|
+
/** storages 接管表访问层;句柄在 open()(含迁移)完成后解析。 */
|
|
24033
|
+
storage;
|
|
24034
|
+
opened;
|
|
24035
|
+
resolveOpened;
|
|
24036
|
+
rejectOpened;
|
|
24037
|
+
/** 事务期间的连接覆盖:storage 写方法经它加入当前事务。 */
|
|
24038
|
+
txOverride;
|
|
23520
24039
|
constructor(db, options) {
|
|
23521
24040
|
this.db = db;
|
|
23522
24041
|
this.options = options;
|
|
23523
24042
|
this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
|
|
24043
|
+
this.opened = new Promise((resolve, reject) => {
|
|
24044
|
+
this.resolveOpened = resolve;
|
|
24045
|
+
this.rejectOpened = reject;
|
|
24046
|
+
});
|
|
24047
|
+
this.opened.catch(() => {});
|
|
24048
|
+
this.storage = createStorageRepository({
|
|
24049
|
+
db: () => this.opened.then(() => this.txOverride ?? this.db),
|
|
24050
|
+
writeAtomically: (fn) => this.db.transaction(async (tx) => {
|
|
24051
|
+
const previous = this.txOverride;
|
|
24052
|
+
this.txOverride = tx;
|
|
24053
|
+
try {
|
|
24054
|
+
return await fn();
|
|
24055
|
+
} finally {
|
|
24056
|
+
this.txOverride = previous;
|
|
24057
|
+
}
|
|
24058
|
+
}),
|
|
24059
|
+
tables: this.tables
|
|
24060
|
+
});
|
|
23524
24061
|
}
|
|
23525
24062
|
async open() {
|
|
24063
|
+
try {
|
|
24064
|
+
await this.doOpen();
|
|
24065
|
+
this.resolveOpened();
|
|
24066
|
+
} catch (error) {
|
|
24067
|
+
this.rejectOpened(error);
|
|
24068
|
+
throw error;
|
|
24069
|
+
}
|
|
24070
|
+
}
|
|
24071
|
+
async doOpen() {
|
|
23526
24072
|
const schema = this.options.schema ?? "public";
|
|
23527
24073
|
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
23528
24074
|
if ((await this.db.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true) {
|
|
@@ -23583,6 +24129,7 @@ var PostgresBackend = class PostgresBackend {
|
|
|
23583
24129
|
insertBridges: (rows) => this.insertBridges(tx, rows),
|
|
23584
24130
|
updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
|
|
23585
24131
|
bumpRevision: (id) => this.bumpRevision(tx, id),
|
|
24132
|
+
refreshTitle: (id) => this.refreshTitle(tx, id),
|
|
23586
24133
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
|
|
23587
24134
|
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence)
|
|
23588
24135
|
};
|
|
@@ -23632,6 +24179,20 @@ var PostgresBackend = class PostgresBackend {
|
|
|
23632
24179
|
async bumpRevision(exec, id) {
|
|
23633
24180
|
await exec.update(this.tables["t_sessions"]).set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
23634
24181
|
}
|
|
24182
|
+
/** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
|
|
24183
|
+
async refreshTitle(exec, id) {
|
|
24184
|
+
const bridges = this.tables["t_session_events"];
|
|
24185
|
+
const entities = this.tables["t_events"];
|
|
24186
|
+
const row = (await exec.select({
|
|
24187
|
+
fSequence: bridges.fSequence,
|
|
24188
|
+
fData: entities.fData
|
|
24189
|
+
}).from(bridges).innerJoin(entities, eq(entities.fEventId, bridges.fEventId)).where(and(eq(bridges.fSessionId, id), eq(entities.fType, "session/title"))).orderBy(desc(bridges.fSequence)).limit(1).execute())[0];
|
|
24190
|
+
const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
|
|
24191
|
+
await exec.update(this.tables["t_sessions"]).set({
|
|
24192
|
+
fTitle: title ?? null,
|
|
24193
|
+
fTitleSeq: title === void 0 ? null : row.fSequence
|
|
24194
|
+
}).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
24195
|
+
}
|
|
23635
24196
|
async deleteBridgeTail(exec, id, fromSequence) {
|
|
23636
24197
|
await exec.delete(this.tables["t_session_events"]).where(and(eq(this.tables["t_session_events"].fSessionId, id), gte(this.tables["t_session_events"].fSequence, fromSequence))).execute();
|
|
23637
24198
|
}
|
|
@@ -23824,6 +24385,7 @@ var SessionBranchRdbProvider = class {
|
|
|
23824
24385
|
await tx.updateSeedLength(id, denseBoundary + 1);
|
|
23825
24386
|
shrunk = denseBoundary + 1;
|
|
23826
24387
|
}
|
|
24388
|
+
await tx.refreshTitle(id);
|
|
23827
24389
|
await tx.bumpRevision(id);
|
|
23828
24390
|
return shrunk;
|
|
23829
24391
|
});
|
|
@@ -24641,7 +25203,476 @@ function adoptLegacyRows(row, eventRows) {
|
|
|
24641
25203
|
};
|
|
24642
25204
|
}
|
|
24643
25205
|
//#endregion
|
|
25206
|
+
//#region ../../vendor/deepseek-harness/packages/storage/storage/lib/index.js
|
|
25207
|
+
/**
|
|
25208
|
+
* Error vocabulary for the storage hub and its backends.
|
|
25209
|
+
* @module @deepseek-ai/dsh-storage/src/error
|
|
25210
|
+
*/
|
|
25211
|
+
/**
|
|
25212
|
+
* Error thrown by the hub and by backend implementations. The `code` is the
|
|
25213
|
+
* stable contract consumers may switch on; `message` is diagnostic prose.
|
|
25214
|
+
*/
|
|
25215
|
+
var StorageError = class extends Error {
|
|
25216
|
+
code;
|
|
25217
|
+
name = "StorageError";
|
|
25218
|
+
/**
|
|
25219
|
+
* @param code - Stable discriminant for the failure class.
|
|
25220
|
+
* @param message - Human-readable diagnostic detail.
|
|
25221
|
+
* @param options - Standard error options (`cause`).
|
|
25222
|
+
*/
|
|
25223
|
+
constructor(code, message, options) {
|
|
25224
|
+
super(message, options);
|
|
25225
|
+
this.code = code;
|
|
25226
|
+
}
|
|
25227
|
+
};
|
|
25228
|
+
/**
|
|
25229
|
+
* Storage hub (`ctx.storage`): a named backend registry plus mounted
|
|
25230
|
+
* data-form facilities. The hub itself performs no IO — backends own media,
|
|
25231
|
+
* data forms (the domain layer first) own semantics.
|
|
25232
|
+
* @module @deepseek-ai/dsh-storage
|
|
25233
|
+
*/
|
|
25234
|
+
/**
|
|
25235
|
+
* Derive the Cordis lifecycle service that one named backend plugin provides.
|
|
25236
|
+
* Domain-form providers inject these keys so activation cannot race backend
|
|
25237
|
+
* registration even though callers continue resolving backends through the
|
|
25238
|
+
* storage registry.
|
|
25239
|
+
* @param name - Backend registry name.
|
|
25240
|
+
* @returns the corresponding lifecycle-only service key.
|
|
25241
|
+
*/
|
|
25242
|
+
function storageBackendServiceKey(name) {
|
|
25243
|
+
return `storage.backend.${name}`;
|
|
25244
|
+
}
|
|
25245
|
+
//#endregion
|
|
25246
|
+
//#region ../session-rdb/src/storage-takeover/projection-cache.ts
|
|
25247
|
+
/**
|
|
25248
|
+
* 投影 checkpoint 服务(`ctx.sessionProjectionCache`)的 rdb 实现:替换上游
|
|
25249
|
+
* `@deepseek-ai/dsh-session-projection-cache` 插件,公开面与语义逐一对齐
|
|
25250
|
+
* (cachedSnapshot / cachedPredecessorTitle / hydratePrepared / write /
|
|
25251
|
+
* coldSnapshot,以及三个强制写点与计数/定时节流),持久层换成 session-rdb
|
|
25252
|
+
* 的语义专用表 `t_session_projcache` / `t_session_projcache_row`。
|
|
25253
|
+
*
|
|
25254
|
+
* 读方法是同步签名(session 列表在请求路径上直接调用),所以服务持有启动时
|
|
25255
|
+
* 从表中加载的内存 checkpoint 表;写先落库、后更新内存,保证读到的内存值
|
|
25256
|
+
* 磁盘上一定存在。
|
|
25257
|
+
*/
|
|
25258
|
+
/** 服务注册名(与上游插件一致,消费者经 `ctx.get` 解析)。 */
|
|
25259
|
+
const SESSION_PROJECTION_CACHE_SERVICE = "sessionProjectionCache";
|
|
25260
|
+
/** 只认 title 的前代提示(与上游一致)。 */
|
|
25261
|
+
const PREDECESSOR_TITLE_KEY = "title";
|
|
25262
|
+
/** rdb 持久化的投影 checkpoint 服务:同步驱动直读介质,异步驱动用写穿镜像支撑同步签名。 */
|
|
25263
|
+
var SessionProjectionCacheRdb = class extends Service {
|
|
25264
|
+
config;
|
|
25265
|
+
repository;
|
|
25266
|
+
ready;
|
|
25267
|
+
static inject = ["sessionProjections", "sessions"];
|
|
25268
|
+
/** 异步驱动(PostgreSQL)的同步读镜像:同步驱动(SQLite)直读介质,这里恒为空。 */
|
|
25269
|
+
records = /* @__PURE__ */ new Map();
|
|
25270
|
+
dirty = /* @__PURE__ */ new Map();
|
|
25271
|
+
/** 读路径是否直读介质(`readProjcacheSync` 存在即同步驱动)。 */
|
|
25272
|
+
directReads;
|
|
25273
|
+
/**
|
|
25274
|
+
* @param ctx - 插件上下文(须已注入 sessionProjections 与 sessions)。
|
|
25275
|
+
* @param config - 写节流参数。
|
|
25276
|
+
* @param repository - storages 接管表访问层。
|
|
25277
|
+
* @param ready - 介质就绪信号(直读介质前必须等到)。
|
|
25278
|
+
*/
|
|
25279
|
+
constructor(ctx, config, repository, ready) {
|
|
25280
|
+
super(ctx, SESSION_PROJECTION_CACHE_SERVICE);
|
|
25281
|
+
this.config = config;
|
|
25282
|
+
this.repository = repository;
|
|
25283
|
+
this.ready = ready;
|
|
25284
|
+
this.directReads = repository.readProjcacheSync !== void 0;
|
|
25285
|
+
}
|
|
25286
|
+
/**
|
|
25287
|
+
* Wait for the medium, then install the write path. Only the async driver
|
|
25288
|
+
* needs a startup mirror: the sync driver serves every read from the table.
|
|
25289
|
+
*/
|
|
25290
|
+
async [Service.init]() {
|
|
25291
|
+
await this.ready;
|
|
25292
|
+
if (!this.directReads) for (const entry of await this.repository.loadProjcache()) this.records.set(entry.sessionId, entry);
|
|
25293
|
+
this.installWritePath();
|
|
25294
|
+
}
|
|
25295
|
+
/** Read one session's stored record: straight from the medium, or from the async mirror. */
|
|
25296
|
+
lookup(id) {
|
|
25297
|
+
if (this.directReads) return this.repository.readProjcacheSync?.(id);
|
|
25298
|
+
return this.records.get(id);
|
|
25299
|
+
}
|
|
25300
|
+
/**
|
|
25301
|
+
* The stored record for one session, accepted only when its bound log
|
|
25302
|
+
* identity matches `expected`. A session id names a slot, not a lifecycle:
|
|
25303
|
+
* a recreated id or a persistence store swapped under a surviving cache
|
|
25304
|
+
* must not let an old record seed state folded from an unrelated log.
|
|
25305
|
+
* @param id - the session whose record is read.
|
|
25306
|
+
* @param expected - the log identity the caller holds (live or stored header).
|
|
25307
|
+
* @returns the identity-matching record, or `undefined` (absent or unrelated).
|
|
25308
|
+
*/
|
|
25309
|
+
recordFor(id, expected) {
|
|
25310
|
+
const record = this.lookup(id);
|
|
25311
|
+
if (record === void 0) return void 0;
|
|
25312
|
+
return identityMatches(record.identity, expected) ? record : void 0;
|
|
25313
|
+
}
|
|
25314
|
+
/**
|
|
25315
|
+
* The cached projection cut for one stored (cold) or live header.
|
|
25316
|
+
* @param meta - authoritative Session header.
|
|
25317
|
+
* @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
|
|
25318
|
+
* @param keys - optional projection keys required by the caller's audience.
|
|
25319
|
+
* @returns the cut (`asOfSeq` = lowest served-row watermark), or `undefined`
|
|
25320
|
+
* when no usable row exists for this lifecycle.
|
|
25321
|
+
*/
|
|
25322
|
+
cachedSnapshot(meta, inheritedEventCount, keys) {
|
|
25323
|
+
const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
|
|
25324
|
+
const snapshot = record === void 0 ? void 0 : this.viewRecord(record, keys);
|
|
25325
|
+
return this.withDirectTitle(meta.id, keys, snapshot);
|
|
25326
|
+
}
|
|
25327
|
+
/**
|
|
25328
|
+
* 标题是会话数据本身(`t_sessions.f_title`,由 rdb 写路径与 rewind 维护):
|
|
25329
|
+
* checkpoint 行里没有 title 时直接取该列,列表消费不依赖缓存行是否存在。
|
|
25330
|
+
*/
|
|
25331
|
+
withDirectTitle(id, keys, snapshot) {
|
|
25332
|
+
if (keys !== void 0 && !keys.includes(PREDECESSOR_TITLE_KEY)) return snapshot;
|
|
25333
|
+
if (snapshot?.values[PREDECESSOR_TITLE_KEY] !== void 0) return snapshot;
|
|
25334
|
+
const direct = this.repository.readSessionTitleSync?.(id);
|
|
25335
|
+
if (direct === void 0) return snapshot;
|
|
25336
|
+
const values = {
|
|
25337
|
+
...snapshot?.values,
|
|
25338
|
+
[PREDECESSOR_TITLE_KEY]: direct.title
|
|
25339
|
+
};
|
|
25340
|
+
return {
|
|
25341
|
+
asOfSeq: snapshot === void 0 ? direct.seq : Math.min(snapshot.asOfSeq, direct.seq),
|
|
25342
|
+
values
|
|
25343
|
+
};
|
|
25344
|
+
}
|
|
25345
|
+
/**
|
|
25346
|
+
* Read only a predecessor checkpoint's title as a zero-I/O listing hint.
|
|
25347
|
+
* @param meta - authoritative listed Session header.
|
|
25348
|
+
* @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
|
|
25349
|
+
* @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
|
|
25350
|
+
* when the record is current, newer, unrelated, missing, or incompatible
|
|
25351
|
+
* with the title unit.
|
|
25352
|
+
*/
|
|
25353
|
+
cachedPredecessorTitle(meta, inheritedEventCount) {
|
|
25354
|
+
const expected = identityOf(meta, inheritedEventCount);
|
|
25355
|
+
const record = this.lookup(meta.id);
|
|
25356
|
+
if (record === void 0 || !predecessorIdentityMatches(record.identity, expected)) return;
|
|
25357
|
+
const title = this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
|
|
25358
|
+
return title === void 0 ? void 0 : {
|
|
25359
|
+
...title,
|
|
25360
|
+
asOfSeq: -1
|
|
25361
|
+
};
|
|
25362
|
+
}
|
|
25363
|
+
/** View selected wire rows and bind them to their lowest served watermark. */
|
|
25364
|
+
viewRecord(record, keys) {
|
|
25365
|
+
const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
|
|
25366
|
+
const servedKeys = Object.keys(values);
|
|
25367
|
+
if (servedKeys.length === 0) return void 0;
|
|
25368
|
+
const firstKey = servedKeys[0];
|
|
25369
|
+
let asOfSeq = record.rows[firstKey].seq;
|
|
25370
|
+
for (const key of servedKeys.slice(1)) {
|
|
25371
|
+
const row = record.rows[key];
|
|
25372
|
+
if (row.seq < asOfSeq) asOfSeq = row.seq;
|
|
25373
|
+
}
|
|
25374
|
+
return {
|
|
25375
|
+
asOfSeq,
|
|
25376
|
+
values
|
|
25377
|
+
};
|
|
25378
|
+
}
|
|
25379
|
+
/**
|
|
25380
|
+
* Hydrate projection cells for an already-prepared Session without another
|
|
25381
|
+
* persistence read. The cache seeds matching rows; the supplied exact log
|
|
25382
|
+
* advances every unit to the observation cut. No checkpoint is written
|
|
25383
|
+
* because the logical observation may contain recovery events not yet durable.
|
|
25384
|
+
* @param session - exact unpublished Session retained by persistence.
|
|
25385
|
+
* @param events - exact logical event prefix represented by the observation.
|
|
25386
|
+
* @returns all projection values at the event cut.
|
|
25387
|
+
*/
|
|
25388
|
+
hydratePrepared(session, events) {
|
|
25389
|
+
const record = this.recordFor(session.id, identityOf(session.header, session.inheritedEventCount));
|
|
25390
|
+
if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
|
|
25391
|
+
try {
|
|
25392
|
+
return this.ctx.sessionProjections.hydrate(session, record.rows, events, SessionLogOffset(0));
|
|
25393
|
+
} catch {
|
|
25394
|
+
return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
|
|
25395
|
+
}
|
|
25396
|
+
}
|
|
25397
|
+
/**
|
|
25398
|
+
* Durably checkpoint one live session NOW (all mandatory points call this).
|
|
25399
|
+
* NOT fail-soft — callers on the fail-soft paths contain it.
|
|
25400
|
+
* @param session - the live session to checkpoint.
|
|
25401
|
+
* @returns resolution after durability.
|
|
25402
|
+
*/
|
|
25403
|
+
async write(session) {
|
|
25404
|
+
const rows = this.ctx.sessionProjections.checkpoint(session);
|
|
25405
|
+
this.markClean(session);
|
|
25406
|
+
if (this.ctx.sessions.get(session.id) === session) await this.ctx.sessions.flush(session);
|
|
25407
|
+
await this.put(session.id, identityOf(session.header, session.inheritedEventCount), rows);
|
|
25408
|
+
}
|
|
25409
|
+
/**
|
|
25410
|
+
* Cold-read one session's projections from its complete log. Each unit is
|
|
25411
|
+
* seeded from the identity-checked cached rows and the refreshed checkpoint
|
|
25412
|
+
* is written back (fail-soft, fire-and-forget).
|
|
25413
|
+
* @param meta - the stored session header (identity witness).
|
|
25414
|
+
* @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
|
|
25415
|
+
* @param events - the session's complete log, in seq order.
|
|
25416
|
+
* @returns the projection cut at the log end.
|
|
25417
|
+
*/
|
|
25418
|
+
coldSnapshot(meta, inheritedEventCount, events) {
|
|
25419
|
+
const identity = identityOf(meta, inheritedEventCount);
|
|
25420
|
+
const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identity)?.rows ?? {}, events, SessionLogOffset(0), meta, inheritedEventCount);
|
|
25421
|
+
this.put(meta.id, identity, restored.checkpoint).catch((error) => {
|
|
25422
|
+
this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
|
|
25423
|
+
});
|
|
25424
|
+
return restored.snapshot;
|
|
25425
|
+
}
|
|
25426
|
+
installWritePath() {
|
|
25427
|
+
this.ctx.on("session/event", (session, event) => {
|
|
25428
|
+
if (event.type === "turn/end") {
|
|
25429
|
+
this.flushSoft(session, "turn/end");
|
|
25430
|
+
return;
|
|
25431
|
+
}
|
|
25432
|
+
const state = this.dirty.get(session) ?? {
|
|
25433
|
+
pending: 0,
|
|
25434
|
+
timer: void 0
|
|
25435
|
+
};
|
|
25436
|
+
this.dirty.set(session, state);
|
|
25437
|
+
state.pending += 1;
|
|
25438
|
+
if (state.pending >= this.config.writeEveryEvents) {
|
|
25439
|
+
this.flushSoft(session, "count threshold");
|
|
25440
|
+
return;
|
|
25441
|
+
}
|
|
25442
|
+
state.timer ??= setTimeout(() => {
|
|
25443
|
+
this.flushSoft(session, "interval");
|
|
25444
|
+
}, this.config.writeIntervalMs);
|
|
25445
|
+
});
|
|
25446
|
+
this.ctx.on("session/created", (session) => {
|
|
25447
|
+
this.flushSoft(session, "create");
|
|
25448
|
+
});
|
|
25449
|
+
this.ctx.on("session/disposed", (session) => {
|
|
25450
|
+
this.flushSoft(session, "detach");
|
|
25451
|
+
this.markClean(session);
|
|
25452
|
+
this.dirty.delete(session);
|
|
25453
|
+
});
|
|
25454
|
+
this.ctx.effect(() => () => {
|
|
25455
|
+
for (const state of this.dirty.values()) if (state.timer !== void 0) clearTimeout(state.timer);
|
|
25456
|
+
this.dirty.clear();
|
|
25457
|
+
}, "sessionProjectionCacheRdb.timers");
|
|
25458
|
+
}
|
|
25459
|
+
/** One fail-soft durable checkpoint. */
|
|
25460
|
+
async flushSoft(session, trigger) {
|
|
25461
|
+
try {
|
|
25462
|
+
await this.write(session);
|
|
25463
|
+
} catch (error) {
|
|
25464
|
+
this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`);
|
|
25465
|
+
}
|
|
25466
|
+
}
|
|
25467
|
+
/** Reset one session's dirty bookkeeping (its checkpoint is being written). */
|
|
25468
|
+
markClean(session) {
|
|
25469
|
+
const state = this.dirty.get(session);
|
|
25470
|
+
if (state === void 0) return;
|
|
25471
|
+
state.pending = 0;
|
|
25472
|
+
if (state.timer !== void 0) {
|
|
25473
|
+
clearTimeout(state.timer);
|
|
25474
|
+
state.timer = void 0;
|
|
25475
|
+
}
|
|
25476
|
+
}
|
|
25477
|
+
/** Replace one session's stored record with its log identity and a detached snapshot of `rows`. */
|
|
25478
|
+
async put(id, identity, rows) {
|
|
25479
|
+
const detached = detachJson(rows);
|
|
25480
|
+
await this.repository.putProjcache(id, detached);
|
|
25481
|
+
if (!this.directReads) this.records.set(id, {
|
|
25482
|
+
sessionId: id,
|
|
25483
|
+
identity,
|
|
25484
|
+
rows: detached
|
|
25485
|
+
});
|
|
25486
|
+
}
|
|
25487
|
+
};
|
|
25488
|
+
/** Detach one checkpoint from live unit state, refusing non-lossless JSON. */
|
|
25489
|
+
function detachJson(rows) {
|
|
25490
|
+
let text;
|
|
25491
|
+
try {
|
|
25492
|
+
text = JSON.stringify(rows);
|
|
25493
|
+
} catch (error) {
|
|
25494
|
+
throw new TypeError(`projection checkpoint is not losslessly JSON-serializable: ${String(error)}`, { cause: error });
|
|
25495
|
+
}
|
|
25496
|
+
if (text === void 0) throw new TypeError("projection checkpoint is not losslessly JSON-serializable");
|
|
25497
|
+
return JSON.parse(text);
|
|
25498
|
+
}
|
|
25499
|
+
/** Project a header onto the identity fields a record is bound to. */
|
|
25500
|
+
function identityOf(header, inheritedEventCount) {
|
|
25501
|
+
const cut = SessionLogOffset(inheritedEventCount);
|
|
25502
|
+
if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
|
|
25503
|
+
return {
|
|
25504
|
+
formatVersion: header.version,
|
|
25505
|
+
createdAt: header.createdAt,
|
|
25506
|
+
...header.cwd === void 0 ? {} : { cwd: header.cwd },
|
|
25507
|
+
isSeeded: header.isSeeded,
|
|
25508
|
+
inheritedEventCount: cut
|
|
25509
|
+
};
|
|
25510
|
+
}
|
|
25511
|
+
/**
|
|
25512
|
+
* Whether a stored record's bound identity names the caller's lifecycle.
|
|
25513
|
+
* An absent format generation cannot prove the fold semantics and never
|
|
25514
|
+
* matches. Once the format matches, absent lineage fields (records admitted
|
|
25515
|
+
* via compatible versions predate them) read as the unseeded lineage.
|
|
25516
|
+
*/
|
|
25517
|
+
function identityMatches(stored, expected) {
|
|
25518
|
+
return stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected);
|
|
25519
|
+
}
|
|
25520
|
+
/** Match one predecessor cache record to the authoritative listed lifecycle. */
|
|
25521
|
+
function predecessorIdentityMatches(stored, expected) {
|
|
25522
|
+
return (stored.formatVersion === void 0 || stored.formatVersion < expected.formatVersion) && lifecycleIdentityMatches(stored, expected);
|
|
25523
|
+
}
|
|
25524
|
+
/** Match the format-independent fields that distinguish one Session lifecycle. */
|
|
25525
|
+
function lifecycleIdentityMatches(stored, expected) {
|
|
25526
|
+
return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
|
|
25527
|
+
}
|
|
25528
|
+
//#endregion
|
|
25529
|
+
//#region ../session-rdb/src/storage-takeover/storage-backend.ts
|
|
25530
|
+
/**
|
|
25531
|
+
* storages 接管:注册在 storage hub 上的 `rdb` KV 后端。它把 workspace 域
|
|
25532
|
+
* (上游 single 布局:一张 `workspaces` 表 + global 单例)映射到 session-rdb
|
|
25533
|
+
* 的语义专用表,与事件日志同库同连接;域层(`ctx.storageDomain`)通过
|
|
25534
|
+
* `routes` 把 `workspace` 域路由到本后端。
|
|
25535
|
+
*
|
|
25536
|
+
* 本后端只服务已知域的专用表:未知 unit 名 fail loud,而不是静默落到通用
|
|
25537
|
+
* 存储——表结构是显式维护的(见 docs/schema.md)。
|
|
25538
|
+
*/
|
|
25539
|
+
/** 本后端唯一服务的域:上游 `workspaceDomainSpec`(v2,single 布局)。 */
|
|
25540
|
+
const WORKSPACE_UNIT = "workspace";
|
|
25541
|
+
const WORKSPACE_TABLE = "workspaces";
|
|
25542
|
+
/** 只服务 workspace 域的 KV 后端。 */
|
|
25543
|
+
var RdbStorageBackend = class {
|
|
25544
|
+
repository;
|
|
25545
|
+
kv = { open: (descriptor) => this.openUnit(descriptor) };
|
|
25546
|
+
/** 已打开(或打开中)的 unit 名;重复 open 是调用方 bug。 */
|
|
25547
|
+
open = /* @__PURE__ */ new Set();
|
|
25548
|
+
closed = false;
|
|
25549
|
+
/**
|
|
25550
|
+
* @param repository - storages 接管表访问层(与事件日志同介质)。
|
|
25551
|
+
*/
|
|
25552
|
+
constructor(repository) {
|
|
25553
|
+
this.repository = repository;
|
|
25554
|
+
}
|
|
25555
|
+
async openUnit(descriptor) {
|
|
25556
|
+
if (this.closed) throw new StorageError("closed", "rdb storage backend is closed");
|
|
25557
|
+
if (descriptor.name !== WORKSPACE_UNIT) throw new Error(`rdb storage backend serves only the '${WORKSPACE_UNIT}' domain (requested '${descriptor.name}')`);
|
|
25558
|
+
if (!descriptor.tables.includes(WORKSPACE_TABLE) || descriptor.hasGlobal !== true) throw new Error(`rdb storage backend expects the '${WORKSPACE_UNIT}' domain shape (table '${WORKSPACE_TABLE}' plus a global slot)`);
|
|
25559
|
+
if (this.open.has(descriptor.name)) throw new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`);
|
|
25560
|
+
const stored = await this.repository.readUnitVersion(descriptor.name);
|
|
25561
|
+
if (stored === void 0) await this.repository.insertUnitVersion(descriptor.name, descriptor.version);
|
|
25562
|
+
else if (stored !== descriptor.version) throw new StorageError("version-mismatch", `kv unit '${descriptor.name}' is stamped version ${stored} on the medium, incompatible with descriptor version ${descriptor.version}`);
|
|
25563
|
+
this.open.add(descriptor.name);
|
|
25564
|
+
return new WorkspaceKvUnit(this.repository, descriptor, () => {
|
|
25565
|
+
this.open.delete(descriptor.name);
|
|
25566
|
+
});
|
|
25567
|
+
}
|
|
25568
|
+
/**
|
|
25569
|
+
* Release the backend. The medium (the session database) belongs to the
|
|
25570
|
+
* owning session-rdb plugin, so closing open units here does not close it.
|
|
25571
|
+
* @returns resolution after the name table is cleared.
|
|
25572
|
+
*/
|
|
25573
|
+
async close() {
|
|
25574
|
+
this.closed = true;
|
|
25575
|
+
this.open.clear();
|
|
25576
|
+
}
|
|
25577
|
+
};
|
|
25578
|
+
/** workspace 域的 KV unit:每个原语一条 SQL 语句,值形状与上游记录一致。 */
|
|
25579
|
+
var WorkspaceKvUnit = class {
|
|
25580
|
+
repository;
|
|
25581
|
+
descriptor;
|
|
25582
|
+
onClose;
|
|
25583
|
+
closed = false;
|
|
25584
|
+
constructor(repository, descriptor, onClose) {
|
|
25585
|
+
this.repository = repository;
|
|
25586
|
+
this.descriptor = descriptor;
|
|
25587
|
+
this.onClose = onClose;
|
|
25588
|
+
}
|
|
25589
|
+
async loadAll() {
|
|
25590
|
+
this.assertOpen();
|
|
25591
|
+
const records = Object.create(null);
|
|
25592
|
+
for (const { id, record } of await this.repository.listWorkspaces()) records[id] = record;
|
|
25593
|
+
const state = await this.repository.readWorkspaceState();
|
|
25594
|
+
return {
|
|
25595
|
+
tables: { [WORKSPACE_TABLE]: records },
|
|
25596
|
+
global: state
|
|
25597
|
+
};
|
|
25598
|
+
}
|
|
25599
|
+
async putRecord(table, key, value) {
|
|
25600
|
+
this.assertOpen();
|
|
25601
|
+
this.assertTable(table);
|
|
25602
|
+
await this.repository.putWorkspace(key, workspaceRecordOf(value));
|
|
25603
|
+
}
|
|
25604
|
+
async deleteRecord(table, key) {
|
|
25605
|
+
this.assertOpen();
|
|
25606
|
+
this.assertTable(table);
|
|
25607
|
+
await this.repository.deleteWorkspace(key);
|
|
25608
|
+
}
|
|
25609
|
+
async setGlobal(value) {
|
|
25610
|
+
this.assertOpen();
|
|
25611
|
+
await this.repository.writeWorkspaceState(workspaceStateOf(value));
|
|
25612
|
+
}
|
|
25613
|
+
async close() {
|
|
25614
|
+
if (this.closed) return;
|
|
25615
|
+
this.closed = true;
|
|
25616
|
+
this.onClose();
|
|
25617
|
+
}
|
|
25618
|
+
assertOpen() {
|
|
25619
|
+
if (this.closed) throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
|
|
25620
|
+
}
|
|
25621
|
+
assertTable(table) {
|
|
25622
|
+
if (table !== WORKSPACE_TABLE) throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
|
|
25623
|
+
}
|
|
25624
|
+
};
|
|
25625
|
+
/** Narrow one opaque KV record to the workspace record the domain shipped. */
|
|
25626
|
+
function workspaceRecordOf(value) {
|
|
25627
|
+
const record = value;
|
|
25628
|
+
if (typeof record !== "object" || record === null || typeof record.path !== "string" || typeof record.title !== "string" || !Array.isArray(record.sessionIds) || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string") throw new TypeError("workspace record does not match the stored shape");
|
|
25629
|
+
return {
|
|
25630
|
+
path: record.path,
|
|
25631
|
+
title: record.title,
|
|
25632
|
+
sessionIds: record.sessionIds.map((id) => id),
|
|
25633
|
+
createdAt: record.createdAt,
|
|
25634
|
+
updatedAt: record.updatedAt
|
|
25635
|
+
};
|
|
25636
|
+
}
|
|
25637
|
+
/** Narrow one opaque KV global to the workspace registry state the domain shipped. */
|
|
25638
|
+
function workspaceStateOf(value) {
|
|
25639
|
+
const state = value;
|
|
25640
|
+
if (typeof state !== "object" || state === null || typeof state.initialized !== "boolean" || !Array.isArray(state.workspaceIds) || !Array.isArray(state.archivedSessionIds)) throw new TypeError("workspace registry state does not match the stored shape");
|
|
25641
|
+
return {
|
|
25642
|
+
initialized: state.initialized,
|
|
25643
|
+
workspaceIds: state.workspaceIds.map((id) => id),
|
|
25644
|
+
archivedSessionIds: state.archivedSessionIds.map((id) => id),
|
|
25645
|
+
...state.pendingMutation === void 0 ? {} : { pendingMutation: state.pendingMutation }
|
|
25646
|
+
};
|
|
25647
|
+
}
|
|
25648
|
+
//#endregion
|
|
25649
|
+
//#region ../session-rdb/src/storage-takeover/index.ts
|
|
25650
|
+
/**
|
|
25651
|
+
* Install the storages takeover on the session-rdb plugin context.
|
|
25652
|
+
* @param ctx - session-rdb plugin context.
|
|
25653
|
+
* @param options - repository and projection-cache throttle parameters.
|
|
25654
|
+
*/
|
|
25655
|
+
function installStorageTakeover(ctx, options) {
|
|
25656
|
+
ctx.inject(["storage"], (storageCtx) => {
|
|
25657
|
+
const backend = new RdbStorageBackend(options.repository);
|
|
25658
|
+
storageCtx.effect(() => {
|
|
25659
|
+
const unregister = storageCtx.storage.backend.register("rdb", backend);
|
|
25660
|
+
return async () => {
|
|
25661
|
+
unregister();
|
|
25662
|
+
await backend.close();
|
|
25663
|
+
};
|
|
25664
|
+
}, "session-rdb.storageBackend");
|
|
25665
|
+
storageCtx.provide(storageBackendServiceKey("rdb"), backend);
|
|
25666
|
+
});
|
|
25667
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
25668
|
+
new SessionProjectionCacheRdb(projectionCtx, options.projectionCache, options.repository, options.ready);
|
|
25669
|
+
});
|
|
25670
|
+
}
|
|
25671
|
+
//#endregion
|
|
24644
25672
|
//#region ../session-rdb/src/index.ts
|
|
25673
|
+
/** 投影缓存默认写节流(与上游 base 装配的部署值一致)。 */
|
|
25674
|
+
const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
|
|
25675
|
+
const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5e3;
|
|
24645
25676
|
/** 会话级写所有权与 live 路由簿记。 */
|
|
24646
25677
|
var RdbBackendTracker = class {
|
|
24647
25678
|
name;
|
|
@@ -24910,11 +25941,25 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
24910
25941
|
"truncate",
|
|
24911
25942
|
"persist"
|
|
24912
25943
|
]).default("wal"),
|
|
24913
|
-
busyTimeout: Schema.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS)
|
|
25944
|
+
busyTimeout: Schema.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS),
|
|
25945
|
+
projectionCache: Schema.object({
|
|
25946
|
+
writeEveryEvents: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
|
|
25947
|
+
writeIntervalMs: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
|
|
25948
|
+
}).default({
|
|
25949
|
+
writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
|
|
25950
|
+
writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
|
|
25951
|
+
})
|
|
24914
25952
|
}), Schema.object({
|
|
24915
25953
|
type: Schema.const("postgres"),
|
|
24916
25954
|
connectionString: Schema.string().required(),
|
|
24917
|
-
schema: Schema.string().default("public")
|
|
25955
|
+
schema: Schema.string().default("public"),
|
|
25956
|
+
projectionCache: Schema.object({
|
|
25957
|
+
writeEveryEvents: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
|
|
25958
|
+
writeIntervalMs: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
|
|
25959
|
+
}).default({
|
|
25960
|
+
writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
|
|
25961
|
+
writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
|
|
25962
|
+
})
|
|
24918
25963
|
})]);
|
|
24919
25964
|
static settingsNs = "session-rdb";
|
|
24920
25965
|
name = "session-rdb";
|
|
@@ -24945,6 +25990,14 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
24945
25990
|
this.installLiveRouting(ctx);
|
|
24946
25991
|
new SessionBranchRdb(this.ctx);
|
|
24947
25992
|
registerSessionImport(this.ctx, this);
|
|
25993
|
+
installStorageTakeover(this.ctx, {
|
|
25994
|
+
repository: this.backend.storage,
|
|
25995
|
+
ready: this.ready,
|
|
25996
|
+
projectionCache: {
|
|
25997
|
+
writeEveryEvents: this.config.projectionCache?.writeEveryEvents ?? DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
|
|
25998
|
+
writeIntervalMs: this.config.projectionCache?.writeIntervalMs ?? DEFAULT_PROJECTION_WRITE_INTERVAL_MS
|
|
25999
|
+
}
|
|
26000
|
+
});
|
|
24948
26001
|
}
|
|
24949
26002
|
async init() {
|
|
24950
26003
|
await this.backend.open();
|
|
@@ -25455,6 +26508,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
25455
26508
|
}
|
|
25456
26509
|
if (eventRows.length > 0) await tx.insertEvents(eventRows);
|
|
25457
26510
|
await tx.insertBridges(bridgeRows);
|
|
26511
|
+
if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
|
|
25458
26512
|
return {
|
|
25459
26513
|
headEventId: parentId,
|
|
25460
26514
|
headSequence: nextSeq - 1
|