refactor: complete v2 refactoring plan (Phases 1-5)

Phase 1 — Quick Wins: centralize formatMoney/formatCents, extract
findUniqueOrThrow helper (19 routers), shared Prisma select constants,
useInvalidatePlanningViews hook, status badge consolidation, composite
DB indexes.

Phase 2 — Timeline Split: extract TimelineContext, TimelineResourcePanel,
TimelineProjectPanel; split 28-dep useMemo into 3 focused memos.
TimelineView.tsx reduced from 1,903 to 538 lines.

Phase 3 — Query Performance: server-side filtering for getEntriesView,
remove availability from timeline resource select, SSE event debouncing
(50ms batch window).

Phase 4 — Estimate Workspace: extract 7 tab components and 3 editor
components. EstimateWorkspaceClient 1,298→306 lines,
EstimateWorkspaceDraftEditor 1,205→581 lines.

Phase 5 — Package Cleanup: split commit-dispo-import-batch (1,112→573
lines), extract shared pagination helper with 11 tests.

All tests pass: 209 API, 254 engine, 67 application.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-14 23:03:42 +01:00
parent 4dabb9d4ce
commit ad0855902b
65 changed files with 7108 additions and 4740 deletions
+81 -3
View File
@@ -12,6 +12,70 @@ type Subscriber = (event: SseEvent) => void;
// Module-level subscriber registry (shared between EventBus and publishLocal)
const subscribers = new Set<Subscriber>();
// ---------------------------------------------------------------------------
// Debounce buffer: aggregates rapid events of the same type within a 50ms
// window and delivers a single event per type to subscribers.
// ---------------------------------------------------------------------------
const DEBOUNCE_MS = 50;
interface BufferEntry {
payloads: Record<string, unknown>[];
timer: ReturnType<typeof setTimeout>;
firstTimestamp: string;
}
const debounceBuffer = new Map<SseEventType, BufferEntry>();
/** Flush a single event type from the buffer and deliver to subscribers. */
function flushEventType(type: SseEventType): void {
const entry = debounceBuffer.get(type);
if (!entry) return;
debounceBuffer.delete(type);
const event: SseEvent =
entry.payloads.length === 1
? { type, payload: entry.payloads[0]!, timestamp: entry.firstTimestamp }
: {
type,
payload: { _batch: entry.payloads },
timestamp: entry.firstTimestamp,
};
for (const fn of subscribers) {
fn(event);
}
}
/** Flush all pending debounce timers immediately (for cleanup / tests). */
export function flushPendingEvents(): void {
for (const [type, entry] of debounceBuffer) {
clearTimeout(entry.timer);
debounceBuffer.delete(type);
const event: SseEvent =
entry.payloads.length === 1
? { type, payload: entry.payloads[0]!, timestamp: entry.firstTimestamp }
: {
type,
payload: { _batch: entry.payloads },
timestamp: entry.firstTimestamp,
};
for (const fn of subscribers) {
fn(event);
}
}
}
/** Cancel all pending debounce timers without delivering (for shutdown). */
export function cancelPendingEvents(): void {
for (const [, entry] of debounceBuffer) {
clearTimeout(entry.timer);
}
debounceBuffer.clear();
}
// Redis connection — use env var REDIS_URL or fallback to default dev URL
const REDIS_URL = process.env["REDIS_URL"] ?? "redis://localhost:6380";
const CHANNEL = "planarchy:sse";
@@ -81,10 +145,24 @@ class EventBus {
}
}
// Local delivery: deliver to subscribers connected to THIS instance (called from Redis subscriber)
// Local delivery with debounce: buffer events of the same type within a 50ms
// window and then deliver a single (possibly aggregated) event to subscribers.
function publishLocal(event: SseEvent): void {
for (const fn of subscribers) {
fn(event);
const existing = debounceBuffer.get(event.type);
if (existing) {
// Another event of the same type is already buffered — append payload and
// reset the timer so the window starts fresh from the latest arrival.
existing.payloads.push(event.payload);
clearTimeout(existing.timer);
existing.timer = setTimeout(() => flushEventType(event.type), DEBOUNCE_MS);
} else {
// First event of this type — start a new debounce window.
debounceBuffer.set(event.type, {
payloads: [event.payload],
timer: setTimeout(() => flushEventType(event.type), DEBOUNCE_MS),
firstTimestamp: event.timestamp,
});
}
}