ADR-002: Event-Driven Architecture with TypedEventBus
Table of Contentsβ
- Status
- Context
- Decision
- Architecture Flow
- Event Categories
- Consequences
- Quality Assurance
- Implementation Requirements
- Compliance
- Related ADRs
Statusβ
Accepted - Core communication mechanism using TypedEventBus (main) plus a dedicated renderer event bridge over IPC.
Contextβ
The application needed a way to decouple components and enable reactive communication between:
- Backend services and frontend UI
- Different services within the backend
- Multiple UI components reacting to state changes
- Cross-cutting concerns like logging, monitoring, and error handling
Traditional direct method calls would create tight coupling, make the system difficult to extend and test, and provide no mechanism for monitoring or debugging inter-component communication.
Decisionβ
We will use an Event-Driven Architecture based on a custom TypedEventBus with the following characteristics:
Event propagation overviewβ
Delivery guarantees (what we provide)β
- In-process (Electron main): best-effort, at-most-once delivery to each listener.
- Listener errors are caught and logged, but a throwing listener can prevent later listeners from running (Node.js
EventEmittersemantics). - Middleware errors abort emission (the event is not emitted).
- Listener errors are caught and logged, but a throwing listener can prevent later listeners from running (Node.js
- Cross-process (main -> renderer): best-effort broadcast via
BrowserWindow.webContents.send.- No acknowledgement / retry protocol exists at the IPC level.
- If a window is destroyed, not yet created, or temporarily unresponsive, the event may be dropped.
This architecture is optimized for desktop UI responsiveness and local consistency (SQLite as the source of truth), not for durable distributed messaging guarantees.
1. Enhanced Type Safetyβ
interface UptimeEvents extends Record<string, unknown> {
"site:added": {
site: Site;
source: SiteAddedSource;
timestamp: number;
};
"monitor:status-changed": StatusUpdate;
"database:transaction-completed": {
duration: number;
operation: string;
success: boolean;
timestamp: number;
};
"system:error": {
error: Error;
context: string;
severity: "low" | "medium" | "high" | "critical";
recovery?: string;
timestamp: number;
};
}
// Usage with compile-time type checking
await eventBus.emitTyped("site:added", {
site: newSite,
source: "user",
timestamp: Date.now(),
});
2. Advanced Metadata and Correlationβ
- Unique correlation IDs for request tracing across system boundaries
- Automatic timestamps for event ordering and debugging
- Bus identification for multi-bus architectures
- Event metadata enrichment for comprehensive monitoring
3. Consistent Event Namingβ
- Domain-based naming:
domain:action(e.g.,site:added,monitor:status-changed) - Hierarchical structure: Major category followed by specific action
- Past tense verbs for completed actions
4. Production-Ready Middleware Supportβ
// Logging middleware with correlation tracking
eventBus.use(async (eventName, data, next) => {
const correlationId = data._meta?.correlationId;
logger.debug(`[Event] ${eventName} [${correlationId}]`, data);
await next();
logger.debug(`[Event] ${eventName} completed [${correlationId}]`);
});
// Rate limiting middleware
eventBus.use(
createRateLimitMiddleware({
maxEventsPerSecond: 100,
burstLimit: 10,
onRateLimit: ({ event, reason }) => {
logger.warn(`Rate limit exceeded for ${event} (${reason})`);
},
})
);
// Validation middleware
eventBus.use(
createValidationMiddleware({
"monitor:status-changed": (data) => validateMonitorStatusData(data),
"site:added": (data) => validateSiteData(data),
})
);
5. Memory-Safe IPC Event Forwardingβ
Backend events are forwarded from the Electron main process to renderer windows via the renderer event bridge (RendererEventBridge) and exposed to the renderer via the preload events domain API (electron/preload/domains/eventsApi.ts). Renderer code subscribes through src/services/EventsService.ts and receives explicit cleanup functions.
// Backend emits event with automatic IPC forwarding
await this.eventBus.emitTyped("monitor:status-changed", eventData);
// Frontend receives with automatic cleanup functions
import { EventsService } from "@app/services/EventsService";
const registerMonitorUpdates = async () =>
await EventsService.onMonitorStatusChanged((data) => {
monitorStore.applyStatusUpdate(data);
});
// Later in component cleanup
useEffect(() => {
let cleanup: (() => void) | undefined;
void registerMonitorUpdates().then((unsubscribe) => {
cleanup = unsubscribe;
});
return () => {
cleanup?.();
};
}, []);
> **Important:** IPC event delivery is best-effort. Consumers must tolerate dropped events and be idempotent when duplicates occur.
6. Advanced Memory Managementβ
- Max listeners: Configurable limit (default: 50) prevents memory leaks
- Automatic cleanup: All event listeners provide cleanup functions
- Middleware limits: Configurable middleware chain size (default: 20)
- Event validation: Type-safe event structures prevent runtime errors
7. Idempotency and Ordering Requirementsβ
Because delivery is at-most-once in-process (and best-effort across IPC), consumers must be safe under drops, duplicates, and reordering.
- Prefer source-of-truth reads (SQLite) over reconstructing state solely from event streams.
- For renderer state synchronization (
state:sync), use the existingrevisionfield to ignore stale payloads and enforce monotonic ordering. - For UI updates driven by events, handlers must be idempotent (applying the same logical update twice yields the same result) and should fall back to refresh/read pathways when expected intermediate events are missing.
8. Validation Boundary and βDead-Letterβ Handlingβ
This app does not use a traditional message-broker DLQ.
Instead, we rely on boundary validation + observable drops:
- Preload payload guard failures are dropped and logged with diagnostics (see
reportPreloadGuardFailureinelectron/preload/domains/eventsApi.ts). - Renderer broadcast failures (
webContents.sendthrowing) are caught and logged byelectron/services/events/RendererEventBridge.ts.
Recommended future enhancement: maintain a bounded in-memory dead-letter ring buffer in the main process (optionally persisted into diagnostics bundles) with:
- channel name
- correlation id / metadata
- payload preview (redacted)
- failure reason
- timestamp
9. Schema Evolution and Versioningβ
- Prefer additive changes (adding optional fields).
- For breaking changes, introduce a new event channel or add an explicit
versiondiscriminant and support both versions during migration. - For large payload channels (notably
state:sync), enforce payload size budgets and compaction (implemented inRendererEventBridge.sendStateSyncEvent).
Architecture Flowβ
Event Categoriesβ
1. Site Eventsβ
Public Events:
site:added- When a site is successfully addedsite:updated- When site properties are modifiedsite:removed- When a site is deletedsites:state-synchronized- Main-process state sync event (forwarded to the renderer asstate-sync-event)
Historical note: the former site:cache-updated and site:cache-miss
topics were retired in favor of the internal namespace. Cache telemetry now
flows exclusively through those internal channels and the orchestrator
converts them into the canonical cache:invalidated broadcast when the
renderer must react.
Internal Events:
internal:site:added- Internal site creation eventsinternal:site:updated- Internal site modification eventsinternal:site:removed- Internal site deletion eventsinternal:site:cache-updated- Internal cache managementinternal:site:cache-miss- Internal cache lookup miss telemetryinternal:site:start-monitoring-requested- Internal monitoring controlinternal:site:stop-monitoring-requested- Internal monitoring controlinternal:site:restart-monitoring-requested- Internal monitoring controlinternal:site:restart-monitoring-response- Internal monitoring responsesinternal:site:is-monitoring-active-requested- Internal status queriesinternal:site:is-monitoring-active-response- Internal status responses
Emission flow:
SiteManageremits onlyinternal:site:*topics. TheUptimeOrchestratorenriches those payloads and rebroadcasts any renderer-facingsite:*events, translating cache telemetry into the canonicalcache:invalidatedpipeline when appropriate.
2. Monitor Eventsβ
Public Events:
monitor:added- When a monitor is createdmonitor:removed- When a monitor is deletedmonitor:status-changed- When monitor status changesmonitor:up- When monitor detects service is onlinemonitor:down- When monitor detects service is offlinemonitor:check-completed- When a health check finishes
Internal Events:
internal:monitor:started- Internal monitor activationinternal:monitor:stopped- Internal monitor deactivationinternal:monitor:all-started- When all monitors are activatedinternal:monitor:all-stopped- When all monitors are deactivatedinternal:monitor:manual-check-completed- Manual check results
Emission flow:
MonitorManagerraisesinternal:monitor:*events for lifecycle transitions and continues to emit high-frequency telemetry such asmonitor:status-changeddirectly. TheUptimeOrchestratortranslates the lifecycle events intomonitoring:*plus the canonicalcache:invalidatedbroadcasts (using{ type: "all" }for global transitions).
internal:monitor:site-setup-completed- Site monitor setup completion
3. Database Eventsβ
database:transaction-completed- When database transactions finishdatabase:error- When database operations faildatabase:success- When database operations succeeddatabase:retry- When database operations are retrieddatabase:backup-created- When database backups are created
Internal Database Events:
internal:database:initialized- Database initialization completioninternal:database:data-exported- Data export completioninternal:database:data-imported- Data import completioninternal:database:backup-downloaded- Backup download completioninternal:database:history-limit-updated- History retention changesinternal:database:sites-refreshed- Site data refreshinternal:database:get-sites-from-cache-requested- Cache requestsinternal:database:get-sites-from-cache-response- Cache responsesinternal:database:update-sites-cache-requested- Cache update requests
4. System Eventsβ
monitoring:started- When monitoring system startsmonitoring:stopped- When monitoring system stopssystem:startup- Application startupsystem:shutdown- Application shutdownsystem:error- System-level errors
5. Performance and Configuration Eventsβ
performance:metric- Performance measurementsperformance:warning- Performance threshold alertsconfig:changed- Configuration changescache:invalidated- Cache invalidation events
Event Summary Table (Public Events)β
The following table summarizes the primary public events by domain. Internal
events (internal:*) are documented inline in the sections above and are
omitted here for brevity.
| Domain | Event name | Description |
|---|---|---|
| Site | site:added | When a site is successfully added. |
| Site | site:updated | When site properties are modified. |
| Site | site:removed | When a site is deleted. |
| Site / State Sync | sites:state-synchronized | Main-process state sync event (forwarded to renderer as state-sync-event) |
| Monitor | monitor:added | When a monitor is created. |
| Monitor | monitor:removed | When a monitor is deleted. |
| Monitor | monitor:status-changed | When monitor status changes. |
| Monitor | monitor:up | When a monitor detects service is online. |
| Monitor | monitor:down | When a monitor detects service is offline. |
| Monitor | monitor:check-completed | When a health check finishes. |
| Database | database:transaction-completed | When database transactions finish. |
| Database | database:error | When database operations fail. |
| Database | database:success | When database operations succeed. |
| Database | database:retry | When database operations are retried. |
| Database | database:backup-created | When database backups are created. |
| System / Monitoring | monitoring:started | When the monitoring system starts. |
| System / Monitoring | monitoring:stopped | When the monitoring system stops. |
| System | system:startup | Application startup. |
| System | system:shutdown | Application shutdown. |
| System | system:error | System-level errors. |
| Performance | performance:metric | Performance measurements. |
| Performance | performance:warning | Performance threshold alerts. |
| Configuration | config:changed | Configuration changes. |
| Cache | cache:invalidated | Cache invalidation events. |
Event catalog note: The table above intentionally focuses on the externally relevant public events. The authoritative event payload types live in
shared/types/events.tsand the Electron event bus definitions. Long term, a generated event catalog (similar todocs/Architecture/generated/IPC_CHANNEL_INVENTORY.mdfor IPC channels) may be introduced to keep this table and the code-level contracts perfectly synchronized.
Consequencesβ
Positiveβ
- Decoupled architecture - Components don't need direct references
- Enhanced type safety - Compile-time checking prevents runtime errors
- Extensibility - Easy to add new event listeners without modifying emitters
- Advanced debugging - Correlation IDs and metadata enable comprehensive request tracing
- Superior testability - Easy to mock and verify event emissions
- Memory safety - Automatic cleanup and configurable limits prevent leaks
- Production monitoring - Middleware enables comprehensive observability
- Cross-cutting concerns - Logging, validation, and rate limiting handled declaratively
Negativeβ
- Initial complexity - Indirect flow can be harder to follow initially
- Minimal performance overhead - Event processing adds negligible latency
- Learning curve - Developers need to understand event-driven patterns
- Debugging complexity - Async event flows require correlation tracking
- Delivery semantics - At-most-once / best-effort delivery requires consumers to tolerate drops/duplicates/reordering
- No durable DLQ - Failures are observable and diagnosable, but not automatically retried
Quality Assuranceβ
Memory Managementβ
- Automatic cleanup: All event listeners return cleanup functions
- Configurable limits: Max listeners and middleware prevent resource exhaustion
- Leak prevention: Proper cleanup in component unmount lifecycle
Error Handlingβ
- Middleware semantics: Middleware errors abort emission. Middleware must be conservative and avoid throwing on non-fatal conditions.
- Event validation: Type-safe structures prevent runtime errors
- Listener robustness: Listener errors are caught/logged, but listeners should still catch locally to avoid preventing later listeners from running.
Performanceβ
- Rate limiting: Middleware prevents event flooding
- Efficient forwarding: IPC events use optimized serialization
- Minimal overhead: Event processing designed for production use
Implementation Requirementsβ
Event Emissionβ
// In services/managers
await this.eventBus.emitTyped("domain:action", {
// Event-specific data
timestamp: Date.now(),
// ... other properties
});
Event Listeningβ
// Type-safe event listening
eventBus.onTyped("domain:action", (data) => {
// data is properly typed
// _meta is automatically available
});
IPC Integrationβ
import { RENDERER_EVENT_CHANNELS } from "@shared/ipc/rendererEvents";
import type { RendererEventPayload } from "@shared/ipc/rendererEvents";
import type { RendererEventBridge } from "@electron/services/events/RendererEventBridge";
declare const rendererEventBridge: RendererEventBridge;
declare const payload: RendererEventPayload<
typeof RENDERER_EVENT_CHANNELS.TEST_EVENT
>;
// Best-effort broadcast from Electron main to all renderer windows
rendererEventBridge.sendToRenderers(
RENDERER_EVENT_CHANNELS.TEST_EVENT,
payload
);
Complianceβ
All communication follows this pattern:
- Service layer emits domain events
- UI components listen to events via IPC
- Database operations emit lifecycle events
- Error handling emits failure events
Current Implementation Audit (2025-11-04)β
- Inspected
electron/events/TypedEventBus.tsto confirm middleware, correlation metadata, and type-safe emit/on helpers remain the single event backbone. - Verified renderer broadcasts are centralized in
electron/services/events/RendererEventBridge.ts(window iteration, error handling, and size budgeting forstate:sync). - Checked
electron/preload/domains/eventsApi.tsandsrc/services/events/EventsService.tsto ensure renderer subscriptions still traverse the preload bridge with validated cleanup handlers.