feat(frontend): useMergedTail merges base + tail with filter
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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[])];
|
||||
}
|
||||
Reference in New Issue
Block a user