UNPKG

ts-event-bus

Version:
298 lines (239 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.slot = slot; exports.connectSlot = connectSlot; exports.defaultSlotConfig = void 0; var _Handler = require("./Handler"); var _Constants = require("./Constants"); const signalNotConnected = () => { throw new Error('Slot not connected'); }; const defaultSlotConfig = { noBuffer: false, autoReconnect: true }; exports.defaultSlotConfig = defaultSlotConfig; const getNotConnectedSlot = config => Object.assign(() => signalNotConnected(), { config, lazy: () => signalNotConnected, on: () => signalNotConnected, slotName: 'Not connected' }); // Key to store local handlers in the `handlers` map const LOCAL_TRANSPORT = 'LOCAL_TRANSPORT'; // Type to store handlers, by transport, by param // Find handlers for given param accross transports const getParamHandlers = (param, handlers) => Object.keys(handlers).reduce((paramHandlers, transportKey) => { return paramHandlers.concat(handlers[transportKey][param] || []); }, []); // Find all params with registered callbacks const findAllUsedParams = handlers => Object.keys(handlers).reduce((params, transportKey) => { const transportHandlers = handlers[transportKey]; const registeredParams = Object.keys(transportHandlers).filter(param => (transportHandlers[param] || []).length > 0); const paramsMaybeDuplicate = [...params, ...registeredParams]; const paramsUniq = [...new Set(paramsMaybeDuplicate)]; return paramsUniq; }, []); /** * Represents an event shared by two modules. * * A module can trigger the event by calling the slot. This will return a promise, * which will be resolved with the response sent by the other module if applicable. * * The slot can also be subscribed to, by using the `on` property. */ /** * A shorthand function used to declare slots in event bus object literals * It returns a fake slot, that will throw if triggered or subscribed to. * Slots need to be connected in order to be functional. */ function slot(config = defaultSlotConfig) { return getNotConnectedSlot(config); } function connectSlot(slotName, transports, config = {}) { /* * ======================== * Internals * ======================== */ // These will be all the handlers for this slot, for each transport, for each param const handlers = transports.reduce((acc, _t, ix) => ({ ...acc, [ix]: {} }), { [LOCAL_TRANSPORT]: {} }); // For each transport we create a Promise that will be fulfilled only // when the far-end has registered a handler. // This prevents `triggers` from firing *before* any far-end is listening. const remoteHandlersConnected = transports.reduce((acc, _t, transportKey) => ({ ...acc, [transportKey]: {} }), {}); const awaitHandlerRegistration = (transportKey, param) => { let onHandlerRegistered = () => {}; const remoteHandlerRegistered = new Promise(resolve => onHandlerRegistered = resolve); remoteHandlersConnected[transportKey][param] = { registered: remoteHandlerRegistered, onRegister: onHandlerRegistered }; }; // Lazy callbacks const lazyConnectCallbacks = []; const lazyDisonnectCallbacks = []; const callLazyConnectCallbacks = param => lazyConnectCallbacks.forEach(c => c(param)); const callLazyDisonnectCallbacks = param => lazyDisonnectCallbacks.forEach(c => c(param)); // Signal to all transports that we will accept handlers for this slotName transports.forEach((transport, transportKey) => { const remoteHandlerRegistered = (param = _Constants.DEFAULT_PARAM, handler) => { // If the remote end of the communication channel had blacklisted an // event but is now trying to register a handler. We ignore it and // consider the blacklist to be the source of truth if (!remoteHandlersConnected[transportKey]) { return; } // Store handler const paramHandlers = handlers[transportKey][param] || []; handlers[transportKey][param] = paramHandlers.concat(handler); // Call lazy callbacks if needed if (getParamHandlers(param, handlers).length === 1) { callLazyConnectCallbacks(param); } // Release potential buffered events if (!remoteHandlersConnected[transportKey][param]) { awaitHandlerRegistration(String(transportKey), param); } // call onRegister callback on slot for each transport. It will // release the event once triggered. If one is not registered then // event will not be sent. remoteHandlersConnected[transportKey][param].onRegister(); }; const remoteHandlerUnregistered = (param = _Constants.DEFAULT_PARAM, handler) => { const paramHandlers = handlers[transportKey][param] || []; const handlerIndex = paramHandlers.indexOf(handler); if (handlerIndex > -1) handlers[transportKey][param].splice(handlerIndex, 1); if (getParamHandlers(param, handlers).length === 0) callLazyDisonnectCallbacks(param); if (remoteHandlersConnected[transportKey]) awaitHandlerRegistration(String(transportKey), param); }; const remoteEventListChangedListener = () => { // Because the remote end communicated a blacklist of event it does // not want to listen, and because the local end has registered a // handler for the remote end for this blacklisted events. The local // ends need to: // 1 - resolve the onRegister promise // 2 - remove the useless handler from the remote handlers list if (remoteHandlersConnected[transportKey]) { Object.keys(remoteHandlersConnected[transportKey]).forEach(param => { remoteHandlersConnected[transportKey][param].onRegister(); }); } delete remoteHandlersConnected[transportKey]; }; transport.addRemoteHandlerRegistrationCallback(slotName, remoteHandlerRegistered); transport.addRemoteHandlerUnregistrationCallback(slotName, remoteHandlerUnregistered); transport.addRemoteEventListChangedListener(slotName, remoteEventListChangedListener); }); /* * ======================== * API * ======================== */ /* * Sends data through the slot. */ // Signature for Slot(<data>) using default param // Signature for Slot(<param>, <data>) // Combined signatures function trigger(firstArg, secondArg) { const paramUsed = arguments.length === 2; const data = paramUsed ? secondArg : firstArg; const param = paramUsed ? firstArg : _Constants.DEFAULT_PARAM; // Called when all transports are ready: // 1. When only the LOCAL_TRANSPORT exists // 2. With noBuffer option and all transport channels are connected // 3. Without noBuffer option and all transport channels are connected and handler registered const callHandlersWithParameters = () => { const allParamHandlers = getParamHandlers(param, handlers); return (0, _Handler.callHandlers)(data, allParamHandlers); }; // In this case: only the LOCAL_TRANSPORT handler is defined, // we don't need to check any connection or buffering status if (transports.length === 0) { return callHandlersWithParameters(); } // Autoreconnect disconnected transports // The transport's handler will be called when the remote one will be registered // (default connect and registration flow with awaitHandlerRegistration) const transportConnectionPromises = []; if (config.autoReconnect) { transports.forEach(_t => { // Connection status is handle into autoReconnect method transportConnectionPromises.push(_t.autoReconnect()); }); } // In case of noBuffer config we wait all connections are established before calling the handlers // NOTE: there is a conceptual issue here, as all resolved response from all transports are expected // if one transport failed, the trigger initiator won't receive an answer if (config.noBuffer) { return Promise.all(transportConnectionPromises).then(() => { return callHandlersWithParameters(); }); } else { transports.forEach((_t, transportKey) => { if (remoteHandlersConnected[transportKey] && !remoteHandlersConnected[transportKey][param]) { awaitHandlerRegistration(String(transportKey), param); } }); const transportPromises = transports.reduce((acc, _t, transportKey) => { var _ref; return [...acc, ...((_ref = remoteHandlersConnected[transportKey] && [remoteHandlersConnected[transportKey][param].registered]) !== null && _ref !== void 0 ? _ref : [])]; }, []); return Promise.all(transportPromises).then(() => { return callHandlersWithParameters(); }); } } /* * Allows a client to be notified when a first * client connects to the slot with `.on`, and when the * last client disconnects from it. */ function lazy(firstClientConnectCallback, lastClientDisconnectCallback) { lazyConnectCallbacks.push(firstClientConnectCallback); lazyDisonnectCallbacks.push(lastClientDisconnectCallback); // Call connect callback immediately if handlers were already registered findAllUsedParams(handlers).forEach(firstClientConnectCallback); return () => { // Call disconnect callback findAllUsedParams(handlers).forEach(lastClientDisconnectCallback); // Stop lazy connect and disconnect processes const connectIx = lazyConnectCallbacks.indexOf(firstClientConnectCallback); if (connectIx > -1) lazyConnectCallbacks.splice(connectIx, 1); const disconnectIx = lazyDisonnectCallbacks.indexOf(lastClientDisconnectCallback); if (disconnectIx > -1) lazyDisonnectCallbacks.splice(disconnectIx, 1); }; } /* * Allows a client to be notified when someone * sends data through the slot. */ // Signature for Slot.on(<handler>) using default param // Signature for Slot.on(<param>, <handler>) // Combined signatures function on(paramOrHandler, handlerIfParam) { // Get param and handler from arguments, depending if param was passed or not let param = ""; let handler = () => new Promise(r => r()); if (typeof paramOrHandler === 'string') { param = paramOrHandler; handler = handlerIfParam || handler; } else { param = _Constants.DEFAULT_PARAM; handler = paramOrHandler; } // Register a remote handler with all of our remote transports transports.forEach(t => t.registerHandler(slotName, param, handler)); // Store this handler handlers[LOCAL_TRANSPORT][param] = (handlers[LOCAL_TRANSPORT][param] || []).concat(handler); // Call lazy connect callbacks if there is at least one handler const paramHandlers = getParamHandlers(param, handlers); if (paramHandlers.length === 1) callLazyConnectCallbacks(param); // Return the unsubscription function return () => { // Unregister remote handler with all of our remote transports transports.forEach(t => t.unregisterHandler(slotName, param, handler)); const localParamHandlers = handlers[LOCAL_TRANSPORT][param] || []; const ix = localParamHandlers.indexOf(handler); if (ix !== -1) handlers[LOCAL_TRANSPORT][param].splice(ix, 1); // Call lazy disconnect callbacks if there are no handlers anymore const paramHandlers = getParamHandlers(param, handlers); if (paramHandlers.length === 0) callLazyDisonnectCallbacks(param); }; } return Object.assign(trigger, { on, lazy, config, slotName }); }