@morlay/session-rdb 0.0.20 → 0.0.21-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +59 -31
  2. package/dist/artifact.d.mts +25 -65
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{schema-BkQN5GyT.d.mts → backend-DpdtxYpz.d.mts} +346 -93
  5. package/dist/{branch-Co6xlbi0.mjs → branch-5JzX9rUq.mjs} +163 -52
  6. package/dist/deletion.d.mts +7 -0
  7. package/dist/deletion.mjs +2 -0
  8. package/dist/dist-vIVO6bA-.mjs +1524 -0
  9. package/dist/import.d.mts +4 -4
  10. package/dist/import.mjs +28 -11
  11. package/dist/index.d.mts +2 -3
  12. package/dist/index.mjs +4 -1631
  13. package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
  14. package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
  15. package/dist/src-CWTWV7vx.mjs +1904 -0
  16. package/dist/storage.d.mts +14 -5
  17. package/dist/storage.mjs +2 -2
  18. package/dist/testing.d.mts +1 -26
  19. package/dist/testing.mjs +10503 -9614
  20. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
  21. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
  22. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
  23. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
  24. package/package.json +28 -29
  25. package/src/adapters/to-postgres.ts +1 -3
  26. package/src/adapters/to-sqlite.ts +0 -2
  27. package/src/adapters/types.ts +0 -3
  28. package/src/artifact.ts +0 -2
  29. package/src/backend.ts +31 -2
  30. package/src/branch.ts +223 -135
  31. package/src/deletion.ts +87 -0
  32. package/src/drizzle/postgres-v2.ts +0 -1
  33. package/src/drizzle/postgres-v3.ts +0 -1
  34. package/src/drizzle/sqlite-v2.ts +0 -1
  35. package/src/drizzle/sqlite-v3.ts +0 -1
  36. package/src/entities/v2/session-events.ts +0 -1
  37. package/src/entities/v3/event-usage.ts +25 -0
  38. package/src/entities/v3/events.ts +1 -3
  39. package/src/entities/v3/index.ts +4 -0
  40. package/src/entities/v3/session-events.ts +1 -2
  41. package/src/entities/v3/session-projcache-rows.ts +0 -8
  42. package/src/entities/v3/sessions.ts +3 -3
  43. package/src/entities/v3/storage-units.ts +0 -1
  44. package/src/entities/v3/workspace-sessions.ts +0 -5
  45. package/src/entities/v3/workspace-state.ts +0 -6
  46. package/src/entities/v3/workspaces.ts +0 -5
  47. package/src/export.ts +103 -0
  48. package/src/gc.ts +76 -0
  49. package/src/import-storages.ts +4 -37
  50. package/src/import.ts +54 -31
  51. package/src/index.ts +150 -114
  52. package/src/legacy.ts +4 -42
  53. package/src/log.ts +63 -106
  54. package/src/postgres.ts +249 -22
  55. package/src/schema.ts +6 -6
  56. package/src/session-query.ts +0 -4
  57. package/src/sqlite.ts +231 -55
  58. package/src/storage-takeover/index.ts +2 -28
  59. package/src/storage-takeover/projection-cache.ts +30 -134
  60. package/src/storage-takeover/repository.ts +20 -59
  61. package/src/storage-takeover/storage-backend.ts +3 -33
  62. package/src/storage-takeover/types.ts +14 -56
  63. package/src/storage.ts +0 -2
  64. package/src/testing/contract.ts +19 -50
  65. package/src/testing/coordinator-contract.ts +10 -25
  66. package/src/testing.ts +1 -4
  67. package/src/usage.ts +191 -0
  68. package/dist/index-D55G9n4k.d.mts +0 -271
  69. package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
package/dist/index.mjs CHANGED
@@ -1,1631 +1,4 @@
1
- import { S as WriteGuard, a as EVENT_ENCODING, b as postgresTableDefs, c as eventDimensions, i as DEFAULT_BUSY_TIMEOUT_MS, o as SCHEMA_VERSION, r as createStorageRepository, t as SqliteBackend, x as toPostgresSchema } from "./sqlite-DYExtbLo.mjs";
2
- import { _ as toJsonlArtifact, c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView } from "./log-DO69NQnn.mjs";
3
- import { n as SessionBranchRdbProvider, r as locateTurnEnd, t as SessionBranchRdb } from "./branch-Co6xlbi0.mjs";
4
- import { registerSessionImport } from "./import.mjs";
5
- import z from "@deepseek-ai/schemastery";
6
- import { randomUUID } from "node:crypto";
7
- import { Pool } from "pg";
8
- import { drizzle } from "drizzle-orm/node-postgres";
9
- import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
10
- import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
11
- import { sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
12
- import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
13
- import { and, desc, eq, gte, sql } from "drizzle-orm";
14
- import { readdirSync } from "node:fs";
15
- import { fileURLToPath } from "node:url";
16
- import { migrate } from "drizzle-orm/node-postgres/migrator";
17
- import { SessionQueryEngine, SessionQueryError } from "@deepseek-ai/dsh-session-query";
18
- import { StorageError, storageBackendServiceKey } from "@deepseek-ai/dsh-storage";
19
- import { Service } from "@deepseek-ai/cordis";
20
- //#region src/postgres.ts
21
- /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
22
- const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
23
- var PostgresBackend = class PostgresBackend {
24
- db;
25
- options;
26
- kind = "postgres";
27
- storeIdentity;
28
- tables;
29
- /** storages 接管表访问层;句柄在 open()(含迁移)完成后解析。 */
30
- storage;
31
- opened;
32
- resolveOpened;
33
- rejectOpened;
34
- /** 事务期间的连接覆盖:storage 写方法经它加入当前事务。 */
35
- txOverride;
36
- constructor(db, options) {
37
- this.db = db;
38
- this.options = options;
39
- this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
40
- this.opened = new Promise((resolve, reject) => {
41
- this.resolveOpened = resolve;
42
- this.rejectOpened = reject;
43
- });
44
- this.opened.catch(() => {});
45
- this.storage = createStorageRepository({
46
- db: () => this.opened.then(() => this.txOverride ?? this.db),
47
- writeAtomically: (fn) => this.db.transaction(async (tx) => {
48
- const previous = this.txOverride;
49
- this.txOverride = tx;
50
- try {
51
- return await fn();
52
- } finally {
53
- this.txOverride = previous;
54
- }
55
- }, { isolationLevel: "serializable" }),
56
- tables: this.tables
57
- });
58
- }
59
- async open() {
60
- try {
61
- await this.doOpen();
62
- this.resolveOpened();
63
- } catch (error) {
64
- this.rejectOpened(error);
65
- throw error;
66
- }
67
- }
68
- async doOpen() {
69
- const schema = this.options.schema ?? "public";
70
- const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
71
- if ((await this.db.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true) {
72
- if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
73
- }
74
- await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
75
- const storeId = await this.db.transaction(async (tx) => {
76
- await tx.insert(this.tables["t_persistence_state"]).values({
77
- fSingleton: 1,
78
- fStoreId: randomUUID()
79
- }).onConflictDoNothing().execute();
80
- const id = (await tx.select({ fStoreId: this.tables["t_persistence_state"].fStoreId }).from(this.tables["t_persistence_state"]).where(eq(this.tables["t_persistence_state"].fSingleton, 1)).execute())[0]?.fStoreId;
81
- if (id === void 0 || id.length === 0) throw new Error("session database has no valid store identity");
82
- return id;
83
- });
84
- this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
85
- }
86
- /** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
87
- async baselineV2() {
88
- const dir = readdirSync(postgresMigrationsDir).find((name) => name.endsWith("_v2_initial"));
89
- if (dir === void 0) throw new Error("missing v2 baseline migration in drizzle/postgres");
90
- await this.db.execute(sql`
91
- CREATE SCHEMA IF NOT EXISTS drizzle
92
- `);
93
- await this.db.execute(sql`
94
- CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
95
- id SERIAL PRIMARY KEY,
96
- hash text NOT NULL,
97
- created_at bigint,
98
- name text,
99
- applied_at timestamp with time zone DEFAULT now()
100
- )
101
- `);
102
- await this.db.execute(sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`);
103
- }
104
- async close() {
105
- await this.options.close();
106
- }
107
- async getSession(id) {
108
- return (await this.db.select().from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
109
- }
110
- async getEventRows(id, fromSequence) {
111
- return (fromSequence === void 0 ? this.eventRows(this.db).where(eq(this.tables["t_session_events"].fSessionId, id)) : this.eventRows(this.db).where(and(eq(this.tables["t_session_events"].fSessionId, id), gte(this.tables["t_session_events"].fSequence, fromSequence)))).orderBy(this.tables["t_session_events"].fSequence).execute();
112
- }
113
- async listSessions() {
114
- return this.db.select().from(this.tables["t_sessions"]).execute();
115
- }
116
- async transaction(fn) {
117
- return this.db.transaction(async (tx) => fn(this.txFor(tx)));
118
- }
119
- txFor(tx) {
120
- return {
121
- upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
122
- getHead: (id) => this.getHead(tx, id),
123
- getSeedLength: (id) => this.getSeedLength(tx, id),
124
- updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
125
- insertEvents: (events) => this.insertEvents(tx, events),
126
- insertBridges: (rows) => this.insertBridges(tx, rows),
127
- updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
128
- bumpRevision: (id) => this.bumpRevision(tx, id),
129
- refreshTitle: (id) => this.refreshTitle(tx, id),
130
- deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
131
- getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence)
132
- };
133
- }
134
- async readMeta(exec, key) {
135
- return (await exec.select({ fValue: this.tables["t_schema_meta"].fValue }).from(this.tables["t_schema_meta"]).where(eq(this.tables["t_schema_meta"].fKey, key)).execute())[0]?.fValue;
136
- }
137
- async upsertSession(exec, storage, incarnation) {
138
- await exec.insert(this.tables["t_sessions"]).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
139
- target: this.tables["t_sessions"].fSessionId,
140
- set: sessionConflictRow(storage)
141
- }).execute();
142
- }
143
- async getHead(exec, id) {
144
- const head = (await exec.select({
145
- fHeadEventId: this.tables["t_sessions"].fHeadEventId,
146
- fHeadSequence: this.tables["t_sessions"].fHeadSequence
147
- }).from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
148
- /* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
149
- if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
150
- return head;
151
- }
152
- async getSeedLength(exec, id) {
153
- const row = (await exec.select({ fSeedLength: this.tables["t_sessions"].fSeedLength }).from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
154
- /* v8 ignore next -- rewind always materializes the row before reading the seed length */
155
- if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
156
- return row.fSeedLength;
157
- }
158
- async updateSeedLength(exec, id, seedLength) {
159
- await exec.update(this.tables["t_sessions"]).set({ fSeedLength: seedLength }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
160
- }
161
- static INSERT_BATCH_ROWS = 1e3;
162
- async insertEvents(exec, events) {
163
- if (events.length === 0) return;
164
- for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_events"]).values(events.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).execute();
165
- }
166
- async insertBridges(exec, rows) {
167
- if (rows.length === 0) return;
168
- for (let i = 0; i < rows.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_session_events"]).values(rows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).execute();
169
- }
170
- async updateHead(exec, id, headEventId, headSequence) {
171
- await exec.update(this.tables["t_sessions"]).set({
172
- fHeadEventId: headEventId,
173
- fHeadSequence: headSequence
174
- }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
175
- }
176
- async bumpRevision(exec, id) {
177
- 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();
178
- }
179
- /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
180
- async refreshTitle(exec, id) {
181
- const bridges = this.tables["t_session_events"];
182
- const entities = this.tables["t_events"];
183
- const row = (await exec.select({
184
- fSequence: bridges.fSequence,
185
- fData: entities.fData
186
- }).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];
187
- const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
188
- await exec.update(this.tables["t_sessions"]).set({
189
- fTitle: title ?? null,
190
- fTitleSeq: title === void 0 ? null : row.fSequence
191
- }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
192
- }
193
- async deleteBridgeTail(exec, id, fromSequence) {
194
- 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();
195
- }
196
- async getPrevBridge(exec, id, sequence) {
197
- return (await exec.select({
198
- fEventId: this.tables["t_session_events"].fEventId,
199
- fSequence: this.tables["t_session_events"].fSequence
200
- }).from(this.tables["t_session_events"]).where(and(eq(this.tables["t_session_events"].fSessionId, id), eq(this.tables["t_session_events"].fSequence, sequence))).execute())[0];
201
- }
202
- eventRows(exec) {
203
- return exec.select({
204
- fEventId: this.tables["t_session_events"].fEventId,
205
- fSequence: this.tables["t_session_events"].fSequence,
206
- fType: this.tables["t_events"].fType,
207
- fKind: this.tables["t_events"].fKind,
208
- fRole: this.tables["t_events"].fRole,
209
- fName: this.tables["t_events"].fName,
210
- fActionId: this.tables["t_events"].fActionId,
211
- fCreatedAt: this.tables["t_events"].fCreatedAt,
212
- fData: this.tables["t_events"].fData,
213
- fSurfaceOp: this.tables["t_session_events"].fSurfaceOp
214
- }).from(this.tables["t_session_events"]).innerJoin(this.tables["t_events"], eq(this.tables["t_session_events"].fEventId, this.tables["t_events"].fEventId));
215
- }
216
- };
217
- //#endregion
218
- //#region src/session-query.ts
219
- var SessionQueryRdb = class extends SessionQueryEngine {
220
- constructor(ctx, config = {}) {
221
- super(ctx, config);
222
- }
223
- async searchSessions() {
224
- throw searchDisabled();
225
- }
226
- async searchEvents() {
227
- throw searchDisabled();
228
- }
229
- };
230
- function searchDisabled() {
231
- return new SessionQueryError("session search is disabled: this deployment serves session queries from the rdb backend without a full-text index", "SESSION_QUERY_SEARCH_DISABLED");
232
- }
233
- //#endregion
234
- //#region src/legacy.ts
235
- /** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
236
- function physicalHeader(row) {
237
- const common = {
238
- type: "session",
239
- version: row.fVersion,
240
- id: row.fSessionId,
241
- createdAt: row.fCreatedAt,
242
- ...row.fCwd !== null ? { cwd: row.fCwd } : {},
243
- ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
244
- ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
245
- delegationDepth: row.fDelegationDepth ?? 0
246
- };
247
- return row.fVersion < 2 ? {
248
- ...common,
249
- ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}
250
- } : {
251
- ...common,
252
- isSeeded: row.fSeedLength !== null
253
- };
254
- }
255
- /** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
256
- function physicalEvent(row) {
257
- const stored = JSON.parse(row.fData);
258
- const full = typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof stored["type"] === "string" && "data" in stored;
259
- return {
260
- type: row.fType,
261
- seq: row.fSequence,
262
- time: row.fCreatedAt,
263
- data: full ? stored["data"] : stored,
264
- ...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
265
- };
266
- }
267
- /**
268
- * 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
269
- * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
270
- * @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
271
- * @returns 当前逻辑 header、继承前缀长度与事件。
272
- * @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
273
- */
274
- function convertLegacyRows(row, eventRows) {
275
- const restore = sessionFormatCatalog.createRestore(physicalHeader(row), {
276
- recovery: "strict",
277
- validation: "transformed"
278
- });
279
- for (const eventRow of eventRows) restore.decodeRow(physicalEvent(eventRow));
280
- const artifact = restore.finish();
281
- return {
282
- meta: artifact.header,
283
- inheritedEventCount: artifact.inheritedEventCount,
284
- events: artifact.events
285
- };
286
- }
287
- /** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
288
- function isLegacyVersion(version) {
289
- return version < SESSION_FORMAT_VERSION;
290
- }
291
- /**
292
- * 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
293
- *
294
- * 旧写入器只在建会话时落一次 header version,之后跟随上游演进继续追加
295
- * 事件——同一个 log 因此可能是混合世代(v0 时代的行 + 新版本字段),不是
296
- * 任何单一已发布格式,严格迁移链必然拒绝。这些行的数据形状(消息包装、
297
- * 类型名)已经是当前格式,只需 header 版本归一到当前、按稠密 seq 读取;
298
- * 事件数据的补全与 surface 修复由读取视图修复(repairReadView)负责。
299
- *
300
- * 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
301
- * (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
302
- *
303
- * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
304
- * @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
305
- * @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
306
- * 继承前缀长度、稠密事件与可选 torn tail 起点。
307
- * @throws scanRows 的已提交区损坏错误(seq gap / 不可解析行)。
308
- */
309
- function adoptLegacyRows(row, eventRows) {
310
- const { preserved, tornFrom } = scanRows(eventRows, 0);
311
- repairRequestHeaders(preserved);
312
- return {
313
- meta: {
314
- ...rowToMeta(row),
315
- version: SESSION_FORMAT_VERSION
316
- },
317
- inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
318
- events: preserved,
319
- ...tornFrom !== void 0 ? { tornFrom } : {}
320
- };
321
- }
322
- //#endregion
323
- //#region src/storage-takeover/projection-cache.ts
324
- /**
325
- * 投影 checkpoint 服务(`ctx.sessionProjectionCache`)的 rdb 实现:替换上游
326
- * `@deepseek-ai/dsh-session-projection-cache` 插件,公开面与语义逐一对齐
327
- * (cachedSnapshot / cachedPredecessorTitle / hydratePrepared / write /
328
- * coldSnapshot,以及三个强制写点与计数/定时节流),持久层换成 session-rdb
329
- * 的语义专用表 `t_session_projcache` / `t_session_projcache_row`。
330
- *
331
- * 读方法是同步签名(session 列表在请求路径上直接调用),所以服务持有启动时
332
- * 从表中加载的内存 checkpoint 表;写先落库、后更新内存,保证读到的内存值
333
- * 磁盘上一定存在。
334
- */
335
- /** 服务注册名(与上游插件一致,消费者经 `ctx.get` 解析)。 */
336
- const SESSION_PROJECTION_CACHE_SERVICE = "sessionProjectionCache";
337
- /** 只认 title 的前代提示(与上游一致)。 */
338
- const PREDECESSOR_TITLE_KEY = "title";
339
- /** rdb 持久化的投影 checkpoint 服务:同步驱动直读介质,异步驱动用写穿镜像支撑同步签名。 */
340
- var SessionProjectionCacheRdb = class extends Service {
341
- static inject = ["sessionProjections", "sessions"];
342
- /** 异步驱动(PostgreSQL)的同步读镜像:同步驱动(SQLite)直读介质,这里恒为空。 */
343
- records = /* @__PURE__ */ new Map();
344
- dirty = /* @__PURE__ */ new Map();
345
- /** 读路径是否直读介质(`readProjcacheSync` 存在即同步驱动)。 */
346
- directReads;
347
- config;
348
- repository;
349
- ready;
350
- /**
351
- * 本类按 cordis class plugin 装载(`ctx.plugin(SessionProjectionCacheRdb, options)`):
352
- * `static inject` 等依赖就绪后构造,构造后框架调用 `[Service.init]` 安装写入
353
- * 路径——直接 `new` 不会触发 init,写入路径会静默缺失。
354
- * @param ctx - 插件上下文(已注入 sessionProjections 与 sessions)。
355
- * @param options - 写节流 + storages 接管访问层 + 介质就绪信号。
356
- */
357
- constructor(ctx, options) {
358
- super(ctx, SESSION_PROJECTION_CACHE_SERVICE);
359
- this.config = options;
360
- this.repository = options.repository;
361
- this.ready = options.ready;
362
- this.directReads = options.repository.readProjcacheSync !== void 0;
363
- }
364
- /**
365
- * Install the write path before the async medium wait: listeners must exist
366
- * from plugin activation on, or sessions created while the medium settles
367
- * lose their mandatory checkpoints. Only the async driver needs the startup
368
- * mirror; the sync driver serves every read from the table.
369
- */
370
- async [Service.init]() {
371
- this.installWritePath();
372
- await this.ready;
373
- const pruned = await this.repository.pruneStaleProjcache();
374
- if (pruned > 0) this.ctx.logger.info(`session projection cache: pruned stale snapshot(s) for ${String(pruned)} session(s)`);
375
- if (!this.directReads) for (const entry of await this.repository.loadProjcache()) this.records.set(entry.sessionId, entry);
376
- }
377
- /** Read one session's stored record: straight from the medium, or from the async mirror. */
378
- lookup(id) {
379
- if (this.directReads) return this.repository.readProjcacheSync?.(id);
380
- return this.records.get(id);
381
- }
382
- /**
383
- * The stored record for one session, accepted only when its bound log
384
- * identity matches `expected`. A session id names a slot, not a lifecycle:
385
- * a recreated id or a persistence store swapped under a surviving cache
386
- * must not let an old record seed state folded from an unrelated log.
387
- * @param id - the session whose record is read.
388
- * @param expected - the log identity the caller holds (live or stored header).
389
- * @returns the identity-matching record, or `undefined` (absent or unrelated).
390
- */
391
- recordFor(id, expected) {
392
- const record = this.lookup(id);
393
- if (record === void 0) return void 0;
394
- return identityMatches(record.identity, expected) ? record : void 0;
395
- }
396
- /**
397
- * The cached projection cut for one stored (cold) or live header.
398
- * @param meta - authoritative Session header.
399
- * @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
400
- * @param keys - optional projection keys required by the caller's audience.
401
- * @returns the cut (`asOfSeq` = lowest served-row watermark), or `undefined`
402
- * when no usable row exists for this lifecycle.
403
- */
404
- cachedSnapshot(meta, inheritedEventCount, keys) {
405
- const record = this.recordFor(meta.id, identityOf(meta, inheritedEventCount));
406
- const snapshot = record === void 0 ? void 0 : this.viewRecord(record, keys);
407
- return this.withDirectTitle(meta.id, keys, snapshot);
408
- }
409
- /**
410
- * 标题是会话数据本身(`t_sessions.f_title`,由 rdb 写路径与 rewind 维护):
411
- * checkpoint 行里没有 title 时直接取该列,列表消费不依赖缓存行是否存在。
412
- */
413
- withDirectTitle(id, keys, snapshot) {
414
- if (keys !== void 0 && !keys.includes(PREDECESSOR_TITLE_KEY)) return snapshot;
415
- if (snapshot?.values[PREDECESSOR_TITLE_KEY] !== void 0) return snapshot;
416
- const direct = this.repository.readSessionTitleSync?.(id);
417
- if (direct === void 0) return snapshot;
418
- const values = {
419
- ...snapshot?.values,
420
- [PREDECESSOR_TITLE_KEY]: direct.title
421
- };
422
- return {
423
- asOfSeq: snapshot === void 0 ? direct.seq : Math.min(snapshot.asOfSeq, direct.seq),
424
- values
425
- };
426
- }
427
- /**
428
- * Read only a predecessor checkpoint's title as a zero-I/O listing hint.
429
- * @param meta - authoritative listed Session header.
430
- * @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
431
- * @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
432
- * when the record is current, newer, unrelated, missing, or incompatible
433
- * with the title unit.
434
- */
435
- cachedPredecessorTitle(meta, inheritedEventCount) {
436
- const expected = identityOf(meta, inheritedEventCount);
437
- const record = this.lookup(meta.id);
438
- if (record === void 0 || !predecessorIdentityMatches(record.identity, expected)) return;
439
- const title = this.viewRecord(record, [PREDECESSOR_TITLE_KEY]);
440
- return title === void 0 ? void 0 : {
441
- ...title,
442
- asOfSeq: -1
443
- };
444
- }
445
- /** View selected wire rows and bind them to their lowest served watermark. */
446
- viewRecord(record, keys) {
447
- const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys);
448
- const servedKeys = Object.keys(values);
449
- if (servedKeys.length === 0) return void 0;
450
- const firstKey = servedKeys[0];
451
- let asOfSeq = record.rows[firstKey].seq;
452
- for (const key of servedKeys.slice(1)) {
453
- const row = record.rows[key];
454
- if (row.seq < asOfSeq) asOfSeq = row.seq;
455
- }
456
- return {
457
- asOfSeq,
458
- values
459
- };
460
- }
461
- /**
462
- * Hydrate projection cells for an already-prepared Session without another
463
- * persistence read. The cache seeds matching rows; the supplied exact log
464
- * advances every unit to the observation cut. No checkpoint is written
465
- * because the logical observation may contain recovery events not yet durable.
466
- * @param session - exact unpublished Session retained by persistence.
467
- * @param events - exact logical event prefix represented by the observation.
468
- * @returns all projection values at the event cut.
469
- */
470
- hydratePrepared(session, events) {
471
- const record = this.recordFor(session.id, identityOf(session.header, session.inheritedEventCount));
472
- if (record === void 0) return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
473
- try {
474
- return this.ctx.sessionProjections.hydrate(session, record.rows, events, SessionLogOffset(0));
475
- } catch {
476
- return this.ctx.sessionProjections.hydrate(session, {}, events, SessionLogOffset(0));
477
- }
478
- }
479
- /**
480
- * Durably checkpoint one live session NOW (all mandatory points call this).
481
- * NOT fail-soft — callers on the fail-soft paths contain it.
482
- * @param session - the live session to checkpoint.
483
- * @returns resolution after durability.
484
- */
485
- async write(session) {
486
- const rows = this.ctx.sessionProjections.checkpoint(session);
487
- this.markClean(session);
488
- if (this.ctx.sessions.get(session.id) === session) await this.ctx.sessions.flush(session);
489
- await this.put(session.id, identityOf(session.header, session.inheritedEventCount), rows);
490
- }
491
- /**
492
- * Cold-read one session's projections from its complete log. Each unit is
493
- * seeded from the identity-checked cached rows and the refreshed checkpoint
494
- * is written back (fail-soft, fire-and-forget).
495
- * @param meta - the stored session header (identity witness).
496
- * @param inheritedEventCount - exact inherited prefix length for projection initialization and identity.
497
- * @param events - the session's complete log, in seq order.
498
- * @returns the projection cut at the log end.
499
- */
500
- coldSnapshot(meta, inheritedEventCount, events) {
501
- const identity = identityOf(meta, inheritedEventCount);
502
- const restored = this.ctx.sessionProjections.restore(this.recordFor(meta.id, identity)?.rows ?? {}, events, SessionLogOffset(0), meta, inheritedEventCount);
503
- this.put(meta.id, identity, restored.checkpoint).catch((error) => {
504
- this.ctx.logger.warn(`session projection cache: cold-read write-back for "${meta.id}" failed (cache stays stale): ${String(error)}`);
505
- });
506
- return restored.snapshot;
507
- }
508
- installWritePath() {
509
- this.ctx.on("session/event", (session, event) => {
510
- if (event.type === "turn/end") {
511
- this.flushSoft(session, "turn/end");
512
- return;
513
- }
514
- const state = this.dirty.get(session) ?? {
515
- pending: 0,
516
- timer: void 0
517
- };
518
- this.dirty.set(session, state);
519
- state.pending += 1;
520
- if (state.pending >= this.config.writeEveryEvents) {
521
- this.flushSoft(session, "count threshold");
522
- return;
523
- }
524
- state.timer ??= setTimeout(() => {
525
- this.flushSoft(session, "interval");
526
- }, this.config.writeIntervalMs);
527
- });
528
- this.ctx.on("session/created", (session) => {
529
- this.flushSoft(session, "create");
530
- });
531
- this.ctx.on("session/disposed", (session) => {
532
- this.flushSoft(session, "detach");
533
- this.markClean(session);
534
- this.dirty.delete(session);
535
- });
536
- this.ctx.effect(() => () => {
537
- for (const state of this.dirty.values()) if (state.timer !== void 0) clearTimeout(state.timer);
538
- this.dirty.clear();
539
- }, "sessionProjectionCacheRdb.timers");
540
- }
541
- /** One fail-soft durable checkpoint. */
542
- async flushSoft(session, trigger) {
543
- try {
544
- await this.write(session);
545
- } catch (error) {
546
- this.ctx.logger.warn(`session projection cache: ${trigger} write for "${session.id}" failed (cache stays stale): ${String(error)}`);
547
- }
548
- }
549
- /** Reset one session's dirty bookkeeping (its checkpoint is being written). */
550
- markClean(session) {
551
- const state = this.dirty.get(session);
552
- if (state === void 0) return;
553
- state.pending = 0;
554
- if (state.timer !== void 0) {
555
- clearTimeout(state.timer);
556
- state.timer = void 0;
557
- }
558
- }
559
- /** Replace one session's stored record with its log identity and a detached snapshot of `rows`. */
560
- async put(id, identity, rows) {
561
- const detached = detachJson(rows);
562
- await this.repository.putProjcache(id, detached);
563
- if (!this.directReads) this.records.set(id, {
564
- sessionId: id,
565
- identity,
566
- rows: detached
567
- });
568
- }
569
- };
570
- /** Detach one checkpoint from live unit state, refusing non-lossless JSON. */
571
- function detachJson(rows) {
572
- let text;
573
- try {
574
- text = JSON.stringify(rows);
575
- } catch (error) {
576
- throw new TypeError(`projection checkpoint is not losslessly JSON-serializable: ${String(error)}`, { cause: error });
577
- }
578
- if (text === void 0) throw new TypeError("projection checkpoint is not losslessly JSON-serializable");
579
- return JSON.parse(text);
580
- }
581
- /** Project a header onto the identity fields a record is bound to. */
582
- function identityOf(header, inheritedEventCount) {
583
- const cut = SessionLogOffset(inheritedEventCount);
584
- if (!header.isSeeded && cut !== 0) throw new Error("unseeded projection-cache identity inherited event count must be 0");
585
- return {
586
- formatVersion: header.version,
587
- createdAt: header.createdAt,
588
- ...header.cwd === void 0 ? {} : { cwd: header.cwd },
589
- isSeeded: header.isSeeded,
590
- inheritedEventCount: cut
591
- };
592
- }
593
- /**
594
- * Whether a stored record's bound identity names the caller's lifecycle.
595
- * An absent format generation cannot prove the fold semantics and never
596
- * matches. Once the format matches, absent lineage fields (records admitted
597
- * via compatible versions predate them) read as the unseeded lineage.
598
- */
599
- function identityMatches(stored, expected) {
600
- return stored.formatVersion === expected.formatVersion && lifecycleIdentityMatches(stored, expected);
601
- }
602
- /** Match one predecessor cache record to the authoritative listed lifecycle. */
603
- function predecessorIdentityMatches(stored, expected) {
604
- return (stored.formatVersion === void 0 || stored.formatVersion < expected.formatVersion) && lifecycleIdentityMatches(stored, expected);
605
- }
606
- /** Match the format-independent fields that distinguish one Session lifecycle. */
607
- function lifecycleIdentityMatches(stored, expected) {
608
- return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
609
- }
610
- //#endregion
611
- //#region src/storage-takeover/storage-backend.ts
612
- /**
613
- * storages 接管:注册在 storage hub 上的 `rdb` KV 后端。它把 workspace 域
614
- * (上游 single 布局:一张 `workspaces` 表 + global 单例)映射到 session-rdb
615
- * 的语义专用表,与事件日志同库同连接;域层(`ctx.storageDomain`)通过
616
- * `routes` 把 `workspace` 域路由到本后端。
617
- *
618
- * 本后端只服务已知域的专用表:未知 unit 名 fail loud,而不是静默落到通用
619
- * 存储——表结构是显式维护的(见 docs/schema.md)。
620
- */
621
- /** 本后端唯一服务的域:上游 `workspaceDomainSpec`(v2,single 布局)。 */
622
- const WORKSPACE_UNIT = "workspace";
623
- const WORKSPACE_TABLE = "workspaces";
624
- /** 只服务 workspace 域的 KV 后端。 */
625
- var RdbStorageBackend = class {
626
- repository;
627
- kv = { open: (descriptor) => this.openUnit(descriptor) };
628
- /** 已打开(或打开中)的 unit;重复 open 是调用方 bug。 */
629
- open = /* @__PURE__ */ new Map();
630
- closed = false;
631
- /**
632
- * @param repository - storages 接管表访问层(与事件日志同介质)。
633
- */
634
- constructor(repository) {
635
- this.repository = repository;
636
- }
637
- async openUnit(descriptor) {
638
- if (this.closed) throw new StorageError("closed", "rdb storage backend is closed");
639
- if (descriptor.name !== WORKSPACE_UNIT) throw new Error(`rdb storage backend serves only the '${WORKSPACE_UNIT}' domain (requested '${descriptor.name}')`);
640
- 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)`);
641
- if (descriptor.layout !== void 0 && descriptor.layout !== "single") throw new Error(`rdb storage backend serves only the 'single' layout (domain '${descriptor.name}' declares '${descriptor.layout}')`);
642
- if (this.open.has(descriptor.name)) throw new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`);
643
- const unit = new WorkspaceKvUnit(this.repository, descriptor);
644
- this.open.set(descriptor.name, unit);
645
- try {
646
- const stored = await this.repository.readUnitVersion(descriptor.name);
647
- if (stored === void 0) await this.repository.insertUnitVersion(descriptor.name, descriptor.version);
648
- 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}`);
649
- } catch (error) {
650
- this.open.delete(descriptor.name);
651
- throw error;
652
- }
653
- unit.onClose(() => {
654
- this.open.delete(descriptor.name);
655
- });
656
- return unit;
657
- }
658
- /**
659
- * Release the backend. New writes are rejected immediately; already-queued
660
- * unit writes drain first (upstream backend contract). The medium (the
661
- * session database) belongs to the owning session-rdb plugin, so closing
662
- * open units here does not close it.
663
- * @returns resolution after every open unit drained.
664
- */
665
- async close() {
666
- this.closed = true;
667
- const units = [...this.open.values()];
668
- this.open.clear();
669
- await Promise.all(units.map((unit) => unit.close()));
670
- }
671
- };
672
- /** workspace 域的 KV unit:每个原语一条 SQL 语句,值形状与上游记录一致。 */
673
- var WorkspaceKvUnit = class {
674
- repository;
675
- descriptor;
676
- closed = false;
677
- /** 在途写操作:close 先拒绝新写,再 drain 它们(不丢已受理的写)。 */
678
- inflight = /* @__PURE__ */ new Set();
679
- onClosed;
680
- constructor(repository, descriptor) {
681
- this.repository = repository;
682
- this.descriptor = descriptor;
683
- }
684
- /** Install the name-release callback after the backend registered this unit. */
685
- onClose(release) {
686
- this.onClosed = release;
687
- }
688
- async loadAll() {
689
- this.assertOpen();
690
- const records = Object.create(null);
691
- for (const { id, record } of await this.repository.listWorkspaces()) records[id] = record;
692
- const state = await this.repository.readWorkspaceState();
693
- return {
694
- tables: { [WORKSPACE_TABLE]: records },
695
- global: state
696
- };
697
- }
698
- async putRecord(table, key, value) {
699
- this.assertOpen();
700
- this.assertTable(table);
701
- await this.track(this.repository.putWorkspace(key, workspaceRecordOf(value)));
702
- }
703
- async deleteRecord(table, key) {
704
- this.assertOpen();
705
- this.assertTable(table);
706
- await this.track(this.repository.deleteWorkspace(key));
707
- }
708
- async setGlobal(value) {
709
- this.assertOpen();
710
- await this.track(this.repository.writeWorkspaceState(workspaceStateOf(value)));
711
- }
712
- async close() {
713
- if (this.closed) return;
714
- this.closed = true;
715
- while (this.inflight.size > 0) await Promise.allSettled(this.inflight);
716
- this.onClosed?.();
717
- }
718
- /** Register one accepted write so close can drain it. */
719
- async track(operation) {
720
- const settled = operation.then(() => void 0, () => void 0);
721
- this.inflight.add(settled);
722
- try {
723
- await operation;
724
- } finally {
725
- this.inflight.delete(settled);
726
- }
727
- }
728
- assertOpen() {
729
- if (this.closed) throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
730
- }
731
- assertTable(table) {
732
- if (table !== WORKSPACE_TABLE) throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
733
- }
734
- };
735
- /** Narrow one opaque KV record to the workspace record the domain shipped. */
736
- function workspaceRecordOf(value) {
737
- const record = value;
738
- 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");
739
- return {
740
- path: record.path,
741
- title: record.title,
742
- sessionIds: record.sessionIds.map((id) => id),
743
- createdAt: record.createdAt,
744
- updatedAt: record.updatedAt
745
- };
746
- }
747
- /** Narrow one opaque KV global to the workspace registry state the domain shipped. */
748
- function workspaceStateOf(value) {
749
- const state = value;
750
- 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");
751
- return {
752
- initialized: state.initialized,
753
- workspaceIds: state.workspaceIds.map((id) => id),
754
- archivedSessionIds: state.archivedSessionIds.map((id) => id),
755
- ...state.pendingMutation === void 0 ? {} : { pendingMutation: state.pendingMutation }
756
- };
757
- }
758
- //#endregion
759
- //#region src/storage-takeover/index.ts
760
- /**
761
- * Install the storages takeover on the session-rdb plugin context.
762
- * @param ctx - session-rdb plugin context.
763
- * @param options - repository and projection-cache throttle parameters.
764
- */
765
- function installStorageTakeover(ctx, options) {
766
- ctx.inject(["storage"], (storageCtx) => {
767
- const backend = new RdbStorageBackend(options.repository);
768
- storageCtx.effect(() => {
769
- const unregister = storageCtx.storage.backend.register("rdb", backend);
770
- return async () => {
771
- unregister();
772
- await backend.close();
773
- };
774
- }, "session-rdb.storageBackend");
775
- storageCtx.provide(storageBackendServiceKey("rdb"), backend);
776
- });
777
- ctx.plugin(SessionProjectionCacheRdb, {
778
- ...options.projectionCache,
779
- repository: options.repository,
780
- ready: options.ready
781
- });
782
- }
783
- //#endregion
784
- //#region src/index.ts
785
- /** 投影缓存默认写节流(与上游 base 装配的部署值一致)。 */
786
- const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
787
- const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5e3;
788
- /** 会话级写所有权与 live 路由簿记。 */
789
- var RdbBackendTracker = class {
790
- name;
791
- /** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
792
- writers = /* @__PURE__ */ new Map();
793
- pending = /* @__PURE__ */ new Map();
794
- openHandles = /* @__PURE__ */ new Set();
795
- counter = 0;
796
- constructor(name) {
797
- this.name = name;
798
- }
799
- registerCreated(header, inheritedEventCount) {
800
- if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
801
- this.writers.set(header.id, null);
802
- this.pending.set(header.id, {
803
- header,
804
- revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
805
- inheritedEventCount,
806
- cursor: 0,
807
- everAppended: false
808
- });
809
- }
810
- /** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
811
- updatePending(id, cursor, everAppended) {
812
- const entry = this.pending.get(id);
813
- if (entry === void 0) return;
814
- this.pending.set(id, {
815
- ...entry,
816
- cursor,
817
- everAppended
818
- });
819
- }
820
- claimWrite(id) {
821
- if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
822
- this.writers.set(id, null);
823
- }
824
- releaseClaim(id) {
825
- this.writers.delete(id);
826
- }
827
- pendingOf(id) {
828
- return this.pending.get(id);
829
- }
830
- hasPending(id) {
831
- return this.pending.has(id);
832
- }
833
- pendingEntries() {
834
- return this.pending.entries();
835
- }
836
- materialized(id) {
837
- this.pending.delete(id);
838
- }
839
- adopt(handle) {
840
- this.openHandles.add(handle);
841
- if (handle.access === "write") this.writers.set(handle.id, handle);
842
- return handle;
843
- }
844
- release(handle, materialized) {
845
- this.openHandles.delete(handle);
846
- if (handle.access !== "write") return;
847
- this.writers.delete(handle.id);
848
- if (!materialized) this.pending.delete(handle.id);
849
- }
850
- writerOf(id) {
851
- const writer = this.writers.get(id);
852
- return writer === null ? void 0 : writer;
853
- }
854
- async flushAll() {
855
- const errors = [];
856
- for (const writer of this.writers.values()) {
857
- if (writer === null) continue;
858
- try {
859
- await writer.drainLive();
860
- await writer.flush();
861
- } catch (error) {
862
- if (error instanceof SessionHandleClosedError) continue;
863
- errors.push(error);
864
- }
865
- }
866
- if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
867
- }
868
- async closeAll() {
869
- const errors = [];
870
- for (const handle of this.openHandles) try {
871
- await handle.close();
872
- } catch (error) {
873
- errors.push(error);
874
- }
875
- if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
876
- }
877
- };
878
- /** 一个打开会话的存储句柄:read / append / flush / close。 */
879
- var RdbSessionHandle = class RdbSessionHandle {
880
- persistence;
881
- id;
882
- header;
883
- access;
884
- state;
885
- chain = Promise.resolve();
886
- closing;
887
- /** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
888
- cursor;
889
- materialized;
890
- /** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
891
- buffered = [];
892
- batchTimer;
893
- drainPaused = false;
894
- draining;
895
- /** write open 时发现的 torn tail 起点(append 前先截断)。 */
896
- tornTruncateTo;
897
- /** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
898
- everAppended = false;
899
- constructor(persistence, id, header, access, state) {
900
- this.persistence = persistence;
901
- this.id = id;
902
- this.header = header;
903
- this.access = access;
904
- this.state = state;
905
- this.cursor = state.cursor;
906
- this.materialized = state.materialized;
907
- this.tornTruncateTo = state.tornTruncateTo;
908
- }
909
- get inheritedEventCount() {
910
- return this.state.inheritedEventCount;
911
- }
912
- /** 输入空间 cursor(下一个待落库的上游 seq)。 */
913
- get cursorValue() {
914
- return this.cursor;
915
- }
916
- async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
917
- this.assertOpen("read");
918
- if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
919
- if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
920
- options?.signal?.throwIfAborted();
921
- const log = await this.persistence.readLog(this.id, {}, options?.signal);
922
- if (log === void 0) {
923
- if (this.persistence.tracker.hasPending(this.id)) return {
924
- eventState: "detached",
925
- events: []
926
- };
927
- throw new SessionPersistenceNotFoundError(this.id);
928
- }
929
- repairReadView(log.events);
930
- return {
931
- eventState: "detached",
932
- events: log.events.slice(offset, offset + length)
933
- };
934
- }
935
- async append(events, options) {
936
- this.assertOpen("append");
937
- const batch = materializeAppendBatch(events);
938
- return this.run("append", async () => {
939
- options?.signal?.throwIfAborted();
940
- if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
941
- if (batch.length === 0) return;
942
- this.everAppended = true;
943
- assertContiguous(this.id, batch, this.cursor);
944
- await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
945
- this.tornTruncateTo = void 0;
946
- this.cursor += batch.length;
947
- this.materialized = true;
948
- this.persistence.tracker.updatePending(this.id, this.cursor, true);
949
- });
950
- }
951
- async flush(options) {
952
- return this.run("flush", async () => {
953
- options?.signal?.throwIfAborted();
954
- if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
955
- if (this.materialized) return;
956
- await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
957
- this.materialized = true;
958
- });
959
- }
960
- close() {
961
- return this.closing ??= (async () => {
962
- let drainFailure;
963
- for (;;) {
964
- try {
965
- await this.drainLive();
966
- } catch (error) {
967
- drainFailure = error;
968
- break;
969
- }
970
- await this.chain;
971
- if (this.buffered.length === 0) break;
972
- }
973
- await this.chain;
974
- const failures = [];
975
- if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
976
- this.persistence.tracker.release(this, this.materialized || this.everAppended);
977
- if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
978
- if (failures[0] !== void 0) throw failures[0];
979
- })();
980
- }
981
- [Symbol.asyncDispose]() {
982
- return this.close();
983
- }
984
- /** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
985
- enqueueLive(event, reportBackgroundFailure) {
986
- this.buffered.push(structuredClone(event));
987
- if (this.batchTimer !== void 0 || this.drainPaused) return;
988
- this.batchTimer = setTimeout(() => {
989
- this.batchTimer = void 0;
990
- this.drainLive().catch(reportBackgroundFailure);
991
- }, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
992
- }
993
- /** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
994
- resetAfterRewind(cursor, inheritedEventCount) {
995
- this.cursor = cursor;
996
- if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
997
- }
998
- /** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
999
- drainLive() {
1000
- return this.draining ??= this.drainBuffered().finally(() => {
1001
- this.draining = void 0;
1002
- });
1003
- }
1004
- async drainBuffered() {
1005
- if (this.batchTimer !== void 0) {
1006
- clearTimeout(this.batchTimer);
1007
- this.batchTimer = void 0;
1008
- }
1009
- this.drainPaused = false;
1010
- while (this.buffered.length > 0) await this.enqueueChain(async () => {
1011
- const batch = this.buffered.splice(0);
1012
- try {
1013
- const fresh = batch.filter((event) => event.seq >= this.cursor);
1014
- if (fresh.length === 0) return;
1015
- for (const [index, event] of fresh.entries()) if (event.seq !== this.cursor + index) throw new Error(`append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`);
1016
- await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
1017
- this.tornTruncateTo = void 0;
1018
- this.cursor += fresh.length;
1019
- this.materialized = true;
1020
- } catch (error) {
1021
- this.buffered = batch.concat(this.buffered);
1022
- this.drainPaused = true;
1023
- throw error;
1024
- }
1025
- });
1026
- }
1027
- enqueueChain(op) {
1028
- const next = this.chain.then(op);
1029
- this.chain = next.catch(() => {});
1030
- return next;
1031
- }
1032
- async run(operation, op) {
1033
- this.assertOpen(operation);
1034
- return this.enqueueChain(async () => {
1035
- this.assertOpen(operation);
1036
- return op();
1037
- });
1038
- }
1039
- assertOpen(operation) {
1040
- if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
1041
- }
1042
- static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
1043
- };
1044
- var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
1045
- config;
1046
- static inject = ["sessions", "settings"];
1047
- static Config = z.union([z.object({
1048
- type: z.const("sqlite"),
1049
- path: z.string().required(),
1050
- journalMode: z.union([
1051
- "wal",
1052
- "delete",
1053
- "truncate",
1054
- "persist"
1055
- ]).default("wal"),
1056
- busyTimeout: z.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS),
1057
- projectionCache: z.object({
1058
- writeEveryEvents: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
1059
- writeIntervalMs: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
1060
- }).default({
1061
- writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
1062
- writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
1063
- })
1064
- }), z.object({
1065
- type: z.const("postgres"),
1066
- connectionString: z.string().required(),
1067
- schema: z.string().default("public"),
1068
- projectionCache: z.object({
1069
- writeEveryEvents: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS),
1070
- writeIntervalMs: z.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS)
1071
- }).default({
1072
- writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
1073
- writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
1074
- })
1075
- })]);
1076
- static settingsNs = "session-rdb";
1077
- name = "session-rdb";
1078
- tracker = new RdbBackendTracker(this.name);
1079
- backend;
1080
- storeIdentity;
1081
- ready;
1082
- writeGuard = new WriteGuard();
1083
- reuseEventIds = /* @__PURE__ */ new Map();
1084
- /** live 路由:session/created 后 handle 就绪前的缓冲。 */
1085
- liveBuffers = /* @__PURE__ */ new Map();
1086
- liveReady = /* @__PURE__ */ new Map();
1087
- constructor(ctx, config, injectedBackend) {
1088
- let resolved = config;
1089
- const settings = ctx.reflect.get("settings");
1090
- if (settings !== void 0) {
1091
- const scope = settings.register(SessionPersistenceRdb.settingsNs, SessionPersistenceRdb.Config, { base: config });
1092
- resolved = scope.get();
1093
- scope.watch(() => {
1094
- ctx.logger.warn("session-rdb: settings changed; restart to apply the new configuration");
1095
- });
1096
- }
1097
- super(ctx);
1098
- this.config = config;
1099
- this.config = resolved;
1100
- this.backend = injectedBackend ?? createBackend(resolved);
1101
- this.ready = this.init();
1102
- this.installLiveRouting(ctx);
1103
- new SessionBranchRdb(this.ctx);
1104
- this.ctx.plugin(SessionQueryRdb, {});
1105
- registerSessionImport(this.ctx, this);
1106
- installStorageTakeover(this.ctx, {
1107
- repository: this.backend.storage,
1108
- ready: this.ready,
1109
- projectionCache: {
1110
- writeEveryEvents: this.config.projectionCache?.writeEveryEvents ?? DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
1111
- writeIntervalMs: this.config.projectionCache?.writeIntervalMs ?? DEFAULT_PROJECTION_WRITE_INTERVAL_MS
1112
- }
1113
- });
1114
- }
1115
- async init() {
1116
- await this.backend.open();
1117
- this.storeIdentity = this.backend.storeIdentity;
1118
- }
1119
- async create(header, options) {
1120
- options?.signal?.throwIfAborted();
1121
- const snapshot = materializeCreateHeader(header);
1122
- if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
1123
- const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
1124
- if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
1125
- await this.ready;
1126
- options?.signal?.throwIfAborted();
1127
- if (this.tracker.hasPending(snapshot.id) || await this.backend.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
1128
- this.tracker.registerCreated(snapshot, inheritedEventCount);
1129
- return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
1130
- cursor: 0,
1131
- materialized: false,
1132
- inheritedEventCount
1133
- }));
1134
- }
1135
- async open(id, access, options) {
1136
- options?.signal?.throwIfAborted();
1137
- await this.ready;
1138
- options?.signal?.throwIfAborted();
1139
- const pending = this.tracker.pendingOf(id);
1140
- if (access === "read") {
1141
- if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
1142
- cursor: 0,
1143
- materialized: false,
1144
- inheritedEventCount: pending.inheritedEventCount
1145
- }));
1146
- const log = await this.readLog(id, {}, options?.signal);
1147
- if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1148
- repairReadView(log.events);
1149
- validateStoredEvents(log.meta, log.events);
1150
- return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
1151
- cursor: log.events.length,
1152
- materialized: true,
1153
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
1154
- }));
1155
- }
1156
- this.tracker.claimWrite(id);
1157
- try {
1158
- if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
1159
- cursor: pending.cursor,
1160
- materialized: false,
1161
- inheritedEventCount: pending.inheritedEventCount
1162
- }));
1163
- const log = await this.readLog(id, {}, options?.signal);
1164
- if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1165
- repairReadView(log.events);
1166
- validateStoredEvents(log.meta, log.events);
1167
- if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
1168
- this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
1169
- return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
1170
- cursor: log.events.length,
1171
- materialized: true,
1172
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
1173
- ...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
1174
- }));
1175
- } catch (error) {
1176
- this.tracker.releaseClaim(id);
1177
- throw error;
1178
- }
1179
- }
1180
- flush() {
1181
- return this.tracker.flushAll();
1182
- }
1183
- async stat(id, options) {
1184
- options?.signal?.throwIfAborted();
1185
- await this.ready;
1186
- options?.signal?.throwIfAborted();
1187
- const pending = this.tracker.pendingOf(id);
1188
- if (pending !== void 0) return {
1189
- header: pending.header,
1190
- revision: pending.revision
1191
- };
1192
- const row = await this.backend.getSession(id);
1193
- if (row === void 0) return void 0;
1194
- return {
1195
- header: rowToMeta(row),
1196
- revision: this.rowRevision(row)
1197
- };
1198
- }
1199
- async list(options) {
1200
- const signal = options?.signal;
1201
- const snapshots = [];
1202
- const listed = /* @__PURE__ */ new Set();
1203
- for (const [id, pending] of this.tracker.pendingEntries()) {
1204
- snapshots.push({
1205
- header: pending.header,
1206
- revision: pending.revision
1207
- });
1208
- listed.add(id);
1209
- }
1210
- signal?.throwIfAborted();
1211
- await this.ready;
1212
- signal?.throwIfAborted();
1213
- const rows = await this.backend.listSessions();
1214
- signal?.throwIfAborted();
1215
- for (const row of rows) {
1216
- if (listed.has(row.fSessionId)) continue;
1217
- snapshots.push({
1218
- header: rowToMeta(row),
1219
- revision: this.rowRevision(row)
1220
- });
1221
- }
1222
- return snapshots;
1223
- }
1224
- /** 导出当前世代的 jsonl artifact,视图只读不落库。 */
1225
- async readRaw(id, signal) {
1226
- signal?.throwIfAborted();
1227
- await this.ready;
1228
- signal?.throwIfAborted();
1229
- const log = await this.readLog(id, {}, signal);
1230
- if (log === void 0) return void 0;
1231
- repairReadView(log.events);
1232
- const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
1233
- return {
1234
- meta: log.meta,
1235
- inheritedEventCount,
1236
- filename: sessionFormatLogFilename(SESSION_FORMAT_VERSION),
1237
- content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
1238
- };
1239
- }
1240
- /** 空会话 materialize(flush 的持久化屏障)。 */
1241
- async materializeEmpty(meta, inheritedEventCount) {
1242
- await this.ready;
1243
- await this.backend.transaction(async (tx) => {
1244
- await tx.upsertSession({
1245
- meta,
1246
- inheritedEventCount
1247
- }, randomUUID());
1248
- await tx.bumpRevision(meta.id);
1249
- });
1250
- this.tracker.materialized(meta.id);
1251
- this.writeGuard.confirmHead(meta.id, -1);
1252
- }
1253
- /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
1254
- * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
1255
- * 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
1256
- * 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
1257
- async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
1258
- await this.ready;
1259
- if (events.length === 0) return false;
1260
- validateStoredEvents(meta, [...events]);
1261
- const reuse = this.reuseEventIds.get(meta.id);
1262
- if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
1263
- let confirmedHead = -1;
1264
- await this.backend.transaction(async (tx) => {
1265
- if (tornTruncateTo !== void 0) {
1266
- await tx.deleteBridgeTail(meta.id, tornTruncateTo);
1267
- const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
1268
- if (prev === void 0) await tx.updateHead(meta.id, "", -1);
1269
- else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
1270
- }
1271
- await tx.upsertSession({
1272
- meta,
1273
- inheritedEventCount
1274
- }, randomUUID());
1275
- const head = await tx.getHead(meta.id);
1276
- this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
1277
- const { headEventId, headSequence } = await appendEventTail(tx, meta, events, {
1278
- parentId: head.fHeadEventId,
1279
- nextSeq: head.fHeadSequence + 1
1280
- }, reuse);
1281
- await tx.updateHead(meta.id, headEventId, headSequence);
1282
- await tx.bumpRevision(meta.id);
1283
- confirmedHead = headSequence;
1284
- });
1285
- this.writeGuard.confirmHead(meta.id, confirmedHead);
1286
- this.tracker.materialized(meta.id);
1287
- return true;
1288
- }
1289
- /** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
1290
- async readLog(id, options = {}, signal) {
1291
- signal?.throwIfAborted();
1292
- await this.ready;
1293
- signal?.throwIfAborted();
1294
- const row = await this.backend.getSession(id);
1295
- if (row === void 0) return void 0;
1296
- const meta = rowToMeta(row);
1297
- const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
1298
- signal?.throwIfAborted();
1299
- if (isLegacyVersion(row.fVersion)) try {
1300
- const converted = convertLegacyRows(row, eventRows);
1301
- return {
1302
- meta: converted.meta,
1303
- inheritedEventCount: converted.inheritedEventCount,
1304
- events: converted.events,
1305
- incarnation: row.fIncarnation,
1306
- revision: row.fRevision,
1307
- storedCount: eventRows.length,
1308
- migrated: true
1309
- };
1310
- } catch (error) {
1311
- this.ctx.logger.warn(`session-rdb: session "${id}" is not a single released format; adopting its stored rows as current-format data (${error instanceof Error ? error.message : String(error)})`);
1312
- const adopted = adoptLegacyRows(row, eventRows);
1313
- return {
1314
- meta: adopted.meta,
1315
- inheritedEventCount: adopted.inheritedEventCount,
1316
- events: adopted.events,
1317
- incarnation: row.fIncarnation,
1318
- revision: row.fRevision,
1319
- storedCount: eventRows.length,
1320
- migrated: false,
1321
- ...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
1322
- };
1323
- }
1324
- const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
1325
- return {
1326
- meta,
1327
- inheritedEventCount: row.fSeedLength ?? 0,
1328
- events: preserved,
1329
- incarnation: row.fIncarnation,
1330
- revision: row.fRevision,
1331
- storedCount: eventRows.length,
1332
- migrated: false,
1333
- ...tornFrom !== void 0 ? { tornFrom } : {}
1334
- };
1335
- }
1336
- /**
1337
- * 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
1338
- *
1339
- * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
1340
- * 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
1341
- * append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
1342
- * (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
1343
- * fork 子会话引用,孤儿由惰性 GC 处理)。
1344
- */
1345
- async rewriteMigratedLog(id, log) {
1346
- await this.backend.transaction(async (tx) => {
1347
- await tx.deleteBridgeTail(id, 0);
1348
- await tx.upsertSession({
1349
- meta: log.meta,
1350
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
1351
- }, randomUUID());
1352
- const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
1353
- parentId: "",
1354
- nextSeq: 0
1355
- });
1356
- await tx.updateHead(id, headEventId, headSequence);
1357
- await tx.bumpRevision(id);
1358
- });
1359
- }
1360
- async listSnapshots(signal) {
1361
- signal?.throwIfAborted();
1362
- await this.ready;
1363
- signal?.throwIfAborted();
1364
- const snapshots = [];
1365
- const listed = /* @__PURE__ */ new Set();
1366
- for (const [id, pending] of this.tracker.pendingEntries()) {
1367
- snapshots.push({
1368
- header: pending.header,
1369
- revision: pending.revision,
1370
- inheritedEventCount: pending.inheritedEventCount
1371
- });
1372
- listed.add(id);
1373
- }
1374
- const rows = await this.backend.listSessions();
1375
- signal?.throwIfAborted();
1376
- for (const row of rows) {
1377
- if (listed.has(row.fSessionId)) continue;
1378
- snapshots.push({
1379
- header: rowToMeta(row),
1380
- revision: this.rowRevision(row),
1381
- inheritedEventCount: row.fSeedLength ?? 0
1382
- });
1383
- }
1384
- return snapshots;
1385
- }
1386
- async readStoredRevision(id, signal) {
1387
- signal?.throwIfAborted();
1388
- await this.ready;
1389
- signal?.throwIfAborted();
1390
- const row = await this.backend.getSession(id);
1391
- if (row === void 0) return void 0;
1392
- return this.rowRevision(row);
1393
- }
1394
- /** 便捷:create + append + close(测试与导入路径共用)。 */
1395
- async createAndAppend(header, events, inheritedEventCount) {
1396
- const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
1397
- try {
1398
- if (events.length > 0) await handle.append(events);
1399
- } finally {
1400
- await handle.close();
1401
- }
1402
- }
1403
- /** 便捷:open(read) + read 全量 + close。 */
1404
- async load(id, signal) {
1405
- const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
1406
- try {
1407
- const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
1408
- const row = await this.backend.getSession(id);
1409
- if (row === void 0) throw new SessionPersistenceNotFoundError(id);
1410
- if (isLegacyVersion(row.fVersion)) return {
1411
- meta: handle.header,
1412
- inheritedEventCount: handle.inheritedEventCount,
1413
- events
1414
- };
1415
- return {
1416
- meta: rowToMeta(row),
1417
- inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
1418
- events
1419
- };
1420
- } finally {
1421
- await handle.close();
1422
- }
1423
- }
1424
- /** 便捷:open(write) + append + close。 */
1425
- async append(id, events) {
1426
- const handle = await this.open(id, "write");
1427
- try {
1428
- await handle.append(events);
1429
- } finally {
1430
- await handle.close();
1431
- }
1432
- }
1433
- /** 便捷:readFrom(稠密后缀)。 */
1434
- async readFrom(id, fromSeq, signal) {
1435
- const log = await this.readLog(id, { fromSeq }, signal);
1436
- if (log === void 0) throw new SessionPersistenceNotFoundError(id);
1437
- return {
1438
- meta: log.meta,
1439
- inheritedEventCount: log.inheritedEventCount,
1440
- events: log.events
1441
- };
1442
- }
1443
- async close() {
1444
- await this.ready;
1445
- await this.backend.close();
1446
- }
1447
- registerReuseEventIds(childId, map) {
1448
- this.reuseEventIds.set(childId, new Map(map));
1449
- }
1450
- internals() {
1451
- return {
1452
- backend: this.backend,
1453
- writeGuard: this.writeGuard,
1454
- create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) }),
1455
- append: (id, events) => this.append(id, events),
1456
- load: (id) => this.load(id),
1457
- inspect: (id, signal) => this.load(id, signal),
1458
- readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
1459
- listSnapshots: (signal) => this.listSnapshots(signal),
1460
- readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
1461
- registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
1462
- };
1463
- }
1464
- rowRevision(row) {
1465
- return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
1466
- }
1467
- installLiveRouting(ctx) {
1468
- ctx.on("session/created", (session) => {
1469
- this.liveBuffers.set(session.id, []);
1470
- const ready = this.ensureLiveHandle(session);
1471
- this.liveReady.set(session.id, ready);
1472
- ready.catch((error) => {
1473
- ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
1474
- });
1475
- });
1476
- for (const session of ctx.sessions.list()) {
1477
- this.liveBuffers.set(session.id, []);
1478
- const ready = this.ensureLiveHandle(session);
1479
- this.liveReady.set(session.id, ready);
1480
- ready.catch((error) => {
1481
- ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
1482
- });
1483
- }
1484
- ctx.on("session/event", (session, event) => {
1485
- const handle = this.tracker.writerOf(session.id);
1486
- if (handle !== void 0) {
1487
- handle.enqueueLive(event, (error) => {
1488
- ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
1489
- });
1490
- return;
1491
- }
1492
- this.liveBuffers.get(session.id)?.push(structuredClone(event));
1493
- });
1494
- ctx.on("session/flush", (session) => {
1495
- const handle = this.tracker.writerOf(session.id);
1496
- if (handle === void 0) {
1497
- const ready = this.liveReady.get(session.id);
1498
- if (ready === void 0) return void 0;
1499
- return ready.then(() => {
1500
- const settled = this.tracker.writerOf(session.id);
1501
- if (settled === void 0) return void 0;
1502
- return settled.drainLive().then(() => settled.flush());
1503
- });
1504
- }
1505
- return handle.drainLive().then(() => handle.flush());
1506
- });
1507
- ctx.on("session/disposed", (session) => {
1508
- const ready = this.liveReady.get(session.id);
1509
- this.liveBuffers.delete(session.id);
1510
- this.liveReady.delete(session.id);
1511
- const closeHandle = () => {
1512
- const handle = this.tracker.writerOf(session.id);
1513
- if (handle === void 0) return;
1514
- handle.close().catch((error) => {
1515
- ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
1516
- });
1517
- };
1518
- if (ready === void 0) {
1519
- closeHandle();
1520
- return;
1521
- }
1522
- ready.then(closeHandle, closeHandle);
1523
- });
1524
- ctx.effect(() => async () => {
1525
- await Promise.allSettled(this.liveReady.values());
1526
- await this.tracker.closeAll();
1527
- await this.close();
1528
- }, `${this.name} open handles`);
1529
- }
1530
- /** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
1531
- async ensureLiveHandle(session) {
1532
- const id = session.header.id;
1533
- if (this.tracker.writerOf(id) !== void 0) return;
1534
- await this.ready;
1535
- const stored = await this.readLog(id, {});
1536
- let handle;
1537
- if (stored === void 0) {
1538
- handle = await this.create(session.header, { inheritedEventCount: session.inheritedEventCount });
1539
- const seed = session.snapshotEvents();
1540
- if (seed.length > 0) await handle.append(seed);
1541
- } else {
1542
- if (stored.meta.cwd !== session.header.cwd) throw new Error(`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`);
1543
- if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
1544
- assertVersion(stored.meta);
1545
- repairReadView(stored.events);
1546
- const seed = session.snapshotEvents();
1547
- if (!seedCoversPrefix(seed, stored.events)) throw new Error(`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`);
1548
- handle = await this.open(id, "write");
1549
- const suffix = seed.slice(stored.events.length);
1550
- if (suffix.length > 0) await handle.append(suffix);
1551
- }
1552
- const buffered = this.liveBuffers.get(id);
1553
- if (buffered !== void 0 && buffered.length > 0) {
1554
- this.liveBuffers.set(id, []);
1555
- for (const event of buffered) handle.enqueueLive(event, () => {});
1556
- }
1557
- }
1558
- };
1559
- function seedCoversPrefix(seed, prefix) {
1560
- return prefix.length <= seed.length && prefix.every((event, index) => {
1561
- const seedEvent = seed[index];
1562
- return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
1563
- });
1564
- }
1565
- function createBackend(config) {
1566
- if (config.type === "sqlite") return new SqliteBackend({
1567
- path: config.path,
1568
- journalMode: config.journalMode ?? "wal",
1569
- busyTimeout: config.busyTimeout ?? 5e3
1570
- });
1571
- const pool = new Pool({ connectionString: config.connectionString });
1572
- pool.on("error", () => {});
1573
- return new PostgresBackend(drizzle({ client: pool }), {
1574
- identityBase: [
1575
- "postgres",
1576
- pool.options.host ?? "localhost",
1577
- String(pool.options.port ?? 5432),
1578
- pool.options.database ?? "",
1579
- config.schema ?? "public"
1580
- ].join(":"),
1581
- schema: config.schema ?? "public",
1582
- close: () => pool.end()
1583
- });
1584
- }
1585
- async function appendEventTail(tx, meta, events, anchor, reuse) {
1586
- let parentId = anchor.parentId;
1587
- let nextSeq = anchor.nextSeq;
1588
- const eventRows = [];
1589
- const bridgeRows = [];
1590
- for (const event of events) {
1591
- const reusedId = reuse?.get(event.seq);
1592
- const eventId = reusedId ?? randomUUID();
1593
- if (reusedId === void 0) {
1594
- const { kind, role, name, actionId } = eventDimensions(event);
1595
- const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
1596
- eventRows.push({
1597
- fEventId: eventId,
1598
- fParentId: parentId,
1599
- fType: event.type,
1600
- fKind: kind,
1601
- fRole: role,
1602
- fName: name,
1603
- fActionId: actionId,
1604
- fEncoding: EVENT_ENCODING,
1605
- fData: JSON.stringify({
1606
- ...envelope,
1607
- data
1608
- }),
1609
- fCreatedAt: event.time
1610
- });
1611
- }
1612
- const surfaceOp = event.surfaceOp === void 0 ? null : JSON.stringify(event.surfaceOp);
1613
- bridgeRows.push({
1614
- fSessionId: meta.id,
1615
- fEventId: eventId,
1616
- fSequence: nextSeq,
1617
- fSurfaceOp: surfaceOp
1618
- });
1619
- parentId = eventId;
1620
- nextSeq++;
1621
- }
1622
- if (eventRows.length > 0) await tx.insertEvents(eventRows);
1623
- await tx.insertBridges(bridgeRows);
1624
- if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
1625
- return {
1626
- headEventId: parentId,
1627
- headSequence: nextSeq - 1
1628
- };
1629
- }
1630
- //#endregion
1631
- export { SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
1
+ import { o as SCHEMA_VERSION } from "./sqlite-fpvm5Dzs.mjs";
2
+ import { n as SessionPersistenceRdb, t as SessionDeletionError } from "./src-CWTWV7vx.mjs";
3
+ import { n as SessionBranchRdbProvider, r as locateTurnEnd, t as SessionBranchRdb } from "./branch-5JzX9rUq.mjs";
4
+ export { SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };