mod.ts
1 /** 2 * This module contains Class to create a tree of events. Since we don't have a dom tree we creaete a synthetic tree that allows subscribing to events 3 * along a path in the tree. When an Event is triggered on a path, it will "bubble" up the tree until it reaches the root. All events bubble up the tree. 4 * We cannot use native event emmitters because we cannot overwrite the `event.target` property and we need to know the listener count to manage subscriptions. 5 * 6 * ## Subscriptions 7 * We can utilize the event tree to manage subscriptions to the relay(s). When listeners are added and removed an event is emitted on the root node's `meta` event emmiter. 8 * This event contains an array of `SubscriptionUpdate` objects that describe the changes to the subscriptions. 9 * `SubscriptionUpdate`s are aware of the path in the tree where the subscription was added or removed and will not emit events for children nodes if the parent node has a subscription. 10 * @module 11 */ 12 13 import { type codec, get, set } from "@massmarket/utils"; 14 15 /** A callback function that gets called when an event is emitted */ 16 export type EventListener<T = unknown> = ( 17 event: T, 18 ) => void; 19 20 export type Step = codec.CodecKey; 21 export type Path = Readonly<Step[]>; 22 23 /** 24 * Retrieves a property type in a series of nested objects. 25 * Read more: https://stackoverflow.com/a/61648690. 26 */ 27 export type DeepIndex<T, KS extends Path, Fail = undefined> = KS extends 28 [infer F, ...infer R] 29 ? R extends Path 30 ? F extends keyof Exclude<T, undefined> 31 ? DeepIndex<Exclude<T, undefined>[F], R, Fail> 32 : T extends Map<infer X, infer I> ? F extends X ? DeepIndex<I, R, Fail> 33 : Fail // F is not in T, time to check map // never? 34 : Fail 35 : Fail 36 : T; // end 37 38 /** Used to express changes to which part of the tree is needs a subscription to */ 39 export interface SubscriptionUpdate { 40 /** Whether to add a subscription or remove a subscription */ 41 subscribe: boolean; 42 /** the path on the object tree to subscribe/unsubscribe */ 43 path: Path; 44 } 45 46 /** 47 * A very simple class that emits events 48 * @template T the type of the event 49 */ 50 export class EventEmmiter<T> { 51 /** A map of listeners */ 52 listeners = new Set<EventListener<T>>(); 53 54 /** registers a callback to listen to events */ 55 on(listener: EventListener<T>) { 56 this.listeners.add(listener); 57 } 58 59 /** removes a callback */ 60 off(listener: EventListener<T>) { 61 this.listeners.delete(listener); 62 } 63 64 once(listener: EventListener<T>) { 65 const onceListener = (event: T) => { 66 this.off(onceListener); 67 listener(event); 68 }; 69 this.on(onceListener); 70 } 71 72 /** emits an event */ 73 emit(event: T) { 74 this.listeners.forEach((listener) => { 75 listener(event); 76 }); 77 } 78 } 79 80 class Node<T = unknown> { 81 edges: Map<codec.CodecKey, Node<T>> = new Map(); 82 readonly emmiter = new EventEmmiter<T>(); 83 constructor(public value: T | undefined = undefined) {} 84 emit(rootValue: T) { 85 if (rootValue !== this.value) { 86 // update the vale 87 this.value = rootValue; 88 this.emmiter.emit(rootValue); 89 for (const [name, child] of this.edges) { 90 const childRootValue = get(rootValue, name); 91 child.emit( 92 childRootValue, 93 ); 94 } 95 } 96 } 97 } 98 99 /** 100 * This class dispatch's an event and bubbles that event up the tree 101 * @template T the type of the event 102 */ 103 export default class EventTree<T> { 104 /** The event emmiter for this node */ 105 readonly root; 106 /** The event emmiter for the meta events, this only works on the root node of the tree */ 107 // readonly meta = new EventEmmiter<SubscriptionUpdate[]>(); 108 constructor(public value: T) { 109 this.root = new Node(value); 110 } 111 #getOrExtendPath<ET>( 112 path: Path = [], 113 ): Node<ET> { 114 let last: Node<T> | Node = this.root; 115 let next: Node | undefined; 116 for (const node of path) { 117 next = get(last.edges, node); 118 if (!next) { 119 next = new Node(); 120 set(last.edges, node, next); 121 } 122 last = next; 123 } 124 return last as Node<ET>; 125 } 126 *#path(path: Path = []) { 127 let next: Node<T> | Node | undefined = this.root; 128 yield { node: next, step: undefined }; 129 for (const step of path) { 130 next = get(next.edges, step); 131 yield { node: next, step }; 132 if (next === undefined) { 133 return; 134 } 135 } 136 } 137 // TODO: if path is known return the correct type 138 on<P extends Path, ET = DeepIndex<T, P>>( 139 listener: EventListener<ET>, 140 path?: P, 141 ) { 142 this.#getOrExtendPath<ET>(path).emmiter.on(listener); 143 } 144 off<P extends Path, ET = DeepIndex<T, P>>( 145 listener: EventListener<ET>, 146 path?: P, 147 ) { 148 const [...walk] = this.#path(path); 149 let { node, step } = walk.pop()!; 150 let parent, nextStep; 151 (node as Node<ET>)?.emmiter.off(listener); 152 153 while (walk.length && node) { 154 if (node.emmiter.listeners.size === 0 && node.edges.size === 0) { 155 ({ node: parent, step: nextStep } = walk.pop()!); 156 // make sure we are not the root 157 if (parent) { 158 parent.edges.delete(step!); 159 } 160 step = nextStep; 161 node = parent; 162 } else { 163 break; 164 } 165 } 166 } 167 once<P extends Path, ET = DeepIndex<T, P>>( 168 listener: EventListener<ET>, 169 path?: P, 170 ) { 171 const l = (event: ET) => { 172 listener(event); 173 this.off(l, path); 174 }; 175 this.on(l, path); 176 } 177 emit(event: Readonly<T>) { 178 this.root.emit(event); 179 } 180 }