feat(09-02): wire App.tsx granular event routing with batch handler

- Replace createEventBatcher with createDomainEventBatcher in App.tsx
- Add handleGranularBatch for task, run, approval, agent, and budget events
- Task/run/approval events trigger targeted IPC fetch and Zustand patch
- Run completion and budget changes trigger full refresh (cross-entity)
- Failed entity fetches fall back to removeTask or full refresh
- Unknown event types trigger full refresh (PERF-03 safety)
- Sidebar badges refresh after every successful granular batch
- Add 12 integration tests for handleGranularBatch in selective-ipc.test.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-27 19:49:55 -04:00
1 parent ffab9888ed
commit 41c5dccae4
2 files changed
+363 -9

No files matched your search

+106 -8
View File
@@ -37,7 +37,7 @@ import type {
import type { AppLocale } from "@shared/locale";
import { createTranslator, LocaleContext, TranslationContext } from "./i18n";
import { useAppStore } from "./state/store";
import { createEventBatcher } from "./lib/ipc-batcher";
import { createDomainEventBatcher, type GranularEventEntry } from "./lib/ipc-batcher";
import { CompanyOnboarding } from "./components/CompanyOnboarding";
import { Inbox, StandupView, PerformanceView } from "./components/inbox";
import { ConnectorEditor } from "./components/connectors";
@@ -236,17 +236,113 @@ export function App() {
refreshRef.current = refresh;
}, [refresh]);
// IPC event subscription with batcher for coalescing rapid domain-changed events
// IPC event subscription with enhanced batcher for granular + full-refresh coalescing
useEffect(() => {
void refreshRef.current();
const store = useAppStore;
const batcher = createEventBatcher(() => {
void refreshRef.current();
}, 50);
async function handleGranularBatch(events: GranularEventEntry[]) {
const companyId = store.getState().currentCompanyId;
if (!companyId) {
void refreshRef.current();
return;
}
let needsFullRefresh = false;
for (const entry of events) {
// run-updated:completed touches too many entity types -- full refresh
if (entry.type === "run-updated" && entry.action === "completed") {
needsFullRefresh = true;
break;
}
try {
switch (entry.type) {
case "task-updated": {
if (entry.action === "deleted") {
store.getState().removeTask(entry.entityId);
} else {
const result = await window.agentCompany.getTask({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchTask(result.data);
} else {
store.getState().removeTask(entry.entityId);
}
}
break;
}
case "run-updated": {
if (entry.action === "deleted") {
store.getState().removeRun(entry.entityId);
} else {
const result = await window.agentCompany.getRun({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchRun(result.data);
} else {
store.getState().removeRun(entry.entityId);
}
}
break;
}
case "approval-updated": {
if (entry.action === "deleted") {
store.getState().removeApproval(entry.entityId);
} else {
const result = await window.agentCompany.getApproval({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchApproval(result.data);
} else {
store.getState().removeApproval(entry.entityId);
}
}
break;
}
case "agent-updated": {
const result = await window.agentCompany.getAgent({ companyId, id: entry.entityId });
if (result.ok) {
store.getState().patchAgent(result.data);
} else {
needsFullRefresh = true;
}
break;
}
case "budget-changed": {
// Budget is a computed aggregate -- full refresh is needed
needsFullRefresh = true;
break;
}
default: {
// Unknown granular event type -- fall back (per D-05 / PERF-03)
needsFullRefresh = true;
break;
}
}
} catch {
// IPC error during entity fetch -- fall back to full refresh (per pitfall 1)
needsFullRefresh = true;
break;
}
if (needsFullRefresh) break;
}
if (needsFullRefresh) {
void refreshRef.current();
} else {
// Refresh sidebar badges after granular patches (per pitfall 3)
void store.getState().refreshBadges();
}
}
const batcher = createDomainEventBatcher(
() => { void refreshRef.current(); },
(events) => { void handleGranularBatch(events); },
50,
);
const unsubscribe = window.agentCompany.subscribe((event: DesktopEvent) => {
if (event.type === "run-log") {
// Pass through immediately -- no batching for streaming logs
const MAX_LOG_BYTES_PER_RUN = 512_000;
const MAX_TRACKED_RUNS = 50;
const existing = logBufferRef.current[event.runId] ?? "";
@@ -273,8 +369,10 @@ export function App() {
store.getState().navigateTo(event.section, event.entityId);
return;
}
// All data-mutating events: batch the snapshot reload
batcher.schedule();
// All data-mutating events go through the enhanced batcher
// domain-changed, task-updated, run-updated, approval-updated,
// agent-updated, budget-changed, and unknown types (trigger full refresh)
batcher.push(event);
});
return () => {
unsubscribe();
+257 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import type {
DesktopEvent,
TaskUpdatedEvent,
@@ -7,6 +7,7 @@ import type {
AgentUpdatedEvent,
BudgetChangedEvent,
} from "@shared/types";
import type { GranularEventEntry } from "@renderer/lib/ipc-batcher";
describe("selective IPC event types", () => {
it("TaskUpdatedEvent discriminated by type field", () => {
@@ -115,3 +116,258 @@ describe("selective IPC event types", () => {
expect(typeof exhaustiveCheck).toBe("function");
});
});
// Extracted granular batch handler logic for testability
// Mirrors the handleGranularBatch function in App.tsx
async function handleGranularBatch(
events: GranularEventEntry[],
deps: {
companyId: string | null;
getTask: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: any }>;
getRun: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: any }>;
getApproval: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: any }>;
getAgent: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: any }>;
patchTask: (task: any) => void;
removeTask: (id: string) => void;
patchRun: (run: any) => void;
removeRun: (id: string) => void;
patchApproval: (approval: any) => void;
removeApproval: (id: string) => void;
patchAgent: (agent: any) => void;
refreshBadges: () => Promise<void>;
fullRefresh: () => void;
},
) {
if (!deps.companyId) {
deps.fullRefresh();
return;
}
let needsFullRefresh = false;
for (const entry of events) {
if (entry.type === "run-updated" && entry.action === "completed") {
needsFullRefresh = true;
break;
}
try {
switch (entry.type) {
case "task-updated": {
if (entry.action === "deleted") {
deps.removeTask(entry.entityId);
} else {
const result = await deps.getTask({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchTask(result.data);
} else {
deps.removeTask(entry.entityId);
}
}
break;
}
case "run-updated": {
if (entry.action === "deleted") {
deps.removeRun(entry.entityId);
} else {
const result = await deps.getRun({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchRun(result.data);
} else {
deps.removeRun(entry.entityId);
}
}
break;
}
case "approval-updated": {
if (entry.action === "deleted") {
deps.removeApproval(entry.entityId);
} else {
const result = await deps.getApproval({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchApproval(result.data);
} else {
deps.removeApproval(entry.entityId);
}
}
break;
}
case "agent-updated": {
const result = await deps.getAgent({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchAgent(result.data);
} else {
needsFullRefresh = true;
}
break;
}
case "budget-changed": {
needsFullRefresh = true;
break;
}
default: {
needsFullRefresh = true;
break;
}
}
} catch {
needsFullRefresh = true;
break;
}
if (needsFullRefresh) break;
}
if (needsFullRefresh) {
deps.fullRefresh();
} else {
void deps.refreshBadges();
}
}
function createMockDeps() {
return {
companyId: "company-1" as string | null,
getTask: vi.fn().mockResolvedValue({ ok: true, data: { id: "t-1", title: "Test task" } }),
getRun: vi.fn().mockResolvedValue({ ok: true, data: { id: "r-1", status: "running" } }),
getApproval: vi.fn().mockResolvedValue({ ok: true, data: { id: "a-1", state: "pending" } }),
getAgent: vi.fn().mockResolvedValue({ ok: true, data: { id: "ag-1", name: "Agent" } }),
patchTask: vi.fn(),
removeTask: vi.fn(),
patchRun: vi.fn(),
removeRun: vi.fn(),
patchApproval: vi.fn(),
removeApproval: vi.fn(),
patchAgent: vi.fn(),
refreshBadges: vi.fn().mockResolvedValue(undefined),
fullRefresh: vi.fn(),
};
}
describe("handleGranularBatch", () => {
it("task-updated event calls getTask and patchTask", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.getTask).toHaveBeenCalledWith({ companyId: "company-1", id: "t-1" });
expect(deps.patchTask).toHaveBeenCalledWith({ id: "t-1", title: "Test task" });
expect(deps.fullRefresh).not.toHaveBeenCalled();
expect(deps.refreshBadges).toHaveBeenCalled();
});
it("deleted task calls removeTask without fetching", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "deleted" }],
deps,
);
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
expect(deps.getTask).not.toHaveBeenCalled();
expect(deps.fullRefresh).not.toHaveBeenCalled();
});
it("run-updated:completed triggers full refresh", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "run-updated", entityId: "r-1", action: "completed" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.getRun).not.toHaveBeenCalled();
expect(deps.refreshBadges).not.toHaveBeenCalled();
});
it("failed getTask fetch falls back to removeTask", async () => {
const deps = createMockDeps();
deps.getTask.mockResolvedValue({ ok: false, error: { code: "NOT_FOUND", message: "nope" } });
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
expect(deps.patchTask).not.toHaveBeenCalled();
});
it("calls refreshBadges after successful granular processing", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "created" }],
deps,
);
expect(deps.refreshBadges).toHaveBeenCalled();
expect(deps.fullRefresh).not.toHaveBeenCalled();
});
it("approval-updated event calls getApproval and patchApproval", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "approval-updated", entityId: "a-1", action: "decided" }],
deps,
);
expect(deps.getApproval).toHaveBeenCalledWith({ companyId: "company-1", id: "a-1" });
expect(deps.patchApproval).toHaveBeenCalledWith({ id: "a-1", state: "pending" });
});
it("agent-updated event calls getAgent and patchAgent", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "agent-updated", entityId: "ag-1", action: "paused" }],
deps,
);
expect(deps.getAgent).toHaveBeenCalledWith({ companyId: "company-1", id: "ag-1" });
expect(deps.patchAgent).toHaveBeenCalledWith({ id: "ag-1", name: "Agent" });
});
it("budget-changed triggers full refresh", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "budget-changed", entityId: "c-1", action: "changed" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.refreshBadges).not.toHaveBeenCalled();
});
it("no companyId triggers full refresh", async () => {
const deps = createMockDeps();
deps.companyId = null;
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.getTask).not.toHaveBeenCalled();
});
it("IPC error falls back to full refresh", async () => {
const deps = createMockDeps();
deps.getTask.mockRejectedValue(new Error("IPC failed"));
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
});
it("unknown event type triggers full refresh", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "some-unknown-event", entityId: "x-1", action: "created" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
});
it("run-updated:status_changed fetches and patches run", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "run-updated", entityId: "r-1", action: "status_changed" }],
deps,
);
expect(deps.getRun).toHaveBeenCalledWith({ companyId: "company-1", id: "r-1" });
expect(deps.patchRun).toHaveBeenCalledWith({ id: "r-1", status: "running" });
expect(deps.fullRefresh).not.toHaveBeenCalled();
});
});