78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// 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<T extends { id: string }>(
|
|
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<string>();
|
|
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[])];
|
|
}
|