@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
text/typescript
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";
}