/ packages / eventTree / mod.ts
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  }