refactor: migrate docs-dev from storybook to vitepress config and update devcontainer configuration
This commit is contained in:
@@ -1,366 +0,0 @@
|
||||
[β Back to Root](../../README.md)
|
||||
|
||||
# Event Bus (`@repo/core-events`)
|
||||
|
||||
The Global Pub/Sub & Hardware Integration Blueprint.
|
||||
|
||||
> This module provides a strictly-typed, global event bus for the monorepo ecosystem. It decouples cross-component communication and manages real-time hardware signals (such as printers and POS peripherals), ensuring a reactive and memory-safe architecture across all applications.
|
||||
|
||||
---
|
||||
|
||||
## π§ System 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 custom React hooks.
|
||||
|
||||
**This package is a pure tool.** It ships zero application-specific events. Each consuming app (`apps/web`, `apps/desktop`, etc.) registers its own events autonomously using **TypeScript Declaration Merging** β the exact same Inversion of Control (IoC) pattern utilized by our `@repo/core-api` factory and `@repo/core-storage` engine.
|
||||
|
||||
### Architectural Topology
|
||||
|
||||
### 1. Conceptual Topology: The Pub/Sub Data Flow
|
||||
This diagram illustrates the high-level concept of our decoupled architecture, demonstrating how application-specific types merge into the core bus.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
%% βββ Styling Definitions (Dark-Mode Friendly Enterprise Palette) βββ
|
||||
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||
classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||
classDef busEntity fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||
|
||||
%% βββ Nodes βββ
|
||||
TYPES[[App-Specific Event Types]]
|
||||
PUB([Publisher Component])
|
||||
BUS{Global Event Bus 'mitt'}
|
||||
SUB([Subscriber Component])
|
||||
|
||||
%% βββ Flow βββ
|
||||
TYPES -.->|Declaration Merging| BUS
|
||||
PUB ===>|emit 'event', payload| BUS
|
||||
BUS ===>|useAppEvent 'event'| SUB
|
||||
|
||||
%% βββ Apply Styles βββ
|
||||
class TYPES,PUB,SUB appEntity;
|
||||
class BUS busEntity;
|
||||
```
|
||||
|
||||
### 2. System Architecture: Core Engine vs. App Autonomy
|
||||
This detailed diagram shows the exact boundaries between the @repo/core-events engine and the consuming application, highlighting real-world publishers (e.g., Cashier UI) and subscribers.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% βββ Styling Definitions (Dark-Mode Friendly Enterprise Palette) βββ
|
||||
classDef appComponent fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||
classDef injection fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||
classDef registry fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||
classDef coreBus fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||
|
||||
%% βββ Subgraphs βββ
|
||||
subgraph Core ["@repo/core-events (Pure Tool)"]
|
||||
R[AppEventRegistry Empty Interface]
|
||||
T[AppEvents Mapped Type]
|
||||
E((Global Event Bus mitt))
|
||||
H[Hooks: useAppEvent / usePublishEvent]
|
||||
end
|
||||
|
||||
subgraph Apps ["apps/web (App Autonomy)"]
|
||||
D[[events.d.ts Declaration Merging]]
|
||||
|
||||
%% Publishers
|
||||
A([Cashier UI])
|
||||
B([Profile Settings])
|
||||
C([WebSocket Client])
|
||||
|
||||
%% Subscribers
|
||||
X([Electron IPC Bridge])
|
||||
Y([IndexedDB Sync])
|
||||
Z([Stock Grid Row])
|
||||
end
|
||||
|
||||
%% βββ Flow & Relationships βββ
|
||||
D -.->|Augments| R
|
||||
R ---> T ---> E
|
||||
E ---> H
|
||||
|
||||
%% Emitting Events
|
||||
A ===>|DEVICE:PRINT_RECEIPT| E
|
||||
B ===>|AUTH:PROFILE_UPDATED| E
|
||||
C ===>|WS:STOCK_UPDATE| E
|
||||
|
||||
%% Subscribing to Events
|
||||
E -.->|Triggers| X
|
||||
E -.->|Triggers| Y
|
||||
E -.->|Triggers| Z
|
||||
|
||||
%% βββ Apply Styles βββ
|
||||
class A,B,C,X,Y,Z appComponent;
|
||||
class D injection;
|
||||
class R,T registry;
|
||||
class E,H coreBus;
|
||||
|
||||
%% βββ Subgraph Backgrounds (Transparent for Native GitHub Support) βββ
|
||||
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||
style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||
```
|
||||
|
||||
### Core Value Proposition
|
||||
|
||||
By routing communication through this centralized event bus, we achieve:
|
||||
|
||||
* **App Autonomy**: The core defines the engine. The app defines the contract. There is zero circular dependency.
|
||||
* **Zero Coupling**: Publishers and subscribers do not need to import, reference, or know about each other's existence.
|
||||
* **Extreme Performance**: Components can subscribe to high-frequency data streams (like WebSockets or hardware signals) 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, proactively preventing the most common source of memory leaks in Single Page Architectures (SPAs).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Defining Events (Module Augmentation)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Do NOT add application events to `packages/core-events/src/events.registry.ts`.**
|
||||
> The core registry is intentionally empty. Each app owns its own event contract.
|
||||
|
||||
The core exports an open `AppEventRegistry` interface. Apps extend it using TypeScript's `declare module` syntax β the same pattern used for `@types/*` across the JS ecosystem.
|
||||
|
||||
### Step 1: Create an augmentation file in your app
|
||||
|
||||
> [!WARNING]
|
||||
> The `import type {}` line is **mandatory**. Without it, TypeScript treats `declare module` as an ambient module declaration that **replaces** the module's types instead of merging into them. All actual exports (`useAppEvent`, `publish`, etc.) would become invisible.
|
||||
|
||||
```typescript
|
||||
// apps/web/src/types/events.d.ts
|
||||
|
||||
// This import makes this file a module augmentation (merge)
|
||||
// instead of an ambient declaration (replace).
|
||||
import type {} from '@repo/core-events';
|
||||
|
||||
declare module '@repo/core-events' {
|
||||
// Define your payload shapes
|
||||
interface OrderPayload {
|
||||
orderId: string;
|
||||
total: number;
|
||||
items: Array<{ sku: string; qty: number }>;
|
||||
}
|
||||
|
||||
// Extend the registry
|
||||
interface AppEventRegistry {
|
||||
'STORE:ORDER_PLACED': OrderPayload;
|
||||
'STORE:ORDER_CANCELLED': { orderId: string; reason: string };
|
||||
'UI:SIDEBAR_TOGGLED': { collapsed: boolean };
|
||||
|
||||
// Explicit payloads for the examples below:
|
||||
'DEVICE:PRINT_RECEIPT': { receiptId: string; items: any[]; total: number; cashierName: string; timestamp: number };
|
||||
'WS:STOCK_UPDATE': { id: string; price: number };
|
||||
'AUTH:PROFILE_UPDATED': { id: string; name: string; email: string; avatar: string; updatedAt: number };
|
||||
'SYSTEM:ERROR': { source: string; error: Error };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Use it β autocomplete works immediately
|
||||
|
||||
```tsx
|
||||
import { usePublishEvent, useAppEvent } from '@repo/core-events';
|
||||
|
||||
function CheckoutButton() {
|
||||
const publish = usePublishEvent();
|
||||
// β
'STORE:ORDER_PLACED' autocompletes.
|
||||
// β
Payload shape is enforced by TypeScript.
|
||||
publish('STORE:ORDER_PLACED', { orderId: '123', total: 99, items: [] });
|
||||
}
|
||||
|
||||
function OrderTracker() {
|
||||
// β
payload is fully typed as OrderPayload
|
||||
useAppEvent('STORE:ORDER_PLACED', (payload) => {
|
||||
console.log(payload.orderId); // string
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Why this pattern?
|
||||
|
||||
| Concern | Old (Hardcoded) | New (Module Augmentation) |
|
||||
|---|---|---|
|
||||
| Core knows about app events? | β Yes β violates IoC | β
No β core is a pure tool |
|
||||
| Adding events requires editing core? | β Yes | β
No β edit your app's `.d.ts` only |
|
||||
| Multiple apps share the same registry? | β Collision risk | β
Each app has its own `.d.ts` |
|
||||
| Type safety / autocomplete | β
Works | β
Works identically |
|
||||
|
||||
---
|
||||
|
||||
## Usage Outside React (Vanilla TS)
|
||||
|
||||
For utility files, API interceptors, Web Workers, or vanilla functions where React hooks cannot be used, import the raw `eventBus` instance directly.
|
||||
|
||||
```ts
|
||||
import { eventBus } from '@repo/core-events';
|
||||
|
||||
// Publishing
|
||||
eventBus.publish('STORE:ORDER_CANCELLED', { orderId: '123', reason: 'Out of stock' });
|
||||
|
||||
// Subscribing
|
||||
const handler = (payload) => {
|
||||
console.log('Order cancelled:', payload.orderId);
|
||||
};
|
||||
|
||||
eventBus.subscribe('STORE:ORDER_CANCELLED', handler);
|
||||
|
||||
// CRITICAL: Always unsubscribe when done to prevent memory leaks in non-React contexts!
|
||||
eventBus.unsubscribe('STORE:ORDER_CANCELLED', handler);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Here are three real-world architectural patterns powered by the Event Bus. All event types below are registered in `apps/web/src/types/events.d.ts`, **not** in the core package.
|
||||
|
||||
### 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',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
return <Button onClick={handlePrint}>Print Receipt</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
**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 (
|
||||
<table>
|
||||
<tbody>
|
||||
{stockIds.map((id) => (
|
||||
<StockRow key={id} stockId={id} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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 (
|
||||
<tr>
|
||||
<td>{stockId}</td>
|
||||
<td>{data?.price}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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, properly escalating errors if the storage fails.
|
||||
|
||||
**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',
|
||||
avatar: '[https://example.com/avatar.png](https://example.com/avatar.png)',
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
return <Button onClick={handleSave}>Save Profile</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
**Subscriber (Storage Sync Listener)**:
|
||||
```tsx
|
||||
import { useAppEvent, usePublishEvent } from '@repo/core-events';
|
||||
import { secureIndexedDB } from '@repo/core-storage';
|
||||
|
||||
export function StorageSyncListener() {
|
||||
const publish = usePublishEvent();
|
||||
|
||||
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((error) => {
|
||||
// Escalate to global error handler instead of swallowing it
|
||||
publish('SYSTEM:ERROR', { source: 'StorageSyncListener', error });
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user