import { resolve } from "node:path"; import type { CursorMarkerInspection, PendingJournal } from "./journal.ts"; import type { JournalPhase, ManifestId, SessionFileIdentity, SnapshotManifest } from "./model.ts"; import type { RestorePlan, RestoreResult } from "./restore-engine.ts"; export interface JournalRecoveryDependencies { readonly sessionIdentity: SessionFileIdentity; readonly workspaceIdentity: string; readonly getLogicalLeafId: () => string | null; readonly loadPending: () => Promise; /** 仅允许严格证明无工作区 mutation 的 foreign PREPARED 空事务被忽略。 */ readonly assessForeignTransaction?: (journal: PendingJournal) => Promise; /** 判断 transaction 是否已被完全补偿(mutation 全部 CLEANED 且净效果回到操作前状态),可直接 settle。 */ readonly assessCompensatedTransaction?: (journal: PendingJournal) => Promise; readonly inspectCursor: (journal: PendingJournal) => Promise; readonly finalizeCursor: (journal: PendingJournal, inspection: Extract) => Promise; readonly recoverMutations: ( journal: PendingJournal, decision: "rollback" | "roll_forward", ) => Promise<{ readonly kind: "clean" } | { readonly kind: "conflict"; readonly paths: number }>; readonly capture: () => Promise; readonly loadManifest: (id: ManifestId) => Promise; readonly planRestore: ( current: SnapshotManifest, target: SnapshotManifest, scopePaths?: readonly string[], ) => Promise; readonly applyRestore: ( plan: RestorePlan, target: SnapshotManifest, operation: { readonly opId: string }, ) => Promise; readonly settle: (opId: string, phase: Extract) => Promise; } export type JournalRecoveryResult = | { readonly kind: "clean"; readonly operations: 0 } | { readonly kind: "recovered"; readonly operations: number } | { readonly kind: "locked"; readonly reason: string; readonly operations: number; readonly files?: number; readonly opId?: string; }; /** * 根据 durable cursor marker 决定向前补完或回滚。 * 恢复操作是 set-state 且可重复执行;任何身份或 leaf 证据不一致都 fail closed。 */ export class JournalRecovery { private readonly dependencies: JournalRecoveryDependencies; constructor(dependencies: JournalRecoveryDependencies) { this.dependencies = dependencies; } async recover(): Promise { let recovered = 0; let pending: readonly PendingJournal[]; try { pending = await this.dependencies.loadPending(); } catch { return { kind: "locked", reason: "journal_invalid", operations: recovered }; } if (pending.length === 0) return { kind: "clean", operations: 0 }; for (const journal of pending) { const identityError = this.identityError(journal); if (identityError !== null) { if (identityError === "session_identity_mismatch") { if (await this.settleCompensated(journal)) { recovered += 1; continue; } if (await this.canIgnoreForeignTransaction(journal)) continue; } return { kind: "locked", reason: identityError, operations: recovered }; } // 同一会话(重启或第二窗口)内,完全补偿的事务同样可直接 settle: // 工作区义务已全部履行且无 cursor 提交证据,session leaf 位置对 // ABORTED 终结不构成安全输入,无需通过 leaf 校验。 if (await this.settleCompensated(journal)) { recovered += 1; continue; } let inspection: CursorMarkerInspection; try { inspection = await this.dependencies.inspectCursor(journal); } catch { return { kind: "locked", reason: "cursor_inspection_failed", operations: recovered }; } if (inspection.kind === "conflict") { return { kind: "locked", reason: "cursor_conflict", operations: recovered }; } const committedLeaf = journal.descriptor.action === "tree" && journal.state.observedLogicalLeaf !== undefined ? journal.state.observedLogicalLeaf : journal.descriptor.toLogicalLeaf; const expectedLeaf = inspection.kind === "match" ? committedLeaf : journal.descriptor.fromLogicalLeaf; if (this.dependencies.getLogicalLeafId() !== expectedLeaf) { return { kind: "locked", reason: "session_leaf_mismatch", operations: recovered }; } const mutationDecision = inspection.kind === "match" ? "roll_forward" : "rollback"; let mutationResult: Awaited>; try { mutationResult = await this.dependencies.recoverMutations(journal, mutationDecision); } catch { return { kind: "locked", reason: "mutation_recovery_failed", operations: recovered }; } if (mutationResult.kind === "conflict") { return { kind: "locked", reason: "mutation_conflict", operations: recovered, files: mutationResult.paths, opId: journal.descriptor.opId, }; } try { if (journal.descriptor.scopePaths.length > 0) { const current = await this.dependencies.capture(); const targetId = inspection.kind === "match" ? journal.descriptor.targetManifestId : journal.descriptor.rollbackManifestId; const target = await this.dependencies.loadManifest(targetId); const plan = await this.dependencies.planRestore(current, target, journal.descriptor.scopePaths); const applied = await this.dependencies.applyRestore( plan, target, { opId: journal.descriptor.opId }, ); if (applied.code !== "ok") { return { kind: "locked", reason: "restore_failed", operations: recovered }; } } if (inspection.kind === "match") { await this.dependencies.finalizeCursor(journal, inspection); } await this.dependencies.settle( journal.descriptor.opId, inspection.kind === "match" ? "COMMITTED" : "ABORTED", ); recovered += 1; } catch { return { kind: "locked", reason: "recovery_failed", operations: recovered }; } } return { kind: "recovered", operations: recovered }; } private async canIgnoreForeignTransaction(journal: PendingJournal): Promise { const assess = this.dependencies.assessForeignTransaction; if (assess === undefined) return false; try { return await assess(journal); } catch { // 评估失败等同于证据不足,保留 foreign identity lock。 return false; } } /** * 终结完全补偿的 pending transaction(own session 重启/第二窗口与 foreign 会话均适用)。 * 调用方持有 workspace lock,pending 非终态事务的 owner 操作已确定性结束; * mutation 义务全部履行且净效果回到操作前状态时,无需触碰工作区即可 settle。 * cursor marker 必须缺失——marker 存在意味着操作已提交,与补偿证据矛盾,fail closed。 */ private async settleCompensated(journal: PendingJournal): Promise { const assess = this.dependencies.assessCompensatedTransaction; if (assess === undefined) return false; let compensated: boolean; try { compensated = await assess(journal); } catch { // 评估失败等同于证据不足,退回原有恢复语义。 return false; } if (!compensated) return false; let inspection: CursorMarkerInspection; try { inspection = await this.dependencies.inspectCursor(journal); } catch { return false; } if (inspection.kind !== "absent") return false; try { await this.dependencies.settle(journal.descriptor.opId, "ABORTED"); } catch { return false; } return true; } private identityError(journal: PendingJournal): string | null { if (journal.descriptor.workspaceIdentity !== this.dependencies.workspaceIdentity) { return "workspace_identity_mismatch"; } const observed = journal.descriptor.sessionIdentity; const expected = this.dependencies.sessionIdentity; if (resolve(observed.path) !== resolve(expected.path) || observed.headerChecksum !== expected.headerChecksum) { return "session_identity_mismatch"; } return null; } }