import { useCallback, useEffect, useRef, useState } from "react"; import type { Organization } from "fhir/r4"; import { outcomeUnknown, startSaveOperation, type SaveTransport } from "../utils/saveOperation"; import { relayServerFailure } from "../utils/fetchFailureMessage"; import { onViewerChange, viewerEpoch } from "../utils/writeThroughHold"; export type SettingSaveStatus = "idle" | "saving" | "saved" | "refused" | "unconfirmed" | "checking"; export interface SettingSaveState { status: SettingSaveStatus; message?: string; /** The practice as read by the last successful check after an unconfirmed save. */ stored?: Organization; } /** Only the stored practice itself, as an Organization with this practice's id, proves what is stored. */ const isPracticeRecord = (value: unknown, practiceId: string): value is Organization => Boolean(value) && typeof value === "object" && (value as Organization).resourceType === "Organization" && (value as Organization).id === practiceId; const UNPROVED = "We could not confirm that change. Check what is stored before changing it again."; const answeredUnknown = (error: unknown): boolean => outcomeUnknown(error) || (error as { body?: { code?: unknown } } | null)?.body?.code === "OUTCOME_UNKNOWN"; /** * One practice setting saved from one opening (this practice under this viewer); each save and check is its own * bounded attempt of that opening. An unknown or unproved save holds further saves until a check reads this practice, * and a hold outlives a viewer change for the new opening to clear with its own check. */ export function useOwnedSettingSave(practiceId: string | undefined, { read, publish, }: { read: (practiceId: string, transport: SaveTransport) => Promise; publish: (organization: Organization) => void; }) { const opening = useRef({ live: false, practiceId }); const [state, setState] = useState({ status: "idle" }); const current = useRef(state); current.current = state; const [viewerAt, setViewerAt] = useState(viewerEpoch); useEffect(() => onViewerChange(() => { opening.current.live = false; setViewerAt(viewerEpoch()); }), []); useEffect(() => { const previous = opening.current; const here = { live: true, practiceId }; opening.current = here; const uncertain = ["saving", "unconfirmed", "checking"].includes(current.current.status); setState(previous.practiceId === practiceId && uncertain ? { status: "unconfirmed", message: UNPROVED } : { status: "idle" }); return () => { here.live = false; }; }, [practiceId, viewerAt]); const check = useCallback(async () => { const here = opening.current; const id = here.practiceId; if (!here.live || !id) return; const attempt = startSaveOperation(() => here.live && opening.current === here); setState((was) => ({ ...was, status: "checking", message: undefined })); try { const organization = await attempt.step(() => read(id, attempt.transport)); if (!attempt.belongs()) return; if (!isPracticeRecord(organization, id)) { setState((was) => ({ ...was, status: "unconfirmed", message: "We could not check what is stored. Check again." })); return; } publish(organization); setState({ status: "idle", stored: organization }); } catch { if (!attempt.belongs()) return; setState((was) => ({ ...was, status: "unconfirmed", message: "We could not check what is stored. Check again." })); } }, [read, publish]); const save = useCallback(async ( write: (transport: SaveTransport) => Promise, { onSaved, refusal }: { onSaved?: (answer: T) => void; refusal: string }, ): Promise => { const here = opening.current; const id = here.practiceId; const held = current.current.status; if (!here.live || !id || held === "saving" || held === "unconfirmed" || held === "checking") return; const attempt = startSaveOperation(() => here.live && opening.current === here); setState({ status: "saving" }); try { const answer = await attempt.step(() => write(attempt.transport), true); if (!attempt.belongs()) return; if (!isPracticeRecord(answer?.organization, id)) { setState({ status: "unconfirmed", message: UNPROVED }); await check(); return; } publish(answer.organization); setState({ status: "saved" }); onSaved?.(answer); } catch (error) { if (!attempt.belongs()) return; if (!answeredUnknown(error)) { setState({ status: "refused", message: relayServerFailure(error, refusal) }); return; } setState({ status: "unconfirmed", message: "We could not confirm that change. Checking what is stored." }); await check(); } }, [check, publish]); const owner = opening.current; const ownsOpening = useCallback(() => owner.live && opening.current === owner && owner.practiceId === practiceId && viewerEpoch() === viewerAt, [owner, practiceId, viewerAt]); return { state, save, check, owner, ownsOpening }; }