@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/src/index.ts
CHANGED
|
@@ -5,53 +5,60 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { Pool } from "pg";
|
|
6
6
|
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
|
|
7
7
|
import {
|
|
8
|
+
SessionAlreadyExistsError,
|
|
9
|
+
SessionAlreadyOwnedError,
|
|
10
|
+
SessionHandleClosedError,
|
|
8
11
|
SessionPersistence,
|
|
12
|
+
SessionPersistenceNotFoundError,
|
|
9
13
|
SessionPersistenceRevision,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
SessionReadOnlyError,
|
|
15
|
+
assertContiguous,
|
|
16
|
+
assertVersion,
|
|
17
|
+
materializeAppendBatch,
|
|
18
|
+
materializeCreateHeader,
|
|
19
|
+
validateStoredEvents,
|
|
20
|
+
type SessionAccess,
|
|
21
|
+
type SessionHandle,
|
|
22
|
+
type SessionHandleAppendOptions,
|
|
23
|
+
type SessionHandleFlushOptions,
|
|
24
|
+
type SessionHandleReadOptions,
|
|
25
|
+
type SessionHandleReadResult,
|
|
26
|
+
type SessionPersistenceCreateOptions,
|
|
27
|
+
type SessionPersistenceListOptions,
|
|
28
|
+
type SessionPersistenceOpenOptions,
|
|
13
29
|
type SessionPersistenceSnapshot,
|
|
14
|
-
type
|
|
15
|
-
type StoredPrefix,
|
|
16
|
-
type StoredSuffix,
|
|
30
|
+
type SessionPersistenceStatOptions,
|
|
17
31
|
} from "@deepseek-ai/dsh-session-persistence";
|
|
18
32
|
import {
|
|
19
33
|
SessionLogOffset,
|
|
34
|
+
type Session,
|
|
20
35
|
type SessionEvent,
|
|
21
|
-
type SurfaceEventType,
|
|
22
|
-
type SessionId,
|
|
23
36
|
type SessionHeader,
|
|
37
|
+
type SessionId,
|
|
38
|
+
type SurfaceEventType,
|
|
24
39
|
} from "@deepseek-ai/dsh-session";
|
|
25
|
-
import { type Backend, type BackendTx, type EventInsert
|
|
40
|
+
import { type Backend, type BackendTx, type EventInsert } from "./backend.ts";
|
|
26
41
|
import { WriteGuard } from "./write-guard.ts";
|
|
27
|
-
import {
|
|
28
|
-
buildSeqMap,
|
|
29
|
-
recomputeReplaceProvenance,
|
|
30
|
-
repairOrphanInboxSplices,
|
|
31
|
-
repairSurfaceOps,
|
|
32
|
-
rowToMeta,
|
|
33
|
-
scanRows,
|
|
34
|
-
toJsonlArtifact,
|
|
35
|
-
} from "./log.ts";
|
|
42
|
+
import { repairReadView, rowToMeta, scanRows, toJsonlArtifact } from "./log.ts";
|
|
36
43
|
import {
|
|
37
44
|
DEFAULT_BUSY_TIMEOUT_MS,
|
|
38
45
|
eventDimensions,
|
|
39
46
|
EVENT_ENCODING,
|
|
40
|
-
isPersistedEvent,
|
|
41
47
|
type JournalMode,
|
|
42
48
|
} from "./schema.ts";
|
|
43
49
|
import { SqliteBackend } from "./sqlite.ts";
|
|
44
50
|
import { PostgresBackend } from "./postgres.ts";
|
|
45
51
|
import { SessionBranchRdb } from "./branch.ts";
|
|
46
52
|
import { registerSessionImport } from "./import.ts";
|
|
53
|
+
import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
47
54
|
|
|
48
|
-
export { SCHEMA_VERSION
|
|
55
|
+
export { SCHEMA_VERSION } from "./schema.ts";
|
|
49
56
|
export { SessionBranchRdb, SessionBranchRdbProvider, locateTurnEnd } from "./branch.ts";
|
|
50
57
|
|
|
51
58
|
export interface SessionPersistenceRdbInternals {
|
|
52
59
|
readonly backend: Backend;
|
|
53
60
|
readonly writeGuard: WriteGuard;
|
|
54
|
-
create(meta: SessionHeader, inheritedEventCount?: number): Promise<
|
|
61
|
+
create(meta: SessionHeader, inheritedEventCount?: number): Promise<SessionHandle>;
|
|
55
62
|
append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
|
|
56
63
|
load(id: SessionId): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
|
|
57
64
|
inspect(
|
|
@@ -62,7 +69,7 @@ export interface SessionPersistenceRdbInternals {
|
|
|
62
69
|
id: SessionId,
|
|
63
70
|
fromSeq: number,
|
|
64
71
|
signal?: AbortSignal,
|
|
65
|
-
): Promise<
|
|
72
|
+
): Promise<{ meta: SessionHeader; inheritedEventCount: number; events: readonly SessionEvent[] }>;
|
|
66
73
|
listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
|
|
67
74
|
readStoredRevision(
|
|
68
75
|
id: SessionId,
|
|
@@ -90,10 +97,342 @@ export type Config =
|
|
|
90
97
|
schema?: string;
|
|
91
98
|
};
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
/** 一个已创建但未 materialize 的会话(本进程可见,其他进程不可见)。 */
|
|
101
|
+
interface PendingSession {
|
|
102
|
+
readonly header: SessionHeader;
|
|
103
|
+
readonly revision: SessionPersistenceRevision;
|
|
104
|
+
readonly inheritedEventCount: SessionLogOffset;
|
|
105
|
+
/** 输入空间 cursor(全 delta 批次也推进)。 */
|
|
106
|
+
readonly cursor: number;
|
|
107
|
+
/** 是否调用过 append(close 时保留 pending)。 */
|
|
108
|
+
readonly everAppended: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 会话级写所有权与 live 路由簿记。 */
|
|
112
|
+
class RdbBackendTracker {
|
|
113
|
+
/** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
|
|
114
|
+
private readonly writers = new Map<SessionId, RdbSessionHandle | null>();
|
|
115
|
+
private readonly pending = new Map<SessionId, PendingSession>();
|
|
116
|
+
private readonly openHandles = new Set<RdbSessionHandle>();
|
|
117
|
+
private counter = 0;
|
|
118
|
+
|
|
119
|
+
constructor(private readonly name: string) {}
|
|
120
|
+
|
|
121
|
+
registerCreated(header: SessionHeader, inheritedEventCount: SessionLogOffset): void {
|
|
122
|
+
if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
|
|
123
|
+
this.writers.set(header.id, null);
|
|
124
|
+
this.pending.set(header.id, {
|
|
125
|
+
header,
|
|
126
|
+
revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
|
|
127
|
+
inheritedEventCount,
|
|
128
|
+
cursor: 0,
|
|
129
|
+
everAppended: false,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
|
|
134
|
+
updatePending(id: SessionId, cursor: number, everAppended: boolean): void {
|
|
135
|
+
const entry = this.pending.get(id);
|
|
136
|
+
if (entry === undefined) return;
|
|
137
|
+
this.pending.set(id, { ...entry, cursor, everAppended });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
claimWrite(id: SessionId): void {
|
|
141
|
+
if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
|
|
142
|
+
this.writers.set(id, null);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
releaseClaim(id: SessionId): void {
|
|
146
|
+
this.writers.delete(id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pendingOf(id: SessionId): PendingSession | undefined {
|
|
150
|
+
return this.pending.get(id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
hasPending(id: SessionId): boolean {
|
|
154
|
+
return this.pending.has(id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
pendingEntries(): IterableIterator<[SessionId, PendingSession]> {
|
|
158
|
+
return this.pending.entries();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
materialized(id: SessionId): void {
|
|
162
|
+
this.pending.delete(id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
adopt(handle: RdbSessionHandle): RdbSessionHandle {
|
|
166
|
+
this.openHandles.add(handle);
|
|
167
|
+
if (handle.access === "write") this.writers.set(handle.id, handle);
|
|
168
|
+
return handle;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
release(handle: RdbSessionHandle, materialized: boolean): void {
|
|
172
|
+
this.openHandles.delete(handle);
|
|
173
|
+
if (handle.access !== "write") return;
|
|
174
|
+
this.writers.delete(handle.id);
|
|
175
|
+
if (!materialized) this.pending.delete(handle.id);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
writerOf(id: SessionId): RdbSessionHandle | undefined {
|
|
179
|
+
const writer = this.writers.get(id);
|
|
180
|
+
return writer === null ? undefined : writer;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async flushAll(): Promise<void> {
|
|
184
|
+
const errors: unknown[] = [];
|
|
185
|
+
for (const writer of this.writers.values()) {
|
|
186
|
+
if (writer === null) continue;
|
|
187
|
+
try {
|
|
188
|
+
await writer.drainLive();
|
|
189
|
+
await writer.flush();
|
|
190
|
+
} catch (error: unknown) {
|
|
191
|
+
if (error instanceof SessionHandleClosedError) continue;
|
|
192
|
+
errors.push(error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async closeAll(): Promise<void> {
|
|
199
|
+
const errors: unknown[] = [];
|
|
200
|
+
for (const handle of this.openHandles) {
|
|
201
|
+
try {
|
|
202
|
+
await handle.close();
|
|
203
|
+
} catch (error: unknown) {
|
|
204
|
+
errors.push(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 一个打开会话的存储句柄:read / append / flush / close。 */
|
|
212
|
+
class RdbSessionHandle implements SessionHandle {
|
|
213
|
+
private chain: Promise<unknown> = Promise.resolve();
|
|
214
|
+
private closing: Promise<void> | undefined;
|
|
215
|
+
/** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
|
|
216
|
+
private cursor: number;
|
|
217
|
+
private materialized: boolean;
|
|
218
|
+
/** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
|
|
219
|
+
private buffered: SessionEvent[] = [];
|
|
220
|
+
private batchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
221
|
+
private drainPaused = false;
|
|
222
|
+
private draining: Promise<void> | undefined;
|
|
223
|
+
/** write open 时发现的 torn tail 起点(append 前先截断)。 */
|
|
224
|
+
private tornTruncateTo: number | undefined;
|
|
225
|
+
/** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
|
|
226
|
+
private everAppended = false;
|
|
227
|
+
|
|
228
|
+
constructor(
|
|
229
|
+
private readonly persistence: SessionPersistenceRdb,
|
|
230
|
+
readonly id: SessionId,
|
|
231
|
+
readonly header: SessionHeader,
|
|
232
|
+
readonly access: SessionAccess,
|
|
233
|
+
private readonly state: {
|
|
234
|
+
cursor: number;
|
|
235
|
+
materialized: boolean;
|
|
236
|
+
inheritedEventCount: SessionLogOffset;
|
|
237
|
+
tornTruncateTo?: number;
|
|
238
|
+
},
|
|
239
|
+
) {
|
|
240
|
+
this.cursor = state.cursor;
|
|
241
|
+
this.materialized = state.materialized;
|
|
242
|
+
this.tornTruncateTo = state.tornTruncateTo;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get inheritedEventCount(): SessionLogOffset {
|
|
246
|
+
return this.state.inheritedEventCount;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 输入空间 cursor(下一个待落库的上游 seq)。 */
|
|
250
|
+
get cursorValue(): number {
|
|
251
|
+
return this.cursor;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async read(
|
|
255
|
+
offset = 0,
|
|
256
|
+
length = Number.MAX_SAFE_INTEGER,
|
|
257
|
+
options?: SessionHandleReadOptions,
|
|
258
|
+
): Promise<SessionHandleReadResult> {
|
|
259
|
+
this.assertOpen("read");
|
|
260
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
261
|
+
throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
|
|
262
|
+
}
|
|
263
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
264
|
+
throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
|
|
265
|
+
}
|
|
266
|
+
options?.signal?.throwIfAborted();
|
|
267
|
+
const log = await this.persistence.readLog(this.id, {}, options?.signal);
|
|
268
|
+
if (log === undefined) {
|
|
269
|
+
if (this.persistence.tracker.hasPending(this.id)) {
|
|
270
|
+
return { eventState: "detached", events: [] };
|
|
271
|
+
}
|
|
272
|
+
throw new SessionPersistenceNotFoundError(this.id);
|
|
273
|
+
}
|
|
274
|
+
// 读取时修复(视图只读,不落库):结算字段补全、非法 surface 替换降级
|
|
275
|
+
// 或按 metering 数量夹取、metering range 对齐、provenance 重算、孤儿
|
|
276
|
+
// inbox splice 改写。
|
|
277
|
+
repairReadView(log.events);
|
|
278
|
+
return { eventState: "detached", events: log.events.slice(offset, offset + length) };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async append(
|
|
282
|
+
events: readonly SessionEvent[],
|
|
283
|
+
options?: SessionHandleAppendOptions,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
this.assertOpen("append");
|
|
286
|
+
const batch = materializeAppendBatch(events);
|
|
287
|
+
return this.run("append", async () => {
|
|
288
|
+
options?.signal?.throwIfAborted();
|
|
289
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
|
|
290
|
+
if (batch.length === 0) return;
|
|
291
|
+
this.everAppended = true;
|
|
292
|
+
assertContiguous(this.id, batch, this.cursor);
|
|
293
|
+
await this.persistence.appendBatch(
|
|
294
|
+
this.header,
|
|
295
|
+
this.state.inheritedEventCount,
|
|
296
|
+
batch,
|
|
297
|
+
this.tornTruncateTo,
|
|
298
|
+
);
|
|
299
|
+
this.tornTruncateTo = undefined;
|
|
300
|
+
// 原样存储(与上游 JSONL 一致):cursor 推进 batch.length,全部落库。
|
|
301
|
+
this.cursor += batch.length;
|
|
302
|
+
this.materialized = true;
|
|
303
|
+
this.persistence.tracker.updatePending(this.id, this.cursor, true);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async flush(options?: SessionHandleFlushOptions): Promise<void> {
|
|
308
|
+
return this.run("flush", async () => {
|
|
309
|
+
options?.signal?.throwIfAborted();
|
|
310
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
|
|
311
|
+
if (this.materialized) return;
|
|
312
|
+
await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
|
|
313
|
+
this.materialized = true;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
close(): Promise<void> {
|
|
318
|
+
return (this.closing ??= (async () => {
|
|
319
|
+
let drainFailure: unknown;
|
|
320
|
+
for (;;) {
|
|
321
|
+
try {
|
|
322
|
+
await this.drainLive();
|
|
323
|
+
} catch (error: unknown) {
|
|
324
|
+
drainFailure = error;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
await this.chain;
|
|
328
|
+
if (this.buffered.length === 0) break;
|
|
329
|
+
}
|
|
330
|
+
await this.chain;
|
|
331
|
+
const failures: Error[] = [];
|
|
332
|
+
if (drainFailure !== undefined) {
|
|
333
|
+
failures.push(
|
|
334
|
+
drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
this.persistence.tracker.release(this, this.materialized || this.everAppended);
|
|
338
|
+
if (failures.length > 1)
|
|
339
|
+
throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
|
|
340
|
+
if (failures[0] !== undefined) throw failures[0];
|
|
341
|
+
})());
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
[Symbol.asyncDispose](): Promise<void> {
|
|
345
|
+
return this.close();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
|
|
349
|
+
enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void {
|
|
350
|
+
this.buffered.push(structuredClone(event));
|
|
351
|
+
if (this.batchTimer !== undefined || this.drainPaused) return;
|
|
352
|
+
this.batchTimer = setTimeout(() => {
|
|
353
|
+
this.batchTimer = undefined;
|
|
354
|
+
this.drainLive().catch(reportBackgroundFailure);
|
|
355
|
+
}, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
|
|
359
|
+
resetAfterRewind(cursor: number, inheritedEventCount?: number): void {
|
|
360
|
+
this.cursor = cursor;
|
|
361
|
+
if (inheritedEventCount !== undefined) {
|
|
362
|
+
(this.state as { inheritedEventCount: SessionLogOffset }).inheritedEventCount =
|
|
363
|
+
SessionLogOffset(inheritedEventCount);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
|
|
368
|
+
drainLive(): Promise<void> {
|
|
369
|
+
return (this.draining ??= this.drainBuffered().finally(() => {
|
|
370
|
+
this.draining = undefined;
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async drainBuffered(): Promise<void> {
|
|
375
|
+
if (this.batchTimer !== undefined) {
|
|
376
|
+
clearTimeout(this.batchTimer);
|
|
377
|
+
this.batchTimer = undefined;
|
|
378
|
+
}
|
|
379
|
+
this.drainPaused = false;
|
|
380
|
+
while (this.buffered.length > 0) {
|
|
381
|
+
await this.enqueueChain(async () => {
|
|
382
|
+
const batch = this.buffered.splice(0);
|
|
383
|
+
try {
|
|
384
|
+
// 过滤已由 ensureLiveHandle 落库的 seed 前缀(上游 seq 空间,
|
|
385
|
+
// 与 public append 的 cursor 同空间)。直接走持久化原语(本函数
|
|
386
|
+
// 已在 chain 内,走 public append 会自锁)。
|
|
387
|
+
const fresh = batch.filter((event) => event.seq >= this.cursor);
|
|
388
|
+
if (fresh.length === 0) return;
|
|
389
|
+
for (const [index, event] of fresh.entries()) {
|
|
390
|
+
if (event.seq !== this.cursor + index) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
await this.persistence.appendBatch(
|
|
397
|
+
this.header,
|
|
398
|
+
this.state.inheritedEventCount,
|
|
399
|
+
fresh,
|
|
400
|
+
this.tornTruncateTo,
|
|
401
|
+
);
|
|
402
|
+
this.tornTruncateTo = undefined;
|
|
403
|
+
this.cursor += fresh.length;
|
|
404
|
+
this.materialized = true;
|
|
405
|
+
} catch (error: unknown) {
|
|
406
|
+
this.buffered = batch.concat(this.buffered);
|
|
407
|
+
this.drainPaused = true;
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private enqueueChain(op: () => Promise<void>): Promise<void> {
|
|
415
|
+
const next = this.chain.then(op);
|
|
416
|
+
this.chain = next.catch(() => {});
|
|
417
|
+
return next;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private async run(operation: string, op: () => Promise<void>): Promise<void> {
|
|
421
|
+
this.assertOpen(operation);
|
|
422
|
+
return this.enqueueChain(async () => {
|
|
423
|
+
this.assertOpen(operation);
|
|
424
|
+
return op();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private assertOpen(operation: string): void {
|
|
429
|
+
if (this.closing !== undefined) throw new SessionHandleClosedError(this.id, operation);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static readonly LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export class SessionPersistenceRdb extends SessionPersistence {
|
|
97
436
|
static inject = ["sessions", "settings"];
|
|
98
437
|
|
|
99
438
|
static Config: z<Config> = z.union([
|
|
@@ -114,37 +453,20 @@ export class SessionPersistenceRdb
|
|
|
114
453
|
|
|
115
454
|
override readonly name = "session-rdb";
|
|
116
455
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
override async readRaw(
|
|
120
|
-
id: SessionId,
|
|
121
|
-
signal?: AbortSignal,
|
|
122
|
-
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionRawArtifact | undefined> {
|
|
123
|
-
signal?.throwIfAborted();
|
|
124
|
-
await this.ready;
|
|
125
|
-
signal?.throwIfAborted();
|
|
126
|
-
const log = await this.readLog(id, {}, signal);
|
|
127
|
-
if (log === undefined) return undefined;
|
|
128
|
-
repairSurfaceOps(log.events);
|
|
129
|
-
recomputeReplaceProvenance(log.events);
|
|
130
|
-
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
131
|
-
return {
|
|
132
|
-
meta: log.meta,
|
|
133
|
-
inheritedEventCount: SessionLogOffset(inheritedEventCount),
|
|
134
|
-
filename: "session.jsonl",
|
|
135
|
-
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
456
|
+
readonly tracker = new RdbBackendTracker(this.name);
|
|
138
457
|
|
|
139
458
|
private readonly backend: Backend;
|
|
140
459
|
private storeIdentity!: string;
|
|
141
460
|
private readonly ready: Promise<void>;
|
|
142
|
-
private readonly coordinator: PersistenceCoordinator<number>;
|
|
143
461
|
|
|
144
462
|
private readonly writeGuard = new WriteGuard();
|
|
145
463
|
|
|
146
464
|
private readonly reuseEventIds = new Map<SessionId, Map<number, string>>();
|
|
147
465
|
|
|
466
|
+
/** live 路由:session/created 后 handle 就绪前的缓冲。 */
|
|
467
|
+
private readonly liveBuffers = new Map<SessionId, SessionEvent[]>();
|
|
468
|
+
private readonly liveReady = new Map<SessionId, Promise<void>>();
|
|
469
|
+
|
|
148
470
|
constructor(
|
|
149
471
|
ctx: Context,
|
|
150
472
|
public config: Config,
|
|
@@ -172,7 +494,7 @@ export class SessionPersistenceRdb
|
|
|
172
494
|
this.config = resolved;
|
|
173
495
|
this.backend = injectedBackend ?? createBackend(resolved);
|
|
174
496
|
this.ready = this.init();
|
|
175
|
-
this.
|
|
497
|
+
this.installLiveRouting(ctx);
|
|
176
498
|
// 分支 provider 服务(rewind / forkFrom / timeline),随 fiber 卸载自动回滚。
|
|
177
499
|
new SessionBranchRdb(this.ctx);
|
|
178
500
|
// 导入端点:webServer + connection 就绪后注册 `/api/session.import`。
|
|
@@ -184,114 +506,245 @@ export class SessionPersistenceRdb
|
|
|
184
506
|
this.storeIdentity = this.backend.storeIdentity;
|
|
185
507
|
}
|
|
186
508
|
|
|
187
|
-
// --- SessionPersistence service surface
|
|
509
|
+
// --- SessionPersistence service surface ---
|
|
188
510
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
511
|
+
async create(
|
|
512
|
+
header: SessionHeader,
|
|
513
|
+
options?: SessionPersistenceCreateOptions,
|
|
514
|
+
): Promise<SessionHandle> {
|
|
515
|
+
options?.signal?.throwIfAborted();
|
|
516
|
+
const snapshot = materializeCreateHeader(header);
|
|
517
|
+
if (snapshot.isSeeded && options?.inheritedEventCount === undefined) {
|
|
518
|
+
throw new TypeError("seeded session metadata requires an inherited event count");
|
|
519
|
+
}
|
|
520
|
+
const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
|
|
521
|
+
if (!snapshot.isSeeded && inheritedEventCount !== 0) {
|
|
522
|
+
throw new TypeError("unseeded session metadata inherited event count must be 0");
|
|
523
|
+
}
|
|
524
|
+
await this.ready;
|
|
525
|
+
options?.signal?.throwIfAborted();
|
|
526
|
+
if (
|
|
527
|
+
this.tracker.hasPending(snapshot.id) ||
|
|
528
|
+
(await this.backend.getSession(snapshot.id)) !== undefined
|
|
529
|
+
) {
|
|
530
|
+
throw new SessionAlreadyExistsError(snapshot.id);
|
|
531
|
+
}
|
|
532
|
+
this.tracker.registerCreated(snapshot, inheritedEventCount);
|
|
533
|
+
return this.tracker.adopt(
|
|
534
|
+
new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
|
|
535
|
+
cursor: 0,
|
|
536
|
+
materialized: false,
|
|
537
|
+
inheritedEventCount,
|
|
538
|
+
}),
|
|
197
539
|
);
|
|
198
540
|
}
|
|
199
541
|
|
|
200
|
-
|
|
201
|
-
return this.coordinator.append(id, events);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
load(id: SessionId): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection> {
|
|
205
|
-
return this.coordinator.load(id);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
inspect(
|
|
542
|
+
async open(
|
|
209
543
|
id: SessionId,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
544
|
+
access: SessionAccess,
|
|
545
|
+
options?: SessionPersistenceOpenOptions,
|
|
546
|
+
): Promise<SessionHandle> {
|
|
547
|
+
options?.signal?.throwIfAborted();
|
|
548
|
+
await this.ready;
|
|
549
|
+
options?.signal?.throwIfAborted();
|
|
550
|
+
const pending = this.tracker.pendingOf(id);
|
|
551
|
+
if (access === "read") {
|
|
552
|
+
if (pending !== undefined) {
|
|
553
|
+
return this.tracker.adopt(
|
|
554
|
+
new RdbSessionHandle(this, id, pending.header, "read", {
|
|
555
|
+
cursor: 0,
|
|
556
|
+
materialized: false,
|
|
557
|
+
inheritedEventCount: pending.inheritedEventCount,
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
562
|
+
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
563
|
+
// fail-closed:未知事件类型(非 ignorable)拒绝解释。
|
|
564
|
+
validateStoredEvents(log.meta, log.events);
|
|
565
|
+
return this.tracker.adopt(
|
|
566
|
+
new RdbSessionHandle(this, id, log.meta, "read", {
|
|
567
|
+
cursor: log.events.length,
|
|
568
|
+
materialized: true,
|
|
569
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
this.tracker.claimWrite(id);
|
|
574
|
+
try {
|
|
575
|
+
// pending(created 未 materialize)会话:write open 接管其所有权。
|
|
576
|
+
if (pending !== undefined) {
|
|
577
|
+
return this.tracker.adopt(
|
|
578
|
+
new RdbSessionHandle(this, id, pending.header, "write", {
|
|
579
|
+
cursor: pending.cursor,
|
|
580
|
+
materialized: false,
|
|
581
|
+
inheritedEventCount: pending.inheritedEventCount,
|
|
582
|
+
}),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
586
|
+
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
587
|
+
validateStoredEvents(log.meta, log.events);
|
|
588
|
+
// 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件坐标与
|
|
589
|
+
// 存储桥接行数不再相等——写打开时把迁移视图整体落库,使读写同坐标;
|
|
590
|
+
// 否则 append 按存储 head 重编号会撞上已有行或写坏 log。
|
|
591
|
+
if (log.migrated && log.events.length !== log.storedCount) {
|
|
592
|
+
await this.rewriteMigratedLog(id, log);
|
|
593
|
+
}
|
|
594
|
+
// 确认 head:本实例已读该会话,后续 append 的并发校验以此为基准。
|
|
595
|
+
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
596
|
+
return this.tracker.adopt(
|
|
597
|
+
new RdbSessionHandle(this, id, log.meta, "write", {
|
|
598
|
+
cursor: log.events.length,
|
|
599
|
+
materialized: true,
|
|
600
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
601
|
+
...(log.tornFrom !== undefined ? { tornTruncateTo: log.tornFrom } : {}),
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
} catch (error: unknown) {
|
|
605
|
+
this.tracker.releaseClaim(id);
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
213
608
|
}
|
|
214
609
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
fromSeq: number,
|
|
218
|
-
signal?: AbortSignal,
|
|
219
|
-
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionEventSuffix> {
|
|
220
|
-
// 校验委托给 coordinator;这里只做品牌转换,避免 cordis proxy 下同步 throw 逃逸。
|
|
221
|
-
return this.coordinator.readFrom(id, fromSeq as SessionLogOffset, signal);
|
|
610
|
+
flush(): Promise<void> {
|
|
611
|
+
return this.tracker.flushAll();
|
|
222
612
|
}
|
|
223
613
|
|
|
224
|
-
|
|
614
|
+
async stat(
|
|
225
615
|
id: SessionId,
|
|
226
|
-
|
|
227
|
-
): Promise<
|
|
228
|
-
|
|
616
|
+
options?: SessionPersistenceStatOptions,
|
|
617
|
+
): Promise<SessionPersistenceSnapshot | undefined> {
|
|
618
|
+
options?.signal?.throwIfAborted();
|
|
619
|
+
await this.ready;
|
|
620
|
+
options?.signal?.throwIfAborted();
|
|
621
|
+
const pending = this.tracker.pendingOf(id);
|
|
622
|
+
if (pending !== undefined) {
|
|
623
|
+
return { header: pending.header, revision: pending.revision };
|
|
624
|
+
}
|
|
625
|
+
const row = await this.backend.getSession(id);
|
|
626
|
+
if (row === undefined) return undefined;
|
|
627
|
+
return {
|
|
628
|
+
header: rowToMeta(row),
|
|
629
|
+
revision: this.rowRevision(row),
|
|
630
|
+
};
|
|
229
631
|
}
|
|
230
632
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
633
|
+
async list(
|
|
634
|
+
options?: SessionPersistenceListOptions,
|
|
635
|
+
): Promise<readonly SessionPersistenceSnapshot[]> {
|
|
636
|
+
const signal = options?.signal;
|
|
637
|
+
const snapshots: SessionPersistenceSnapshot[] = [];
|
|
638
|
+
const listed = new Set<SessionId>();
|
|
639
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
640
|
+
snapshots.push({ header: pending.header, revision: pending.revision });
|
|
641
|
+
listed.add(id);
|
|
642
|
+
}
|
|
643
|
+
signal?.throwIfAborted();
|
|
644
|
+
await this.ready;
|
|
645
|
+
signal?.throwIfAborted();
|
|
646
|
+
const rows = await this.backend.listSessions();
|
|
647
|
+
signal?.throwIfAborted();
|
|
648
|
+
for (const row of rows) {
|
|
649
|
+
if (listed.has(row.fSessionId as SessionId)) continue;
|
|
650
|
+
snapshots.push({ header: rowToMeta(row), revision: this.rowRevision(row) });
|
|
651
|
+
}
|
|
652
|
+
return snapshots;
|
|
235
653
|
}
|
|
236
654
|
|
|
237
|
-
|
|
655
|
+
// --- RDB 特有能力(rewind / fork / 导出 / 测试支撑) ---
|
|
656
|
+
|
|
657
|
+
/** 导出 artifact(jsonl v2 文本),视图只读不落库。 */
|
|
658
|
+
async readRaw(
|
|
238
659
|
id: SessionId,
|
|
239
|
-
fromSeq: number,
|
|
240
660
|
signal?: AbortSignal,
|
|
241
|
-
): Promise<
|
|
242
|
-
|
|
661
|
+
): Promise<
|
|
662
|
+
| { meta: SessionHeader; inheritedEventCount: number; filename: string; content: string }
|
|
663
|
+
| undefined
|
|
664
|
+
> {
|
|
665
|
+
signal?.throwIfAborted();
|
|
666
|
+
await this.ready;
|
|
667
|
+
signal?.throwIfAborted();
|
|
668
|
+
const log = await this.readLog(id, {}, signal);
|
|
243
669
|
if (log === undefined) return undefined;
|
|
670
|
+
repairReadView(log.events);
|
|
671
|
+
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
244
672
|
return {
|
|
245
673
|
meta: log.meta,
|
|
246
|
-
inheritedEventCount
|
|
247
|
-
|
|
674
|
+
inheritedEventCount,
|
|
675
|
+
filename: "session.jsonl",
|
|
676
|
+
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events),
|
|
248
677
|
};
|
|
249
678
|
}
|
|
250
679
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
this.writeGuard.confirmHead(id,
|
|
263
|
-
// replace 的 provenance 读取时重计算(sourceEventSeqs 不落库)。
|
|
264
|
-
recomputeReplaceProvenance(log.events);
|
|
265
|
-
// 孤儿 inbox splice(引用已被截断排队消息的插入/消费)会使上游 Inbox
|
|
266
|
-
// 增量重放失败、整个会话不可 resume——读取时改写为 no-op 修复。
|
|
267
|
-
repairOrphanInboxSplices(log.events);
|
|
268
|
-
return {
|
|
269
|
-
meta: log.meta,
|
|
270
|
-
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
271
|
-
events: log.events,
|
|
272
|
-
// revision 表示必须与 readStoredRevision 的表示一致(见 listSnapshots)。
|
|
273
|
-
revision: SessionPersistenceRevision(
|
|
274
|
-
`${this.storeIdentity}:incarnation:${log.incarnation}:revision:${log.revision}`,
|
|
275
|
-
),
|
|
276
|
-
...(log.tornFrom !== undefined ? { tornMarker: log.tornFrom } : {}),
|
|
277
|
-
};
|
|
680
|
+
/** 空会话 materialize(flush 的持久化屏障)。 */
|
|
681
|
+
async materializeEmpty(
|
|
682
|
+
meta: SessionHeader,
|
|
683
|
+
inheritedEventCount: SessionLogOffset,
|
|
684
|
+
): Promise<void> {
|
|
685
|
+
await this.ready;
|
|
686
|
+
await this.backend.transaction(async (tx) => {
|
|
687
|
+
await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
|
|
688
|
+
await tx.bumpRevision(meta.id);
|
|
689
|
+
});
|
|
690
|
+
this.tracker.materialized(meta.id);
|
|
691
|
+
this.writeGuard.confirmHead(meta.id, -1);
|
|
278
692
|
}
|
|
279
693
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
694
|
+
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
695
|
+
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
|
|
696
|
+
* 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
697
|
+
* 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
698
|
+
async appendBatch(
|
|
699
|
+
meta: SessionHeader,
|
|
700
|
+
inheritedEventCount: SessionLogOffset,
|
|
701
|
+
events: readonly SessionEvent[],
|
|
702
|
+
tornTruncateTo?: number,
|
|
703
|
+
): Promise<boolean> {
|
|
285
704
|
await this.ready;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
);
|
|
705
|
+
if (events.length === 0) return false;
|
|
706
|
+
// 写路径 v2 校验:与读路径同契约(validateStoredEvents),保证新入库
|
|
707
|
+
// 数据只能是 v2 形状。拷贝避免 adopt 替换污染调用方数组。
|
|
708
|
+
validateStoredEvents(meta, [...events]);
|
|
709
|
+
// fork 派生会话的 seed 复用源会话事件行(不复制);消费后清除。
|
|
710
|
+
const reuse = this.reuseEventIds.get(meta.id);
|
|
711
|
+
if (reuse !== undefined) this.reuseEventIds.delete(meta.id);
|
|
712
|
+
let confirmedHead = -1;
|
|
713
|
+
await this.backend.transaction(async (tx) => {
|
|
714
|
+
if (tornTruncateTo !== undefined) {
|
|
715
|
+
await tx.deleteBridgeTail(meta.id, tornTruncateTo);
|
|
716
|
+
const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
|
|
717
|
+
if (prev === undefined) {
|
|
718
|
+
await tx.updateHead(meta.id, "", -1);
|
|
719
|
+
} else {
|
|
720
|
+
await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
|
|
724
|
+
const head = await tx.getHead(meta.id);
|
|
725
|
+
// 重编号前拒绝第二个写入者:多实例共享数据库时,第二个写入者经陈旧
|
|
726
|
+
// 视图 append 会把事件静默重编号到对方尾部、损坏 log。磁盘 head 必须
|
|
727
|
+
// 等于本实例确认过的最后一个 head。
|
|
728
|
+
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
729
|
+
const { headEventId, headSequence } = await appendEventTail(
|
|
730
|
+
tx,
|
|
731
|
+
meta,
|
|
732
|
+
events,
|
|
733
|
+
{ parentId: head.fHeadEventId, nextSeq: head.fHeadSequence + 1 },
|
|
734
|
+
reuse,
|
|
735
|
+
);
|
|
736
|
+
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
737
|
+
await tx.bumpRevision(meta.id);
|
|
738
|
+
confirmedHead = headSequence;
|
|
739
|
+
});
|
|
740
|
+
// 提交后才确认新 head:回滚不得留下本实例实际未写的已确认 head。
|
|
741
|
+
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
742
|
+
this.tracker.materialized(meta.id);
|
|
743
|
+
return true;
|
|
292
744
|
}
|
|
293
745
|
|
|
294
|
-
|
|
746
|
+
/** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
|
|
747
|
+
async readLog(
|
|
295
748
|
id: SessionId,
|
|
296
749
|
options: { fromSeq?: number } = {},
|
|
297
750
|
signal?: AbortSignal,
|
|
@@ -306,6 +759,12 @@ export class SessionPersistenceRdb
|
|
|
306
759
|
incarnation: string;
|
|
307
760
|
|
|
308
761
|
revision: number;
|
|
762
|
+
|
|
763
|
+
/** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
|
|
764
|
+
storedCount: number;
|
|
765
|
+
|
|
766
|
+
/** 是否经上游迁移链转换(v0/v1 → 当前格式)。 */
|
|
767
|
+
migrated: boolean;
|
|
309
768
|
}
|
|
310
769
|
| undefined
|
|
311
770
|
> {
|
|
@@ -315,126 +774,205 @@ export class SessionPersistenceRdb
|
|
|
315
774
|
const row = await this.backend.getSession(id);
|
|
316
775
|
if (row === undefined) return undefined;
|
|
317
776
|
const meta = rowToMeta(row);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
eventRows = await this.backend.getEventRows(id);
|
|
323
|
-
seqMap = buildSeqMap(eventRows);
|
|
324
|
-
} else {
|
|
325
|
-
// 后缀读:坐标重映射需要每一行的上游 seq,另读轻量两列映射。
|
|
326
|
-
eventRows = await this.backend.getEventRows(id, options.fromSeq);
|
|
327
|
-
const seqRows = await this.backend.getSeqMapRows(id);
|
|
328
|
-
seqMap = buildSeqMap(seqRows);
|
|
329
|
-
}
|
|
777
|
+
const eventRows =
|
|
778
|
+
options.fromSeq === undefined
|
|
779
|
+
? await this.backend.getEventRows(id)
|
|
780
|
+
: await this.backend.getEventRows(id, options.fromSeq);
|
|
330
781
|
signal?.throwIfAborted();
|
|
331
|
-
|
|
782
|
+
// 旧格式(v0/v1)历史数据:行重建为物理记录,经上游迁移链转 v2 逻辑事件。
|
|
783
|
+
// 迁移链自带 seq gap / torn tail 校验(strict recovery),无需 scanRows。
|
|
784
|
+
// 混合世代 log(旧写入器跨上游版本追加)不是任何单一已发布格式,迁移链
|
|
785
|
+
// 必然拒绝——回退为当前格式视图(header 版本归一 + 读取视图修复)。
|
|
786
|
+
if (isLegacyVersion(row.fVersion)) {
|
|
787
|
+
try {
|
|
788
|
+
const converted = convertLegacyRows(row, eventRows);
|
|
789
|
+
return {
|
|
790
|
+
meta: converted.meta,
|
|
791
|
+
inheritedEventCount: converted.inheritedEventCount,
|
|
792
|
+
events: converted.events,
|
|
793
|
+
incarnation: row.fIncarnation,
|
|
794
|
+
revision: row.fRevision,
|
|
795
|
+
storedCount: eventRows.length,
|
|
796
|
+
migrated: true,
|
|
797
|
+
};
|
|
798
|
+
} catch (error: unknown) {
|
|
799
|
+
this.ctx.logger.warn(
|
|
800
|
+
`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)})`,
|
|
801
|
+
);
|
|
802
|
+
const adopted = adoptLegacyRows(row, eventRows);
|
|
803
|
+
return {
|
|
804
|
+
meta: adopted.meta,
|
|
805
|
+
inheritedEventCount: adopted.inheritedEventCount,
|
|
806
|
+
events: adopted.events,
|
|
807
|
+
incarnation: row.fIncarnation,
|
|
808
|
+
revision: row.fRevision,
|
|
809
|
+
storedCount: eventRows.length,
|
|
810
|
+
migrated: false,
|
|
811
|
+
...(adopted.tornFrom !== undefined ? { tornFrom: adopted.tornFrom } : {}),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
332
816
|
return {
|
|
333
817
|
meta,
|
|
334
818
|
inheritedEventCount: row.fSeedLength ?? 0,
|
|
335
819
|
events: preserved,
|
|
336
820
|
incarnation: row.fIncarnation,
|
|
337
821
|
revision: row.fRevision,
|
|
822
|
+
storedCount: eventRows.length,
|
|
823
|
+
migrated: false,
|
|
338
824
|
...(tornFrom !== undefined ? { tornFrom } : {}),
|
|
339
825
|
};
|
|
340
826
|
}
|
|
341
827
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
828
|
+
/**
|
|
829
|
+
* 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
|
|
830
|
+
*
|
|
831
|
+
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
832
|
+
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
833
|
+
* append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
|
|
834
|
+
* (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
|
|
835
|
+
* fork 子会话引用,孤儿由惰性 GC 处理)。
|
|
836
|
+
*/
|
|
837
|
+
private async rewriteMigratedLog(
|
|
838
|
+
id: SessionId,
|
|
839
|
+
log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
|
|
346
840
|
): Promise<void> {
|
|
347
|
-
await this.ready;
|
|
348
|
-
const persisted = events.filter(isPersistedEvent);
|
|
349
|
-
if (persisted.length === 0) return;
|
|
350
|
-
const meta = storage.meta;
|
|
351
|
-
// fork 派生会话的 seed 复用源会话事件行(不复制);消费后清除。
|
|
352
|
-
const reuse = this.reuseEventIds.get(meta.id);
|
|
353
|
-
if (reuse !== undefined) this.reuseEventIds.delete(meta.id);
|
|
354
|
-
let confirmedHead = -1;
|
|
355
841
|
await this.backend.transaction(async (tx) => {
|
|
356
|
-
await tx.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// 等于本实例确认过的最后一个 head。
|
|
361
|
-
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
362
|
-
const { headEventId, headSequence } = await appendEventTail(
|
|
363
|
-
tx,
|
|
364
|
-
meta,
|
|
365
|
-
persisted,
|
|
366
|
-
{ parentId: head.fHeadEventId, nextSeq: head.fHeadSequence + 1 },
|
|
367
|
-
reuse,
|
|
842
|
+
await tx.deleteBridgeTail(id, 0);
|
|
843
|
+
await tx.upsertSession(
|
|
844
|
+
{ meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
|
|
845
|
+
randomUUID(),
|
|
368
846
|
);
|
|
369
|
-
await tx.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async commitRepair(
|
|
378
|
-
storage: SessionStorageMetadata,
|
|
379
|
-
tornMarker: number | undefined,
|
|
380
|
-
closers: readonly SessionEvent[],
|
|
381
|
-
): Promise<void> {
|
|
382
|
-
await this.ready;
|
|
383
|
-
const meta = storage.meta;
|
|
384
|
-
const persistedClosers = closers.filter(isPersistedEvent);
|
|
385
|
-
if (tornMarker === undefined && persistedClosers.length === 0) return;
|
|
386
|
-
await this.backend.transaction(async (tx) => {
|
|
387
|
-
if (tornMarker !== undefined) {
|
|
388
|
-
await tx.deleteBridgeTail(meta.id, tornMarker);
|
|
389
|
-
// head 游标回退到最后一个幸存事件(torn tail 从 seq 0 开始时为初始态)。
|
|
390
|
-
const prev = await tx.getPrevBridge(meta.id, tornMarker - 1);
|
|
391
|
-
if (prev === undefined) {
|
|
392
|
-
await tx.updateHead(meta.id, "", -1);
|
|
393
|
-
} else {
|
|
394
|
-
await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (persistedClosers.length > 0) {
|
|
398
|
-
// 锚定实际尾行:head 游标可能滞后于行(手工 torn tail 不更新游标)。
|
|
399
|
-
const last = await tx.getLastBridge(meta.id);
|
|
400
|
-
const { headEventId, headSequence } = await appendEventTail(tx, meta, persistedClosers, {
|
|
401
|
-
parentId: last?.fEventId ?? "",
|
|
402
|
-
nextSeq: (last?.fSequence ?? -1) + 1,
|
|
403
|
-
});
|
|
404
|
-
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
405
|
-
}
|
|
406
|
-
await tx.bumpRevision(meta.id);
|
|
847
|
+
const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
|
|
848
|
+
parentId: "",
|
|
849
|
+
nextSeq: 0,
|
|
850
|
+
});
|
|
851
|
+
await tx.updateHead(id, headEventId, headSequence);
|
|
852
|
+
await tx.bumpRevision(id);
|
|
407
853
|
});
|
|
408
|
-
// 修复后重确认 head:截断会回退它、closers 会推进它,下一次 append 不得
|
|
409
|
-
// 基于陈旧确认被拒绝(或更糟,被静默重编号)。
|
|
410
|
-
const row = await this.backend.getSession(meta.id);
|
|
411
|
-
this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
|
|
412
854
|
}
|
|
413
855
|
|
|
414
|
-
async
|
|
856
|
+
async listSnapshots(
|
|
857
|
+
signal?: AbortSignal,
|
|
858
|
+
): Promise<Array<SessionPersistenceSnapshot & { inheritedEventCount: number }>> {
|
|
415
859
|
signal?.throwIfAborted();
|
|
416
860
|
await this.ready;
|
|
417
861
|
signal?.throwIfAborted();
|
|
862
|
+
const snapshots: Array<SessionPersistenceSnapshot & { inheritedEventCount: number }> = [];
|
|
863
|
+
const listed = new Set<SessionId>();
|
|
864
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
865
|
+
snapshots.push({
|
|
866
|
+
header: pending.header,
|
|
867
|
+
revision: pending.revision,
|
|
868
|
+
inheritedEventCount: pending.inheritedEventCount,
|
|
869
|
+
});
|
|
870
|
+
listed.add(id);
|
|
871
|
+
}
|
|
418
872
|
const rows = await this.backend.listSessions();
|
|
419
873
|
signal?.throwIfAborted();
|
|
420
|
-
|
|
874
|
+
for (const row of rows) {
|
|
875
|
+
if (listed.has(row.fSessionId as SessionId)) continue;
|
|
876
|
+
snapshots.push({
|
|
877
|
+
header: rowToMeta(row),
|
|
878
|
+
revision: this.rowRevision(row),
|
|
879
|
+
inheritedEventCount: row.fSeedLength ?? 0,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
return snapshots;
|
|
421
883
|
}
|
|
422
884
|
|
|
423
|
-
async
|
|
885
|
+
async readStoredRevision(
|
|
886
|
+
id: SessionId,
|
|
424
887
|
signal?: AbortSignal,
|
|
425
|
-
): Promise<
|
|
888
|
+
): Promise<SessionPersistenceRevision | undefined> {
|
|
426
889
|
signal?.throwIfAborted();
|
|
427
890
|
await this.ready;
|
|
428
891
|
signal?.throwIfAborted();
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
return
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
892
|
+
const row = await this.backend.getSession(id);
|
|
893
|
+
if (row === undefined) return undefined;
|
|
894
|
+
return this.rowRevision(row);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** 便捷:create + append + close(测试与导入路径共用)。 */
|
|
898
|
+
async createAndAppend(
|
|
899
|
+
header: SessionHeader,
|
|
900
|
+
events: readonly SessionEvent[],
|
|
901
|
+
inheritedEventCount?: number,
|
|
902
|
+
): Promise<void> {
|
|
903
|
+
const handle = await this.create(
|
|
904
|
+
header,
|
|
905
|
+
inheritedEventCount === undefined
|
|
906
|
+
? undefined
|
|
907
|
+
: { inheritedEventCount: SessionLogOffset(inheritedEventCount) },
|
|
908
|
+
);
|
|
909
|
+
try {
|
|
910
|
+
if (events.length > 0) await handle.append(events);
|
|
911
|
+
} finally {
|
|
912
|
+
await handle.close();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** 便捷:open(read) + read 全量 + close。 */
|
|
917
|
+
async load(
|
|
918
|
+
id: SessionId,
|
|
919
|
+
signal?: AbortSignal,
|
|
920
|
+
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection> {
|
|
921
|
+
const handle = await this.open(id, "read", signal === undefined ? undefined : { signal });
|
|
922
|
+
try {
|
|
923
|
+
const { events } = await handle.read(
|
|
924
|
+
0,
|
|
925
|
+
undefined,
|
|
926
|
+
signal === undefined ? undefined : { signal },
|
|
927
|
+
);
|
|
928
|
+
const row = await this.backend.getSession(id);
|
|
929
|
+
if (row === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
930
|
+
// 旧格式(v0/v1)会话:meta 与继承前缀来自转换链(handle.header 已是
|
|
931
|
+
// 转换后的 v2 header);v2 会话用存储行。
|
|
932
|
+
if (isLegacyVersion(row.fVersion)) {
|
|
933
|
+
return {
|
|
934
|
+
meta: handle.header,
|
|
935
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
936
|
+
events,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
meta: rowToMeta(row),
|
|
941
|
+
inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
|
|
942
|
+
events,
|
|
943
|
+
};
|
|
944
|
+
} finally {
|
|
945
|
+
await handle.close();
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** 便捷:open(write) + append + close。 */
|
|
950
|
+
async append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
|
|
951
|
+
const handle = await this.open(id, "write");
|
|
952
|
+
try {
|
|
953
|
+
await handle.append(events);
|
|
954
|
+
} finally {
|
|
955
|
+
await handle.close();
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** 便捷:readFrom(稠密后缀)。 */
|
|
960
|
+
async readFrom(
|
|
961
|
+
id: SessionId,
|
|
962
|
+
fromSeq: number,
|
|
963
|
+
signal?: AbortSignal,
|
|
964
|
+
): Promise<{
|
|
965
|
+
meta: SessionHeader;
|
|
966
|
+
inheritedEventCount: number;
|
|
967
|
+
events: readonly SessionEvent[];
|
|
968
|
+
}> {
|
|
969
|
+
const log = await this.readLog(id, { fromSeq }, signal);
|
|
970
|
+
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
971
|
+
return {
|
|
972
|
+
meta: log.meta,
|
|
973
|
+
inheritedEventCount: log.inheritedEventCount,
|
|
974
|
+
events: log.events,
|
|
975
|
+
};
|
|
438
976
|
}
|
|
439
977
|
|
|
440
978
|
async close(): Promise<void> {
|
|
@@ -450,16 +988,173 @@ export class SessionPersistenceRdb
|
|
|
450
988
|
return {
|
|
451
989
|
backend: this.backend,
|
|
452
990
|
writeGuard: this.writeGuard,
|
|
453
|
-
create: (meta, inheritedEventCount) =>
|
|
991
|
+
create: (meta, inheritedEventCount) =>
|
|
992
|
+
this.create(
|
|
993
|
+
meta,
|
|
994
|
+
inheritedEventCount === undefined
|
|
995
|
+
? undefined
|
|
996
|
+
: { inheritedEventCount: SessionLogOffset(inheritedEventCount) },
|
|
997
|
+
),
|
|
454
998
|
append: (id, events) => this.append(id, events),
|
|
455
999
|
load: (id) => this.load(id),
|
|
456
|
-
inspect: (id, signal) => this.
|
|
1000
|
+
inspect: (id, signal) => this.load(id, signal),
|
|
457
1001
|
readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
|
|
458
1002
|
listSnapshots: (signal) => this.listSnapshots(signal),
|
|
459
1003
|
readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
|
|
460
1004
|
registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map),
|
|
461
1005
|
};
|
|
462
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
private rowRevision(row: import("./backend.ts").SessionRow): SessionPersistenceRevision {
|
|
1009
|
+
return SessionPersistenceRevision(
|
|
1010
|
+
`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// --- live 路由:session/created → create/adopt handle;event → 缓冲;flush → drain ---
|
|
1015
|
+
|
|
1016
|
+
private installLiveRouting(ctx: Context): void {
|
|
1017
|
+
ctx.on("session/created", (session: Session) => {
|
|
1018
|
+
this.liveBuffers.set(session.id, []);
|
|
1019
|
+
const ready = this.ensureLiveHandle(session);
|
|
1020
|
+
this.liveReady.set(session.id, ready);
|
|
1021
|
+
void ready.catch((error: unknown) => {
|
|
1022
|
+
ctx.logger.warn(
|
|
1023
|
+
`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`,
|
|
1024
|
+
);
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
// HMR:插件 apply 时已存在的 live 会话不重放 session/created——补种。
|
|
1028
|
+
for (const session of ctx.sessions.list()) {
|
|
1029
|
+
this.liveBuffers.set(session.id, []);
|
|
1030
|
+
const ready = this.ensureLiveHandle(session);
|
|
1031
|
+
this.liveReady.set(session.id, ready);
|
|
1032
|
+
void ready.catch((error: unknown) => {
|
|
1033
|
+
ctx.logger.warn(
|
|
1034
|
+
`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`,
|
|
1035
|
+
);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
ctx.on("session/event", (session: Session, event: SessionEvent) => {
|
|
1039
|
+
const handle = this.tracker.writerOf(session.id);
|
|
1040
|
+
if (handle !== undefined) {
|
|
1041
|
+
handle.enqueueLive(event, (error) => {
|
|
1042
|
+
ctx.logger.warn(
|
|
1043
|
+
`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`,
|
|
1044
|
+
);
|
|
1045
|
+
});
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
1049
|
+
});
|
|
1050
|
+
ctx.on("session/flush", (session: Session) => {
|
|
1051
|
+
const handle = this.tracker.writerOf(session.id);
|
|
1052
|
+
if (handle === undefined) {
|
|
1053
|
+
const ready = this.liveReady.get(session.id);
|
|
1054
|
+
if (ready === undefined) return undefined;
|
|
1055
|
+
return ready.then(() => {
|
|
1056
|
+
const settled = this.tracker.writerOf(session.id);
|
|
1057
|
+
if (settled === undefined) return undefined;
|
|
1058
|
+
return settled.drainLive().then(() => settled.flush());
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
return handle.drainLive().then(() => handle.flush());
|
|
1062
|
+
});
|
|
1063
|
+
ctx.on("session/disposed", (session: Session) => {
|
|
1064
|
+
const ready = this.liveReady.get(session.id);
|
|
1065
|
+
this.liveBuffers.delete(session.id);
|
|
1066
|
+
this.liveReady.delete(session.id);
|
|
1067
|
+
const closeHandle = (): void => {
|
|
1068
|
+
const handle = this.tracker.writerOf(session.id);
|
|
1069
|
+
if (handle === undefined) return;
|
|
1070
|
+
handle.close().catch((error: unknown) => {
|
|
1071
|
+
ctx.logger.warn(
|
|
1072
|
+
`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`,
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
1075
|
+
};
|
|
1076
|
+
if (ready === undefined) {
|
|
1077
|
+
closeHandle();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
// handle 可能仍在构造(ensureLiveHandle 异步):等就绪后再 close。
|
|
1081
|
+
void ready.then(closeHandle, closeHandle);
|
|
1082
|
+
});
|
|
1083
|
+
ctx.effect(
|
|
1084
|
+
() => async () => {
|
|
1085
|
+
// 先等所有 live handle 就绪(ensureLiveHandle 异步),再统一关闭。
|
|
1086
|
+
await Promise.allSettled(this.liveReady.values());
|
|
1087
|
+
await this.tracker.closeAll();
|
|
1088
|
+
// 等 init(异步 open)settle 后关闭后端连接:dispose 返回后调用方
|
|
1089
|
+
// 可能立即释放存储(pg 测试 drop 数据库),未完成的 open 会以
|
|
1090
|
+
// 无人处理的 rejection 泄漏。
|
|
1091
|
+
await this.close();
|
|
1092
|
+
},
|
|
1093
|
+
`${this.name} open handles`,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
|
|
1098
|
+
private async ensureLiveHandle(session: Session): Promise<void> {
|
|
1099
|
+
const id = session.header.id;
|
|
1100
|
+
if (this.tracker.writerOf(id) !== undefined) return;
|
|
1101
|
+
await this.ready;
|
|
1102
|
+
const stored = await this.readLog(id, {});
|
|
1103
|
+
let handle: RdbSessionHandle;
|
|
1104
|
+
if (stored === undefined) {
|
|
1105
|
+
// 新会话:注册 pending 并返回 write handle;构造 seed 事件不发布
|
|
1106
|
+
// session/event,须在此一次性落库(与旧版 onCreated 同语义)。
|
|
1107
|
+
handle = (await this.create(session.header, {
|
|
1108
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
1109
|
+
})) as RdbSessionHandle;
|
|
1110
|
+
const seed = session.snapshotEvents();
|
|
1111
|
+
if (seed.length > 0) await handle.append(seed);
|
|
1112
|
+
} else {
|
|
1113
|
+
// adopt:校验 cwd / inheritedEventCount / seed 前缀匹配后接管写所有权。
|
|
1114
|
+
if (stored.meta.cwd !== session.header.cwd) {
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`,
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
if (stored.inheritedEventCount !== session.inheritedEventCount) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`session "${id}" is already persisted with a different inherited event count (id collision)`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
assertVersion(stored.meta);
|
|
1125
|
+
// adopt 比较必须与读取视图同源:live seed 来自修复后的读取视图(补
|
|
1126
|
+
// stream、surface 修复),未修复的存储视图会把修复差异误判为 id 冲突。
|
|
1127
|
+
repairReadView(stored.events);
|
|
1128
|
+
const seed = session.snapshotEvents();
|
|
1129
|
+
if (!seedCoversPrefix(seed, stored.events)) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
handle = (await this.open(id, "write")) as RdbSessionHandle;
|
|
1135
|
+
// 持久化 seed 后缀(构造 seed 事件不发布 session/event,缓冲看不到)。
|
|
1136
|
+
const suffix = seed.slice(stored.events.length);
|
|
1137
|
+
if (suffix.length > 0) await handle.append(suffix);
|
|
1138
|
+
}
|
|
1139
|
+
// 把 handle 就绪前缓冲的事件移交。
|
|
1140
|
+
const buffered = this.liveBuffers.get(id);
|
|
1141
|
+
if (buffered !== undefined && buffered.length > 0) {
|
|
1142
|
+
this.liveBuffers.set(id, []);
|
|
1143
|
+
for (const event of buffered) {
|
|
1144
|
+
handle.enqueueLive(event, () => {});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function seedCoversPrefix(seed: readonly SessionEvent[], prefix: readonly SessionEvent[]): boolean {
|
|
1151
|
+
return (
|
|
1152
|
+
prefix.length <= seed.length &&
|
|
1153
|
+
prefix.every((event, index) => {
|
|
1154
|
+
const seedEvent = seed[index];
|
|
1155
|
+
return seedEvent !== undefined && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
1156
|
+
})
|
|
1157
|
+
);
|
|
463
1158
|
}
|
|
464
1159
|
|
|
465
1160
|
function createBackend(config: Config): Backend {
|
|
@@ -504,7 +1199,6 @@ async function appendEventTail(
|
|
|
504
1199
|
fSessionId: SessionId;
|
|
505
1200
|
fEventId: string;
|
|
506
1201
|
fSequence: number;
|
|
507
|
-
fOriginalSeq: number;
|
|
508
1202
|
fSurfaceOp: string | null;
|
|
509
1203
|
}> = [];
|
|
510
1204
|
for (const event of events) {
|
|
@@ -512,6 +1206,15 @@ async function appendEventTail(
|
|
|
512
1206
|
const eventId = reusedId ?? randomUUID();
|
|
513
1207
|
if (reusedId === undefined) {
|
|
514
1208
|
const { kind, role, name, actionId } = eventDimensions(event);
|
|
1209
|
+
// fData 存完整事件(含 ignorable 信封,与 JSONL 每行同构):data 部分
|
|
1210
|
+
// 与信封字段在同一 JSON 记录里,读回时整体解析。surfaceOp 走桥接行
|
|
1211
|
+
// 列(f_surface_op),sourceEventSeqs 不落库(读取时重计算)。
|
|
1212
|
+
const raw = event as SessionEvent & {
|
|
1213
|
+
ignorable?: unknown;
|
|
1214
|
+
surfaceOp?: unknown;
|
|
1215
|
+
sourceEventSeqs?: unknown;
|
|
1216
|
+
};
|
|
1217
|
+
const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = raw;
|
|
515
1218
|
eventRows.push({
|
|
516
1219
|
fEventId: eventId,
|
|
517
1220
|
fParentId: parentId,
|
|
@@ -521,7 +1224,7 @@ async function appendEventTail(
|
|
|
521
1224
|
fName: name,
|
|
522
1225
|
fActionId: actionId,
|
|
523
1226
|
fEncoding: EVENT_ENCODING,
|
|
524
|
-
fData: JSON.stringify(
|
|
1227
|
+
fData: JSON.stringify({ ...envelope, data }),
|
|
525
1228
|
fCreatedAt: event.time,
|
|
526
1229
|
});
|
|
527
1230
|
}
|
|
@@ -533,7 +1236,6 @@ async function appendEventTail(
|
|
|
533
1236
|
fSessionId: meta.id,
|
|
534
1237
|
fEventId: eventId,
|
|
535
1238
|
fSequence: nextSeq,
|
|
536
|
-
fOriginalSeq: event.seq,
|
|
537
1239
|
fSurfaceOp: surfaceOp,
|
|
538
1240
|
});
|
|
539
1241
|
parentId = eventId;
|