fix(sp25): typecheck fixups

Three small follow-ups after the live-tail triplet wired up:

  * evictOldest is now generic over (K extends string|number, V) so
    the addAck / addTa1Ack call sites don't need a type assertion.
  * Acks.test.tsx no longer spreads importOriginal() — TypeScript
    was choking on the inferred 'unknown' from the mock factory.
    Use the same ApiError / mock shape that Claims.test.tsx uses.
  * tail-stream.test.ts guards the optional gen.return() with
    so strict mode TS2722 stops complaining about possibly-undefined.
This commit is contained in:
Nora
2026-07-02 09:23:44 -06:00
parent 146cb6d17d
commit 1363d36046
3 changed files with 16 additions and 12 deletions
+2 -2
View File
@@ -106,7 +106,7 @@ describe("streamTail", () => {
// before we close the stream. // before we close the stream.
await gen.next(); await gen.next();
driver.close(); driver.close();
await gen.return(undefined); await gen.return?.(undefined);
expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledTimes(1);
const [calledUrl] = fetchMock.mock.calls[0] as [string]; const [calledUrl] = fetchMock.mock.calls[0] as [string];
expect(calledUrl).toBe("/api/acks/stream"); expect(calledUrl).toBe("/api/acks/stream");
@@ -121,7 +121,7 @@ describe("streamTail", () => {
const gen = streamTail("ta1_acks"); const gen = streamTail("ta1_acks");
await gen.next(); await gen.next();
driver.close(); driver.close();
await gen.return(undefined); await gen.return?.(undefined);
expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledTimes(1);
const [calledUrl] = fetchMock.mock.calls[0] as [string]; const [calledUrl] = fetchMock.mock.calls[0] as [string];
expect(calledUrl).toBe("/api/ta1-acks/stream"); expect(calledUrl).toBe("/api/ta1-acks/stream");
+7 -3
View File
@@ -9,15 +9,19 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Acks } from "./Acks"; import { Acks } from "./Acks";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
vi.mock("@/lib/api", async (importOriginal) => { vi.mock("@/lib/api", () => {
const actual = await importOriginal(); class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
return { return {
...actual,
api: { api: {
isConfigured: true, isConfigured: true,
listAcks: vi.fn(), listAcks: vi.fn(),
getAck: vi.fn(), getAck: vi.fn(),
}, },
ApiError,
}; };
}); });
+7 -7
View File
@@ -79,11 +79,11 @@ interface TailStore {
*/ */
type RemitListItem = Remittance; type RemitListItem = Remittance;
function evictOldest<T>( function evictOldest<K extends string | number, V>(
order: Array<string | number>, order: K[],
dict: Record<string, T>, dict: Record<string, V>,
batch: number, batch: number,
): { order: Array<string | number>; dict: Record<string, T> } { ): { order: K[]; dict: Record<string, V> } {
if (order.length <= TAIL_CAP) return { order, dict }; if (order.length <= TAIL_CAP) return { order, dict };
// Drain down to the cap; never evict more than `batch` per call so a // Drain down to the cap; never evict more than `batch` per call so a
// 5-item overflow evicts 5, but a 1 000-item overflow evicts 100 in // 5-item overflow evicts 5, but a 1 000-item overflow evicts 100 in
@@ -93,7 +93,7 @@ function evictOldest<T>(
const nextOrder = order.slice(toDrop); const nextOrder = order.slice(toDrop);
// Object.assign is consistently faster than `{ ...dict }` in V8 for // Object.assign is consistently faster than `{ ...dict }` in V8 for
// large dicts; we follow up with `delete` for each evicted id. // large dicts; we follow up with `delete` for each evicted id.
const nextDict: Record<string, T> = Object.assign({}, dict); const nextDict: Record<string, V> = Object.assign({}, dict);
for (const id of dropped) delete nextDict[String(id)]; for (const id of dropped) delete nextDict[String(id)];
return { order: nextOrder, dict: nextDict }; return { order: nextOrder, dict: nextDict };
} }
@@ -172,7 +172,7 @@ export const useTailStore = create<TailStore>((set) => ({
const nextOrder = s.ackOrder.concat(a.id); const nextOrder = s.ackOrder.concat(a.id);
if (nextOrder.length > TAIL_CAP) { if (nextOrder.length > TAIL_CAP) {
const { order, dict } = evictOldest(nextOrder, nextAcks, EVICT_BATCH); const { order, dict } = evictOldest(nextOrder, nextAcks, EVICT_BATCH);
return { acks: dict, ackOrder: order as number[] }; return { acks: dict, ackOrder: order };
} }
return { acks: nextAcks, ackOrder: nextOrder }; return { acks: nextAcks, ackOrder: nextOrder };
}), }),
@@ -188,7 +188,7 @@ export const useTailStore = create<TailStore>((set) => ({
const nextOrder = s.ta1AckOrder.concat(a.id); const nextOrder = s.ta1AckOrder.concat(a.id);
if (nextOrder.length > TAIL_CAP) { if (nextOrder.length > TAIL_CAP) {
const { order, dict } = evictOldest(nextOrder, nextTa1Acks, EVICT_BATCH); const { order, dict } = evictOldest(nextOrder, nextTa1Acks, EVICT_BATCH);
return { ta1Acks: dict, ta1AckOrder: order as number[] }; return { ta1Acks: dict, ta1AckOrder: order };
} }
return { ta1Acks: nextTa1Acks, ta1AckOrder: nextOrder }; return { ta1Acks: nextTa1Acks, ta1AckOrder: nextOrder };
}), }),