1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 12:55:55 +00:00
FrankerFaceZ/src/utilities/events.ts

1009 lines
29 KiB
TypeScript

// ============================================================================
// EventEmitter
// Homegrown for that lean feeling.
// ============================================================================
/**
* A special value that, when returned from an event listener, will cause the
* listener to be removed and no longer receive events.
*/
export const Detach = Symbol('Detach');
/**
* A special value that, when returned from an event listener, will stop
* iteration and prevent any additional event listeners from receiving the
* event themselves.
*/
export const StopPropagation = Symbol('StopPropagation');
declare global {
interface String {
toSlug(separator: string): string;
toSnakeCase(): string;
}
}
const SNAKE_CAPS = /([a-z])([A-Z])/g,
SNAKE_SPACE = /[ \t\W]/g,
SNAKE_TRIM = /^_+|_+$/g;
String.prototype.toSlug = function(this: string, separator: string = '-') {
let result = this;
if (result.normalize)
result = result.normalize('NFD');
return result
.replace(/[\u0300-\u036f]/g, '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9 ]/g, '')
.replace(/\s+/g, separator);
}
String.prototype.toSnakeCase = function(this: string) {
let result = this;
if (result.normalize)
result = result.normalize('NFD');
return result
.replace(/[\u0300-\u036f]/g, '')
.trim()
.replace(SNAKE_CAPS, '$1_$2')
.replace(SNAKE_SPACE, '_')
.replace(SNAKE_TRIM, '')
.toLowerCase();
}
export type ListenerReturnType = void | typeof Detach | typeof StopPropagation | Promise<void>;
export type EventListener<TArgs extends any[] = any[]> = (...args: TArgs) => ListenerReturnType;
export type ListenerInfo<TArgs extends any[] = any[]> = [
listener: EventListener<TArgs>,
context: any,
ttl: number | false,
priority: number,
prepend: boolean
];
export interface EventMap {
[event: string]: any[];
};
export type EventKey<TEvent extends EventMap> = string & keyof TEvent;
export type AddEventKeyNamespace<
TNamespace extends string,
TKey extends string
> = TKey extends `:${infer Rest}`
? `${TNamespace}${TKey}`
: TKey;
export type RemoveEventKeyNamespace<
TNamespace extends string,
TKey extends string
> = TKey extends `${TNamespace}:${infer Rest}`
? `:${Rest}`
: TKey;
export type NamespacedEvents<TNamespace extends string, TEventMap extends EventMap> = {
[K in keyof TEventMap & string as AddEventKeyNamespace<TNamespace, K>]: TEventMap[K]
};
export type NamespacedEventKey<TNamespace extends string, TEventMap extends EventMap> =
string & keyof TEventMap | RemoveEventKeyNamespace<TNamespace, string & keyof TEventMap>;
export type NamespacedEventArgs<
TKey extends string,
TNamespace extends string,
TEventMap extends EventMap
> = TKey extends keyof TEventMap
? TEventMap[TKey]
: AddEventKeyNamespace<TNamespace, TKey> extends keyof TEventMap
? TEventMap[AddEventKeyNamespace<TNamespace, TKey>]
: never;
/**
* A custom event emitter implementation with support for priorities,
* event listeners that only receive a certain number of events,
* event listeners that can remove themselves by returning a special
* symbol, and other useful behaviors.
*
* You'll likely not be using this class directly, instead relying
* on {@link Module}s directly. Modules are subclasses of
* {@link HierarchicalEventEmitter} which itself is a subclass of
* this {@link EventEmitter}.
*/
export class EventEmitter<
TEventMap extends EventMap = {},
TNamespace extends string = ''
> {
static Detach = Detach;
static StopPropagation = StopPropagation;
/** @private */
protected __listeners: Record<string, ListenerInfo[] | null>;
protected __running: Set<string>;
private __dead_events: number;
constructor() {
this.__listeners = {};
this.__running = new Set;
this.__dead_events = 0;
}
private __cleanListeners() {
if ( ! this.__dead_events )
return;
const new_listeners: Record<string, ListenerInfo[]> = {},
old_listeners = this.__listeners;
for(const [key, val] of Object.entries(old_listeners)) {
if ( val?.length )
new_listeners[key] = val;
}
this.__listeners = new_listeners;
this.__dead_events = 0;
}
private __sortListeners(event: string) {
// Don't bother sorting while an event is running, since
// we'll need to sort it at the end when the lists are
// recombined.
if ( this.__running.has(event) )
return;
const list = this.__listeners[event];
if ( list )
list.sort((a, b) => a[3] - b[3]);
}
/**
* Transfer all the event listeners from this EventEmitter to another
* EventEmitter instance, removing them from this EventEmitter in
* the process.
*
* @param other The EventEmitter to transfer our listeners to.
*/
transferListeners(other: EventEmitter<TEventMap, TNamespace>) {
if ( !(other instanceof EventEmitter) )
throw new Error('other must also be EventEmitter');
// If the existing listener has no live topics, we can just go ahead
// and copy the listeners object directly.
const live_topics = other.events().length + other.__running.size;
if ( ! live_topics ) {
other.__listeners = this.__listeners;
other.__dead_events = this.__dead_events;
this.__listeners = {};
this.__dead_events = 0;
return;
}
// Unfortuantely, since we got here, we'll need to do things the
// old fashioned way.
for(const [key, val] of Object.entries(this.__listeners)) {
if ( ! val || ! val.length )
continue;
let other_val = other.__listeners[key];
if ( Array.isArray(other_val) ) {
other_val.push(...val);
other.__sortListeners(key);
} else
other.__listeners[key] = val;
}
// Reset our state before we leave.
this.__listeners = {};
this.__dead_events = 0;
}
// ========================================================================
// Public Methods
// ========================================================================
/**
* Create a new {@link FFZEvent} instance. This is a convenience method that
* wraps {@link FFZEvent.makeEvent}
*/
makeEvent<TData extends Record<string, any> = {}>(data: TData): TypedFFZEvent<TData> {
return FFZEvent.makeEvent(data);
}
/**
* Create a new {@link FFZWaitableEvent} instance. This is a convenience
* method that wraps {@link FFZWaitableEvent.makeEvent}
*/
makeWaitableEvent<TData extends Record<string, any> = {}, TReturn = void>(data: TData): TypedFFZWaitableEvent<TData, TReturn> {
return FFZWaitableEvent.makeEvent(data);
}
/**
* Register an event listener for a given event.
*
* @param event The event to listen for.
* @param fn The event listener function.
* @param ctx A context (aka `this`) to call the function with.
* @param priority The priority of the event listener. Default is 0.
* @param prepend Whether the listener should be added to the start of the
* list or the end, as a less direct method of establishing priority.
*/
on<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
fn: EventListener<NamespacedEventArgs<K, TNamespace, TEventMap>>,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
if ( typeof fn !== 'function' )
throw new TypeError('fn must be a function');
const info: ListenerInfo = [
fn as EventListener,
ctx,
false,
priority ?? 0,
prepend
];
const list = this.__listeners[event];
if ( list ) {
if ( prepend )
list.unshift(info);
else
list.push(info);
this.__sortListeners(event);
} else
this.__listeners[event] = [info];
}
/**
* Register an event listener for a given event that will only fire
* once before being automatically removed.
*
* @see {@link many}
*
* @param event The event to listen for.
* @param fn The event listener function.
* @param ctx A context (aka `this`) to call the function with.
* @param priority The priority of the event listener. Default is 0.
* @param prepend Whether the listener should be added to the start of the
* list or the end, as a less direct method of establishing priority.
*/
once<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
fn: EventListener<NamespacedEventArgs<K, TNamespace, TEventMap>>,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
return this.many(event, 1, fn, ctx, priority, prepend);
}
/**
* Register an event listener for a given event that will only fire
* a specific number of times before being automatically removed.
*
* @param event The event to listen for.
* @param ttl The number of times the listener should fire.
* @param fn The event listener function.
* @param ctx A context (aka `this`) to call the function with.
* @param priority The priority of the event listener. Default is 0.
* @param prepend Whether the listener should be added to the start of the
* list or the end, as a less direct method of establishing priority.
*/
many<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
ttl: number,
fn: EventListener<NamespacedEventArgs<K, TNamespace, TEventMap>>,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
if ( typeof fn !== 'function' )
throw new TypeError('fn must be a function');
if ( typeof ttl !== 'number' || isNaN(ttl) || ! isFinite(ttl) || ttl < 1 )
throw new TypeError('ttl must be a positive, finite number');
const info: ListenerInfo = [
fn as EventListener,
ctx,
ttl,
priority ?? 0,
prepend
];
const list = this.__listeners[event];
if ( list ) {
if ( prepend )
list.unshift(info);
else
list.push(info);
this.__sortListeners(event);
} else
this.__listeners[event] = [info];
}
/**
* Wait for the given event to fire and return its value.
*
* Internally, this works by registering a temporary event listener
* with a `ttl` of `1` that, when called, calls the `resolve` method
* of the promise returned by this function.
*
* @param event The event to listen for.
* @param ctx A context (aka `this`) to associate this with. While the
* context is not used for calling any functions, it's used to track the
* source of this event listener so that it can be removed easily by
* context if necessary.
* @param priority The priority of the event listener. Default is 0.
* @param prepend Whether the listener should be added to the start of the
* list or the end, as a less direct method of establishing priority.
* @returns A {@link Promise} that resolves with the first event of
* this type to fire.
*/
waitFor<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
return new Promise<NamespacedEventArgs<K, TNamespace, TEventMap>>(resolve => {
const info: ListenerInfo = [
((...args: NamespacedEventArgs<K, TNamespace, TEventMap>) => resolve(args)) as EventListener,
ctx,
1,
priority ?? 0,
prepend
];
const list = this.__listeners[event];
if ( list ) {
if ( prepend )
list.unshift(info);
else
list.push(info);
this.__sortListeners(event);
} else
this.__listeners[event] = [info];
});
}
/**
* Remove one or more event listeners, across one or more events.
* You must provide at least one of `event`, `fn`, and `ctx`. All
* event listeners that match your provided criteria will be removed.
*
* This can be used to remove all event listeners for a given event
* or to remove all event listeners registered by a given context,
* in addition to the obvious ability to remove a specific
* event listener.
*
* @param event Optional. The event to remove listener(s) of.
* @param fn Optional. The event listener function to remove.
* @param ctx Optional. The context to remove listeners of.
*/
off<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event?: K,
fn?: EventListener,
ctx?: any
) {
if ( event == null ) {
if ( ! fn && ! ctx )
throw new Error('you must provide at least one constraint when removing listeners');
for(const evt in Object.keys(this.__listeners)) {
if ( ! this.__running.has(evt) )
this.off(evt as any, fn, ctx);
}
return;
}
if ( this.__running.has(event) )
throw new Error(`concurrent modification: tried removing event listener while event is running`);
let list = this.__listeners[event];
if ( ! list )
return;
// If fn and ctx were both not provided, then clear the list.
if ( ! fn && ! ctx )
list = null;
else {
// Remove any entries from the list where:
// 1. fn and ctx both match if both were provided
// 2. fn matches if only fn was provided
// 3. ctx matches if only ctx was provided
list = list.filter(([f, c]) => !((! fn || f === fn) && (!ctx || ctx === c)));
if ( ! list.length )
list = null;
}
this.__listeners[event] = list;
// We don't use delete since that triggers performance issues
// when used on objects. Instead, we record that we have a
// dead event so we can clean it up later.
if ( ! list )
this.__dead_events++;
}
/**
* Remove all event listeners registered by a specific context.
* This can now be handled by calling {@link off} directly and
* it should be avoided.
* @deprecated
*/
offContext<K extends NamespacedEventKey<TNamespace, TEventMap>>(event: K, ctx?: any) {
return this.off(event, undefined, ctx);
}
/**
* Return a list of all event keys with at least one listener.
*/
events() {
this.__cleanListeners();
return Object.keys(this.__listeners);
}
/**
* Return a list of all listeners for a given event. This
* includes metadata including the listener's `ttl`.
* @param event
* @returns
*/
listeners<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K)
: ListenerInfo<NamespacedEventArgs<K, TNamespace, TEventMap>> {
const list = this.__listeners[event];
return list ? Array.from(list) as any : [];
}
/**
* Determine whether there are currently any listeners for
* the specified event. */
hasListeners<K extends NamespacedEventKey<TNamespace, TEventMap>>(event: K) {
return !! this.__listeners[event]
}
/**
* Emit an event to all its listeners.
*
* This will call each listener of an event in order, and it handles
* event listener lifetimes with `ttl` and `Detach`. It also supports
* the `StopPropagation` return value or, if the event data is an
* instance of {@link FFZEvent}, the {@link FFZEvent.propagationStopped}
* property.
*
* If the event data is an instance of {@link FFZWaitableEvent} and
* the event listener returns a {@link Promise}, that {@link Promise}
* will automatically be waited for with {@link FFZWaitableEvent.waitFor}.
*
* @param event The event to emit.
* @param data The data for the event. This will vary depending on
* the event being emitted.
*/
emit<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
...data: NamespacedEventArgs<K, TNamespace, TEventMap>
) {
let list = this.__listeners[event];
if ( ! list )
return;
if ( this.__running.has(event) )
throw new Error(`concurrent access: tried to emit event while event is running`);
// Track removals separately to make iteration over the event list
// much, much simpler.
const removed = new Set<ListenerInfo>;
// Set the current list of listeners to null because we don't want
// to enter some kind of loop if a new listener is added as the result
// of an existing listener.
this.__listeners[event] = null;
this.__running.add(event);
for(const item of list) {
const [fn, ctx, ttl] = item;
let ret: ListenerReturnType = undefined;
try {
ret = fn.apply(ctx, data);
} catch(err) {
// Abusing as any so we can have log as a getter/setter
// on Module without complaint.
if ( (this as any).log ) {
(this as any).log.capture(err, {tags: {event}, extra:{args: data}});
(this as any).log.error(err);
}
}
if ( ret === Detach )
removed.add(item);
else if ( ttl !== false ) {
if ( ttl <= 1 )
removed.add(item);
else
item[2] = ttl - 1;
}
// Automatically wait for a promise, if the return value is a promise
// and we're dealing with a waitable event.
if ( ret instanceof Promise ) {
if ( (data[0] instanceof FFZWaitableEvent) )
data[0].waitFor(ret);
}
if ( (data[0] instanceof FFZEvent && data[0].propagationStopped) || ret === StopPropagation )
break;
}
// Remove any dead listeners from the list.
if ( removed.size ) {
for(const item of removed) {
const idx = list.indexOf(item);
if ( idx !== -1 )
list.splice(idx, 1);
}
}
let need_sort = false;
// Were more listeners added while we were running? Just combine
// the two lists if so.
const new_items = this.__listeners[event];
if ( new_items ) {
list = list.concat(new_items);
need_sort = true;
}
// If we have items, store the list back. Otherwise, mark that we
// have a dead listener.
if ( list.length )
this.__listeners[event] = list;
else {
this.__listeners[event] = null;
this.__dead_events++;
}
this.__running.delete(event);
// Finally, now that running is off, sort our listeners if we
// have need.
if ( need_sort )
this.__sortListeners(event);
}
}
/**
* FFZEvent is a convenience class for use when emitting events when you would
* like to potentially receive feedback from event listeners. The event
* object may be changed by event listeners, with the emitter checking the
* event object for changes once the event is done.
*
* This also includes support for the standard {@link Event} interfaces of
* {@link stopPropagation} and {@link preventDefault}.
*
* @param TData The custom data type the event should be constructed with.
*/
export class FFZEvent<TData> {
/**
* Create a new {@link FFZEvent} with proper type inheritence from the
* supplied data. This should always be used to construct an event
* instance, rather than creating the event manually.
*/
static makeEvent<TData extends Record<string, any> = {}>(data: TData): TypedFFZEvent<TData> {
return new FFZEvent(data) as TypedFFZEvent<TData>;
}
/** Whether or not {@link preventDefault} has been called. */
defaultPrevented: boolean;
/** Whether or not {@link stopPropagation} has been called. */
propagationStopped: boolean;
/** Create a new FFZEvent instance with the provided data. */
constructor(data: TData) {
this.defaultPrevented = false;
this.propagationStopped = false;
Object.assign(this, data);
}
/**
* Intended for emitter use only.
*
* Reset this FFZEvent instance. This does not reset custom data,
* but only returns {@link defaultPrevented} and {@link propagationStopped}
* to their original values.
*
* Subclasses of FFZEvent may override this method to extend
* its behavior.
*/
_reset() {
this.defaultPrevented = false;
this.propagationStopped = false;
}
/**
* Stop the propagation of this event, ensuring that no further
* event listeners receive it.
*/
stopPropagation() {
this.propagationStopped = true;
}
/**
* Prevent whatever default behavior is associated with this event.
*/
preventDefault() {
this.defaultPrevented = true;
}
}
/**
* TypedFFZEvent is a convenience type returned by {@link FFZEvent.makeEvent}
* so that custom data passed to the event can be accessed in a
* type safe way.
*/
export type TypedFFZEvent<TData> = FFZEvent<TData> & {
[K in keyof TData]: TData[K];
}
/**
* TypedFFZWaitableEvent is a convenience type returned by {@link FFZWaitableEvent.makeEvent}
* so that custom data passed to the event can be accessed in a
* type safe way.
*/
export type TypedFFZWaitableEvent<TData, TReturn = void> = FFZWaitableEvent<TData, TReturn> & {
[K in keyof TData]: TData[K];
}
/**
* FFZWaitableEvent is a subclass of FFZEvent that adds a system for supporting
* asynchronous return values.
*
* Event listeners may return a {@link Promise} or call {@link waitFor} directly
* to register their promises, and the emitter is then responsible for calling
* the {@link _wait} method after the event has been emitted.
*
* @param TData The custom data type the event should be constructed with.
* @param TReturn The expected return type of {@link Promise}s used with
* {@link waitFor} or by listeners returning {@link Promise}s.
*/
export class FFZWaitableEvent<TData, TReturn = void> extends FFZEvent<TData> {
/**
* Create a new {@link FFZWaitableEvent} with proper type inheritence from the
* supplied data. This should always be used to construct an event
* instance, rather than creating the event manually.
*/
static makeEvent<TData extends Record<string, any> = {}, TReturn = void>(data: TData): TypedFFZWaitableEvent<TData, TReturn> {
return new FFZWaitableEvent<TData, TReturn>(data) as TypedFFZWaitableEvent<TData, TReturn>;
}
private __waiter?: Promise<Awaited<TReturn>[]> | null;
private __waiter_results: Awaited<TReturn>[] | null = null;
private __promises?: Promise<TReturn>[] | null;
/**
* Intended for emitter use only.
*
* Wait for all registered {@link Promise}s to return.
*
* @returns
*/
_wait(): Promise<Awaited<TReturn>[]> | null {
// If we're already waiting, keep on waiting.
if ( this.__waiter )
return this.__waiter;
// If we have no promises, just return
// any pending results.
if ( ! this.__promises ) {
const out = this.__waiter_results;
this.__waiter_results = null;
// Make sure to return the results as a promise.
return out ? Promise.resolve(out) : null;
}
// We had promises, so we need to wait some more.
const promises = this.__promises;
this.__promises = null;
return this.__waiter = Promise.all(promises).then(results => {
// Store the results for later return.
if ( this.__waiter_results )
this.__waiter_results.push(...results);
else
this.__waiter_results = results;
// Now call _wait() again.
this.__waiter = null;
return this._wait() ?? [];
}).catch(err => {
// But if there was an error, oh no.
this.__waiter = null;
throw err;
});
}
/**
* Reset the FFZWaitableEvent instance. In addition to calling
* the super method {@link FFZEvent._reset}, this also clears
* the Promise-related data stored on the event.
*/
_reset() {
super._reset();
this.__waiter = null;
this.__waiter_results = null;
this.__promises = null;
}
/**
* Wait for a {@link Promise} to complete before considering the event
* as completed.
*
* Event listeners should either call this method with their Promise
* if performing asynchronous work, or else return the Promise directly
* from their event listener.
*
* @param promise The Promise to wait for.
*/
waitFor(promise: Promise<TReturn>) {
if ( ! this.__promises )
this.__promises = [promise];
else
this.__promises.push(promise);
}
}
/**
* HierarchicalEventEmitter is a subclass of {@link EventEmitter} that allows
* you to create a tree of event emitters that all share the same pool of
* event listeners.
*
* This is useful because the event emitter can then emit events using
* a simplified name while other emitters can listen to that event using
* its full name.
*
* For example, an emitter with the path `chat.emotes` could call {@link emit}
* with the event key `:update-default-sets` while other emitters would then
* have access to listen to that event with the full key
* `chat.emotes:update-default-sets`.
*
* This behavior powers the {@link Module} system.
*/
export class HierarchicalEventEmitter<
TNamespace extends string,
TEventMap extends EventMap = {}
> extends EventEmitter<TEventMap, TNamespace> {
/** The local name of this event emitter, not including the names of its parents. */
name: string;
/** @private */
protected __path?: string;
private __path_parts: string[];
/** The parent of this event emitter, if it has one. */
parent?: HierarchicalEventEmitter<any, any>;
/** The root event emitter. */
root: HierarchicalEventEmitter<any, any>;
constructor(name?: string, parent?: HierarchicalEventEmitter<any, any>) {
super();
this.name = name || (this.constructor.name || '').toSnakeCase();
this.parent = parent;
if ( parent ) {
this.root = parent.root;
this.__listeners = parent.__listeners;
this.__running = parent.__running;
this.__path = name && parent.__path ? `${parent.__path}.${name}` : name;
} else {
this.root = this as any;
this.__path = undefined;
}
this.__path_parts = this.__path ? this.__path.split('.') : [];
}
// ========================================================================
// Public Properties
// ========================================================================
/**
* The full path of this event emitter, including not just its own name
* but its ancestors' names as well, separated by periods.
*/
get path(): TNamespace {
return this.__path as TNamespace;
}
// ========================================================================
// Public Methods
// ========================================================================
transferListeners(other: EventEmitter<TEventMap, TNamespace>): void {
if ( other instanceof HierarchicalEventEmitter && other.root === this.root )
return;
throw new Error('Transfering listeners from a HierarchicalEventEmitter is not supported.');
}
/**
* Take an event path and convert it to an absolute path. For example,
* if this emitter's path is `chat.emotes` and you call this with the
* path `:update-default-sets` it will return `chat.emotes:update-default-sets`.
*
* You can also traverse to specific parents by using periods (`.`) at the
* start of the path. A single period (`.`) is equal to the current emitter's
* path, while each additional period moves up by one ancestor. For example,
* to get this emitter's parent, you could use `..`. This can be combined
* with other strings to access siblings, as well as events on ancestors
* or siblings.
*
* For example, if this emitter's path is `chat.emotes` and you call this
* with the path `..:update-line-tokens` it will return `chat:update-line-tokens`.
*
* As another example, assuming the same emitter path and you call this with
* the path `..emoji:populated` it will return `chat.emoji:populated`.
*
* @param path The path to resolve.
* @returns The resolved path.
*/
abs_path(path: string): string {
if ( typeof path !== 'string' || ! path.length )
throw new TypeError('path must be a non-empty string');
let i = 0, chr;
const parts = this.__path_parts,
depth = parts.length;
do {
chr = path.charAt(i);
if ( path.charAt(i) === '.' ) {
if ( i > depth )
throw new Error('invalid path: reached top of stack');
continue;
}
break;
} while ( ++i < path.length );
const event = chr === ':';
if ( i === 0 )
return event && this.__path ? `${this.__path}${path}` : path;
const prefix = parts.slice(0, depth - (i-1)).join('.'),
remain = path.slice(i);
if ( ! prefix.length )
return remain;
else if ( ! remain.length )
return prefix;
else if ( event )
return prefix + remain;
return `${prefix}.${remain}`;
}
on<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
fn: EventListener<NamespacedEventArgs<K, TNamespace, TEventMap>>,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
return super.on(this.abs_path(event) as K, fn, ctx, priority, prepend);
}
once<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
fn: EventListener<NamespacedEventArgs<K, TNamespace, TEventMap>>,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
return super.once(this.abs_path(event) as K, fn, ctx, priority, prepend);
}
many<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
ttl: number,
fn: EventListener<NamespacedEventArgs<K, TNamespace, TEventMap>>,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
return super.many(this.abs_path(event) as K, ttl, fn, ctx, priority, prepend);
}
waitFor<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
ctx?: any,
priority?: number,
prepend: boolean = false
) {
return super.waitFor<K>(this.abs_path(event) as K, ctx, priority, prepend);
}
off<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event?: K, fn?: EventListener, ctx?: any
) {
return super.off(event && this.abs_path(event) as K, fn, ctx);
}
listeners<K extends NamespacedEventKey<TNamespace, TEventMap>>(event: K) {
return super.listeners(this.abs_path(event) as K);
}
hasListeners<K extends NamespacedEventKey<TNamespace, TEventMap>>(event: K) {
return super.hasListeners(this.abs_path(event) as K);
}
emit<K extends NamespacedEventKey<TNamespace, TEventMap>>(
event: K,
...args: NamespacedEventArgs<K, TNamespace, TEventMap>
) {
return super.emit(this.abs_path(event) as K, ...args);
}
/**
* Return a list of all event keys with at least one listener.
* This will only return events owned by this emitter, or by
* descendents of this emitter.
*/
eventsWithChildren() {
const keys = super.events(),
path = this.__path || '',
len = path.length;
return keys.filter(x => {
const y = x.charAt(len);
return x.startsWith(path) && (y === '' || y === '.' || y === ':');
});
}
/**
* Return a list of all event keys with at least one listener.
* This will not return any events owned by other emitters.
*/
events() {
const keys = super.events(),
path = this.__path || '',
len = path.length;
return keys.filter(x => {
const y = x.charAt(len);
return x.startsWith(path) && (y === '' || y === ':');
});
}
}
export default HierarchicalEventEmitter;