EventBus
class exported by @mwillbanks/tuil-events.
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
| Member | Type | Required | Description | Related types |
|---|---|---|---|---|
#definitions | Record<string, EventDefinition<unknown>> | Yes | The #definitions member uses the Record<string, EventDefinition<unknown>> contract. | EventDefinition |
#subscriptions | Set<Subscription> | Yes | The #subscriptions member uses the Set<Subscription> contract. | — |
#observers | Set<(event: ObservedEvent) => void> | Yes | The #observers member uses the Set<(event: ObservedEvent) => void> contract. | ObservedEvent |
#history | ObservedEvent[] | Yes | The #history member uses the ObservedEvent[] contract. | ObservedEvent |
#disposed | boolean | Yes | The #disposed member uses the boolean contract. | — |
__constructor | any | Yes | The __constructor member uses the any contract. | — |
register | <TType extends keyof TEvents & string>(type: TType, definition: EventDefinition<TEvents[TType]>) => () => void | Yes | The 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) => () => void | Yes | The 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) => () => void | Yes | The observe member uses the (observer: (event: ObservedEvent) => void) => () => void contract. | ObservedEvent |
history | () => readonly ObservedEvent[] | Yes | The 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]>> | Yes | The 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 | () => void | Yes | The dispose member uses the () => void contract. | — |
#dispatchRouted | (event: MutableTuilEvent<string, unknown>, path: readonly string[]) => Promise<void> | Yes | The #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> | Yes | The #dispatch member uses the (event: MutableTuilEvent<string, unknown>, phase: EventPhase, currentTarget?: string) => Promise<void> contract. | EventPhase |
#assertActive | () => void | Yes | The #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.
Related types
EventDefinitionEventDefinitionsEventEmitOptionsEventMapEventPhaseEventSubscriptionOptionsObservedEventTuilEvent
Source
View the secondary source reference