import type { CanvasConflictChoice } from "./merge-snapshot"; export type VideoCanvasSaveStatus = "saved" | "pending" | "saving" | "error"; /** Keep edits made during a request; never retry a stale revision as an overwrite. */ export class VideoCanvasSaveQueue { private latest: string; private saved: string; private running: Promise | null = null; private blocked = false; private conflict: CanvasRevisionConflict | null = null; private status: VideoCanvasSaveStatus = "saved"; constructor( baseline: string, private revision: string, private readonly write: (snapshot: string, revision: string) => Promise, private readonly changed: (status: VideoCanvasSaveStatus, message?: string) => void, private readonly reconciliation?: { merge: ( base: string, local: string, remote: string, choice?: CanvasConflictChoice, ) => { snapshot: string; conflicts: string[] }; apply: (snapshot: string) => void; conflict: (fields: string[]) => void; }, ) { this.latest = baseline; this.saved = baseline; } get backup() { return { base: this.saved, snapshot: this.latest, revision: this.revision }; } restoreBackup(base: string, snapshot: string) { if (!this.reconciliation || snapshot === base) return; const remote = new CanvasRevisionConflict( "检测到未保存草稿与服务器有冲突,请选择保留哪一处修改", this.saved, this.revision, ); const merged = this.reconciliation.merge(base, snapshot, this.saved); if (merged.conflicts.length) { this.saved = base; this.latest = snapshot; this.conflict = remote; this.blocked = true; this.reconciliation.apply(snapshot); this.reconciliation.conflict(merged.conflicts); this.setStatus("error", remote.message); } else { this.rebase(merged.snapshot, remote); this.setStatus(this.latest === this.saved ? "saved" : "pending"); } } get failed() { return this.status === "error"; } get dirty() { return this.latest !== this.saved || this.running !== null; } update(snapshot: string) { this.latest = snapshot; if (!this.running && this.status !== "error") { this.setStatus(this.dirty ? "pending" : "saved"); } } private setStatus(status: VideoCanvasSaveStatus, message?: string) { this.status = status; this.changed(status, message); } flush(): Promise { if (this.running) return this.running; if (this.blocked || !this.dirty) return Promise.resolve(); this.running = this.drain().finally(() => { this.running = null; }); return this.running; } private rebase(snapshot: string, conflict: CanvasRevisionConflict) { this.saved = conflict.remoteSnapshot!; this.revision = conflict.remoteRevision!; this.latest = snapshot; this.conflict = null; this.blocked = false; this.reconciliation?.conflict([]); this.reconciliation?.apply(snapshot); } async resolveConflict(choice: CanvasConflictChoice) { await this.running; if (!this.conflict?.remoteSnapshot || !this.reconciliation) return; const merged = this.reconciliation.merge( this.saved, this.latest, this.conflict.remoteSnapshot, choice, ); this.rebase(merged.snapshot, this.conflict); await this.flush(); } private async drain() { this.setStatus("saving"); try { let merges = 0; while (this.latest !== this.saved) { const snapshot = this.latest; try { this.revision = await this.write(snapshot, this.revision); this.saved = snapshot; } catch (error) { if ( error instanceof CanvasRevisionConflict && error.remoteSnapshot && error.remoteRevision && this.reconciliation ) { this.conflict = error; const merged = this.reconciliation.merge(this.saved, this.latest, error.remoteSnapshot); if (merged.conflicts.length) { this.reconciliation.conflict(merged.conflicts); throw error; } this.rebase(merged.snapshot, error); if (merges++ >= 3) { throw new Error("其他页面正在频繁保存,当前修改已保留,请稍后重试保存"); } continue; } throw error; } } this.setStatus("saved"); } catch (error) { this.blocked = error instanceof CanvasRevisionConflict; this.setStatus("error", error instanceof Error ? error.message : "保存失败,请重试"); } } } export class CanvasRevisionConflict extends Error { constructor( message: string, public remoteSnapshot?: string, public remoteRevision?: string, ) { super(message); } }