// --------------------------------------------------------------------------- // Live-tail merge hook (sub-project 5, Phase 5 Task 20). // // Reads the matching slice of `useTailStore` and returns a single array: // `baseItems` first (in the order the caller supplied them), then any new // tail items in arrival order, with duplicates of base ids removed. // // Design notes: // - The dedup runs against `baseItems` (not the other way around) because // the base list is the authoritative snapshot from the page's JSON // fetch — the tail slice is an opportunistic delta, so the base wins // when an id appears in both. // - The optional `filterFn` is applied to the tail slice AFTER dedup so a // page-specific filter (e.g. "only show me submitted claims") doesn't // accidentally include items that were already filtered out of base. // - We don't `useMemo` here: `baseItems` is a new array reference on // every render of the caller, so memo deps would invalidate anyway. // The merge is cheap (one Set build + two filters) and zustand's // selector ensures we only re-render when the slice actually changes. // --------------------------------------------------------------------------- import { useTailStore } from "@/store/tail-store"; import type { TailResource } from "@/lib/tail-stream"; export function useMergedTail( resource: TailResource, baseItems: T[], filterFn?: (item: T) => boolean, ): T[] { // Select the slice that matches the resource. We return a fresh array // each time so the consumer gets a stable iteration order even when // zustand hands us a new container (Record or array) reference. const tailSlice = useTailStore((s) => { switch (resource) { case "claims": { // The store keys claims by id in a Record for O(1) updates, but // also keeps an `claimOrder` array so we can iterate in arrival // order. Filter out any holes (defensive — shouldn't happen but // type-narrows the result to Claim[]). const out: unknown[] = []; for (const id of s.claimOrder) { const v = s.claims[id]; if (v !== undefined) out.push(v); } return out; } case "remittances": { const out: unknown[] = []; for (const id of s.remitOrder) { const v = s.remittances[id]; if (v !== undefined) out.push(v); } return out; } case "activity": // Activity is already an array — the store appends in arrival // order, so iteration order is exactly what we want. return s.activity as unknown[]; } }); const baseIds = new Set(); for (const b of baseItems) baseIds.add(b.id); const tailAfterDedup: unknown[] = []; for (const t of tailSlice) { const id = (t as { id: string }).id; if (baseIds.has(id)) continue; tailAfterDedup.push(t); } const tailAfterFilter = filterFn ? tailAfterDedup.filter((t) => filterFn(t as T)) : tailAfterDedup; return [...baseItems, ...(tailAfterFilter as T[])]; }