61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
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<void> | null = null;
|
|
private blocked = false;
|
|
private status: VideoCanvasSaveStatus = "saved";
|
|
|
|
constructor(
|
|
baseline: string,
|
|
private revision: string,
|
|
private readonly write: (snapshot: string, revision: string) => Promise<string>,
|
|
private readonly changed: (status: VideoCanvasSaveStatus, message?: string) => void,
|
|
) {
|
|
this.latest = baseline;
|
|
this.saved = baseline;
|
|
}
|
|
|
|
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<void> {
|
|
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 async drain() {
|
|
this.setStatus("saving");
|
|
try {
|
|
while (this.latest !== this.saved) {
|
|
const snapshot = this.latest;
|
|
this.revision = await this.write(snapshot, this.revision);
|
|
this.saved = snapshot;
|
|
}
|
|
this.setStatus("saved");
|
|
} catch (error) {
|
|
this.blocked = error instanceof CanvasRevisionConflict;
|
|
this.setStatus("error", error instanceof Error ? error.message : "保存失败,请重试");
|
|
}
|
|
}
|
|
}
|
|
|
|
export class CanvasRevisionConflict extends Error {}
|