import { useEffect, useRef, useState } from 'react'; import { credentials, authStorageArea } from '../services/authStorage'; import { environment } from '../environments/environment'; import { viewerEpoch, onViewerChange, writeThroughViewer } from '../utils/writeThroughHold'; import { SaveRetiredError } from '../utils/saveOperation'; import { readPracticeRemovals, retainPracticeRemoval, type PracticeRemovalIntent, type PracticeRemovalOutcome } from '../utils/practiceAgencyRemoval'; import type { RequestAdmission } from '../services/HttpService'; function custody(organizationId?: string): { owner: string; opening: string } | undefined { const viewer = writeThroughViewer()?.split('|'); if (!viewer || viewer.length !== 3 || !viewer[0] || viewer[1] || viewer[2] !== organizationId) return; const view = credentials().current(); if (!organizationId || view.state !== 'signed-in' || view.session.tuple.expiresAt <= Date.now()) return; try { const token = view.session.tuple.accessToken.split('.')[1]; const claims: unknown = JSON.parse(atob(token.replace(/-/g, '+').replace(/_/g, '/'))); if (!claims || typeof claims !== 'object' || !('iss' in claims) || !('sub' in claims) || typeof claims.iss !== 'string' || !claims.iss || typeof claims.sub !== 'string' || !claims.sub) return; const owner = JSON.stringify([window.location.origin, environment.credentialStack, environment.sessionPortal, environment.orgAuthorizationApi, view.session.tuple.clientId, claims.iss, claims.sub, organizationId]); return { owner, opening: JSON.stringify([owner, view.session.epoch, view.session.generation, viewerEpoch()]) }; } catch { return; } } type Call = (intent: PracticeRemovalIntent, mode: 'send' | 'status' | 'resume', admit: RequestAdmission) => Promise; export function usePracticeAgencyRemoval(organizationId: string | undefined, call: Call) { const [, update] = useState(0); useEffect(() => { const wake = () => update(value => value + 1); const stopCredentials = credentials().onChange(wake); const stopViewer = onViewerChange(wake); const storage = () => wake(); window.addEventListener('storage', storage); return () => { stopCredentials(); stopViewer(); window.removeEventListener('storage', storage); }; }, []); const current = custody(organizationId); const life = useRef({ opening: current?.opening, serial: 0 }); if (life.current.opening !== current?.opening) life.current = { opening: current?.opening, serial: life.current.serial + 1 }; useEffect(() => () => { life.current.serial += 1; }, []); const capture = () => { const mine = life.current.serial; const stamp = current?.opening; return () => { if (!stamp || life.current.serial !== mine || custody(organizationId)?.opening !== stamp) throw new SaveRetiredError(); }; }; const [history, setHistory] = useState<{ owner: string; rows: PracticeRemovalIntent[] }>(); const [results, setResults] = useState>({}); const [fault, setFault] = useState<{ owner: string; message: string }>(); const [busyOwner, setBusyOwner] = useState(); const active = useRef(false); useEffect(() => { active.current = false; setBusyOwner(undefined); if (!current) return; try { const store = authStorageArea(); if (!store) throw new Error('Browser storage is unavailable.'); setHistory({ owner: current.owner, rows: readPracticeRemovals(store, current.owner) }); setFault(undefined); } catch { setFault({ owner: current.owner, message: 'Saved requests could not be read. No new request can be sent.' }); } }, [current?.opening]); const run = async (intent: PracticeRemovalIntent, mode: 'send' | 'status' | 'resume') => { const admit = capture(); try { admit(); } catch { return; } if (!current || intent.owner !== current.owner || active.current) return; active.current = true; setBusyOwner(current.owner); try { const store = authStorageArea(); if (!store) throw new Error('Browser storage is unavailable.'); const rows = retainPracticeRemoval(store, intent); admit(); setHistory({ owner: current.owner, rows }); const result = await call(intent, mode, admit); admit(); const previous = results[intent.operationId]; if (previous?.kind === 'fenced' && (result.kind !== 'fenced' || result.originalActor?.pool !== previous.originalActor?.pool || result.originalActor?.sub !== previous.originalActor?.sub || result.admittedAt !== previous.admittedAt || result.fencedGeneration !== previous.fencedGeneration || result.revision! < previous.revision!)) throw new Error('The original result changed or could not be verified.'); setResults(previousResults => ({ ...previousResults, [intent.operationId]: result })); setFault(undefined); return result; } catch (failure) { try { admit(); } catch { return; } const detail = failure instanceof Error ? failure.message : 'The original answer is unknown.'; setFault({ owner: current.owner, message: `${detail} Check the saved request before continuing. Its result is unconfirmed.` }); } finally { try { admit(); active.current = false; setBusyOwner(undefined); } catch { /* A later opening owns its busy state. */ } } }; const rows = current && history?.owner === current.owner ? history.rows : []; const start = async (contractorId: string, name: string) => { const admit = capture(); try { admit(); } catch { return; } if (!current || fault?.owner === current.owner || active.current) return; const unresolved = rows.find(row => row.contractorId === contractorId && results[row.operationId]?.kind !== 'fenced'); if (unresolved) { setFault({ owner: current.owner, message: 'Check or resume the saved request for this contractor before starting another.' }); return; } return run({ schema: 1, operationId: crypto.randomUUID(), owner: current.owner, organizationId: organizationId!, contractorId, name, createdAt: new Date().toISOString() }, 'send'); }; return { rows, results, start, run, capture, ready: !!current, busy: !!current && busyOwner === current.owner, error: current && fault?.owner === current.owner ? fault.message : undefined, opening: current?.opening }; }