UNPKG

@byloth/core

Version:

An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧

419 lines (392 loc) • 13.8 kB
import { ReferenceException } from "../exceptions/index.js"; import type { Callback, CallbackMap, InternalsEventsMap, WildcardEventsMap } from "./types.js"; type P = InternalsEventsMap; type S = WildcardEventsMap & InternalsEventsMap; /** * A class implementing the * {@link https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern|Publish-subscribe} pattern. * * It can be used to create a simple event system where objects can subscribe * to events and receive notifications when the events are published. * It's a simple and efficient way to decouple the objects and make them communicate with each other. * * Using generics, it's also possible to define the type of the events and the callbacks that can be subscribed to them. * * --- * * @example * ```ts * interface EventsMap * { * "player:spawn": (evt: SpawnEvent) => void; * "player:move": ({ x, y }: Point) => void; * "player:death": () => void; * } * * const publisher = new Publisher<EventsMap>(); * * let unsubscribe: () => void; * publisher.subscribe("player:death", unsubscribe); * publisher.subscribe("player:spawn", (evt) => * { * unsubscribe = publisher.subscribe("player:move", ({ x, y }) => { [...] }); * }); * ``` * * --- * * @template T * A map containing the names of the emittable events and the * related callback signatures that can be subscribed to them. * Default is `Record<string, (...args: unknown[]) => unknown>`. */ export default class Publisher<T extends CallbackMap<T> = CallbackMap> { /** * A map containing all the subscribers for each event. * * The keys are the names of the events they are subscribed to. * The values are the arrays of the subscribers themselves. */ protected readonly _subscribers: Map<string, Callback<unknown[], unknown>[]>; /** * Initializes a new instance of the {@link Publisher} class. * * --- * * @example * ```ts * const publisher = new Publisher(); * ``` */ public constructor() { this._subscribers = new Map(); } /** * Creates a new scoped instance of the {@link Publisher} class, * which can be used to publish and subscribe events within a specific context. * * It can receive all events published to the parent publisher while also allowing * the scoped publisher to handle its own events independently. * In fact, events published to the scoped publisher won't be propagated back to the parent publisher. * * --- * * @example * ```ts * const publisher = new Publisher(); * const context = publisher.createScope(); * * publisher.subscribe("player:death", () => console.log("Player has died.")); * context.subscribe("player:spawn", () => console.log("Player has spawned.")); * * publisher.publish("player:spawn"); // "Player has spawned." * context.publish("player:death"); // * no output * * ``` * * --- * * @template U * A map containing the additional names of the emittable events and * the related callback signatures that can be subscribed to them. * Default is `{ }`. * * @return * A new instance of the {@link Publisher} class that can be * used to publish and subscribe events within a specific context. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type public createScope<U extends CallbackMap<U> = { }>(): Publisher<U & T> { const scope = new Publisher(); this.subscribe("__internals__:clear", () => scope.clear()); this.subscribe("*", (event, ...args): void => { scope.publish(event, ...args); }); return scope; } /** * Publishes an event to all the subscribers. * This events will trigger the wildcard listeners as well, if any. * * --- * * @example * ```ts * publisher.subscribe("player:move", (coords) => { [...] }); * publisher.subscribe("player:move", ({ x, y }) => { [...] }); * publisher.subscribe("player:move", (evt) => { [...] }); * * publisher.publish("player:move", { x: 10, y: 20 }); * ``` * * --- * * @template K The key of the map containing the callback signature to publish. * * @param event The name of the event to publish. * @param args The arguments to pass to the subscribers. * * @returns An array containing the return values of all the subscribers. */ public publish<K extends keyof T>(event: K & string, ...args: Parameters<T[K]>): ReturnType<T[K]>[]; /** * Publishes an internal event to all the subscribers. * * Internal events follow the pattern `__${string}__:${string}` and are used for internal * communication within the publisher system. These events won't trigger wildcard listeners. * Please note to use this method only if you know what you are doing. * * --- * * @example * ```ts * publisher.subscribe("__internals__:clear", () => console.log("Clearing...")); * publisher.publish("__internals__:clear"); // "Clearing..." * ``` * * --- * * @template K The key of the internal events map containing the callback signature to publish. * * @param event The name of the internal event to publish. * @param args The arguments to pass to the subscribers. * * @returns An array containing the return values of all the subscribers. */ public publish<K extends keyof P>(event: K & string, ...args: Parameters<P[K]>): ReturnType<P[K]>[]; public publish(event: string, ...args: unknown[]): unknown[] { let results: unknown[]; let subscribers = this._subscribers.get(event); if (subscribers) { results = subscribers.slice() .map((subscriber) => subscriber(...args)); } else { results = []; } if (!(event.startsWith("__"))) { subscribers = this._subscribers.get("*"); if (subscribers) { subscribers.slice() .forEach((subscriber) => subscriber(event, ...args)); } } return results; } /** * Subscribes to an event and adds a subscriber to be executed when the event is published. * * --- * * @example * ```ts * let unsubscribe: () => void; * publisher.subscribe("player:death", unsubscribe); * publisher.subscribe("player:spawn", (evt) => * { * unsubscribe = publisher.subscribe("player:move", ({ x, y }) => { [...] }); * }); * ``` * * --- * * @template K The key of the map containing the callback signature to subscribe. * * @param event The name of the event to subscribe to. * @param subscriber The subscriber to execute when the event is published. * * @returns A function that can be used to unsubscribe the subscriber from the event. */ public subscribe<K extends keyof T>(event: K & string, subscriber: T[K]): Callback; /** * Subscribes to the wildcard event to listen to all published events. * * The wildcard subscriber will be called for every event published, receiving * the event type as the first parameter followed by all event arguments. * * --- * * @example * ```ts * publisher.subscribe("*", (type, ...args) => * { * console.log(`Event "${type}" was fired with args:`, args); * }); * ``` * * --- * * @template K The key of the wildcard events map (always `*`). * * @param event The wildcard event name (`*`). * @param subscriber The subscriber to execute for all published events. * * @returns A function that can be used to unsubscribe the subscriber from the wildcard event. */ public subscribe<K extends keyof S>(event: K & string, subscriber: S[K]): Callback; public subscribe(event: string, subscriber: Callback<unknown[], unknown>): Callback { const subscribers = this._subscribers.get(event) ?? []; subscribers.push(subscriber); this._subscribers.set(event, subscribers); return () => { const index = subscribers.indexOf(subscriber); if (index < 0) { throw new ReferenceException("Unable to unsubscribe the required subscriber. " + "The subscription was already unsubscribed."); } subscribers.splice(index, 1); }; } /** * Unsubscribes from an event and removes a subscriber from being executed when the event is published. * * --- * * @example * ```ts * const onPlayerMove = ({ x, y }: Point) => { [...] }; * * publisher.subscribe("player:spawn", (evt) => publisher.subscribe("player:move", onPlayerMove)); * publisher.subscribe("player:death", () => publisher.unsubscribe("player:move", onPlayerMove)); * ``` * * --- * * @template K The key of the map containing the callback signature to unsubscribe. * * @param event The name of the event to unsubscribe from. * @param subscriber The subscriber to remove from the event. */ public unsubscribe<K extends keyof T>(event: K & string, subscriber: T[K]): void; /** * Unsubscribes from the wildcard event and removes a subscriber from being executed for all events. * * This removes a previously registered wildcard listener that was capturing all published events. * * --- * * @example * ```ts * const wildcardHandler = (type: string, ...args: unknown[]) => console.log(type, args); * * publisher.subscribe("*", wildcardHandler); * publisher.unsubscribe("*", wildcardHandler); * ``` * * --- * * @template K The key of the wildcard events map (always `*`). * * @param event The wildcard event name (`*`). * @param subscriber The wildcard subscriber to remove. */ public unsubscribe<K extends keyof S>(event: K & string, subscriber: S[K]): void; public unsubscribe(event: string, subscriber: Callback<unknown[], unknown>): void { const subscribers = this._subscribers.get(event); if (!(subscribers)) { throw new ReferenceException("Unable to unsubscribe the required subscriber. " + "The subscription was already unsubscribed or was never subscribed."); } const index = subscribers.indexOf(subscriber); if (index < 0) { throw new ReferenceException("Unable to unsubscribe the required subscriber. " + "The subscription was already unsubscribed or was never subscribed."); } subscribers.splice(index, 1); if (subscribers.length === 0) { this._subscribers.delete(event); } } /** * Unsubscribes all subscribers from a specific event and removes * them from being executed when the event is published. * * --- * * @example * ```ts * publisher.subscribe("player:spawn", (evt) => { [...] }); * publisher.subscribe("player:move", (coords) => { [...] }); * publisher.subscribe("player:move", () => { [...] }); * publisher.subscribe("player:move", ({ x, y }) => { [...] }); * publisher.subscribe("player:death", () => { [...] }); * * // All these subscribers are working fine... * * publisher.unsubscribeAll("player:move"); * * // ... but now "player:move" subscribers are gone! * ``` * * --- * * @template K The key of the map containing the event to clear. * * @param event The name of the event to unsubscribe all subscribers from. */ public unsubscribeAll<K extends keyof T>(event: K & string): void; /** * Unsubscribes all subscribers from the wildcard event and removes * them from being executed for all published events. * * --- * * @example * ```ts * publisher.subscribe("player:spawn", (evt) => { [...] }); * publisher.subscribe("*", (type, ...args) => { [...] }); * publisher.subscribe("*", (type, arg1, arg2, arg3) => { [...] }); * publisher.subscribe("*", (_, arg, ...rest) => { [...] }); * publisher.subscribe("player:death", () => { [...] }); * * // All these subscribers are working fine... * * publisher.unsubscribeAll("*"); * * // ... but now wildcard subscribers are gone! * ``` * * --- * * @template K The key of the wildcard events map (`*`). * * @param event The wildcard event name (`*`). */ public unsubscribeAll<K extends keyof S>(event: K & string): void; public unsubscribeAll(event: string): void { this._subscribers.delete(event); } /** * Unsubscribes all the subscribers from all the events. * * --- * * @example * ```ts * publisher.subscribe("player:spawn", (evt) => { [...] }); * publisher.subscribe("player:move", (coords) => { [...] }); * publisher.subscribe("*", () => { [...] }); * publisher.subscribe("player:move", ({ x, y }) => { [...] }); * publisher.subscribe("player:death", () => { [...] }); * * // All these subscribers are working fine... * * publisher.clear(); * * // ... but now they're all gone! * ``` */ public clear(): void { this.publish("__internals__:clear"); this._subscribers.clear(); } public readonly [Symbol.toStringTag]: string = "Publisher"; }