import { isAsOfIncomplete, isServedAtCutoff } from '../utils/searchTruncation'; import { useChartRecordStanding, useFreshAfterReauthorization, type RecordAccessStanding, type RecordFreshness } from './useRecordAccessStanding'; import { useEffect, useState, useCallback, useRef } from 'react'; import { usePatientServiceContext, useMedicationStatementServiceContext } from '../services'; import { describeFetchFailure, isRefusedRead } from '../utils/fetchFailureMessage'; import { patientCacheGuard } from '../utils/cacheOwner'; import { viewerEpoch } from '../utils/writeThroughHold'; /** Whose read a state is: the patient opening, viewer epoch and read it belongs to. */ type ReadOwner = { opening: number; epoch: number; read: number }; interface UseFhirMedicationStatementsOptions { /** The record standing to answer to; the open chart's when not given. */ standing?: RecordAccessStanding; autoLoad?: boolean; errorMessage?: string; forceReload?: boolean; } interface UseFhirMedicationStatementsReturn { /** After a reauthorization, whether this list has been read again under it. */ freshness: RecordFreshness; loading: boolean; error: string | undefined; /** The last load was answered with a 403: refused, not lost. */ refused: boolean; /** This patient's read has answered, one way or the other. Until then an empty list is unknown. */ answered: boolean; /** Whether the server was asked for this patient at all: no patient, no question, never an empty answer. */ attempted: boolean; /** The service's list is this patient's. Anything else is another patient's, never this one's data. */ listIsThisPatients: boolean; /** The error and refusal are this patient's newest read, not an earlier patient's or an older read's. */ failureIsThisPatients: boolean; /** Served as of a cutoff and not in full: what it left out is unknown, never none. */ incomplete: boolean; loadMedicationStatements: () => Promise; clearError: () => void; medicationStatementsBundle: ReturnType['medicationStatementsBundle']; medicationStatementsList: ReturnType['medicationStatementsList']; } export const useFhirMedicationStatements = (options: UseFhirMedicationStatementsOptions = {}): UseFhirMedicationStatementsReturn => { const { autoLoad = true, errorMessage = "Failed to load medication statements", forceReload = false } = options; const { patientState } = usePatientServiceContext(); const { cachedForPatientId, fetchMedicationStatementsWithPolling, listRead, medicationStatementsBundle, medicationStatementsList } = useMedicationStatementServiceContext(); // After a reauthorization this list is read again past every cache; until then it is not taken for current. const chartStanding = useChartRecordStanding().standing; const freshness = useFreshAfterReauthorization(options.standing ?? chartStanding, () => ( patientState ? fetchMedicationStatementsWithPolling(patientState, true) : Promise.reject(new Error('No patient to read.'))), { resource: 'MedicationStatement', patientId: patientState?.id, listRead, servedAtCutoff: isServedAtCutoff(medicationStatementsBundle) }); // Each patient seen is a new opening, so a repeated id never revives an earlier opening's read. Every read state // below names its opening, viewer epoch and read, and is shown only while all three are still current. const opening = useRef<{ patientId?: string; n: number }>({ patientId: patientState?.id, n: 0 }); if (opening.current.patientId !== patientState?.id) opening.current = { patientId: patientState?.id, n: opening.current.n + 1 }; const generation = useRef(0); const [asked, setAsked] = useState(); const [loading, setLoading] = useState(); const [failure, setFailure] = useState<{ owner: ReadOwner; error: string; refused: boolean } | undefined>(); const current = (owner: ReadOwner | undefined): owner is ReadOwner => owner !== undefined && owner.opening === opening.current.n && owner.epoch === viewerEpoch(); const loadMedicationStatements = useCallback(async () => { if (!patientState) { return; } const mine: ReadOwner = { opening: opening.current.n, epoch: viewerEpoch(), read: ++generation.current }; const owns = () => mine.read === generation.current && mine.opening === opening.current.n && mine.epoch === viewerEpoch(); setAsked(mine); setLoading(mine); setFailure(undefined); try { await fetchMedicationStatementsWithPolling(patientState); } catch (err) { if (!owns()) return; console.error('Failed to load medication statements:', err); setFailure({ owner: mine, error: describeFetchFailure(err, errorMessage), refused: isRefusedRead(err) }); } finally { setLoading((shown) => (shown === mine ? undefined : shown)); } }, [patientState, fetchMedicationStatementsWithPolling, errorMessage]); const clearError = useCallback(() => { setFailure(undefined); }, []); useEffect(() => { if (!autoLoad) return; if (!patientState) return; // A bundle belonging to SOMEBODY ELSE is not a reason not to ask. This used to fire only when // there was no bundle at all, so moving between two charts left the previous patient's list in // place, with no request and nothing on screen to say it was not theirs. // NOT a refetch on every navigation: same patient, bundle loaded, no request. if (!medicationStatementsBundle || cachedForPatientId !== patientState.id || forceReload) { loadMedicationStatements(); } }, [autoLoad, patientState, medicationStatementsBundle, cachedForPatientId, forceReload, loadMedicationStatements]); // The cache outlives a change of patient: hand out only the patient asked about. const guard = patientCacheGuard(cachedForPatientId, patientState?.id); const listIsThisPatients = cachedForPatientId !== undefined && cachedForPatientId === patientState?.id; const shownFailure = current(failure?.owner) && failure?.owner.read === generation.current ? failure : undefined; const failureIsThisPatients = shownFailure !== undefined; return { freshness, loading: guard.loading(current(loading) && loading.read === generation.current, failureIsThisPatients), error: shownFailure?.error, refused: shownFailure?.refused === true, answered: failureIsThisPatients || (Boolean(medicationStatementsBundle) && listIsThisPatients), // Asked only when this opening's read began or its list is held; a patient alone is not a question. attempted: Boolean(patientState) && (listIsThisPatients || current(asked)), listIsThisPatients, failureIsThisPatients, incomplete: isAsOfIncomplete(guard.bundle(medicationStatementsBundle, null)), loadMedicationStatements, clearError, medicationStatementsBundle: guard.bundle(medicationStatementsBundle, null), medicationStatementsList: guard.list(medicationStatementsList) }; };