export type SaveResponse = | { 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 { initialVersion: number; createPayload: (draft: Draft, expectedVersion: number) => Payload; save: (payload: Payload) => Promise>; onStatus?: (status: SaveStatus) => void; onSaved?: (snapshot: Snapshot | undefined, version: number) => void; } export class ManualSaveCoordinator { private version: number; private pending: Draft | undefined; private saving = false; private disposed = false; constructor(private readonly options: ManualSaveCoordinatorOptions) { 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 { 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); } }