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:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user