refactor: decouple core-events registry by implementing consumer-side TypeScript module augmentation and reorganizing showcase demos
This commit is contained in:
@@ -6,11 +6,13 @@
|
||||
|
||||
`@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.
|
||||
|
||||
**This package is a pure tool.** It ships zero application-specific events. Each consuming app (`apps/web`, `apps/landing`, etc.) registers its own events autonomously using TypeScript Declaration Merging — the same Inversion of Control pattern used by `@repo/core-api`'s `createHttpClient` factory.
|
||||
|
||||
By routing communication through a centralized event bus, we achieve:
|
||||
- **App Autonomy**: The core defines the bus. The app defines the contract. No circular knowledge.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -18,23 +20,27 @@ By routing communication through a centralized event bus, we achieve:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Publishers
|
||||
subgraph "@repo/core-events (Pure Tool)"
|
||||
R["AppEventRegistry<br/>(empty interface)"]
|
||||
T["AppEvents = mapped type"]
|
||||
E((Event Bus<br/>mitt))
|
||||
H[useAppEvent / usePublishEvent]
|
||||
R --> T --> E
|
||||
E --> H
|
||||
end
|
||||
|
||||
subgraph "apps/web (App Autonomy)"
|
||||
D["events.d.ts<br/>declare module augmentation"]
|
||||
A[Cashier UI]
|
||||
B[Profile Settings]
|
||||
C[WebSocket Client]
|
||||
end
|
||||
|
||||
subgraph Core
|
||||
E((Event Bus<br/>mitt))
|
||||
R[[AppEvents<br/>Registry]] -.-> E
|
||||
end
|
||||
|
||||
subgraph Subscribers
|
||||
X[Electron IPC Bridge]
|
||||
Y[IndexedDB Sync]
|
||||
Z[Stock Grid Row]
|
||||
end
|
||||
|
||||
D -. "merges into" .-> R
|
||||
|
||||
A -- "DEVICE:PRINT_RECEIPT" --> E
|
||||
B -- "AUTH:PROFILE_UPDATED" --> E
|
||||
C -- "WS:STOCK_UPDATE" --> E
|
||||
@@ -45,38 +51,82 @@ graph TD
|
||||
|
||||
style E fill:#4263eb,color:#fff,stroke:#fff
|
||||
style R fill:#2b8a3e,color:#fff,stroke:#fff
|
||||
style D fill:#e67700,color:#fff,stroke:#fff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Defining Events
|
||||
## Defining Events (Module Augmentation)
|
||||
|
||||
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.
|
||||
> [!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.
|
||||
|
||||
To add a new event, simply extend the `AppEvents` type:
|
||||
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
|
||||
// packages/core-events/src/events.registry.ts
|
||||
// apps/web/src/types/events.d.ts
|
||||
|
||||
export interface CheckoutPayload {
|
||||
orderId: string;
|
||||
total: number;
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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: [...] });
|
||||
}
|
||||
|
||||
export type AppEvents = {
|
||||
// Existing events...
|
||||
'DEVICE:PRINT_RECEIPT': PrintReceiptPayload;
|
||||
|
||||
// Your new event:
|
||||
'STORE:CHECKOUT_COMPLETED': CheckoutPayload;
|
||||
};
|
||||
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 Examples
|
||||
|
||||
Here are three real-world architectural patterns powered by the Event Bus.
|
||||
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)
|
||||
|
||||
@@ -97,7 +147,8 @@ export function CashierUI() {
|
||||
receiptId: 'RCP-123',
|
||||
items: [...],
|
||||
total: 45.00,
|
||||
cashierName: 'Firman'
|
||||
cashierName: 'Firman',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -198,6 +249,8 @@ export function ProfileSettingsUI() {
|
||||
id: 'user-1',
|
||||
name: 'Firman',
|
||||
email: 'firman@eigen.co.id',
|
||||
avatar: 'https://example.com/avatar.png',
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user