# @repo/core-events [← Back to Root](../../README.md) ## Overview `@repo/core-events` is the **decoupled Nervous System** of the ERP. It provides a highly performant, strictly typed Event Bus powered by `mitt` and React hooks. By routing communication through a centralized event bus, we achieve: - **Zero Coupling**: Publishers and subscribers don't need to import or know about each other. - **Extreme Performance**: Components can subscribe to high-frequency data streams (like WebSockets) and update their own local state *without* triggering massive React tree re-renders. - **Memory Safety**: The provided `useAppEvent` hook automatically handles subscription cleanup on component unmount, preventing the most common source of memory leaks in SPA architectures. - **Strict Contracts**: The `AppEvents` registry enforces payload shapes at compile-time, ensuring publishers and subscribers always agree on the data contract. --- ## Architecture ```mermaid graph TD subgraph Publishers A[Cashier UI] B[Profile Settings] C[WebSocket Client] end subgraph Core E((Event Bus
mitt)) R[[AppEvents
Registry]] -.-> E end subgraph Subscribers X[Electron IPC Bridge] Y[IndexedDB Sync] Z[Stock Grid Row] end A -- "DEVICE:PRINT_RECEIPT" --> E B -- "AUTH:PROFILE_UPDATED" --> E C -- "WS:STOCK_UPDATE" --> E E -.-> X E -.-> Y E -.-> Z style E fill:#4263eb,color:#fff,stroke:#fff style R fill:#2b8a3e,color:#fff,stroke:#fff ``` --- ## Defining Events Every event in the system MUST be registered in `src/events.registry.ts`. This provides a single source of truth and full autocomplete across the codebase. To add a new event, simply extend the `AppEvents` type: ```typescript // packages/core-events/src/events.registry.ts export interface CheckoutPayload { orderId: string; total: number; } export type AppEvents = { // Existing events... 'DEVICE:PRINT_RECEIPT': PrintReceiptPayload; // Your new event: 'STORE:CHECKOUT_COMPLETED': CheckoutPayload; }; ``` --- ## Usage Examples Here are three real-world architectural patterns powered by the Event Bus. ### Example 1: Hardware Abstraction (Cross-Platform) **Problem**: The web app needs to print receipts. If running in a browser, it should use `window.print()`. If running in the Electron wrapper, it must use the secure IPC bridge (`window.electronAPI.print()`). We don't want the UI components cluttered with platform-detection logic. **Solution**: The UI publishes a blind event. A headless listener handles the platform routing. **Publisher (Cashier UI)**: ```tsx import { usePublishEvent } from '@repo/core-events'; export function CashierUI() { const publish = usePublishEvent(); const handlePrint = () => { // Fire and forget. Zero knowledge of how printing actually happens. publish('DEVICE:PRINT_RECEIPT', { receiptId: 'RCP-123', items: [...], total: 45.00, cashierName: 'Firman' }); }; return ; } ``` **Subscriber (Headless Listener)**: ```tsx import { useAppEvent } from '@repo/core-events'; export function PrinterListener() { useAppEvent('DEVICE:PRINT_RECEIPT', (payload) => { const isElectron = typeof window !== 'undefined' && !!window.electronAPI; if (isElectron) { // Route via secure Electron IPC bridge window.electronAPI.print({ silent: true }); } else { // Fallback to standard browser print dialog window.print(); } }); return null; // Renders nothing } ``` --- ### Example 2: Extreme Performance (High-Frequency Data) **Problem**: A massive data grid (1,000+ rows) receives 50 WebSocket updates per second. If the parent grid holds the state and passes it down via props, React will attempt to re-render all 1,000 rows 50 times a second, crushing the browser. **Solution**: The parent grid renders empty rows. Each row subscribes to the event bus and filters updates so it only re-renders when its specific data changes. **Parent Grid (Never re-renders)**: ```tsx export function LiveStockGrid() { // Generates 1000 IDs once. No stock data is stored here! const stockIds = generateStockIds(1000); return ( {stockIds.map((id) => ( ))}
); } ``` **Child Row (Targeted Updates)**: ```tsx import { memo, useState } from 'react'; import { useAppEvent } from '@repo/core-events'; export const StockRow = memo(function StockRow({ stockId }) { const [data, setData] = useState(null); useAppEvent('WS:STOCK_UPDATE', (payload) => { // CRITICAL: Filter out events for other rows. // 999 out of 1000 rows will exit here instantly without causing a re-render. if (payload.id !== stockId) return; // Only the targeted row updates its local state setData(payload); }); return ( {stockId} {data?.price} ); }); ``` --- ### Example 3: Background Sync (Auth to IndexedDB) **Problem**: When a user updates their profile, we need to persist it to the secure local IndexedDB. We don't want to tightly couple our UI forms to the `@repo/core-storage` package. **Solution**: The UI form announces the profile update. A dedicated storage listener persists it in the background. **Publisher (Profile UI)**: ```tsx import { usePublishEvent } from '@repo/core-events'; export function ProfileSettingsUI() { const publish = usePublishEvent(); const handleSave = () => { publish('AUTH:PROFILE_UPDATED', { id: 'user-1', name: 'Firman', email: 'firman@eigen.co.id', }); }; return ; } ``` **Subscriber (Storage Sync Listener)**: ```tsx import { useAppEvent } from '@repo/core-events'; import { secureIndexedDB } from '@repo/core-storage'; export function StorageSyncListener() { useAppEvent('AUTH:PROFILE_UPDATED', (payload) => { // Automatically encrypted at rest because 'user_profile' // is defined in ENCRYPTED_KEYS in @repo/core-storage secureIndexedDB.setItem('user_profile', payload).catch(console.error); }); return null; } ```