@morlay/session-rdb 0.0.15 → 0.0.16-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.
- package/README.md +11 -13
- package/dist/artifact.d.mts +30 -16
- package/dist/artifact.mjs +3 -3
- package/dist/{import-DFed8DBt.mjs → import-mZZgDBLd.mjs} +73 -83
- package/dist/import.d.mts +2 -3
- package/dist/import.mjs +2 -2
- package/dist/index-GdKkSSb-.d.mts +239 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +763 -168
- package/dist/log-DBFUBPhv.mjs +394 -0
- package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
- package/dist/sqlite-CnWwGToZ.mjs +698 -0
- package/dist/storage.d.mts +2 -7
- package/dist/storage.mjs +2 -3
- package/dist/testing.d.mts +30 -1
- package/dist/testing.mjs +676 -1991
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
- package/package.json +17 -12
- package/src/adapters/index.ts +10 -2
- package/src/adapters/to-postgres.ts +19 -15
- package/src/adapters/to-sqlite.ts +20 -14
- package/src/{entities → adapters}/types.ts +7 -8
- package/src/backend.ts +0 -7
- package/src/branch.ts +29 -110
- package/src/drizzle/postgres-v2.ts +11 -0
- package/src/drizzle/postgres-v3.ts +11 -0
- package/src/drizzle/sqlite-v2.ts +10 -0
- package/src/drizzle/sqlite-v3.ts +11 -0
- package/src/entities/index.ts +2 -30
- package/src/entities/v2/index.ts +20 -0
- package/src/entities/v2/session-events.ts +11 -0
- package/src/entities/v3/events.ts +27 -0
- package/src/entities/v3/index.ts +27 -0
- package/src/entities/v3/persistence-state.ts +10 -0
- package/src/entities/v3/schema-meta.ts +9 -0
- package/src/entities/v3/session-events.ts +26 -0
- package/src/entities/v3/sessions.ts +20 -0
- package/src/import.ts +65 -57
- package/src/index.ts +929 -227
- package/src/invariant.ts +0 -2
- package/src/legacy.ts +110 -0
- package/src/log.ts +143 -91
- package/src/postgres.ts +75 -93
- package/src/schema.ts +3 -14
- package/src/sqlite.ts +59 -46
- package/src/testing/contract.ts +460 -375
- package/src/testing/coordinator-contract.ts +108 -1374
- package/src/testing.ts +1 -6
- package/dist/index-uUvn6gYp.d.mts +0 -111
- package/dist/schema-vwzwEXNE.mjs +0 -878
- package/dist/sqlite-CG_Qcx5C.mjs +0 -356
- package/src/adapters/ddl.ts +0 -77
- package/src/entities/events.ts +0 -27
- package/src/entities/persistence-state.ts +0 -10
- package/src/entities/schema-meta.ts +0 -9
- package/src/entities/session-events.ts +0 -29
- package/src/entities/sessions.ts +0 -20
- package/src/migrate.ts +0 -137
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { c as
|
|
1
|
+
import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-CnWwGToZ.mjs";
|
|
2
|
+
import { d as sessionConflictRow, f as sessionInsertRow, l as rowToMeta, m as toJsonlArtifact, o as repairReadView, u as scanRows } from "./log-DBFUBPhv.mjs";
|
|
3
|
+
import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-mZZgDBLd.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { Pool } from "pg";
|
|
7
7
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
8
|
-
import {
|
|
9
|
-
import { SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
10
|
-
import {
|
|
8
|
+
import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
|
|
9
|
+
import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
10
|
+
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
11
|
+
import { and, eq, gte, sql } from "drizzle-orm";
|
|
12
|
+
import { readdirSync } from "node:fs";
|
|
13
|
+
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
11
14
|
//#region src/postgres.ts
|
|
15
|
+
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
16
|
+
const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url).pathname;
|
|
12
17
|
var PostgresBackend = class PostgresBackend {
|
|
13
18
|
db;
|
|
14
19
|
options;
|
|
@@ -21,45 +26,47 @@ var PostgresBackend = class PostgresBackend {
|
|
|
21
26
|
this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
|
|
22
27
|
}
|
|
23
28
|
async open() {
|
|
29
|
+
const schema = this.options.schema ?? "public";
|
|
30
|
+
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
31
|
+
if ((await this.db.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true) {
|
|
32
|
+
if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
|
|
33
|
+
}
|
|
34
|
+
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
24
35
|
const storeId = await this.db.transaction(async (tx) => {
|
|
25
|
-
const schema = this.options.schema ?? "public";
|
|
26
|
-
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
27
|
-
const metaExists = (await tx.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true;
|
|
28
|
-
for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) await tx.execute(sql.raw(statement));
|
|
29
|
-
if (!metaExists) await tx.insert(this.tables["t_schema_meta"]).values([{
|
|
30
|
-
fKey: "schema_version",
|
|
31
|
-
fValue: String(2)
|
|
32
|
-
}, {
|
|
33
|
-
fKey: "application_id",
|
|
34
|
-
fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID)
|
|
35
|
-
}]).execute();
|
|
36
|
-
const version = await this.readMeta(tx, "schema_version");
|
|
37
|
-
const applicationId = await this.readMeta(tx, "application_id");
|
|
38
|
-
if (version === void 0 || applicationId === void 0) throw new Error("session database has an unversioned schema or application identity");
|
|
39
|
-
if (Number(version) !== 2) throw new Error(`session database has schema version ${version}, incompatible with this build (2)`);
|
|
40
|
-
if (Number(applicationId) !== 1146308688) throw new Error(`session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
|
|
41
36
|
await tx.insert(this.tables["t_persistence_state"]).values({
|
|
42
37
|
fSingleton: 1,
|
|
43
38
|
fStoreId: randomUUID()
|
|
44
39
|
}).onConflictDoNothing().execute();
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
return
|
|
40
|
+
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;
|
|
41
|
+
if (id === void 0 || id.length === 0) throw new Error("session database has no valid store identity");
|
|
42
|
+
return id;
|
|
48
43
|
});
|
|
49
44
|
this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
|
|
50
45
|
}
|
|
46
|
+
/** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
|
|
47
|
+
async baselineV2() {
|
|
48
|
+
const dir = readdirSync(postgresMigrationsDir).find((name) => name.endsWith("_v2_initial"));
|
|
49
|
+
if (dir === void 0) throw new Error("missing v2 baseline migration in drizzle/postgres");
|
|
50
|
+
await this.db.execute(sql`
|
|
51
|
+
CREATE SCHEMA IF NOT EXISTS drizzle
|
|
52
|
+
`);
|
|
53
|
+
await this.db.execute(sql`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
|
|
55
|
+
id SERIAL PRIMARY KEY,
|
|
56
|
+
hash text NOT NULL,
|
|
57
|
+
created_at bigint,
|
|
58
|
+
name text,
|
|
59
|
+
applied_at timestamp with time zone DEFAULT now()
|
|
60
|
+
)
|
|
61
|
+
`);
|
|
62
|
+
await this.db.execute(sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`);
|
|
63
|
+
}
|
|
51
64
|
async close() {
|
|
52
65
|
await this.options.close();
|
|
53
66
|
}
|
|
54
67
|
async getSession(id) {
|
|
55
68
|
return (await this.db.select().from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
|
|
56
69
|
}
|
|
57
|
-
async getSeqMapRows(id) {
|
|
58
|
-
return this.db.select({
|
|
59
|
-
fSequence: this.tables["t_session_events"].fSequence,
|
|
60
|
-
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq
|
|
61
|
-
}).from(this.tables["t_session_events"]).where(eq(this.tables["t_session_events"].fSessionId, id)).execute();
|
|
62
|
-
}
|
|
63
70
|
async getEventRows(id, fromSequence) {
|
|
64
71
|
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();
|
|
65
72
|
}
|
|
@@ -80,8 +87,7 @@ var PostgresBackend = class PostgresBackend {
|
|
|
80
87
|
updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
|
|
81
88
|
bumpRevision: (id) => this.bumpRevision(tx, id),
|
|
82
89
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
|
|
83
|
-
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence)
|
|
84
|
-
getLastBridge: (id) => this.getLastBridge(tx, id)
|
|
90
|
+
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence)
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
93
|
async readMeta(exec, key) {
|
|
@@ -138,17 +144,10 @@ var PostgresBackend = class PostgresBackend {
|
|
|
138
144
|
fSequence: this.tables["t_session_events"].fSequence
|
|
139
145
|
}).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];
|
|
140
146
|
}
|
|
141
|
-
async getLastBridge(exec, id) {
|
|
142
|
-
return (await exec.select({
|
|
143
|
-
fEventId: this.tables["t_session_events"].fEventId,
|
|
144
|
-
fSequence: this.tables["t_session_events"].fSequence
|
|
145
|
-
}).from(this.tables["t_session_events"]).where(eq(this.tables["t_session_events"].fSessionId, id)).orderBy(desc(this.tables["t_session_events"].fSequence)).limit(1).execute())[0];
|
|
146
|
-
}
|
|
147
147
|
eventRows(exec) {
|
|
148
148
|
return exec.select({
|
|
149
149
|
fEventId: this.tables["t_session_events"].fEventId,
|
|
150
150
|
fSequence: this.tables["t_session_events"].fSequence,
|
|
151
|
-
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
152
151
|
fType: this.tables["t_events"].fType,
|
|
153
152
|
fKind: this.tables["t_events"].fKind,
|
|
154
153
|
fRole: this.tables["t_events"].fRole,
|
|
@@ -161,7 +160,343 @@ var PostgresBackend = class PostgresBackend {
|
|
|
161
160
|
}
|
|
162
161
|
};
|
|
163
162
|
//#endregion
|
|
163
|
+
//#region src/legacy.ts
|
|
164
|
+
/** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
165
|
+
function physicalHeader(row) {
|
|
166
|
+
return {
|
|
167
|
+
type: "session",
|
|
168
|
+
version: row.fVersion,
|
|
169
|
+
id: row.fSessionId,
|
|
170
|
+
createdAt: row.fCreatedAt,
|
|
171
|
+
...row.fCwd !== null ? { cwd: row.fCwd } : {},
|
|
172
|
+
...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
|
|
173
|
+
...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {},
|
|
174
|
+
...row.fOrigin !== null ? { origin: row.fOrigin } : {},
|
|
175
|
+
delegationDepth: row.fDelegationDepth ?? 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
179
|
+
function physicalEvent(row) {
|
|
180
|
+
return {
|
|
181
|
+
type: row.fType,
|
|
182
|
+
seq: row.fSequence,
|
|
183
|
+
time: row.fCreatedAt,
|
|
184
|
+
data: JSON.parse(row.fData),
|
|
185
|
+
...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 把旧格式(v0/v1)行转换为 v2 逻辑事件与 header。
|
|
190
|
+
* @param row - 会话行(f_version < 2)。
|
|
191
|
+
* @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
|
|
192
|
+
* @returns v2 逻辑 header、继承前缀长度与事件。
|
|
193
|
+
* @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
|
|
194
|
+
*/
|
|
195
|
+
function convertLegacyRows(row, eventRows) {
|
|
196
|
+
const restore = sessionFormatCatalog.createRestore(physicalHeader(row), {
|
|
197
|
+
recovery: "strict",
|
|
198
|
+
validation: "transformed"
|
|
199
|
+
});
|
|
200
|
+
for (const eventRow of eventRows) restore.decodeRow(physicalEvent(eventRow));
|
|
201
|
+
const artifact = restore.finish();
|
|
202
|
+
return {
|
|
203
|
+
meta: artifact.header,
|
|
204
|
+
inheritedEventCount: artifact.inheritedEventCount,
|
|
205
|
+
events: artifact.events
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** 会话行是否携带旧格式(v0/v1)数据。 */
|
|
209
|
+
function isLegacyVersion(version) {
|
|
210
|
+
return version < 2;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
|
|
214
|
+
*
|
|
215
|
+
* 旧写入器只在建会话时落一次 header version,之后跟随上游演进继续追加
|
|
216
|
+
* 事件——同一个 log 因此可能是混合世代(v0 时代的行 + 新版本字段),不是
|
|
217
|
+
* 任何单一已发布格式,严格迁移链必然拒绝。这些行的数据形状(消息包装、
|
|
218
|
+
* 类型名)已经是当前格式,只需 header 版本归一到当前、按稠密 seq 读取;
|
|
219
|
+
* 事件数据的补全与 surface 修复由读取视图修复(repairReadView)负责。
|
|
220
|
+
*
|
|
221
|
+
* 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
|
|
222
|
+
* (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
|
|
223
|
+
*
|
|
224
|
+
* @param row - 会话行(f_version < 2)。
|
|
225
|
+
* @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
|
|
226
|
+
* @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
|
|
227
|
+
* 继承前缀长度、稠密事件与可选 torn tail 起点。
|
|
228
|
+
* @throws scanRows 的已提交区损坏错误(seq gap / 不可解析行)。
|
|
229
|
+
*/
|
|
230
|
+
function adoptLegacyRows(row, eventRows) {
|
|
231
|
+
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
232
|
+
return {
|
|
233
|
+
meta: {
|
|
234
|
+
...rowToMeta(row),
|
|
235
|
+
version: SESSION_FORMAT_VERSION
|
|
236
|
+
},
|
|
237
|
+
inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
|
|
238
|
+
events: preserved,
|
|
239
|
+
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
164
243
|
//#region src/index.ts
|
|
244
|
+
/** 会话级写所有权与 live 路由簿记。 */
|
|
245
|
+
var RdbBackendTracker = class {
|
|
246
|
+
name;
|
|
247
|
+
/** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
|
|
248
|
+
writers = /* @__PURE__ */ new Map();
|
|
249
|
+
pending = /* @__PURE__ */ new Map();
|
|
250
|
+
openHandles = /* @__PURE__ */ new Set();
|
|
251
|
+
counter = 0;
|
|
252
|
+
constructor(name) {
|
|
253
|
+
this.name = name;
|
|
254
|
+
}
|
|
255
|
+
registerCreated(header, inheritedEventCount) {
|
|
256
|
+
if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
|
|
257
|
+
this.writers.set(header.id, null);
|
|
258
|
+
this.pending.set(header.id, {
|
|
259
|
+
header,
|
|
260
|
+
revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
|
|
261
|
+
inheritedEventCount,
|
|
262
|
+
cursor: 0,
|
|
263
|
+
everAppended: false
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
|
|
267
|
+
updatePending(id, cursor, everAppended) {
|
|
268
|
+
const entry = this.pending.get(id);
|
|
269
|
+
if (entry === void 0) return;
|
|
270
|
+
this.pending.set(id, {
|
|
271
|
+
...entry,
|
|
272
|
+
cursor,
|
|
273
|
+
everAppended
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
claimWrite(id) {
|
|
277
|
+
if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
|
|
278
|
+
this.writers.set(id, null);
|
|
279
|
+
}
|
|
280
|
+
releaseClaim(id) {
|
|
281
|
+
this.writers.delete(id);
|
|
282
|
+
}
|
|
283
|
+
pendingOf(id) {
|
|
284
|
+
return this.pending.get(id);
|
|
285
|
+
}
|
|
286
|
+
hasPending(id) {
|
|
287
|
+
return this.pending.has(id);
|
|
288
|
+
}
|
|
289
|
+
pendingEntries() {
|
|
290
|
+
return this.pending.entries();
|
|
291
|
+
}
|
|
292
|
+
materialized(id) {
|
|
293
|
+
this.pending.delete(id);
|
|
294
|
+
}
|
|
295
|
+
adopt(handle) {
|
|
296
|
+
this.openHandles.add(handle);
|
|
297
|
+
if (handle.access === "write") this.writers.set(handle.id, handle);
|
|
298
|
+
return handle;
|
|
299
|
+
}
|
|
300
|
+
release(handle, materialized) {
|
|
301
|
+
this.openHandles.delete(handle);
|
|
302
|
+
if (handle.access !== "write") return;
|
|
303
|
+
this.writers.delete(handle.id);
|
|
304
|
+
if (!materialized) this.pending.delete(handle.id);
|
|
305
|
+
}
|
|
306
|
+
writerOf(id) {
|
|
307
|
+
const writer = this.writers.get(id);
|
|
308
|
+
return writer === null ? void 0 : writer;
|
|
309
|
+
}
|
|
310
|
+
async flushAll() {
|
|
311
|
+
const errors = [];
|
|
312
|
+
for (const writer of this.writers.values()) {
|
|
313
|
+
if (writer === null) continue;
|
|
314
|
+
try {
|
|
315
|
+
await writer.drainLive();
|
|
316
|
+
await writer.flush();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error instanceof SessionHandleClosedError) continue;
|
|
319
|
+
errors.push(error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
|
|
323
|
+
}
|
|
324
|
+
async closeAll() {
|
|
325
|
+
const errors = [];
|
|
326
|
+
for (const handle of this.openHandles) try {
|
|
327
|
+
await handle.close();
|
|
328
|
+
} catch (error) {
|
|
329
|
+
errors.push(error);
|
|
330
|
+
}
|
|
331
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
/** 一个打开会话的存储句柄:read / append / flush / close。 */
|
|
335
|
+
var RdbSessionHandle = class RdbSessionHandle {
|
|
336
|
+
persistence;
|
|
337
|
+
id;
|
|
338
|
+
header;
|
|
339
|
+
access;
|
|
340
|
+
state;
|
|
341
|
+
chain = Promise.resolve();
|
|
342
|
+
closing;
|
|
343
|
+
/** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
|
|
344
|
+
cursor;
|
|
345
|
+
materialized;
|
|
346
|
+
/** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
|
|
347
|
+
buffered = [];
|
|
348
|
+
batchTimer;
|
|
349
|
+
drainPaused = false;
|
|
350
|
+
draining;
|
|
351
|
+
/** write open 时发现的 torn tail 起点(append 前先截断)。 */
|
|
352
|
+
tornTruncateTo;
|
|
353
|
+
/** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
|
|
354
|
+
everAppended = false;
|
|
355
|
+
constructor(persistence, id, header, access, state) {
|
|
356
|
+
this.persistence = persistence;
|
|
357
|
+
this.id = id;
|
|
358
|
+
this.header = header;
|
|
359
|
+
this.access = access;
|
|
360
|
+
this.state = state;
|
|
361
|
+
this.cursor = state.cursor;
|
|
362
|
+
this.materialized = state.materialized;
|
|
363
|
+
this.tornTruncateTo = state.tornTruncateTo;
|
|
364
|
+
}
|
|
365
|
+
get inheritedEventCount() {
|
|
366
|
+
return this.state.inheritedEventCount;
|
|
367
|
+
}
|
|
368
|
+
/** 输入空间 cursor(下一个待落库的上游 seq)。 */
|
|
369
|
+
get cursorValue() {
|
|
370
|
+
return this.cursor;
|
|
371
|
+
}
|
|
372
|
+
async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
|
|
373
|
+
this.assertOpen("read");
|
|
374
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
|
|
375
|
+
if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
|
|
376
|
+
options?.signal?.throwIfAborted();
|
|
377
|
+
const log = await this.persistence.readLog(this.id, {}, options?.signal);
|
|
378
|
+
if (log === void 0) {
|
|
379
|
+
if (this.persistence.tracker.hasPending(this.id)) return {
|
|
380
|
+
eventState: "detached",
|
|
381
|
+
events: []
|
|
382
|
+
};
|
|
383
|
+
throw new SessionPersistenceNotFoundError(this.id);
|
|
384
|
+
}
|
|
385
|
+
repairReadView(log.events);
|
|
386
|
+
return {
|
|
387
|
+
eventState: "detached",
|
|
388
|
+
events: log.events.slice(offset, offset + length)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async append(events, options) {
|
|
392
|
+
this.assertOpen("append");
|
|
393
|
+
const batch = materializeAppendBatch(events);
|
|
394
|
+
return this.run("append", async () => {
|
|
395
|
+
options?.signal?.throwIfAborted();
|
|
396
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
|
|
397
|
+
if (batch.length === 0) return;
|
|
398
|
+
this.everAppended = true;
|
|
399
|
+
assertContiguous(this.id, batch, this.cursor);
|
|
400
|
+
await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
|
|
401
|
+
this.tornTruncateTo = void 0;
|
|
402
|
+
this.cursor += batch.length;
|
|
403
|
+
this.materialized = true;
|
|
404
|
+
this.persistence.tracker.updatePending(this.id, this.cursor, true);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async flush(options) {
|
|
408
|
+
return this.run("flush", async () => {
|
|
409
|
+
options?.signal?.throwIfAborted();
|
|
410
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
|
|
411
|
+
if (this.materialized) return;
|
|
412
|
+
await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
|
|
413
|
+
this.materialized = true;
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
close() {
|
|
417
|
+
return this.closing ??= (async () => {
|
|
418
|
+
let drainFailure;
|
|
419
|
+
for (;;) {
|
|
420
|
+
try {
|
|
421
|
+
await this.drainLive();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
drainFailure = error;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
await this.chain;
|
|
427
|
+
if (this.buffered.length === 0) break;
|
|
428
|
+
}
|
|
429
|
+
await this.chain;
|
|
430
|
+
const failures = [];
|
|
431
|
+
if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
|
|
432
|
+
this.persistence.tracker.release(this, this.materialized || this.everAppended);
|
|
433
|
+
if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
|
|
434
|
+
if (failures[0] !== void 0) throw failures[0];
|
|
435
|
+
})();
|
|
436
|
+
}
|
|
437
|
+
[Symbol.asyncDispose]() {
|
|
438
|
+
return this.close();
|
|
439
|
+
}
|
|
440
|
+
/** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
|
|
441
|
+
enqueueLive(event, reportBackgroundFailure) {
|
|
442
|
+
this.buffered.push(structuredClone(event));
|
|
443
|
+
if (this.batchTimer !== void 0 || this.drainPaused) return;
|
|
444
|
+
this.batchTimer = setTimeout(() => {
|
|
445
|
+
this.batchTimer = void 0;
|
|
446
|
+
this.drainLive().catch(reportBackgroundFailure);
|
|
447
|
+
}, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
|
|
448
|
+
}
|
|
449
|
+
/** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
|
|
450
|
+
resetAfterRewind(cursor, inheritedEventCount) {
|
|
451
|
+
this.cursor = cursor;
|
|
452
|
+
if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
|
|
453
|
+
}
|
|
454
|
+
/** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
|
|
455
|
+
drainLive() {
|
|
456
|
+
return this.draining ??= this.drainBuffered().finally(() => {
|
|
457
|
+
this.draining = void 0;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async drainBuffered() {
|
|
461
|
+
if (this.batchTimer !== void 0) {
|
|
462
|
+
clearTimeout(this.batchTimer);
|
|
463
|
+
this.batchTimer = void 0;
|
|
464
|
+
}
|
|
465
|
+
this.drainPaused = false;
|
|
466
|
+
while (this.buffered.length > 0) await this.enqueueChain(async () => {
|
|
467
|
+
const batch = this.buffered.splice(0);
|
|
468
|
+
try {
|
|
469
|
+
const fresh = batch.filter((event) => event.seq >= this.cursor);
|
|
470
|
+
if (fresh.length === 0) return;
|
|
471
|
+
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}`);
|
|
472
|
+
await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
|
|
473
|
+
this.tornTruncateTo = void 0;
|
|
474
|
+
this.cursor += fresh.length;
|
|
475
|
+
this.materialized = true;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
this.buffered = batch.concat(this.buffered);
|
|
478
|
+
this.drainPaused = true;
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
enqueueChain(op) {
|
|
484
|
+
const next = this.chain.then(op);
|
|
485
|
+
this.chain = next.catch(() => {});
|
|
486
|
+
return next;
|
|
487
|
+
}
|
|
488
|
+
async run(operation, op) {
|
|
489
|
+
this.assertOpen(operation);
|
|
490
|
+
return this.enqueueChain(async () => {
|
|
491
|
+
this.assertOpen(operation);
|
|
492
|
+
return op();
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
assertOpen(operation) {
|
|
496
|
+
if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
|
|
497
|
+
}
|
|
498
|
+
static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
499
|
+
};
|
|
165
500
|
var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
|
|
166
501
|
config;
|
|
167
502
|
static inject = ["sessions", "settings"];
|
|
@@ -182,29 +517,15 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
182
517
|
})]);
|
|
183
518
|
static settingsNs = "session-rdb";
|
|
184
519
|
name = "session-rdb";
|
|
185
|
-
|
|
186
|
-
async readRaw(id, signal) {
|
|
187
|
-
signal?.throwIfAborted();
|
|
188
|
-
await this.ready;
|
|
189
|
-
signal?.throwIfAborted();
|
|
190
|
-
const log = await this.readLog(id, {}, signal);
|
|
191
|
-
if (log === void 0) return void 0;
|
|
192
|
-
repairSurfaceOps(log.events);
|
|
193
|
-
recomputeReplaceProvenance(log.events);
|
|
194
|
-
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
195
|
-
return {
|
|
196
|
-
meta: log.meta,
|
|
197
|
-
inheritedEventCount: SessionLogOffset(inheritedEventCount),
|
|
198
|
-
filename: "session.jsonl",
|
|
199
|
-
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
|
|
200
|
-
};
|
|
201
|
-
}
|
|
520
|
+
tracker = new RdbBackendTracker(this.name);
|
|
202
521
|
backend;
|
|
203
522
|
storeIdentity;
|
|
204
523
|
ready;
|
|
205
|
-
coordinator;
|
|
206
524
|
writeGuard = new WriteGuard();
|
|
207
525
|
reuseEventIds = /* @__PURE__ */ new Map();
|
|
526
|
+
/** live 路由:session/created 后 handle 就绪前的缓冲。 */
|
|
527
|
+
liveBuffers = /* @__PURE__ */ new Map();
|
|
528
|
+
liveReady = /* @__PURE__ */ new Map();
|
|
208
529
|
constructor(ctx, config, injectedBackend) {
|
|
209
530
|
let resolved = config;
|
|
210
531
|
const settings = ctx.reflect.get("settings");
|
|
@@ -220,7 +541,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
220
541
|
this.config = resolved;
|
|
221
542
|
this.backend = injectedBackend ?? createBackend(resolved);
|
|
222
543
|
this.ready = this.init();
|
|
223
|
-
this.
|
|
544
|
+
this.installLiveRouting(ctx);
|
|
224
545
|
new SessionBranchRdb(this.ctx);
|
|
225
546
|
registerSessionImport(this.ctx, this);
|
|
226
547
|
}
|
|
@@ -228,103 +549,163 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
228
549
|
await this.backend.open();
|
|
229
550
|
this.storeIdentity = this.backend.storeIdentity;
|
|
230
551
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
552
|
+
async create(header, options) {
|
|
553
|
+
options?.signal?.throwIfAborted();
|
|
554
|
+
const snapshot = materializeCreateHeader(header);
|
|
555
|
+
if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
|
|
556
|
+
const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
|
|
557
|
+
if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
|
|
558
|
+
await this.ready;
|
|
559
|
+
options?.signal?.throwIfAborted();
|
|
560
|
+
if (this.tracker.hasPending(snapshot.id) || await this.backend.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
|
|
561
|
+
this.tracker.registerCreated(snapshot, inheritedEventCount);
|
|
562
|
+
return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
|
|
563
|
+
cursor: 0,
|
|
564
|
+
materialized: false,
|
|
565
|
+
inheritedEventCount
|
|
566
|
+
}));
|
|
246
567
|
}
|
|
247
|
-
|
|
248
|
-
|
|
568
|
+
async open(id, access, options) {
|
|
569
|
+
options?.signal?.throwIfAborted();
|
|
570
|
+
await this.ready;
|
|
571
|
+
options?.signal?.throwIfAborted();
|
|
572
|
+
const pending = this.tracker.pendingOf(id);
|
|
573
|
+
if (access === "read") {
|
|
574
|
+
if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
|
|
575
|
+
cursor: 0,
|
|
576
|
+
materialized: false,
|
|
577
|
+
inheritedEventCount: pending.inheritedEventCount
|
|
578
|
+
}));
|
|
579
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
580
|
+
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
581
|
+
validateStoredEvents(log.meta, log.events);
|
|
582
|
+
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
|
|
583
|
+
cursor: log.events.length,
|
|
584
|
+
materialized: true,
|
|
585
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
this.tracker.claimWrite(id);
|
|
589
|
+
try {
|
|
590
|
+
if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
|
|
591
|
+
cursor: pending.cursor,
|
|
592
|
+
materialized: false,
|
|
593
|
+
inheritedEventCount: pending.inheritedEventCount
|
|
594
|
+
}));
|
|
595
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
596
|
+
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
597
|
+
validateStoredEvents(log.meta, log.events);
|
|
598
|
+
if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
|
|
599
|
+
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
600
|
+
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
|
|
601
|
+
cursor: log.events.length,
|
|
602
|
+
materialized: true,
|
|
603
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
604
|
+
...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
|
|
605
|
+
}));
|
|
606
|
+
} catch (error) {
|
|
607
|
+
this.tracker.releaseClaim(id);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
249
610
|
}
|
|
250
|
-
|
|
251
|
-
return this.
|
|
611
|
+
flush() {
|
|
612
|
+
return this.tracker.flushAll();
|
|
252
613
|
}
|
|
253
|
-
async
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
614
|
+
async stat(id, options) {
|
|
615
|
+
options?.signal?.throwIfAborted();
|
|
616
|
+
await this.ready;
|
|
617
|
+
options?.signal?.throwIfAborted();
|
|
618
|
+
const pending = this.tracker.pendingOf(id);
|
|
619
|
+
if (pending !== void 0) return {
|
|
620
|
+
header: pending.header,
|
|
621
|
+
revision: pending.revision
|
|
260
622
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const log = await this.readLog(id, {}, signal);
|
|
264
|
-
if (log === void 0) {
|
|
265
|
-
this.writeGuard.confirmHead(id, -1);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
269
|
-
recomputeReplaceProvenance(log.events);
|
|
270
|
-
repairOrphanInboxSplices(log.events);
|
|
623
|
+
const row = await this.backend.getSession(id);
|
|
624
|
+
if (row === void 0) return void 0;
|
|
271
625
|
return {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
events: log.events,
|
|
275
|
-
revision: SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${log.incarnation}:revision:${log.revision}`),
|
|
276
|
-
...log.tornFrom !== void 0 ? { tornMarker: log.tornFrom } : {}
|
|
626
|
+
header: rowToMeta(row),
|
|
627
|
+
revision: this.rowRevision(row)
|
|
277
628
|
};
|
|
278
629
|
}
|
|
279
|
-
async
|
|
630
|
+
async list(options) {
|
|
631
|
+
const signal = options?.signal;
|
|
632
|
+
const snapshots = [];
|
|
633
|
+
const listed = /* @__PURE__ */ new Set();
|
|
634
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
635
|
+
snapshots.push({
|
|
636
|
+
header: pending.header,
|
|
637
|
+
revision: pending.revision
|
|
638
|
+
});
|
|
639
|
+
listed.add(id);
|
|
640
|
+
}
|
|
280
641
|
signal?.throwIfAborted();
|
|
281
642
|
await this.ready;
|
|
282
643
|
signal?.throwIfAborted();
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
644
|
+
const rows = await this.backend.listSessions();
|
|
645
|
+
signal?.throwIfAborted();
|
|
646
|
+
for (const row of rows) {
|
|
647
|
+
if (listed.has(row.fSessionId)) continue;
|
|
648
|
+
snapshots.push({
|
|
649
|
+
header: rowToMeta(row),
|
|
650
|
+
revision: this.rowRevision(row)
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return snapshots;
|
|
286
654
|
}
|
|
287
|
-
|
|
655
|
+
/** 导出 artifact(jsonl v2 文本),视图只读不落库。 */
|
|
656
|
+
async readRaw(id, signal) {
|
|
288
657
|
signal?.throwIfAborted();
|
|
289
658
|
await this.ready;
|
|
290
659
|
signal?.throwIfAborted();
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let seqMap;
|
|
296
|
-
if (options.fromSeq === void 0) {
|
|
297
|
-
eventRows = await this.backend.getEventRows(id);
|
|
298
|
-
seqMap = buildSeqMap(eventRows);
|
|
299
|
-
} else {
|
|
300
|
-
eventRows = await this.backend.getEventRows(id, options.fromSeq);
|
|
301
|
-
const seqRows = await this.backend.getSeqMapRows(id);
|
|
302
|
-
seqMap = buildSeqMap(seqRows);
|
|
303
|
-
}
|
|
304
|
-
signal?.throwIfAborted();
|
|
305
|
-
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0, seqMap);
|
|
660
|
+
const log = await this.readLog(id, {}, signal);
|
|
661
|
+
if (log === void 0) return void 0;
|
|
662
|
+
repairReadView(log.events);
|
|
663
|
+
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
306
664
|
return {
|
|
307
|
-
meta,
|
|
308
|
-
inheritedEventCount
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
revision: row.fRevision,
|
|
312
|
-
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
665
|
+
meta: log.meta,
|
|
666
|
+
inheritedEventCount,
|
|
667
|
+
filename: "session.jsonl",
|
|
668
|
+
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
|
|
313
669
|
};
|
|
314
670
|
}
|
|
315
|
-
|
|
671
|
+
/** 空会话 materialize(flush 的持久化屏障)。 */
|
|
672
|
+
async materializeEmpty(meta, inheritedEventCount) {
|
|
316
673
|
await this.ready;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
674
|
+
await this.backend.transaction(async (tx) => {
|
|
675
|
+
await tx.upsertSession({
|
|
676
|
+
meta,
|
|
677
|
+
inheritedEventCount
|
|
678
|
+
}, randomUUID());
|
|
679
|
+
await tx.bumpRevision(meta.id);
|
|
680
|
+
});
|
|
681
|
+
this.tracker.materialized(meta.id);
|
|
682
|
+
this.writeGuard.confirmHead(meta.id, -1);
|
|
683
|
+
}
|
|
684
|
+
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
685
|
+
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
|
|
686
|
+
* 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
687
|
+
* 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
688
|
+
async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
|
|
689
|
+
await this.ready;
|
|
690
|
+
if (events.length === 0) return false;
|
|
691
|
+
validateStoredEvents(meta, [...events]);
|
|
320
692
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
321
693
|
if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
|
|
322
694
|
let confirmedHead = -1;
|
|
323
695
|
await this.backend.transaction(async (tx) => {
|
|
324
|
-
|
|
696
|
+
if (tornTruncateTo !== void 0) {
|
|
697
|
+
await tx.deleteBridgeTail(meta.id, tornTruncateTo);
|
|
698
|
+
const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
|
|
699
|
+
if (prev === void 0) await tx.updateHead(meta.id, "", -1);
|
|
700
|
+
else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
701
|
+
}
|
|
702
|
+
await tx.upsertSession({
|
|
703
|
+
meta,
|
|
704
|
+
inheritedEventCount
|
|
705
|
+
}, randomUUID());
|
|
325
706
|
const head = await tx.getHead(meta.id);
|
|
326
707
|
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
327
|
-
const { headEventId, headSequence } = await appendEventTail(tx, meta,
|
|
708
|
+
const { headEventId, headSequence } = await appendEventTail(tx, meta, events, {
|
|
328
709
|
parentId: head.fHeadEventId,
|
|
329
710
|
nextSeq: head.fHeadSequence + 1
|
|
330
711
|
}, reuse);
|
|
@@ -333,51 +714,162 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
333
714
|
confirmedHead = headSequence;
|
|
334
715
|
});
|
|
335
716
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
717
|
+
this.tracker.materialized(meta.id);
|
|
718
|
+
return true;
|
|
336
719
|
}
|
|
337
|
-
|
|
720
|
+
/** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
|
|
721
|
+
async readLog(id, options = {}, signal) {
|
|
722
|
+
signal?.throwIfAborted();
|
|
338
723
|
await this.ready;
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
724
|
+
signal?.throwIfAborted();
|
|
725
|
+
const row = await this.backend.getSession(id);
|
|
726
|
+
if (row === void 0) return void 0;
|
|
727
|
+
const meta = rowToMeta(row);
|
|
728
|
+
const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
|
|
729
|
+
signal?.throwIfAborted();
|
|
730
|
+
if (isLegacyVersion(row.fVersion)) try {
|
|
731
|
+
const converted = convertLegacyRows(row, eventRows);
|
|
732
|
+
return {
|
|
733
|
+
meta: converted.meta,
|
|
734
|
+
inheritedEventCount: converted.inheritedEventCount,
|
|
735
|
+
events: converted.events,
|
|
736
|
+
incarnation: row.fIncarnation,
|
|
737
|
+
revision: row.fRevision,
|
|
738
|
+
storedCount: eventRows.length,
|
|
739
|
+
migrated: true
|
|
740
|
+
};
|
|
741
|
+
} catch (error) {
|
|
742
|
+
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)})`);
|
|
743
|
+
const adopted = adoptLegacyRows(row, eventRows);
|
|
744
|
+
return {
|
|
745
|
+
meta: adopted.meta,
|
|
746
|
+
inheritedEventCount: adopted.inheritedEventCount,
|
|
747
|
+
events: adopted.events,
|
|
748
|
+
incarnation: row.fIncarnation,
|
|
749
|
+
revision: row.fRevision,
|
|
750
|
+
storedCount: eventRows.length,
|
|
751
|
+
migrated: false,
|
|
752
|
+
...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
756
|
+
return {
|
|
757
|
+
meta,
|
|
758
|
+
inheritedEventCount: row.fSeedLength ?? 0,
|
|
759
|
+
events: preserved,
|
|
760
|
+
incarnation: row.fIncarnation,
|
|
761
|
+
revision: row.fRevision,
|
|
762
|
+
storedCount: eventRows.length,
|
|
763
|
+
migrated: false,
|
|
764
|
+
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
|
|
769
|
+
*
|
|
770
|
+
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
771
|
+
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
772
|
+
* append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
|
|
773
|
+
* (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
|
|
774
|
+
* fork 子会话引用,孤儿由惰性 GC 处理)。
|
|
775
|
+
*/
|
|
776
|
+
async rewriteMigratedLog(id, log) {
|
|
342
777
|
await this.backend.transaction(async (tx) => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
});
|
|
355
|
-
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
356
|
-
}
|
|
357
|
-
await tx.bumpRevision(meta.id);
|
|
778
|
+
await tx.deleteBridgeTail(id, 0);
|
|
779
|
+
await tx.upsertSession({
|
|
780
|
+
meta: log.meta,
|
|
781
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
|
|
782
|
+
}, randomUUID());
|
|
783
|
+
const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
|
|
784
|
+
parentId: "",
|
|
785
|
+
nextSeq: 0
|
|
786
|
+
});
|
|
787
|
+
await tx.updateHead(id, headEventId, headSequence);
|
|
788
|
+
await tx.bumpRevision(id);
|
|
358
789
|
});
|
|
359
|
-
const row = await this.backend.getSession(meta.id);
|
|
360
|
-
this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
|
|
361
790
|
}
|
|
362
|
-
async
|
|
791
|
+
async listSnapshots(signal) {
|
|
363
792
|
signal?.throwIfAborted();
|
|
364
793
|
await this.ready;
|
|
365
794
|
signal?.throwIfAborted();
|
|
795
|
+
const snapshots = [];
|
|
796
|
+
const listed = /* @__PURE__ */ new Set();
|
|
797
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
798
|
+
snapshots.push({
|
|
799
|
+
header: pending.header,
|
|
800
|
+
revision: pending.revision,
|
|
801
|
+
inheritedEventCount: pending.inheritedEventCount
|
|
802
|
+
});
|
|
803
|
+
listed.add(id);
|
|
804
|
+
}
|
|
366
805
|
const rows = await this.backend.listSessions();
|
|
367
806
|
signal?.throwIfAborted();
|
|
368
|
-
|
|
807
|
+
for (const row of rows) {
|
|
808
|
+
if (listed.has(row.fSessionId)) continue;
|
|
809
|
+
snapshots.push({
|
|
810
|
+
header: rowToMeta(row),
|
|
811
|
+
revision: this.rowRevision(row),
|
|
812
|
+
inheritedEventCount: row.fSeedLength ?? 0
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
return snapshots;
|
|
369
816
|
}
|
|
370
|
-
async
|
|
817
|
+
async readStoredRevision(id, signal) {
|
|
371
818
|
signal?.throwIfAborted();
|
|
372
819
|
await this.ready;
|
|
373
820
|
signal?.throwIfAborted();
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
return
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
})
|
|
821
|
+
const row = await this.backend.getSession(id);
|
|
822
|
+
if (row === void 0) return void 0;
|
|
823
|
+
return this.rowRevision(row);
|
|
824
|
+
}
|
|
825
|
+
/** 便捷:create + append + close(测试与导入路径共用)。 */
|
|
826
|
+
async createAndAppend(header, events, inheritedEventCount) {
|
|
827
|
+
const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
|
|
828
|
+
try {
|
|
829
|
+
if (events.length > 0) await handle.append(events);
|
|
830
|
+
} finally {
|
|
831
|
+
await handle.close();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/** 便捷:open(read) + read 全量 + close。 */
|
|
835
|
+
async load(id, signal) {
|
|
836
|
+
const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
|
|
837
|
+
try {
|
|
838
|
+
const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
|
|
839
|
+
const row = await this.backend.getSession(id);
|
|
840
|
+
if (row === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
841
|
+
if (isLegacyVersion(row.fVersion)) return {
|
|
842
|
+
meta: handle.header,
|
|
843
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
844
|
+
events
|
|
845
|
+
};
|
|
846
|
+
return {
|
|
847
|
+
meta: rowToMeta(row),
|
|
848
|
+
inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
|
|
849
|
+
events
|
|
850
|
+
};
|
|
851
|
+
} finally {
|
|
852
|
+
await handle.close();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/** 便捷:open(write) + append + close。 */
|
|
856
|
+
async append(id, events) {
|
|
857
|
+
const handle = await this.open(id, "write");
|
|
858
|
+
try {
|
|
859
|
+
await handle.append(events);
|
|
860
|
+
} finally {
|
|
861
|
+
await handle.close();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/** 便捷:readFrom(稠密后缀)。 */
|
|
865
|
+
async readFrom(id, fromSeq, signal) {
|
|
866
|
+
const log = await this.readLog(id, { fromSeq }, signal);
|
|
867
|
+
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
868
|
+
return {
|
|
869
|
+
meta: log.meta,
|
|
870
|
+
inheritedEventCount: log.inheritedEventCount,
|
|
871
|
+
events: log.events
|
|
872
|
+
};
|
|
381
873
|
}
|
|
382
874
|
async close() {
|
|
383
875
|
await this.ready;
|
|
@@ -390,17 +882,117 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
390
882
|
return {
|
|
391
883
|
backend: this.backend,
|
|
392
884
|
writeGuard: this.writeGuard,
|
|
393
|
-
create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount),
|
|
885
|
+
create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) }),
|
|
394
886
|
append: (id, events) => this.append(id, events),
|
|
395
887
|
load: (id) => this.load(id),
|
|
396
|
-
inspect: (id, signal) => this.
|
|
888
|
+
inspect: (id, signal) => this.load(id, signal),
|
|
397
889
|
readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
|
|
398
890
|
listSnapshots: (signal) => this.listSnapshots(signal),
|
|
399
891
|
readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
|
|
400
892
|
registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
|
|
401
893
|
};
|
|
402
894
|
}
|
|
895
|
+
rowRevision(row) {
|
|
896
|
+
return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
|
|
897
|
+
}
|
|
898
|
+
installLiveRouting(ctx) {
|
|
899
|
+
ctx.on("session/created", (session) => {
|
|
900
|
+
this.liveBuffers.set(session.id, []);
|
|
901
|
+
const ready = this.ensureLiveHandle(session);
|
|
902
|
+
this.liveReady.set(session.id, ready);
|
|
903
|
+
ready.catch((error) => {
|
|
904
|
+
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
for (const session of ctx.sessions.list()) {
|
|
908
|
+
this.liveBuffers.set(session.id, []);
|
|
909
|
+
const ready = this.ensureLiveHandle(session);
|
|
910
|
+
this.liveReady.set(session.id, ready);
|
|
911
|
+
ready.catch((error) => {
|
|
912
|
+
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
ctx.on("session/event", (session, event) => {
|
|
916
|
+
const handle = this.tracker.writerOf(session.id);
|
|
917
|
+
if (handle !== void 0) {
|
|
918
|
+
handle.enqueueLive(event, (error) => {
|
|
919
|
+
ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
|
|
920
|
+
});
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
924
|
+
});
|
|
925
|
+
ctx.on("session/flush", (session) => {
|
|
926
|
+
const handle = this.tracker.writerOf(session.id);
|
|
927
|
+
if (handle === void 0) {
|
|
928
|
+
const ready = this.liveReady.get(session.id);
|
|
929
|
+
if (ready === void 0) return void 0;
|
|
930
|
+
return ready.then(() => {
|
|
931
|
+
const settled = this.tracker.writerOf(session.id);
|
|
932
|
+
if (settled === void 0) return void 0;
|
|
933
|
+
return settled.drainLive().then(() => settled.flush());
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
return handle.drainLive().then(() => handle.flush());
|
|
937
|
+
});
|
|
938
|
+
ctx.on("session/disposed", (session) => {
|
|
939
|
+
const ready = this.liveReady.get(session.id);
|
|
940
|
+
this.liveBuffers.delete(session.id);
|
|
941
|
+
this.liveReady.delete(session.id);
|
|
942
|
+
const closeHandle = () => {
|
|
943
|
+
const handle = this.tracker.writerOf(session.id);
|
|
944
|
+
if (handle === void 0) return;
|
|
945
|
+
handle.close().catch((error) => {
|
|
946
|
+
ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
if (ready === void 0) {
|
|
950
|
+
closeHandle();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
ready.then(closeHandle, closeHandle);
|
|
954
|
+
});
|
|
955
|
+
ctx.effect(() => async () => {
|
|
956
|
+
await Promise.allSettled(this.liveReady.values());
|
|
957
|
+
await this.tracker.closeAll();
|
|
958
|
+
await this.close();
|
|
959
|
+
}, `${this.name} open handles`);
|
|
960
|
+
}
|
|
961
|
+
/** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
|
|
962
|
+
async ensureLiveHandle(session) {
|
|
963
|
+
const id = session.header.id;
|
|
964
|
+
if (this.tracker.writerOf(id) !== void 0) return;
|
|
965
|
+
await this.ready;
|
|
966
|
+
const stored = await this.readLog(id, {});
|
|
967
|
+
let handle;
|
|
968
|
+
if (stored === void 0) {
|
|
969
|
+
handle = await this.create(session.header, { inheritedEventCount: session.inheritedEventCount });
|
|
970
|
+
const seed = session.snapshotEvents();
|
|
971
|
+
if (seed.length > 0) await handle.append(seed);
|
|
972
|
+
} else {
|
|
973
|
+
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)`);
|
|
974
|
+
if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
|
|
975
|
+
assertVersion(stored.meta);
|
|
976
|
+
repairReadView(stored.events);
|
|
977
|
+
const seed = session.snapshotEvents();
|
|
978
|
+
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)`);
|
|
979
|
+
handle = await this.open(id, "write");
|
|
980
|
+
const suffix = seed.slice(stored.events.length);
|
|
981
|
+
if (suffix.length > 0) await handle.append(suffix);
|
|
982
|
+
}
|
|
983
|
+
const buffered = this.liveBuffers.get(id);
|
|
984
|
+
if (buffered !== void 0 && buffered.length > 0) {
|
|
985
|
+
this.liveBuffers.set(id, []);
|
|
986
|
+
for (const event of buffered) handle.enqueueLive(event, () => {});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
403
989
|
};
|
|
990
|
+
function seedCoversPrefix(seed, prefix) {
|
|
991
|
+
return prefix.length <= seed.length && prefix.every((event, index) => {
|
|
992
|
+
const seedEvent = seed[index];
|
|
993
|
+
return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
404
996
|
function createBackend(config) {
|
|
405
997
|
if (config.type === "sqlite") return new SqliteBackend({
|
|
406
998
|
path: config.path,
|
|
@@ -431,6 +1023,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
431
1023
|
const eventId = reusedId ?? randomUUID();
|
|
432
1024
|
if (reusedId === void 0) {
|
|
433
1025
|
const { kind, role, name, actionId } = eventDimensions(event);
|
|
1026
|
+
const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
|
|
434
1027
|
eventRows.push({
|
|
435
1028
|
fEventId: eventId,
|
|
436
1029
|
fParentId: parentId,
|
|
@@ -440,7 +1033,10 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
440
1033
|
fName: name,
|
|
441
1034
|
fActionId: actionId,
|
|
442
1035
|
fEncoding: EVENT_ENCODING,
|
|
443
|
-
fData: JSON.stringify(
|
|
1036
|
+
fData: JSON.stringify({
|
|
1037
|
+
...envelope,
|
|
1038
|
+
data
|
|
1039
|
+
}),
|
|
444
1040
|
fCreatedAt: event.time
|
|
445
1041
|
});
|
|
446
1042
|
}
|
|
@@ -449,7 +1045,6 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
449
1045
|
fSessionId: meta.id,
|
|
450
1046
|
fEventId: eventId,
|
|
451
1047
|
fSequence: nextSeq,
|
|
452
|
-
fOriginalSeq: event.seq,
|
|
453
1048
|
fSurfaceOp: surfaceOp
|
|
454
1049
|
});
|
|
455
1050
|
parentId = eventId;
|
|
@@ -463,4 +1058,4 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
463
1058
|
};
|
|
464
1059
|
}
|
|
465
1060
|
//#endregion
|
|
466
|
-
export {
|
|
1061
|
+
export { SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
|