@morlay/session-rdb 0.0.14 → 0.0.16-alpha.0
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 +6 -16
- package/dist/artifact.mjs +3 -3
- package/dist/{import-B-tf5vQn.mjs → import-CKrzjXVB.mjs} +73 -83
- package/dist/import.d.mts +2 -3
- package/dist/import.mjs +2 -2
- package/dist/index-CgGDHQSK.d.mts +225 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +694 -169
- package/dist/log-DTJsxMud.mjs +314 -0
- package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
- package/dist/sqlite-DCy4VTD8.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 +868 -223
- package/src/invariant.ts +0 -2
- package/src/legacy.ts +67 -0
- package/src/log.ts +41 -81
- 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-BfPVmr1X.mjs +0 -875
- package/dist/sqlite-BUWnS0so.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-DCy4VTD8.mjs";
|
|
2
|
+
import { a as repairSurfaceOps, c as scanRows, d as toJsonlArtifact, i as repairOrphanInboxSplices, l as sessionConflictRow, r as recomputeReplaceProvenance, s as rowToMeta, u as sessionInsertRow } from "./log-DTJsxMud.mjs";
|
|
3
|
+
import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-CKrzjXVB.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 {
|
|
8
|
+
import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
|
|
9
9
|
import { SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
10
|
-
import {
|
|
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,315 @@ 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
|
+
//#endregion
|
|
164
213
|
//#region src/index.ts
|
|
214
|
+
/** 会话级写所有权与 live 路由簿记。 */
|
|
215
|
+
var RdbBackendTracker = class {
|
|
216
|
+
name;
|
|
217
|
+
/** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
|
|
218
|
+
writers = /* @__PURE__ */ new Map();
|
|
219
|
+
pending = /* @__PURE__ */ new Map();
|
|
220
|
+
openHandles = /* @__PURE__ */ new Set();
|
|
221
|
+
counter = 0;
|
|
222
|
+
constructor(name) {
|
|
223
|
+
this.name = name;
|
|
224
|
+
}
|
|
225
|
+
registerCreated(header, inheritedEventCount) {
|
|
226
|
+
if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
|
|
227
|
+
this.writers.set(header.id, null);
|
|
228
|
+
this.pending.set(header.id, {
|
|
229
|
+
header,
|
|
230
|
+
revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
|
|
231
|
+
inheritedEventCount,
|
|
232
|
+
cursor: 0,
|
|
233
|
+
everAppended: false
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
|
|
237
|
+
updatePending(id, cursor, everAppended) {
|
|
238
|
+
const entry = this.pending.get(id);
|
|
239
|
+
if (entry === void 0) return;
|
|
240
|
+
this.pending.set(id, {
|
|
241
|
+
...entry,
|
|
242
|
+
cursor,
|
|
243
|
+
everAppended
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
claimWrite(id) {
|
|
247
|
+
if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
|
|
248
|
+
this.writers.set(id, null);
|
|
249
|
+
}
|
|
250
|
+
releaseClaim(id) {
|
|
251
|
+
this.writers.delete(id);
|
|
252
|
+
}
|
|
253
|
+
pendingOf(id) {
|
|
254
|
+
return this.pending.get(id);
|
|
255
|
+
}
|
|
256
|
+
hasPending(id) {
|
|
257
|
+
return this.pending.has(id);
|
|
258
|
+
}
|
|
259
|
+
pendingEntries() {
|
|
260
|
+
return this.pending.entries();
|
|
261
|
+
}
|
|
262
|
+
materialized(id) {
|
|
263
|
+
this.pending.delete(id);
|
|
264
|
+
}
|
|
265
|
+
adopt(handle) {
|
|
266
|
+
this.openHandles.add(handle);
|
|
267
|
+
if (handle.access === "write") this.writers.set(handle.id, handle);
|
|
268
|
+
return handle;
|
|
269
|
+
}
|
|
270
|
+
release(handle, materialized) {
|
|
271
|
+
this.openHandles.delete(handle);
|
|
272
|
+
if (handle.access !== "write") return;
|
|
273
|
+
this.writers.delete(handle.id);
|
|
274
|
+
if (!materialized) this.pending.delete(handle.id);
|
|
275
|
+
}
|
|
276
|
+
writerOf(id) {
|
|
277
|
+
const writer = this.writers.get(id);
|
|
278
|
+
return writer === null ? void 0 : writer;
|
|
279
|
+
}
|
|
280
|
+
async flushAll() {
|
|
281
|
+
const errors = [];
|
|
282
|
+
for (const writer of this.writers.values()) {
|
|
283
|
+
if (writer === null) continue;
|
|
284
|
+
try {
|
|
285
|
+
await writer.drainLive();
|
|
286
|
+
await writer.flush();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error instanceof SessionHandleClosedError) continue;
|
|
289
|
+
errors.push(error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
|
|
293
|
+
}
|
|
294
|
+
async closeAll() {
|
|
295
|
+
const errors = [];
|
|
296
|
+
for (const handle of this.openHandles) try {
|
|
297
|
+
await handle.close();
|
|
298
|
+
} catch (error) {
|
|
299
|
+
errors.push(error);
|
|
300
|
+
}
|
|
301
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
/** 一个打开会话的存储句柄:read / append / flush / close。 */
|
|
305
|
+
var RdbSessionHandle = class RdbSessionHandle {
|
|
306
|
+
persistence;
|
|
307
|
+
id;
|
|
308
|
+
header;
|
|
309
|
+
access;
|
|
310
|
+
state;
|
|
311
|
+
chain = Promise.resolve();
|
|
312
|
+
closing;
|
|
313
|
+
/** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
|
|
314
|
+
cursor;
|
|
315
|
+
materialized;
|
|
316
|
+
/** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
|
|
317
|
+
buffered = [];
|
|
318
|
+
batchTimer;
|
|
319
|
+
drainPaused = false;
|
|
320
|
+
draining;
|
|
321
|
+
/** write open 时发现的 torn tail 起点(append 前先截断)。 */
|
|
322
|
+
tornTruncateTo;
|
|
323
|
+
/** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
|
|
324
|
+
everAppended = false;
|
|
325
|
+
constructor(persistence, id, header, access, state) {
|
|
326
|
+
this.persistence = persistence;
|
|
327
|
+
this.id = id;
|
|
328
|
+
this.header = header;
|
|
329
|
+
this.access = access;
|
|
330
|
+
this.state = state;
|
|
331
|
+
this.cursor = state.cursor;
|
|
332
|
+
this.materialized = state.materialized;
|
|
333
|
+
this.tornTruncateTo = state.tornTruncateTo;
|
|
334
|
+
}
|
|
335
|
+
get inheritedEventCount() {
|
|
336
|
+
return this.state.inheritedEventCount;
|
|
337
|
+
}
|
|
338
|
+
/** 输入空间 cursor(下一个待落库的上游 seq)。 */
|
|
339
|
+
get cursorValue() {
|
|
340
|
+
return this.cursor;
|
|
341
|
+
}
|
|
342
|
+
async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
|
|
343
|
+
this.assertOpen("read");
|
|
344
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
|
|
345
|
+
if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
|
|
346
|
+
options?.signal?.throwIfAborted();
|
|
347
|
+
const log = await this.persistence.readLog(this.id, {}, options?.signal);
|
|
348
|
+
if (log === void 0) {
|
|
349
|
+
if (this.persistence.tracker.hasPending(this.id)) return {
|
|
350
|
+
eventState: "detached",
|
|
351
|
+
events: []
|
|
352
|
+
};
|
|
353
|
+
throw new SessionPersistenceNotFoundError(this.id);
|
|
354
|
+
}
|
|
355
|
+
repairSurfaceOps(log.events);
|
|
356
|
+
recomputeReplaceProvenance(log.events);
|
|
357
|
+
repairOrphanInboxSplices(log.events);
|
|
358
|
+
return {
|
|
359
|
+
eventState: "detached",
|
|
360
|
+
events: log.events.slice(offset, offset + length)
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async append(events, options) {
|
|
364
|
+
this.assertOpen("append");
|
|
365
|
+
const batch = materializeAppendBatch(events);
|
|
366
|
+
return this.run("append", async () => {
|
|
367
|
+
options?.signal?.throwIfAborted();
|
|
368
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
|
|
369
|
+
if (batch.length === 0) return;
|
|
370
|
+
this.everAppended = true;
|
|
371
|
+
assertContiguous(this.id, batch, this.cursor);
|
|
372
|
+
await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
|
|
373
|
+
this.tornTruncateTo = void 0;
|
|
374
|
+
this.cursor += batch.length;
|
|
375
|
+
this.materialized = true;
|
|
376
|
+
this.persistence.tracker.updatePending(this.id, this.cursor, true);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
async flush(options) {
|
|
380
|
+
return this.run("flush", async () => {
|
|
381
|
+
options?.signal?.throwIfAborted();
|
|
382
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
|
|
383
|
+
if (this.materialized) return;
|
|
384
|
+
await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
|
|
385
|
+
this.materialized = true;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
close() {
|
|
389
|
+
return this.closing ??= (async () => {
|
|
390
|
+
let drainFailure;
|
|
391
|
+
for (;;) {
|
|
392
|
+
try {
|
|
393
|
+
await this.drainLive();
|
|
394
|
+
} catch (error) {
|
|
395
|
+
drainFailure = error;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
await this.chain;
|
|
399
|
+
if (this.buffered.length === 0) break;
|
|
400
|
+
}
|
|
401
|
+
await this.chain;
|
|
402
|
+
const failures = [];
|
|
403
|
+
if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
|
|
404
|
+
this.persistence.tracker.release(this, this.materialized || this.everAppended);
|
|
405
|
+
if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
|
|
406
|
+
if (failures[0] !== void 0) throw failures[0];
|
|
407
|
+
})();
|
|
408
|
+
}
|
|
409
|
+
[Symbol.asyncDispose]() {
|
|
410
|
+
return this.close();
|
|
411
|
+
}
|
|
412
|
+
/** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
|
|
413
|
+
enqueueLive(event, reportBackgroundFailure) {
|
|
414
|
+
this.buffered.push(structuredClone(event));
|
|
415
|
+
if (this.batchTimer !== void 0 || this.drainPaused) return;
|
|
416
|
+
this.batchTimer = setTimeout(() => {
|
|
417
|
+
this.batchTimer = void 0;
|
|
418
|
+
this.drainLive().catch(reportBackgroundFailure);
|
|
419
|
+
}, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
|
|
420
|
+
}
|
|
421
|
+
/** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
|
|
422
|
+
resetAfterRewind(cursor, inheritedEventCount) {
|
|
423
|
+
this.cursor = cursor;
|
|
424
|
+
if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
|
|
425
|
+
}
|
|
426
|
+
/** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
|
|
427
|
+
drainLive() {
|
|
428
|
+
return this.draining ??= this.drainBuffered().finally(() => {
|
|
429
|
+
this.draining = void 0;
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async drainBuffered() {
|
|
433
|
+
if (this.batchTimer !== void 0) {
|
|
434
|
+
clearTimeout(this.batchTimer);
|
|
435
|
+
this.batchTimer = void 0;
|
|
436
|
+
}
|
|
437
|
+
this.drainPaused = false;
|
|
438
|
+
while (this.buffered.length > 0) await this.enqueueChain(async () => {
|
|
439
|
+
const batch = this.buffered.splice(0);
|
|
440
|
+
try {
|
|
441
|
+
const fresh = batch.filter((event) => event.seq >= this.cursor);
|
|
442
|
+
if (fresh.length === 0) return;
|
|
443
|
+
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}`);
|
|
444
|
+
await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
|
|
445
|
+
this.tornTruncateTo = void 0;
|
|
446
|
+
this.cursor += fresh.length;
|
|
447
|
+
this.materialized = true;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
this.buffered = batch.concat(this.buffered);
|
|
450
|
+
this.drainPaused = true;
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
enqueueChain(op) {
|
|
456
|
+
const next = this.chain.then(op);
|
|
457
|
+
this.chain = next.catch(() => {});
|
|
458
|
+
return next;
|
|
459
|
+
}
|
|
460
|
+
async run(operation, op) {
|
|
461
|
+
this.assertOpen(operation);
|
|
462
|
+
return this.enqueueChain(async () => {
|
|
463
|
+
this.assertOpen(operation);
|
|
464
|
+
return op();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
assertOpen(operation) {
|
|
468
|
+
if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
|
|
469
|
+
}
|
|
470
|
+
static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
471
|
+
};
|
|
165
472
|
var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
|
|
166
473
|
config;
|
|
167
474
|
static inject = ["sessions", "settings"];
|
|
@@ -182,29 +489,15 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
182
489
|
})]);
|
|
183
490
|
static settingsNs = "session-rdb";
|
|
184
491
|
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
|
-
}
|
|
492
|
+
tracker = new RdbBackendTracker(this.name);
|
|
202
493
|
backend;
|
|
203
494
|
storeIdentity;
|
|
204
495
|
ready;
|
|
205
|
-
coordinator;
|
|
206
496
|
writeGuard = new WriteGuard();
|
|
207
497
|
reuseEventIds = /* @__PURE__ */ new Map();
|
|
498
|
+
/** live 路由:session/created 后 handle 就绪前的缓冲。 */
|
|
499
|
+
liveBuffers = /* @__PURE__ */ new Map();
|
|
500
|
+
liveReady = /* @__PURE__ */ new Map();
|
|
208
501
|
constructor(ctx, config, injectedBackend) {
|
|
209
502
|
let resolved = config;
|
|
210
503
|
const settings = ctx.reflect.get("settings");
|
|
@@ -220,7 +513,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
220
513
|
this.config = resolved;
|
|
221
514
|
this.backend = injectedBackend ?? createBackend(resolved);
|
|
222
515
|
this.ready = this.init();
|
|
223
|
-
this.
|
|
516
|
+
this.installLiveRouting(ctx);
|
|
224
517
|
new SessionBranchRdb(this.ctx);
|
|
225
518
|
registerSessionImport(this.ctx, this);
|
|
226
519
|
}
|
|
@@ -228,103 +521,163 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
228
521
|
await this.backend.open();
|
|
229
522
|
this.storeIdentity = this.backend.storeIdentity;
|
|
230
523
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
524
|
+
async create(header, options) {
|
|
525
|
+
options?.signal?.throwIfAborted();
|
|
526
|
+
const snapshot = materializeCreateHeader(header);
|
|
527
|
+
if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
|
|
528
|
+
const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
|
|
529
|
+
if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
|
|
530
|
+
await this.ready;
|
|
531
|
+
options?.signal?.throwIfAborted();
|
|
532
|
+
if (this.tracker.hasPending(snapshot.id) || await this.backend.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
|
|
533
|
+
this.tracker.registerCreated(snapshot, inheritedEventCount);
|
|
534
|
+
return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
|
|
535
|
+
cursor: 0,
|
|
536
|
+
materialized: false,
|
|
537
|
+
inheritedEventCount
|
|
538
|
+
}));
|
|
246
539
|
}
|
|
247
|
-
|
|
248
|
-
|
|
540
|
+
async open(id, access, options) {
|
|
541
|
+
options?.signal?.throwIfAborted();
|
|
542
|
+
await this.ready;
|
|
543
|
+
options?.signal?.throwIfAborted();
|
|
544
|
+
const pending = this.tracker.pendingOf(id);
|
|
545
|
+
if (access === "read") {
|
|
546
|
+
if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
|
|
547
|
+
cursor: 0,
|
|
548
|
+
materialized: false,
|
|
549
|
+
inheritedEventCount: pending.inheritedEventCount
|
|
550
|
+
}));
|
|
551
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
552
|
+
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
553
|
+
validateStoredEvents(log.meta, log.events);
|
|
554
|
+
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
|
|
555
|
+
cursor: log.events.length,
|
|
556
|
+
materialized: true,
|
|
557
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
this.tracker.claimWrite(id);
|
|
561
|
+
try {
|
|
562
|
+
if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
|
|
563
|
+
cursor: pending.cursor,
|
|
564
|
+
materialized: false,
|
|
565
|
+
inheritedEventCount: pending.inheritedEventCount
|
|
566
|
+
}));
|
|
567
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
568
|
+
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
569
|
+
validateStoredEvents(log.meta, log.events);
|
|
570
|
+
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
571
|
+
return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
|
|
572
|
+
cursor: log.events.length,
|
|
573
|
+
materialized: true,
|
|
574
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
575
|
+
...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
|
|
576
|
+
}));
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this.tracker.releaseClaim(id);
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
249
581
|
}
|
|
250
|
-
|
|
251
|
-
return this.
|
|
582
|
+
flush() {
|
|
583
|
+
return this.tracker.flushAll();
|
|
252
584
|
}
|
|
253
|
-
async
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
585
|
+
async stat(id, options) {
|
|
586
|
+
options?.signal?.throwIfAborted();
|
|
587
|
+
await this.ready;
|
|
588
|
+
options?.signal?.throwIfAborted();
|
|
589
|
+
const pending = this.tracker.pendingOf(id);
|
|
590
|
+
if (pending !== void 0) return {
|
|
591
|
+
header: pending.header,
|
|
592
|
+
revision: pending.revision
|
|
260
593
|
};
|
|
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);
|
|
594
|
+
const row = await this.backend.getSession(id);
|
|
595
|
+
if (row === void 0) return void 0;
|
|
271
596
|
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 } : {}
|
|
597
|
+
header: rowToMeta(row),
|
|
598
|
+
revision: this.rowRevision(row)
|
|
277
599
|
};
|
|
278
600
|
}
|
|
279
|
-
async
|
|
601
|
+
async list(options) {
|
|
602
|
+
const signal = options?.signal;
|
|
603
|
+
const snapshots = [];
|
|
604
|
+
const listed = /* @__PURE__ */ new Set();
|
|
605
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
606
|
+
snapshots.push({
|
|
607
|
+
header: pending.header,
|
|
608
|
+
revision: pending.revision
|
|
609
|
+
});
|
|
610
|
+
listed.add(id);
|
|
611
|
+
}
|
|
280
612
|
signal?.throwIfAborted();
|
|
281
613
|
await this.ready;
|
|
282
614
|
signal?.throwIfAborted();
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
615
|
+
const rows = await this.backend.listSessions();
|
|
616
|
+
signal?.throwIfAborted();
|
|
617
|
+
for (const row of rows) {
|
|
618
|
+
if (listed.has(row.fSessionId)) continue;
|
|
619
|
+
snapshots.push({
|
|
620
|
+
header: rowToMeta(row),
|
|
621
|
+
revision: this.rowRevision(row)
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
return snapshots;
|
|
286
625
|
}
|
|
287
|
-
|
|
626
|
+
/** 导出 artifact(jsonl v2 文本),视图只读不落库。 */
|
|
627
|
+
async readRaw(id, signal) {
|
|
288
628
|
signal?.throwIfAborted();
|
|
289
629
|
await this.ready;
|
|
290
630
|
signal?.throwIfAborted();
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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);
|
|
631
|
+
const log = await this.readLog(id, {}, signal);
|
|
632
|
+
if (log === void 0) return void 0;
|
|
633
|
+
repairSurfaceOps(log.events);
|
|
634
|
+
recomputeReplaceProvenance(log.events);
|
|
635
|
+
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
306
636
|
return {
|
|
307
|
-
meta,
|
|
308
|
-
inheritedEventCount
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
revision: row.fRevision,
|
|
312
|
-
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
637
|
+
meta: log.meta,
|
|
638
|
+
inheritedEventCount,
|
|
639
|
+
filename: "session.jsonl",
|
|
640
|
+
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
|
|
313
641
|
};
|
|
314
642
|
}
|
|
315
|
-
|
|
643
|
+
/** 空会话 materialize(flush 的持久化屏障)。 */
|
|
644
|
+
async materializeEmpty(meta, inheritedEventCount) {
|
|
316
645
|
await this.ready;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
646
|
+
await this.backend.transaction(async (tx) => {
|
|
647
|
+
await tx.upsertSession({
|
|
648
|
+
meta,
|
|
649
|
+
inheritedEventCount
|
|
650
|
+
}, randomUUID());
|
|
651
|
+
await tx.bumpRevision(meta.id);
|
|
652
|
+
});
|
|
653
|
+
this.tracker.materialized(meta.id);
|
|
654
|
+
this.writeGuard.confirmHead(meta.id, -1);
|
|
655
|
+
}
|
|
656
|
+
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
657
|
+
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
|
|
658
|
+
* 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
659
|
+
* 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
660
|
+
async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
|
|
661
|
+
await this.ready;
|
|
662
|
+
if (events.length === 0) return false;
|
|
663
|
+
validateStoredEvents(meta, [...events]);
|
|
320
664
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
321
665
|
if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
|
|
322
666
|
let confirmedHead = -1;
|
|
323
667
|
await this.backend.transaction(async (tx) => {
|
|
324
|
-
|
|
668
|
+
if (tornTruncateTo !== void 0) {
|
|
669
|
+
await tx.deleteBridgeTail(meta.id, tornTruncateTo);
|
|
670
|
+
const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
|
|
671
|
+
if (prev === void 0) await tx.updateHead(meta.id, "", -1);
|
|
672
|
+
else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
673
|
+
}
|
|
674
|
+
await tx.upsertSession({
|
|
675
|
+
meta,
|
|
676
|
+
inheritedEventCount
|
|
677
|
+
}, randomUUID());
|
|
325
678
|
const head = await tx.getHead(meta.id);
|
|
326
679
|
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
327
|
-
const { headEventId, headSequence } = await appendEventTail(tx, meta,
|
|
680
|
+
const { headEventId, headSequence } = await appendEventTail(tx, meta, events, {
|
|
328
681
|
parentId: head.fHeadEventId,
|
|
329
682
|
nextSeq: head.fHeadSequence + 1
|
|
330
683
|
}, reuse);
|
|
@@ -333,51 +686,121 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
333
686
|
confirmedHead = headSequence;
|
|
334
687
|
});
|
|
335
688
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
689
|
+
this.tracker.materialized(meta.id);
|
|
690
|
+
return true;
|
|
336
691
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const meta = storage.meta;
|
|
340
|
-
const persistedClosers = closers.filter(isPersistedEvent);
|
|
341
|
-
if (tornMarker === void 0 && persistedClosers.length === 0) return;
|
|
342
|
-
await this.backend.transaction(async (tx) => {
|
|
343
|
-
if (tornMarker !== void 0) {
|
|
344
|
-
await tx.deleteBridgeTail(meta.id, tornMarker);
|
|
345
|
-
const prev = await tx.getPrevBridge(meta.id, tornMarker - 1);
|
|
346
|
-
if (prev === void 0) await tx.updateHead(meta.id, "", -1);
|
|
347
|
-
else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
348
|
-
}
|
|
349
|
-
if (persistedClosers.length > 0) {
|
|
350
|
-
const last = await tx.getLastBridge(meta.id);
|
|
351
|
-
const { headEventId, headSequence } = await appendEventTail(tx, meta, persistedClosers, {
|
|
352
|
-
parentId: last?.fEventId ?? "",
|
|
353
|
-
nextSeq: (last?.fSequence ?? -1) + 1
|
|
354
|
-
});
|
|
355
|
-
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
356
|
-
}
|
|
357
|
-
await tx.bumpRevision(meta.id);
|
|
358
|
-
});
|
|
359
|
-
const row = await this.backend.getSession(meta.id);
|
|
360
|
-
this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
|
|
361
|
-
}
|
|
362
|
-
async list(signal) {
|
|
692
|
+
/** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
|
|
693
|
+
async readLog(id, options = {}, signal) {
|
|
363
694
|
signal?.throwIfAborted();
|
|
364
695
|
await this.ready;
|
|
365
696
|
signal?.throwIfAborted();
|
|
366
|
-
const
|
|
697
|
+
const row = await this.backend.getSession(id);
|
|
698
|
+
if (row === void 0) return void 0;
|
|
699
|
+
const meta = rowToMeta(row);
|
|
700
|
+
const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
|
|
367
701
|
signal?.throwIfAborted();
|
|
368
|
-
|
|
702
|
+
if (isLegacyVersion(row.fVersion)) {
|
|
703
|
+
const converted = convertLegacyRows(row, eventRows);
|
|
704
|
+
return {
|
|
705
|
+
meta: converted.meta,
|
|
706
|
+
inheritedEventCount: converted.inheritedEventCount,
|
|
707
|
+
events: converted.events,
|
|
708
|
+
incarnation: row.fIncarnation,
|
|
709
|
+
revision: row.fRevision
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
713
|
+
return {
|
|
714
|
+
meta,
|
|
715
|
+
inheritedEventCount: row.fSeedLength ?? 0,
|
|
716
|
+
events: preserved,
|
|
717
|
+
incarnation: row.fIncarnation,
|
|
718
|
+
revision: row.fRevision,
|
|
719
|
+
...tornFrom !== void 0 ? { tornFrom } : {}
|
|
720
|
+
};
|
|
369
721
|
}
|
|
370
722
|
async listSnapshots(signal) {
|
|
371
723
|
signal?.throwIfAborted();
|
|
372
724
|
await this.ready;
|
|
373
725
|
signal?.throwIfAborted();
|
|
726
|
+
const snapshots = [];
|
|
727
|
+
const listed = /* @__PURE__ */ new Set();
|
|
728
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
729
|
+
snapshots.push({
|
|
730
|
+
header: pending.header,
|
|
731
|
+
revision: pending.revision,
|
|
732
|
+
inheritedEventCount: pending.inheritedEventCount
|
|
733
|
+
});
|
|
734
|
+
listed.add(id);
|
|
735
|
+
}
|
|
374
736
|
const rows = await this.backend.listSessions();
|
|
375
737
|
signal?.throwIfAborted();
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
738
|
+
for (const row of rows) {
|
|
739
|
+
if (listed.has(row.fSessionId)) continue;
|
|
740
|
+
snapshots.push({
|
|
741
|
+
header: rowToMeta(row),
|
|
742
|
+
revision: this.rowRevision(row),
|
|
743
|
+
inheritedEventCount: row.fSeedLength ?? 0
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
return snapshots;
|
|
747
|
+
}
|
|
748
|
+
async readStoredRevision(id, signal) {
|
|
749
|
+
signal?.throwIfAborted();
|
|
750
|
+
await this.ready;
|
|
751
|
+
signal?.throwIfAborted();
|
|
752
|
+
const row = await this.backend.getSession(id);
|
|
753
|
+
if (row === void 0) return void 0;
|
|
754
|
+
return this.rowRevision(row);
|
|
755
|
+
}
|
|
756
|
+
/** 便捷:create + append + close(测试与导入路径共用)。 */
|
|
757
|
+
async createAndAppend(header, events, inheritedEventCount) {
|
|
758
|
+
const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
|
|
759
|
+
try {
|
|
760
|
+
if (events.length > 0) await handle.append(events);
|
|
761
|
+
} finally {
|
|
762
|
+
await handle.close();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** 便捷:open(read) + read 全量 + close。 */
|
|
766
|
+
async load(id, signal) {
|
|
767
|
+
const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
|
|
768
|
+
try {
|
|
769
|
+
const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
|
|
770
|
+
const row = await this.backend.getSession(id);
|
|
771
|
+
if (row === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
772
|
+
if (isLegacyVersion(row.fVersion)) return {
|
|
773
|
+
meta: handle.header,
|
|
774
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
775
|
+
events
|
|
776
|
+
};
|
|
777
|
+
return {
|
|
778
|
+
meta: rowToMeta(row),
|
|
779
|
+
inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
|
|
780
|
+
events
|
|
781
|
+
};
|
|
782
|
+
} finally {
|
|
783
|
+
await handle.close();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/** 便捷:open(write) + append + close。 */
|
|
787
|
+
async append(id, events) {
|
|
788
|
+
const handle = await this.open(id, "write");
|
|
789
|
+
try {
|
|
790
|
+
await handle.append(events);
|
|
791
|
+
} finally {
|
|
792
|
+
await handle.close();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/** 便捷:readFrom(稠密后缀)。 */
|
|
796
|
+
async readFrom(id, fromSeq, signal) {
|
|
797
|
+
const log = await this.readLog(id, { fromSeq }, signal);
|
|
798
|
+
if (log === void 0) throw new SessionPersistenceNotFoundError(id);
|
|
799
|
+
return {
|
|
800
|
+
meta: log.meta,
|
|
801
|
+
inheritedEventCount: log.inheritedEventCount,
|
|
802
|
+
events: log.events
|
|
803
|
+
};
|
|
381
804
|
}
|
|
382
805
|
async close() {
|
|
383
806
|
await this.ready;
|
|
@@ -390,17 +813,116 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
|
|
|
390
813
|
return {
|
|
391
814
|
backend: this.backend,
|
|
392
815
|
writeGuard: this.writeGuard,
|
|
393
|
-
create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount),
|
|
816
|
+
create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) }),
|
|
394
817
|
append: (id, events) => this.append(id, events),
|
|
395
818
|
load: (id) => this.load(id),
|
|
396
|
-
inspect: (id, signal) => this.
|
|
819
|
+
inspect: (id, signal) => this.load(id, signal),
|
|
397
820
|
readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
|
|
398
821
|
listSnapshots: (signal) => this.listSnapshots(signal),
|
|
399
822
|
readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
|
|
400
823
|
registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
|
|
401
824
|
};
|
|
402
825
|
}
|
|
826
|
+
rowRevision(row) {
|
|
827
|
+
return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
|
|
828
|
+
}
|
|
829
|
+
installLiveRouting(ctx) {
|
|
830
|
+
ctx.on("session/created", (session) => {
|
|
831
|
+
this.liveBuffers.set(session.id, []);
|
|
832
|
+
const ready = this.ensureLiveHandle(session);
|
|
833
|
+
this.liveReady.set(session.id, ready);
|
|
834
|
+
ready.catch((error) => {
|
|
835
|
+
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
for (const session of ctx.sessions.list()) {
|
|
839
|
+
this.liveBuffers.set(session.id, []);
|
|
840
|
+
const ready = this.ensureLiveHandle(session);
|
|
841
|
+
this.liveReady.set(session.id, ready);
|
|
842
|
+
ready.catch((error) => {
|
|
843
|
+
ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
ctx.on("session/event", (session, event) => {
|
|
847
|
+
const handle = this.tracker.writerOf(session.id);
|
|
848
|
+
if (handle !== void 0) {
|
|
849
|
+
handle.enqueueLive(event, (error) => {
|
|
850
|
+
ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
|
|
851
|
+
});
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
855
|
+
});
|
|
856
|
+
ctx.on("session/flush", (session) => {
|
|
857
|
+
const handle = this.tracker.writerOf(session.id);
|
|
858
|
+
if (handle === void 0) {
|
|
859
|
+
const ready = this.liveReady.get(session.id);
|
|
860
|
+
if (ready === void 0) return void 0;
|
|
861
|
+
return ready.then(() => {
|
|
862
|
+
const settled = this.tracker.writerOf(session.id);
|
|
863
|
+
if (settled === void 0) return void 0;
|
|
864
|
+
return settled.drainLive().then(() => settled.flush());
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return handle.drainLive().then(() => handle.flush());
|
|
868
|
+
});
|
|
869
|
+
ctx.on("session/disposed", (session) => {
|
|
870
|
+
const ready = this.liveReady.get(session.id);
|
|
871
|
+
this.liveBuffers.delete(session.id);
|
|
872
|
+
this.liveReady.delete(session.id);
|
|
873
|
+
const closeHandle = () => {
|
|
874
|
+
const handle = this.tracker.writerOf(session.id);
|
|
875
|
+
if (handle === void 0) return;
|
|
876
|
+
handle.close().catch((error) => {
|
|
877
|
+
ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
if (ready === void 0) {
|
|
881
|
+
closeHandle();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
ready.then(closeHandle, closeHandle);
|
|
885
|
+
});
|
|
886
|
+
ctx.effect(() => async () => {
|
|
887
|
+
await Promise.allSettled(this.liveReady.values());
|
|
888
|
+
await this.tracker.closeAll();
|
|
889
|
+
await this.close();
|
|
890
|
+
}, `${this.name} open handles`);
|
|
891
|
+
}
|
|
892
|
+
/** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
|
|
893
|
+
async ensureLiveHandle(session) {
|
|
894
|
+
const id = session.header.id;
|
|
895
|
+
if (this.tracker.writerOf(id) !== void 0) return;
|
|
896
|
+
await this.ready;
|
|
897
|
+
const stored = await this.readLog(id, {});
|
|
898
|
+
let handle;
|
|
899
|
+
if (stored === void 0) {
|
|
900
|
+
handle = await this.create(session.header, { inheritedEventCount: session.inheritedEventCount });
|
|
901
|
+
const seed = session.snapshotEvents();
|
|
902
|
+
if (seed.length > 0) await handle.append(seed);
|
|
903
|
+
} else {
|
|
904
|
+
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)`);
|
|
905
|
+
if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
|
|
906
|
+
assertVersion(stored.meta);
|
|
907
|
+
const seed = session.snapshotEvents();
|
|
908
|
+
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)`);
|
|
909
|
+
handle = await this.open(id, "write");
|
|
910
|
+
const suffix = seed.slice(stored.events.length);
|
|
911
|
+
if (suffix.length > 0) await handle.append(suffix);
|
|
912
|
+
}
|
|
913
|
+
const buffered = this.liveBuffers.get(id);
|
|
914
|
+
if (buffered !== void 0 && buffered.length > 0) {
|
|
915
|
+
this.liveBuffers.set(id, []);
|
|
916
|
+
for (const event of buffered) handle.enqueueLive(event, () => {});
|
|
917
|
+
}
|
|
918
|
+
}
|
|
403
919
|
};
|
|
920
|
+
function seedCoversPrefix(seed, prefix) {
|
|
921
|
+
return prefix.length <= seed.length && prefix.every((event, index) => {
|
|
922
|
+
const seedEvent = seed[index];
|
|
923
|
+
return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
404
926
|
function createBackend(config) {
|
|
405
927
|
if (config.type === "sqlite") return new SqliteBackend({
|
|
406
928
|
path: config.path,
|
|
@@ -431,6 +953,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
431
953
|
const eventId = reusedId ?? randomUUID();
|
|
432
954
|
if (reusedId === void 0) {
|
|
433
955
|
const { kind, role, name, actionId } = eventDimensions(event);
|
|
956
|
+
const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
|
|
434
957
|
eventRows.push({
|
|
435
958
|
fEventId: eventId,
|
|
436
959
|
fParentId: parentId,
|
|
@@ -440,7 +963,10 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
440
963
|
fName: name,
|
|
441
964
|
fActionId: actionId,
|
|
442
965
|
fEncoding: EVENT_ENCODING,
|
|
443
|
-
fData: JSON.stringify(
|
|
966
|
+
fData: JSON.stringify({
|
|
967
|
+
...envelope,
|
|
968
|
+
data
|
|
969
|
+
}),
|
|
444
970
|
fCreatedAt: event.time
|
|
445
971
|
});
|
|
446
972
|
}
|
|
@@ -449,7 +975,6 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
449
975
|
fSessionId: meta.id,
|
|
450
976
|
fEventId: eventId,
|
|
451
977
|
fSequence: nextSeq,
|
|
452
|
-
fOriginalSeq: event.seq,
|
|
453
978
|
fSurfaceOp: surfaceOp
|
|
454
979
|
});
|
|
455
980
|
parentId = eventId;
|
|
@@ -463,4 +988,4 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
463
988
|
};
|
|
464
989
|
}
|
|
465
990
|
//#endregion
|
|
466
|
-
export {
|
|
991
|
+
export { SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
|