import { RequiredKeys, OptionalKeys, PickByValue, OmitByValue, } from 'utility-types' import { FunctionExt } from '../util' export class Events { private listeners: { [name: string]: any[] } = {} on>( name: Name, handler: Events.Handler, context?: any, ): this on>( name: Name, handler: Events.Handler, context?: any, ): this on>( name: Name, handler: Events.Handler, context?: any, ) { if (handler == null) { return this } if (!this.listeners[name]) { this.listeners[name] = [] } const cache = this.listeners[name] cache.push(handler, context) return this } once>( name: Name, handler: Events.Handler, context?: any, ): this once>( name: Name, handler: Events.Handler, context?: any, ): this once>( name: Name, handler: Events.Handler, context?: any, ) { const cb = (...args: any) => { this.off(name, cb as any) return Private.call([handler, context], args) } return this.on(name, cb as any, this) } off(): this off(name: null, handler: Events.Handler): this off(name: null, handler: null, context: any): this off>( name: Name, handler?: Events.Handler, context?: any, ): this off>( name: Name, handler?: Events.Handler, context?: any, ): this off( name?: string | null, handler?: Events.Handler | null, context?: any, ) { // remove all events. if (!(name || handler || context)) { this.listeners = {} return this } const listeners = this.listeners const names = name ? [name] : Object.keys(listeners) names.forEach((n) => { const cache = listeners[n] if (!cache) { return } // remove all events with specified name. if (!(handler || context)) { delete listeners[n] return } for (let i = cache.length - 2; i >= 0; i -= 2) { if ( !( (handler && cache[i] !== handler) || (context && cache[i + 1] !== context) ) ) { cache.splice(i, 2) } } }) return this } trigger>( name: Name, ): FunctionExt.AsyncBoolean trigger>( name: Name, args: EventArgs[Name], ): FunctionExt.AsyncBoolean trigger>( name: Name, ...args: EventArgs[Name] ): FunctionExt.AsyncBoolean trigger>( name: Name, args?: EventArgs[Name], ): FunctionExt.AsyncBoolean trigger>( name: Name, ...args: EventArgs[Name] ): FunctionExt.AsyncBoolean trigger>( name: Name, ...args: any[] ): FunctionExt.AsyncBoolean trigger>( name: Name, ...args: any[] ) { let returned: FunctionExt.AsyncBoolean = true if (name !== '*') { const list = this.listeners[name] if (list != null) { returned = Private.call([...list], args) } } const list = this.listeners['*'] if (list != null) { return FunctionExt.toAsyncBoolean([ returned, Private.call([...list], [name, ...args]), ]) } return returned } /** * Triggers event with specified event name. Unknown names * will cause a typescript type error. */ protected emit>( name: Name, ): FunctionExt.AsyncBoolean protected emit>( name: Name, args: EventArgs[Name], ): FunctionExt.AsyncBoolean protected emit>( name: Name, ...args: EventArgs[Name] ): FunctionExt.AsyncBoolean protected emit>( name: Name, args?: EventArgs[Name], ): FunctionExt.AsyncBoolean protected emit>( name: Name, ...args: EventArgs[Name] ): FunctionExt.AsyncBoolean protected emit(name: any, ...args: any[]) { return this.trigger(name, ...args) } } export namespace Events { export type Handler = Args extends null | undefined ? () => any : Args extends any[] ? (...args: Args) => any : (args: Args) => any export type EventArgs = { [key: string]: any } export type EventNames = Extract /** * Get union type of keys from `M` that value matching `any[]`. */ export type NamesWithArrayArgs = RequiredKeys< PickByValue > export type NotArrayValueMap = OmitByValue export type OptionalNormalNames = OptionalKeys< NotArrayValueMap > export type RequiredNormalNames = RequiredKeys< NotArrayValueMap > export type OtherNames = EventNames< PickByValue > export type UnknownNames = Exclude> } namespace Private { export function call(list: any[], args?: any[]) { const results: any[] = [] for (let i = 0; i < list.length; i += 2) { const handler = list[i] const context = list[i + 1] const params = Array.isArray(args) ? args : [args] const ret = FunctionExt.apply(handler, context, params) results.push(ret) } return FunctionExt.toAsyncBoolean(results) } }