notes

Log | Files | Refs | README

typescript_channels.md (3948B)


      1 # TypeScript Channels Pattern
      2 
      3 ## What It Does
      4 
      5 `createChannel` creates a pub-sub mechanism that links a websocket listener to a
      6 component, allowing updates triggered by network events to propagate to the UI
      7 without tight coupling between the two.
      8 
      9 ## The Implementation
     10 
     11 ```typescript
     12 /**
     13  * Creates a channel/link between a websocket listener and a component.
     14  * This allows you to keep listeners separate from components but update
     15  * a component's rune whenever a listener event fires.
     16  *
     17  * This is good for simply linkages between a websocket event and a page
     18  * update or rerender.
     19  */
     20 export type Unsubscribe = () => void;
     21 
     22 export type Channel<T> = {
     23   subscribe(fn: (v: T) => void): Unsubscribe;
     24   next(value: T): void;
     25 };
     26 
     27 export function createChannel<T>(): Channel<T> {
     28   const subs = new Set<(v: T) => void>();
     29   return {
     30     subscribe(fn: (v: T) => void): Unsubscribe {
     31       subs.add(fn);
     32       return () => subs.delete(fn);
     33     },
     34     next(value: T): void {
     35       for (const fn of subs) fn(value);
     36     },
     37   };
     38 }
     39 ```
     40 
     41 ## How It's Used
     42 
     43 ### 1. Create a channel with your data type
     44 
     45 ```typescript
     46 // Create a channel for tracking websocket messages
     47 const messageChannel = createChannel<Message>();
     48 
     49 // Create a channel for connection status
     50 const connectionChannel = createChannel<ConnectionStatus>();
     51 ```
     52 
     53 ### 2. Subscribe in a React component
     54 
     55 ```typescript
     56 function MessageDisplay() {
     57   const [messages, setMessages] = useState<Message[]>([]);
     58 
     59   const unsubscribe = messageChannel.subscribe((msg) => {
     60     setMessages((prev) => [...prev, msg]);
     61   });
     62 
     63   // Cleanup on unmount - Note the inner function!
     64   //    useEffect accepts an effect function. If that function returns another function,
     65   //    React treats the inner function as the cleanup for that effect.
     66   //      • Mount: React runs the outer function (the effect body).
     67   //      • Unmount: React runs the returned function (the cleanup).
     68   useEffect(() => () => unsubscribe(), []);
     69   // Equivalent, more explicit: the *effect* returns a *cleanup* function; React runs it on unmount
     70   // (and before re-running the effect if deps change — here `[]` means mount once, cleanup on unmount).
     71   // useEffect(() => {
     72   //     return () => {
     73   //         unsubscribe();
     74   //     };
     75   // }, []);
     76 
     77   return <div>{messages.map((m) => <div key={m.id}>{m.text}</div>)}</div>;
     78 }
     79 ```
     80 
     81 ### 3. Emit from your websocket handler
     82 
     83 ```typescript
     84 // WebSocket connection established
     85 ws.onopen = () => {
     86   connectionChannel.next({ type: "connected" });
     87 };
     88 
     89 // New message received
     90 ws.onmessage = (event) => {
     91   const msg = JSON.parse(event.data);
     92   messageChannel.next(msg);
     93 };
     94 
     95 // Connection closed
     96 ws.onclose = () => {
     97   connectionChannel.next({ type: "disconnected" });
     98 };
     99 ```
    100 
    101 ### 4. Benefits
    102 
    103 **Decoupling**
    104 
    105 - WebSocket logic knows nothing about React components
    106 - Components don't need to import or know about the websocket
    107 - Easy to swap implementations (e.g., change backend without touching UI)
    108 
    109 **Testability**
    110 
    111 - Components test with a mock channel instead of real websocket
    112 - Handlers test without starting a server
    113 
    114 **Reusability**
    115 
    116 - Same channel used by multiple components
    117 - Same handler used with different channels
    118 
    119 ---
    120 
    121 ## Pattern: Channel Listener Pattern
    122 
    123 ```
    124 ┌─────────────┐       Channel Subscribe       ┌─────────────┐
    125 │  WebSocket  │ ────────────────────────────► │    UI       │
    126 │  Handler    │                               │   Component │
    127 └─────────────┘                               └─────────────┘
    128         │                                           │
    129         ▼                                           ▼
    130    .next(message)                           subscribe(fn)
    131 ```
    132 
    133 The pattern decouples event sources from event consumers using a pub-sub
    134 channel.