UNPKG

futoin-asyncevent

Version:

FutoIn AsyncEvent - FTN15 compliant event emitter

230 lines (199 loc) 6.81 kB
'use strict'; /** * @file * * Copyright 2017 FutoIn Project (https://futoin.org) * Copyright 2017 Andrey Galkin <andrey@futoin.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const $as = require( 'futoin-asyncsteps' ); const SYM_EVENT_EMITTER = Symbol( 'FutoIn Event Emitter' ); const ON_PREFIX = '_evt_'; const ONCE_PREFIX = '_evtonce_'; /** * @callback Handler * @param {...any} args - arbitrary arguments */ /** * Asynchronous Event Emitter. * * Please avoid inheriting it, use EventEmitter.attach() instead! */ class EventEmitter { constructor( allowed_events, max_listeners ) { this._max = max_listeners; this._scheduleCall = $as.ActiveAsyncTool.callImmediate; for ( let evt of allowed_events ) { this[`${ON_PREFIX}${evt}`] = []; this[`${ONCE_PREFIX}${evt}`] = []; } } /** * Attach event handler. * @param {string} evt - preconfigured event name * @param {Handler} handler - async event handler */ on( evt, handler ) { try { const hlist = this[`${ON_PREFIX}${evt}`]; if ( hlist.length === this._max ) { console.warn( `Hitting max handler limit for: ${evt}` ); } hlist.push( handler ); } catch { throw new Error( `Unknown event: ${evt}` ); } } /** * Attach once-only event handler. * @param {string} evt - preconfigured event name * @param {Handler} handler - async event handler */ once( evt, handler ) { try { const hlist = this[`${ONCE_PREFIX}${evt}`]; if ( hlist.length === this._max ) { console.warn( `Hitting max once handler limit for: ${evt}` ); } hlist.push( handler ); } catch { throw new Error( `Unknown event: ${evt}` ); } } /** * Remove event handler. * @param {string} evt - preconfigured event name * @param {Handler} handler - async event handler */ off( evt, handler ) { try { const memb = `${ON_PREFIX}${evt}`; this[memb] = this[memb].filter( ( h ) => h !== handler ); const memb_once = `${ONCE_PREFIX}${evt}`; this[memb_once] = this[memb_once].filter( ( h ) => h !== handler ); } catch { throw new Error( `Unknown event: ${evt}` ); } } /** * Emit async event. * @param {string} evt - event name * @param {...any} args - arguments to pass to the event handler */ emit( evt, ...args ) { const handlers = this[`${ON_PREFIX}${evt}`]; const once_handlers = this[`${ONCE_PREFIX}${evt}`]; if ( handlers === undefined ) { throw new Error( `Unknown event: ${evt}` ); } const call_list = []; for ( let h of handlers ) { call_list.push( h ); } if ( once_handlers.length ) { for ( let h of once_handlers ) { call_list.push( h ); } this[`${ONCE_PREFIX}${evt}`] = []; } this._scheduleCall( () => { // --- for ( let h of call_list ) { try { h( ...args ); } catch ( e ) { // let runtime deal with exceptions this._scheduleCall( () => { throw e; } ); } } } ); } static get SYM_EVENT_EMITTER() { return SYM_EVENT_EMITTER; } static [Symbol.hasInstance]( instance ) { return ( ( instance[SYM_EVENT_EMITTER] !== undefined ) || ( instance.constructor === EventEmitter ) ); } /** * Attach event emitter to any instance * @param {object} instance - target object * @param {Array} allowed_events - list of allowed event names * @param {number} [max_listeners] - maximum allowed handlers per event name */ static attach( instance, allowed_events, max_listeners=8 ) { const old_ee = instance[SYM_EVENT_EMITTER]; if ( old_ee !== undefined ) { old_ee._max = max_listeners; for ( let evt of allowed_events ) { if ( `${ON_PREFIX}${evt}` in old_ee ) { throw new Error( `Event "${evt}" has been already registered!` ); } else { old_ee[`${ON_PREFIX}${evt}`] = []; old_ee[`${ONCE_PREFIX}${evt}`] = []; } } return; } const ee = new EventEmitter( allowed_events, max_listeners ); Object.defineProperties( instance, { [SYM_EVENT_EMITTER]: { configurable: false, enumerable: false, writable: false, value: ee, }, on: { configurable: false, enumerable: false, writable: false, value: ( evt, handler ) => ee.on( evt, handler ), }, off: { configurable: false, enumerable: false, writable: false, value: ( evt, handler ) => ee.off( evt, handler ), }, once: { configurable: false, enumerable: false, writable: false, value: ( evt, handler ) => ee.once( evt, handler ), }, emit: { configurable: false, enumerable: false, writable: false, value: ( evt, ...args ) => ee.emit( evt, ...args ), }, }, ); } /** * Dynamically update max listener count * @param {object} instance - target object * @param {number} max_listeners - maximum allowed handlers per event name */ static setMaxListeners( instance, max_listeners ) { instance[SYM_EVENT_EMITTER]._max = max_listeners; } } module.exports = EventEmitter;