export type RunnerListenerCallback<ARG extends unknown[] = any[]> = (...args: ARG) => unknown;
type RunnerItemValid<T extends string, ARG extends unknown[] = any[]> =
{ [K in T]: RunnerListenerCallback<ARG> | unknown };
type RunnerItemAny = Record<string, unknown>;
type RunnerItemEmpty = Record<string, never>;
export type RunnerItem<T = string, ARG extends unknown[] = any[]> =
T extends string ?
RunnerItemValid<T, ARG> & RunnerItemAny | RunnerItemEmpty :
unknown;
/**
* A Runner is a highly performant and simple alternative to signals. Best used in situations
* where events are dispatched to many objects at high frequency (say every frame!)
*
* Like a signal:
*
* ```js
* import { Runner } from '@pixi/runner';
*
* const myObject = {
* loaded: new Runner('loaded'),
* };
*
* const listener = {
* loaded: function() {
* // Do something when loaded
* }
* };
*
* myObject.loaded.add(listener);
*
* myObject.loaded.emit();
* ```
*
* Or for handling calling the same function on many items:
*
* ```js
* import { Runner } from '@pixi/runner';
*
* const myGame = {
* update: new Runner('update'),
* };
*
* const gameObject = {
* update: function(time) {
* // Update my gamey state
* },
* };
*
* myGame.update.add(gameObject);
*
* myGame.update.emit(time);
* ```
*
* Type safety:
*
* ```ts
*
* import { Runner } from '@pixi/runner';
*
* let runner: Runner<'update', [number]>;
*
* // This won't work because the function name 'update' is expected
* runner = new Runner('destroy');
*
* // This is fine
* runner = new Runner('update');
*
* // This won't work because the number is expected
* runner.emit("10");
*
* // This is fine
* runner.emit(10);
*
* // This won't work because provided object does not contain 'update' key
* runner.add({
* destroy: function() {
* // Destroy the game
* },
* });
*
* // This is fine
* runner.add({
* update: function(time) {
* // Update my gamey state
* },
* destroy: function() {
* // Destroy the game
* },
* });
*
* ```
* @template T - The event type.
* @template ARG - The argument types for the event handler functions.
* @memberof PIXI
*/
export class Runner<T = any, ARG extends unknown[] = any[]>
{
public items: any[];
private _name: T;
private _aliasCount: number;
/**
* @param {string} name - The function name that will be executed on the listeners added to this Runner.
*/
constructor(name: T)
{
this.items = [];
this._name = name;
this._aliasCount = 0;
}
/* eslint-disable jsdoc/require-param, jsdoc/check-param-names */
/**
* Dispatch/Broadcast Runner to all listeners added to the queue.
* @param {...any} params - (optional) parameters to pass to each listener
*/
/* eslint-enable jsdoc/require-param, jsdoc/check-param-names */
public emit(a0?: ARG[0], a1?: ARG[1], a2?: ARG[2], a3?: ARG[3],
a4?: ARG[4], a5?: ARG[5], a6?: ARG[6], a7?: ARG[7]): this
{
if (arguments.length > 8)
{
throw new Error('max arguments reached');
}
const { name, items } = this;
this._aliasCount++;
for (let i = 0, len = items.length; i < len; i++)
{
items[i][name](a0, a1, a2, a3, a4, a5, a6, a7);
}
if (items === this.items)
{
this._aliasCount--;
}
return this;
}
private ensureNonAliasedItems(): void
{
if (this._aliasCount > 0 && this.items.length > 1)
{
this._aliasCount = 0;
this.items = this.items.slice(0);
}
}
/**
* Add a listener to the Runner
*
* Runners do not need to have scope or functions passed to them.
* All that is required is to pass the listening object and ensure that it has contains a function that has the same name
* as the name provided to the Runner when it was created.
*
* E.g. A listener passed to this Runner will require a 'complete' function.
*
* ```js
* import { Runner } from '@pixi/runner';
*
* const complete = new Runner('complete');
* ```
*
* The scope used will be the object itself.
* @param {any} item - The object that will be listening.
*/
public add(item: RunnerItem<T, ARG>): this
{
if ((item as any)[this._name])
{
this.ensureNonAliasedItems();
this.remove(item);
this.items.push(item);
}
return this;
}
/**
* Remove a single listener from the dispatch queue.
* @param {any} item - The listener that you would like to remove.
*/
public remove(item: RunnerItem<T, ARG>): this
{
const index = this.items.indexOf(item);
if (index !== -1)
{
this.ensureNonAliasedItems();
this.items.splice(index, 1);
}
return this;
}
/**
* Check to see if the listener is already in the Runner
* @param {any} item - The listener that you would like to check.
*/
public contains(item: RunnerItem<T, ARG>): boolean
{
return this.items.includes(item);
}
/** Remove all listeners from the Runner */
public removeAll(): this
{
this.ensureNonAliasedItems();
this.items.length = 0;
return this;
}
/** Remove all references, don't use after this. */
public destroy(): void
{
this.removeAll();
this.items.length = 0;
this._name = '' as T;
}
/**
* `true` if there are no this Runner contains no listeners
* @readonly
*/
public get empty(): boolean
{
return this.items.length === 0;
}
/**
* The name of the runner.
* @type {string}
*/
public get name(): T
{
return this._name;
}
}
Object.defineProperties(Runner.prototype, {
/**
* Alias for `emit`
* @memberof PIXI.Runner#
* @method dispatch
* @see PIXI.Runner#emit
*/
dispatch: { value: Runner.prototype.emit },
/**
* Alias for `emit`
* @memberof PIXI.Runner#
* @method run
* @see PIXI.Runner#emit
*/
run: { value: Runner.prototype.emit },
});