Files
buzz-sheet/lib/editor/manual-save.ts
T
2026-10-10 02:56:59 +07:00

91 lines
3.1 KiB
TypeScript

export type SaveResponse<Snapshot = unknown> =
| { status: "saved"; version: number; snapshot?: Snapshot }
| { status: "conflict"; currentVersion: number }
| { status: "validation-error"; issues: string[] }
| { status: "transient-error"; message: string };
export type SaveStatus =
| { state: "idle" }
| { state: "dirty" }
| { state: "saving" }
| { state: "saved"; version: number; savedAt: Date }
| { state: "conflict"; currentVersion: number }
| { state: "error"; message: string; issues?: string[] };
interface ManualSaveCoordinatorOptions<Draft, Payload, Snapshot> {
initialVersion: number;
createPayload: (draft: Draft, expectedVersion: number) => Payload;
save: (payload: Payload) => Promise<SaveResponse<Snapshot>>;
onStatus?: (status: SaveStatus) => void;
onSaved?: (snapshot: Snapshot | undefined, version: number) => void;
}
export class ManualSaveCoordinator<Draft, Payload, Snapshot = unknown> {
private version: number;
private pending: Draft | undefined;
private saving = false;
private disposed = false;
constructor(private readonly options: ManualSaveCoordinatorOptions<Draft, Payload, Snapshot>) {
this.version = options.initialVersion;
this.emit({ state: "idle" });
}
updateVersion(version: number): void {
this.version = Math.max(this.version, version);
}
stage(draft: Draft): void {
if (this.disposed) return;
this.pending = draft;
if (!this.saving) this.emit({ state: "dirty" });
}
cancelPending(): void {
this.pending = undefined;
}
async flush(): Promise<void> {
if (this.disposed || this.saving || this.pending === undefined) return;
const draft = this.pending;
this.pending = undefined;
this.saving = true;
this.emit({ state: "saving" });
try {
const response = await this.options.save(this.options.createPayload(draft, this.version));
if (this.disposed) return;
if (response.status === "saved") {
this.updateVersion(response.version);
if (Object.is(this.pending, draft)) this.pending = undefined;
this.options.onSaved?.(response.snapshot, this.version);
this.emit({ state: "saved", version: this.version, savedAt: new Date() });
} else {
this.pending ??= draft;
if (response.status === "conflict") {
this.emit({ state: "conflict", currentVersion: response.currentVersion });
} else if (response.status === "validation-error") {
this.emit({ state: "error", message: "ข้อมูลบางรายการไม่ถูกต้อง", issues: response.issues });
} else {
this.emit({ state: "error", message: response.message });
}
}
} catch (cause) {
if (!this.disposed) {
this.pending ??= draft;
this.emit({ state: "error", message: cause instanceof Error ? cause.message : "บันทึกไม่สำเร็จ" });
}
} finally {
this.saving = false;
}
}
dispose(): void {
this.disposed = true;
this.pending = undefined;
}
private emit(status: SaveStatus): void {
if (!this.disposed) this.options.onStatus?.(status);
}
}