singleton_channels.md (2634B)
1 # Singleton Channel 2 3 ``` 4 Singleton Channel (neutral in-app bus) 5 ----------------------------------------- 6 7 Publishers Subscribers 8 ---------- ----------- 9 +--------+ +--------+ 10 | View A |----+ +-->| View C | 11 +--------+ | | +--------+ 12 v | 13 +---------------------------+ 14 | AppChannel<Event> | (singleton) 15 +---------------------------+ 16 ^ | 17 +--------+ | | +--------+ 18 | Store |----+ +-->| Logger | 19 +--------+ +--------+ 20 ``` 21 22 A `Channel<T>` is a globally shared, typed event bus. Any component can publish 23 a value of type T into it, and any component that has subscribed will receive 24 that value — FIFO (first subscriber registered, first notified). It's simpler 25 than a full store because it carries no persistent state; it just broadcasts a 26 moment-in-time event. 27 28 ```ts 29 class Channel<T> { 30 private subscribers: Array<(data: T) => void> = []; 31 32 subscribe(cb: (data: T) => void): () => void { 33 this.subscribers.push(cb); 34 return () => { 35 this.subscribers = this.subscribers.filter((s) => s !== cb); 36 }; 37 } 38 39 publish(data: T): void { 40 for (const cb of this.subscribers) { 41 cb(data); 42 } 43 } 44 } 45 ``` 46 47 Because it's generic, TypeScript enforces that every publisher and subscriber 48 agrees on the shape of `T` at compile time — you can't accidentally publish a 49 `string` on a `Channel<UserEvent>`. 50 51 ## Defining Global Singleton Channels 52 53 You declare each channel once and export it as a module-level singleton: 54 55 ```ts 56 // channels.ts 57 interface UserLoggedIn { 58 userId: string; 59 role: "admin" | "user"; 60 } 61 interface CartUpdated { 62 itemCount: number; 63 total: number; 64 } 65 66 export const userLoginChannel = new Channel<UserLoggedIn>(); 67 export const cartChannel = new Channel<CartUpdated>(); 68 ``` 69 70 Any file can import and use these without passing them through props or 71 constructors. 72 73 ## Usage Across Components 74 75 ```ts 76 // ComponentA.ts — subscriber 77 import { cartChannel } from './channels'; 78 79 const unsub = cartChannel.subscribe(({ itemCount, total }) => { 80 document.getElementById('cart-count')!.textContent = String(itemCount); 81 }); 82 83 // call unsub() when component is destroyed to avoid memory leaks 84 85 // ComponentB.ts — publisher 86 import { cartChannel } from './channels'; 87 88 function addToCart(item: Item) { 89 // ... update cart logic 90 cartChannel.publish({ itemCount: cart.length, total: cart.reduce(...) }); 91 } 92 ```