import { useEffect } from 'react'; import { Observation } from 'fhir/r4'; import { useObservationServiceContext, useFhirServiceContext, usePatientServiceContext, useWebSocketServiceContext } from '../services'; import { isArchived } from '../utils/archivedResource'; import { useNoticeFence } from './useNoticeFence'; export const useFhirObservationBroadcast = () => { const { subscribe, unsubscribe } = useWebSocketServiceContext(); const { observationsList, setObservationsList, fetchObservation } = useObservationServiceContext(); const { patientState } = usePatientServiceContext(); const { extractReferenceId } = useFhirServiceContext(); const fence = useNoticeFence(patientState?.id); useEffect(() => { if (!patientState) { return; } const handleObservationEvent = (cloudEvent: any) => { const observation: Observation = cloudEvent.data; const eventType = cloudEvent.type; const patientId = extractReferenceId(observation.subject); // The patient's own channel names which result changed and nothing of it: what may be shown is read // again through the gated GET, and a result that read no longer returns leaves the list. // Every event about a result retires a reread still pending for it, and so does a change of patient. const stands = patientState.id === patientId && observation.id ? fence.claim(observation.id) : () => false; if (patientState.id === patientId && cloudEvent.data?.invalidated === true && observation.id) { const id = observation.id; if (eventType === 'com.akashichealthrecords.fhir.observation.deleted') { setObservationsList(prev => prev.filter(p => p.id !== id)); return; } fetchObservation(id, true) .then((fresh) => { if (!stands()) return; setObservationsList(prev => (isArchived(fresh) ? prev.filter(p => p.id !== id) : prev.some(p => p.id === id) ? prev.map(p => p.id === id ? fresh : p) : [...prev, fresh])); }) .catch(() => { if (stands()) setObservationsList(prev => prev.filter(p => p.id !== id)); }); return; } if (patientState.id === patientId) { switch (eventType) { case 'com.akashichealthrecords.fhir.observation.created': // The list has two writers: this broadcast, and the service's own refetch after a write // (several of which POLL until the new id appears). Refetch first and the list already holds // the record, so an unconditional append put a SECOND copy of it on screen - observed on a // patient's questionnaire answers, where one submission rendered as two completed forms. // Keeping the broadcast's copy rather than skipping: its body is at least as fresh. setObservationsList(prev => prev.some(p => p.id === observation.id) ? prev.map(p => p.id === observation.id ? observation : p) : [...prev, observation] ); break; case 'com.akashichealthrecords.fhir.observation.updated': setObservationsList(prev => isArchived(observation) ? prev.filter(p => p.id !== observation.id) : prev.map(p => p.id === observation.id ? observation : p) ); break; case 'com.akashichealthrecords.fhir.observation.deleted': setObservationsList(prev => prev.filter(p => p.id !== observation.id)); break; } } }; subscribe('com.akashichealthrecords.fhir.observation.created', handleObservationEvent); subscribe('com.akashichealthrecords.fhir.observation.updated', handleObservationEvent); subscribe('com.akashichealthrecords.fhir.observation.deleted', handleObservationEvent); return () => { unsubscribe('com.akashichealthrecords.fhir.observation.created', handleObservationEvent); unsubscribe('com.akashichealthrecords.fhir.observation.updated', handleObservationEvent); unsubscribe('com.akashichealthrecords.fhir.observation.deleted', handleObservationEvent); }; }, [patientState, subscribe, unsubscribe, extractReferenceId, observationsList, setObservationsList, fetchObservation, fence]); };