tuil
ReferencePackages@mwillbanks/tuil-eventsAPI

EventBus

class exported by @mwillbanks/tuil-events.

View rawEdit

class

Public class exported by @mwillbanks/tuil-events.

export class EventBus<TEvents extends EventMap = EventMap> {
  readonly #definitions: Record<string, EventDefinition<unknown>>;
  readonly #subscriptions = new Set<Subscription>();
  readonly #observers = new Set<(event: ObservedEvent) => void>();
  readonly #history: ObservedEvent[] = [];
  #disposed = false;

  constructor(definitions: Partial<EventDefinitions<TEvents>> = {}) {
    this.#definitions = {
      ...(definitions as Record<string, EventDefinition<unknown>>),
    };
  }

  register<TType extends keyof TEvents & string>(
    type: TType,
    definition: EventDefinition<TEvents[TType]>,
  ): () => void {
    this.#assertActive();
    if (type in this.#definitions) {
      throw new Error(`Event "${type}" is already declared`);
    }
    const registered = definition as EventDefinition<unknown>;
    this.#definitions[type] = registered;
    let active = true;
    return () => {
      if (!active) return;
      active = false;
      if (this.#definitions[type] === registered) {
        delete this.#definitions[type];
      }
    };
  }

  on<TType extends keyof TEvents & string>(
    type: TType,
    listener: EventListener<TEvents[TType]>,
    options: EventSubscriptionOptions = {},
  ): () => void {
    this.#assertActive();
    const subscription: Subscription = {
      type,
      listener: listener as EventListener<unknown>,
      phase: options.phase ?? "direct",
      target: options.target,
      priority: options.priority ?? 0,
    };
    this.#subscriptions.add(subscription);
    let active = true;
    const signal = options.signal;
    const dispose = () => {
      if (!active) return;
      active = false;
      this.#subscriptions.delete(subscription);
      signal?.removeEventListener("abort", dispose);
    };
    subscription.dispose = dispose;
    if (signal) {
      if (signal.aborted) {
        dispose();
      } else {
        signal.addEventListener("abort", dispose, { once: true });
      }
    }
    return dispose;
  }

  observe(observer: (event: ObservedEvent) => void): () => void {
    this.#assertActive();
    this.#observers.add(observer);
    return () => this.#observers.delete(observer);
  }

  history(): readonly ObservedEvent[] {
    return Object.freeze([...this.#history]);
  }

  async emit<TType extends keyof TEvents & string>(
    type: TType,
    payload: TEvents[TType],
    options: EventEmitOptions = {},
  ): Promise<TuilEvent<TType, TEvents[TType]>> {
    this.#assertActive();
    if (!(type in this.#definitions)) {
      throw new Error(`Event "${type}" has not been declared`);
    }
    const priority = options.priority ?? "normal";
    if (schedulingDelay[priority] > 0) {
      await new Promise((resolve) =>
        setTimeout(resolve, schedulingDelay[priority]),
      );
    }
    const emitted = new MutableTuilEvent(
      crypto.randomUUID(),
      type,
      payload,
      Date.now(),
      options.source,
      options.target,
      Object.freeze({ ...options.metadata }),
      priority,
    );
    if (options.path && options.path.length > 0) {
      await this.#dispatchRouted(emitted, options.path);
    } else {
      await this.#dispatch(emitted, "direct");
    }
    const definition = this.#definitions[type];
    const observedPayload = definition?.redact
      ? definition.redact(payload)
      : payload;
    const observed: ObservedEvent = Object.freeze({
      id: emitted.id,
      type,
      payload: observedPayload,
      timestamp: emitted.timestamp,
      source: emitted.source,
      target: emitted.target,
      metadata: emitted.metadata,
      priority,
      defaultPrevented: emitted.defaultPrevented,
    });
    this.#history.push(observed);
    if (this.#history.length > 200) this.#history.shift();
    for (const observer of this.#observers) {
      observer(observed);
    }
    return emitted;
  }

  dispose(): void {
    this.#disposed = true;
    for (const subscription of this.#subscriptions) {
      subscription.dispose?.();
    }
    this.#subscriptions.clear();
    this.#observers.clear();
    this.#history.length = 0;
  }

  async #dispatchRouted(
    event: MutableTuilEvent<string, unknown>,
    path: readonly string[],
  ): Promise<void> {
    const target = path.at(-1);
    for (const currentTarget of path.slice(0, -1)) {
      if (event.propagationStopped) {
        return;
      }
      await this.#dispatch(event, "capture", currentTarget);
    }
    if (!event.propagationStopped && target) {
      await this.#dispatch(event, "target", target);
    }
    for (const currentTarget of path.slice(0, -1).reverse()) {
      if (event.propagationStopped) {
        return;
      }
      await this.#dispatch(event, "bubble", currentTarget);
    }
  }

  async #dispatch(
    event: MutableTuilEvent<string, unknown>,
    phase: EventPhase,
    currentTarget?: string,
  ): Promise<void> {
    event.phase = phase;
    event.currentTarget = currentTarget;
    const subscriptions = [...this.#subscriptions]
      .filter(
        (subscription) =>
          subscription.type === event.type &&
          subscription.phase === phase &&
          (subscription.target === undefined ||
            subscription.target === currentTarget),
      )
      .sort((left, right) => right.priority - left.priority);
    for (const subscription of subscriptions) {
      await subscription.listener(event);
      if (event.propagationStopped) {
        return;
      }
    }
  }

  #assertActive(): void {
    if (this.#disposed) {
      throw new Error("Event bus is disposed");
    }
  }
}

Members

MemberTypeRequiredDescriptionRelated types
#definitionsRecord<string, EventDefinition<unknown>>YesThe #definitions member uses the Record<string, EventDefinition<unknown>> contract.EventDefinition
#subscriptionsSet<Subscription>YesThe #subscriptions member uses the Set<Subscription> contract.
#observersSet<(event: ObservedEvent) => void>YesThe #observers member uses the Set<(event: ObservedEvent) => void> contract.ObservedEvent
#historyObservedEvent[]YesThe #history member uses the ObservedEvent[] contract.ObservedEvent
#disposedbooleanYesThe #disposed member uses the boolean contract.
__constructoranyYesThe __constructor member uses the any contract.
register<TType extends keyof TEvents & string>(type: TType, definition: EventDefinition<TEvents[TType]>) => () => voidYesThe register member uses the <TType extends keyof TEvents & string>(type: TType, definition: EventDefinition<TEvents[TType]>) => () => void contract.EventDefinition
on<TType extends keyof TEvents & string>(type: TType, listener: EventListener<TEvents[TType]>, options?: EventSubscriptionOptions) => () => voidYesThe on member uses the <TType extends keyof TEvents & string>(type: TType, listener: EventListener<TEvents[TType]>, options?: EventSubscriptionOptions) => () => void contract.EventSubscriptionOptions
observe(observer: (event: ObservedEvent) => void) => () => voidYesThe observe member uses the (observer: (event: ObservedEvent) => void) => () => void contract.ObservedEvent
history() => readonly ObservedEvent[]YesThe history member uses the () => readonly ObservedEvent[] contract.ObservedEvent
emit<TType extends keyof TEvents & string>(type: TType, payload: TEvents[TType], options?: EventEmitOptions) => Promise<TuilEvent<TType, TEvents[TType]>>YesThe emit member uses the <TType extends keyof TEvents & string>(type: TType, payload: TEvents[TType], options?: EventEmitOptions) => Promise<TuilEvent<TType, TEvents[TType]>> contract.EventEmitOptions, TuilEvent
dispose() => voidYesThe dispose member uses the () => void contract.
#dispatchRouted(event: MutableTuilEvent<string, unknown>, path: readonly string[]) => Promise<void>YesThe #dispatchRouted member uses the (event: MutableTuilEvent<string, unknown>, path: readonly string[]) => Promise<void> contract.
#dispatch(event: MutableTuilEvent<string, unknown>, phase: EventPhase, currentTarget?: string) => Promise<void>YesThe #dispatch member uses the (event: MutableTuilEvent<string, unknown>, phase: EventPhase, currentTarget?: string) => Promise<void> contract.EventPhase
#assertActive() => voidYesThe #assertActive member uses the () => void contract.

Parameters

This declaration has no public members.

Returns

This declaration does not return a value.

Throws

No thrown errors are documented for this declaration.

Source

View the secondary source reference

Package

@mwillbanks/tuil-events

On this page