import { useLayoutEffect, useMemo, useRef } from 'react'; import { SaveRetiredError, type SaveOperation, type SaveTransport, type useSaveScope } from './saveOperation'; export interface RowSave { /** The scope still stands and the row is still shown as it was when the save began. */ belongs(): boolean; step: SaveOperation['step']; transport: SaveTransport; } /** * Saves that each decide one row of a list, owned to that row: once the row changes, leaves or is replaced, its save * is retired for good, so the same row shown again later is not answered by it. A request is admitted only while the * list is confirmed. `onRowRetired` is told the key, so the screen can let go of what it held for that row. */ export const useRowOwnedSaves = ( scope: ReturnType, liveKeys: ReadonlySet, decisionsOpen: boolean, onRowRetired?: (key: string) => void, ) => { const live = useRef(liveKeys); live.current = liveKeys; const open = useRef(decisionsOpen); open.current = decisionsOpen; const told = useRef(onRowRetired); told.current = onRowRetired; const inFlight = useRef(new Map()); const keyList = [...liveKeys].join('\n'); useLayoutEffect(() => { inFlight.current.forEach((key, operation) => { if (live.current.has(key)) return; inFlight.current.delete(operation); operation.retire(); told.current?.(key); }); }, [keyList]); return useMemo(() => ({ /** A save for the row `key`, or nothing while the list is unconfirmed or no longer shows that row. */ start: (key: string): RowSave | undefined => { if (!open.current || !live.current.has(key)) return undefined; const operation = scope.start(); inFlight.current.set(operation, key); operation.transport.signal.addEventListener('abort', () => inFlight.current.delete(operation)); const belongs = () => operation.belongs() && inFlight.current.get(operation) === key && live.current.has(key); return { belongs, step: (work, write) => operation.step(work, write), transport: { admit: () => { operation.admit(); if (!belongs() || !open.current) throw new SaveRetiredError(); }, signal: operation.transport.signal, }, }; }, }), [scope]); };