import { useState, useSyncExternalStore } from 'react'; import type { ReferralContactAttempt } from '../services/ReferralWorkbenchService'; import type { ReferralQueueRow } from './useReferralQueue'; import { stillOpen, eitherAborted, type ReferralRequestScope } from '../utils/referralRequestScope'; import { isSaveRetired, outcomeUnknown, SaveRetiredError, useSaveScope, type SaveOperation } from '../utils/saveOperation'; import { onViewerChange, viewerEpoch, writeThroughViewer } from '../utils/writeThroughHold'; type PendingContact = { attempt: ReferralContactAttempt; priorMatches: number; pending: boolean }; // A retired editor cannot establish that its append did not land. Keep its reconciliation separate from its draft. const unresolved = new Map(); const listeners = new Set<() => void>(); const notify = () => listeners.forEach(listener => listener()); const subscribe = (listener: () => void) => { listeners.add(listener); const stop = onViewerChange(listener); return () => { listeners.delete(listener); stop(); }; }; const matches = (stored: ReferralContactAttempt, attempted: ReferralContactAttempt) => (['method', 'occurredAt', 'recipient', 'spokeWith', 'outcome'] as const) .every(field => stored[field] === attempted[field]); type ReadHistory = (taskId: string, scope: ReferralRequestScope) => Promise; type RecordContact = (taskId: string, attempt: ReferralContactAttempt, scope?: ReferralRequestScope) => Promise; export const useReferralContactRecovery = (row: ReferralQueueRow | null, scope?: ReferralRequestScope, allowed = true) => { const key = () => JSON.stringify([writeThroughViewer() ?? viewerEpoch(), scope?.organizationId ?? '', row?.taskId]); const entry = useSyncExternalStore(subscribe, () => unresolved.get(key())); const [checking, setChecking] = useState(false); const [failure, setFailure] = useState(); const [review, setReview] = useState<{ key: string; entry: PendingContact; contacts: ReferralContactAttempt[]; owner: SaveOperation }>(); const reads = useSaveScope(allowed ? key() : undefined, () => { setChecking(false); setFailure(undefined); setReview(undefined); }); const admissible = () => allowed && stillOpen(scope); const record = async (attempt: ReferralContactAttempt, operation: ReferralRequestScope | undefined, write: RecordContact) => { const ownerKey = key(); if (!row?.taskId || !admissible() || !stillOpen(operation) || unresolved.has(ownerKey)) throw new SaveRetiredError(); const pending = { attempt, priorMatches: row.contacts?.filter(each => matches(each, attempt)).length ?? 0, pending: true }; unresolved.set(ownerKey, pending); notify(); let sent = false; const transport = { ...operation, admit: () => { operation?.admit(); }, signal: operation?.signal ?? new AbortController().signal, onSend: () => { operation?.onSend?.(); sent = true; } }; try { const contacts = await write(row.taskId, attempt, transport); if (unresolved.get(ownerKey) === pending) { if (stillOpen(operation) && key() === ownerKey && admissible()) unresolved.delete(ownerKey); else unresolved.set(ownerKey, { ...pending, pending: false }); notify(); } return contacts; } catch (error) { if (unresolved.get(ownerKey) === pending) { if (outcomeUnknown(error) || (sent && (isSaveRetired(error) || operation?.signal.aborted))) unresolved.set(ownerKey, { ...pending, pending: false }); else unresolved.delete(ownerKey); notify(); } throw error; } }; const check = async (read: ReadHistory) => { if (!entry || !row?.taskId || checking || !admissible()) return; const ownerKey = key(); const owner = reads.start(); const admit = () => { owner.admit(); scope?.admit(); if (!admissible() || key() !== ownerKey) throw new SaveRetiredError(); }; const transport = { admit, signal: scope ? eitherAborted(owner.transport.signal, scope.signal) : owner.transport.signal, organizationId: scope?.organizationId }; let keepReview = false; setChecking(true); setFailure(undefined); setReview(undefined); try { const contacts = await owner.step(() => read(row.taskId!, transport)); admit(); if (!contacts) throw new Error('History unavailable'); setReview({ key: ownerKey, entry, contacts, owner }); keepReview = true; } catch { if (owner.belongs() && admissible()) setFailure('Saved contact history could not be confirmed. Recording remains unavailable. Check again.'); } finally { if (owner.belongs()) setChecking(false); if (!keepReview) owner.retire(); } }; const currentReview = review?.key === key() && review.entry === entry && review.owner.belongs() ? review : undefined; // A negative/old search cannot disprove an append still finishing. Only a newly observed matching attempt settles it. const confirmed = Boolean(admissible() && entry && currentReview && currentReview.contacts.filter(each => matches(each, entry.attempt)).length > entry.priorMatches); const reconcile = () => { if (!confirmed || !currentReview || !admissible() || unresolved.get(key()) !== entry) return undefined; const contacts = currentReview.contacts; unresolved.delete(key()); notify(); currentReview.owner.retire(); setReview(undefined); setFailure(undefined); return contacts; }; return { held: Boolean(entry), canCheck: admissible(), pending: entry?.pending, checking, failure, contacts: currentReview?.contacts, checked: Boolean(currentReview), confirmed, record, check, reconcile }; };