feat(09-01): build enhanced event batcher with granular event coalescing

- Add createDomainEventBatcher with entity type+ID dedup within 50ms window
- Domain-changed overrides all granular events in same batch
- More than 10 unique entities triggers full refresh fallback
- Delete action preserved during coalescing (last-write-wins otherwise)
- Unknown event types trigger full refresh as safety fallback
- Legacy createEventBatcher preserved for backward compatibility
- Add 12 comprehensive tests for all coalescing behaviors

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:35:05 -04:00
1 parent 920a764856
commit f172a3d9b3
2 files changed
+275 -1

No files matched your search

+95
View File
@@ -1,3 +1,7 @@
import type { DesktopEvent } from "@shared/types";
// --- Legacy simple batcher -- kept for backward compatibility ---
type BatchCallback = () => void;
export function createEventBatcher(callback: BatchCallback, windowMs = 50) {
@@ -28,3 +32,94 @@ export function createEventBatcher(callback: BatchCallback, windowMs = 50) {
return { schedule, flush, dispose };
}
// --- Enhanced batcher for granular events ---
export interface GranularEventEntry {
type: string;
entityId: string;
action: string;
}
const MAX_GRANULAR_EVENTS = 10;
export function entityKeyFromEvent(event: DesktopEvent): GranularEventEntry | null {
switch (event.type) {
case "task-updated":
return { type: "task-updated", entityId: event.taskId, action: event.action };
case "run-updated":
return { type: "run-updated", entityId: event.runId, action: event.action };
case "approval-updated":
return { type: "approval-updated", entityId: event.approvalId, action: event.action };
case "agent-updated":
return { type: "agent-updated", entityId: event.agentId, action: event.action };
case "budget-changed":
return { type: "budget-changed", entityId: event.companyId, action: "changed" };
default:
return null;
}
}
export function createDomainEventBatcher(
onFullRefresh: () => void,
onGranularBatch: (events: GranularEventEntry[]) => void,
windowMs = 50,
maxGranular = MAX_GRANULAR_EVENTS,
) {
let timer: ReturnType<typeof setTimeout> | null = null;
let hasDomainChanged = false;
const pending = new Map<string, GranularEventEntry>();
function push(event: DesktopEvent) {
if (event.type === "domain-changed") {
hasDomainChanged = true;
} else {
const entry = entityKeyFromEvent(event);
if (entry) {
const key = `${entry.type}:${entry.entityId}`;
const existing = pending.get(key);
// If any event in window has action "deleted", preserve that action
if (existing && existing.action === "deleted" && entry.action !== "deleted") {
// Keep the delete -- it takes priority
} else {
pending.set(key, entry);
}
} else {
// Unknown event type -- treat as domain-changed for safety (per D-05)
hasDomainChanged = true;
}
}
if (timer === null) {
timer = setTimeout(flushInternal, windowMs);
}
}
function flushInternal() {
timer = null;
if (hasDomainChanged || pending.size > maxGranular) {
onFullRefresh();
} else if (pending.size > 0) {
onGranularBatch([...pending.values()]);
}
hasDomainChanged = false;
pending.clear();
}
function flush() {
if (timer !== null) {
clearTimeout(timer);
flushInternal();
}
}
function dispose() {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
hasDomainChanged = false;
pending.clear();
}
return { push, flush, dispose };
}