export type DraftSaveOutcome = | { status: 'saved'; version: number; incarnation?: string } | { status: 'conflict' } | { status: 'failed' }; export type DraftSaveState = 'saving' | 'saved' | 'conflict' | 'failed'; export interface DraftSaveQueueOptions { save: (draft: TDraft, version: number, incarnation?: string) => Promise; onState: (state: DraftSaveState) => void; } export interface DraftSaveQueue { save: (draft: TDraft) => void; flush: (draft: TDraft) => Promise; reset: (version?: number, incarnation?: string) => void; readonly version: number; readonly incarnation: string | undefined; readonly stopped: boolean; } export const createDraftSaveQueue = (options: DraftSaveQueueOptions): DraftSaveQueue => { let version = 0; let incarnation: string | undefined; let stopped = false; let generation = 0; let inFlight = false; type Work = { draft: TDraft; done?: (result: DraftSaveOutcome) => void }; let owed: Work | undefined; const run = (work: Work): void => { if (stopped) { work.done?.({ status: 'conflict' }); return; } if (inFlight) { owed?.done?.({ status: 'failed' }); owed = work; return; } const mine = generation; inFlight = true; options.onState('saving'); void options.save(work.draft, version, incarnation).then(outcome => { if (mine !== generation) { work.done?.({ status: 'failed' }); return; } if (outcome.status === 'saved') { version = outcome.version; incarnation = outcome.incarnation; options.onState('saved'); } else if (outcome.status === 'conflict') { stopped = true; options.onState('conflict'); } else options.onState('failed'); work.done?.(outcome); }).catch(() => { if (mine === generation) options.onState('failed'); work.done?.({ status: 'failed' }); }).finally(() => { // A retired answer must not release or drain the next owner's queue. if (mine !== generation) return; inFlight = false; const next = owed; owed = undefined; if (next) { if (stopped) next.done?.({ status: 'conflict' }); else run(next); } }); }; return { save: draft => run({ draft }), flush: draft => new Promise(resolve => run({ draft, done: resolve })), reset: (nextVersion = 0, nextIncarnation) => { generation += 1; version = nextVersion; incarnation = nextIncarnation; stopped = false; inFlight = false; owed?.done?.({ status: 'failed' }); owed = undefined; }, get version() { return version; }, get incarnation() { return incarnation; }, get stopped() { return stopped; }, }; };