refactor: decouple core-events registry by implementing consumer-side TypeScript module augmentation and reorganizing showcase demos
This commit is contained in:
@@ -2,7 +2,40 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { eventBus, publish, subscribe } from './event-bus';
|
||||
import { useAppEvent, usePublishEvent } from './hooks';
|
||||
import type { StockUpdatePayload, ProfileUpdatedPayload } from './events.registry';
|
||||
|
||||
// ─── Test-Local Event Augmentation ──────────────────────────────
|
||||
//
|
||||
// In a real app, these would live in a `.d.ts` file inside the app's
|
||||
// `src/types/` folder using `declare module '@repo/core-events'`.
|
||||
//
|
||||
// For the core package's own test suite, we augment the registry
|
||||
// directly here so the tests have concrete event types to work with
|
||||
// without polluting the core's shipped types.
|
||||
//
|
||||
declare module './events.registry' {
|
||||
interface AppEventRegistry {
|
||||
'TEST:STOCK_UPDATE': StockUpdatePayload;
|
||||
'TEST:PROFILE_UPDATED': ProfileUpdatedPayload;
|
||||
'TEST:INITIALIZED': undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Test Payload Types ─────────────────────────────────────────
|
||||
|
||||
interface StockUpdatePayload {
|
||||
id: string;
|
||||
price: number;
|
||||
change: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
interface ProfileUpdatedPayload {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
@@ -42,8 +75,8 @@ describe('Event Bus (Core)', () => {
|
||||
it('publishes and subscribes to a typed event', () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
subscribe('WS:STOCK_UPDATE', handler);
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
subscribe('TEST:STOCK_UPDATE', handler);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith(mockStockUpdate);
|
||||
@@ -53,9 +86,9 @@ describe('Event Bus (Core)', () => {
|
||||
const handler1 = vi.fn();
|
||||
const handler2 = vi.fn();
|
||||
|
||||
subscribe('AUTH:PROFILE_UPDATED', handler1);
|
||||
subscribe('AUTH:PROFILE_UPDATED', handler2);
|
||||
publish('AUTH:PROFILE_UPDATED', mockProfile);
|
||||
subscribe('TEST:PROFILE_UPDATED', handler1);
|
||||
subscribe('TEST:PROFILE_UPDATED', handler2);
|
||||
publish('TEST:PROFILE_UPDATED', mockProfile);
|
||||
|
||||
expect(handler1).toHaveBeenCalledTimes(1);
|
||||
expect(handler2).toHaveBeenCalledTimes(1);
|
||||
@@ -65,10 +98,10 @@ describe('Event Bus (Core)', () => {
|
||||
const stockHandler = vi.fn();
|
||||
const profileHandler = vi.fn();
|
||||
|
||||
subscribe('WS:STOCK_UPDATE', stockHandler);
|
||||
subscribe('AUTH:PROFILE_UPDATED', profileHandler);
|
||||
subscribe('TEST:STOCK_UPDATE', stockHandler);
|
||||
subscribe('TEST:PROFILE_UPDATED', profileHandler);
|
||||
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
|
||||
expect(stockHandler).toHaveBeenCalledTimes(1);
|
||||
expect(profileHandler).not.toHaveBeenCalled();
|
||||
@@ -77,15 +110,15 @@ describe('Event Bus (Core)', () => {
|
||||
it('unsubscribes correctly via returned function', () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
const unsub = subscribe('WS:STOCK_UPDATE', handler);
|
||||
const unsub = subscribe('TEST:STOCK_UPDATE', handler);
|
||||
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Unsubscribe
|
||||
unsub();
|
||||
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
// Should still be 1, not 2
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -93,8 +126,8 @@ describe('Event Bus (Core)', () => {
|
||||
it('handles events with undefined payloads', () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
subscribe('APP:INITIALIZED', handler);
|
||||
publish('APP:INITIALIZED', undefined);
|
||||
subscribe('TEST:INITIALIZED', handler);
|
||||
publish('TEST:INITIALIZED', undefined);
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith(undefined);
|
||||
@@ -102,10 +135,10 @@ describe('Event Bus (Core)', () => {
|
||||
|
||||
it('handles rapid-fire events without loss', () => {
|
||||
const handler = vi.fn();
|
||||
subscribe('WS:STOCK_UPDATE', handler);
|
||||
subscribe('TEST:STOCK_UPDATE', handler);
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
publish('WS:STOCK_UPDATE', { ...mockStockUpdate, id: `STOCK-${i}` });
|
||||
publish('TEST:STOCK_UPDATE', { ...mockStockUpdate, id: `STOCK-${i}` });
|
||||
}
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1000);
|
||||
@@ -126,10 +159,10 @@ describe('useAppEvent (React Hook)', () => {
|
||||
it('subscribes on mount and receives events', () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
renderHook(() => useAppEvent('WS:STOCK_UPDATE', handler));
|
||||
renderHook(() => useAppEvent('TEST:STOCK_UPDATE', handler));
|
||||
|
||||
act(() => {
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
@@ -139,11 +172,11 @@ describe('useAppEvent (React Hook)', () => {
|
||||
it('unsubscribes on unmount — MEMORY LEAK PREVENTION', () => {
|
||||
const handler = vi.fn();
|
||||
|
||||
const { unmount } = renderHook(() => useAppEvent('WS:STOCK_UPDATE', handler));
|
||||
const { unmount } = renderHook(() => useAppEvent('TEST:STOCK_UPDATE', handler));
|
||||
|
||||
// Event should be received while mounted
|
||||
act(() => {
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
});
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -152,7 +185,7 @@ describe('useAppEvent (React Hook)', () => {
|
||||
|
||||
// Event should NOT be received after unmount
|
||||
act(() => {
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
});
|
||||
|
||||
// Still 1, proving the handler was properly cleaned up
|
||||
@@ -164,20 +197,20 @@ describe('useAppEvent (React Hook)', () => {
|
||||
|
||||
// Mount and unmount 100 times
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const { unmount } = renderHook(() => useAppEvent('WS:STOCK_UPDATE', handler));
|
||||
const { unmount } = renderHook(() => useAppEvent('TEST:STOCK_UPDATE', handler));
|
||||
unmount();
|
||||
}
|
||||
|
||||
// After 100 cycles, emit one event
|
||||
act(() => {
|
||||
publish('WS:STOCK_UPDATE', mockStockUpdate);
|
||||
publish('TEST:STOCK_UPDATE', mockStockUpdate);
|
||||
});
|
||||
|
||||
// Should be 0 — all handlers should have been cleaned up
|
||||
expect(handler).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Verify the handler map is empty for this event type
|
||||
const handlers = eventBus.all.get('WS:STOCK_UPDATE');
|
||||
const handlers = eventBus.all.get('TEST:STOCK_UPDATE');
|
||||
expect(!handlers || handlers.length === 0).toBe(true);
|
||||
});
|
||||
|
||||
@@ -186,7 +219,7 @@ describe('useAppEvent (React Hook)', () => {
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ value }: { value: string }) =>
|
||||
useAppEvent('AUTH:PROFILE_UPDATED', () => {
|
||||
useAppEvent('TEST:PROFILE_UPDATED', () => {
|
||||
capturedValue = value;
|
||||
}),
|
||||
{ initialProps: { value: 'initial' } },
|
||||
@@ -196,7 +229,7 @@ describe('useAppEvent (React Hook)', () => {
|
||||
rerender({ value: 'updated' });
|
||||
|
||||
act(() => {
|
||||
publish('AUTH:PROFILE_UPDATED', mockProfile);
|
||||
publish('TEST:PROFILE_UPDATED', mockProfile);
|
||||
});
|
||||
|
||||
// Should capture the LATEST value, not the stale 'initial'
|
||||
@@ -210,7 +243,7 @@ describe('useAppEvent (React Hook)', () => {
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ handler }: { handler: () => void }) =>
|
||||
useAppEvent('APP:INITIALIZED', handler),
|
||||
useAppEvent('TEST:INITIALIZED', handler),
|
||||
{ initialProps: { handler: vi.fn() } },
|
||||
);
|
||||
|
||||
@@ -240,12 +273,12 @@ describe('usePublishEvent (React Hook)', () => {
|
||||
|
||||
it('returns a working publish function', () => {
|
||||
const handler = vi.fn();
|
||||
subscribe('AUTH:PROFILE_UPDATED', handler);
|
||||
subscribe('TEST:PROFILE_UPDATED', handler);
|
||||
|
||||
const { result } = renderHook(() => usePublishEvent());
|
||||
|
||||
act(() => {
|
||||
result.current('AUTH:PROFILE_UPDATED', mockProfile);
|
||||
result.current('TEST:PROFILE_UPDATED', mockProfile);
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
Reference in New Issue
Block a user