UNPKG

ts-event-bus

Version:
444 lines (364 loc) 12.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Transport = void 0; var _Handler = require("./Handler"); let _ID = 0; const getId = () => `${_ID++}`; const assertNever = a => { throw new Error(`Should not happen: ${a}`); }; const ERRORS = { TIMED_OUT: 'TIMED_OUT', REMOTE_CONNECTION_CLOSED: 'REMOTE_CONNECTION_CLOSED', CHANNEL_NOT_READY: 'CHANNEL_NOT_READY' }; class Transport { /** * Handlers created by the Transport. When an event is triggered locally, * these handlers will make a request to the far end to handle this event, * and store a PendingRequest */ /** * Callbacks provided by each slot allowing to register remote handlers * created by the Transport */ /** * Callbacks provided by each slot allowing to unregister the remote handlers * created by the Transport, typically when the remote connection is closed. */ /** * Callbacks provided by each slot allowing to remove blacklisted events * declaration from the remote handlers. */ /** * Requests that have been sent to the far end, but have yet to be fulfilled */ constructor(_channel, ignoredEvents) { this._channel = _channel; this._localHandlers = {}; this._localHandlerRegistrations = {}; this._remoteHandlers = {}; this._remoteHandlerRegistrationCallbacks = {}; this._remoteHandlerDeletionCallbacks = {}; this._remoteIgnoredEventsCallbacks = {}; this._pendingRequests = {}; this._channelReady = false; this._channel.onData(message => { switch (message.type) { case 'request': return this._requestReceived(message); case 'response': return this._responseReceived(message); case 'handler_registered': return this._registerRemoteHandler(message); case 'handler_unregistered': return this._unregisterRemoteHandler(message); case 'error': return this._errorReceived(message); case 'event_list': return this._remoteIgnoredEventsReceived(message); default: assertNever(message); } }); this._channel.onConnect(() => { this._channelReady = true; // When the far end connects, signal which local handlers are set Object.keys(this._localHandlerRegistrations).forEach(param => { this._localHandlerRegistrations[param].forEach(msg => { this._channel.send(msg); }); }); // Also send the list of events this end is not interested in so the // far end can know when to wait or not for this end to be ready // when triggering a specific slot. This is necessary only when some // events have been listed as ignored when calling createEventBus if (ignoredEvents) { this._channel.send({ type: "event_list", ignoredEvents }); } }); this._channel.onDisconnect(() => { this._channelReady = false; // When the far end disconnects, remove all the handlers it had set this._unregisterAllRemoteHandlers(); this._rejectAllPendingRequests(new Error(`${ERRORS.REMOTE_CONNECTION_CLOSED}`)); }); // When an error happens on the channel, reject all pending requests // (their integrity cannot be guaranteed since onError does not link // the error to a requestId) this._channel.onError(e => this._rejectAllPendingRequests(e)); } /** * This event is triggered when events have been listed as ignored by the far * end. It will call onRegister on ignored events' handlers to fake their * registration so this end doesn't wait on the far end to have registered * them to be able to trigger them. */ _remoteIgnoredEventsReceived({ ignoredEvents }) { Object.keys(this._remoteIgnoredEventsCallbacks).forEach(slotName => { if (ignoredEvents.includes(slotName)) { this._remoteIgnoredEventsCallbacks[slotName](); } }); } /** * When a request is received from the far end, call all the local subscribers, * and send either a response or an error mirroring the request id, * depending on the status of the resulting promise */ _requestReceived({ slotName, data, id, param }) { // Get local handlers const slotHandlers = this._localHandlers[slotName]; if (!slotHandlers) return; const handlers = slotHandlers[param]; if (!handlers) return; // Call local handlers with the request data (0, _Handler.callHandlers)(data, handlers) // If the resulting promise is fulfilled, send a response to the far end .then(async response => { await this.autoReconnect(); if (!this.isDisconnected()) { this._channel.send({ type: 'response', slotName, id, data: response, param }); } }) // If the resulting promise is rejected, send an error to the far end .catch(async error => { await this.autoReconnect(); if (!this.isDisconnected()) { this._channel.send({ id, message: `${error}`, param, slotName, stack: error.stack || '', type: 'error' }); } }); } /** * When a response is received from the far end, resolve the pending promise * with the received data */ _responseReceived({ slotName, data, id, param }) { const slotRequests = this._pendingRequests[slotName]; if (!slotRequests || !slotRequests[param] || !slotRequests[param][id]) { return; } slotRequests[param][id].resolve(data); delete slotRequests[param][id]; } /** * When an error is received from the far end, reject the pending promise * with the received data */ _errorReceived({ slotName, id, message, stack, param }) { const slotRequests = this._pendingRequests[slotName]; if (!slotRequests || !slotRequests[param] || !slotRequests[param][id]) return; const error = new Error(`${message} on ${slotName} with param ${param}`); error.stack = stack || error.stack; this._pendingRequests[slotName][param][id].reject(error); delete this._pendingRequests[slotName][param][id]; } /** * When the far end signals that a handler has been added for a given slot, * add a handler on our end. When called, this handler will send a request * to the far end, and keep references to the returned Promise's resolution * and rejection function * */ _registerRemoteHandler({ slotName, param }) { const addHandler = this._remoteHandlerRegistrationCallbacks[slotName]; if (!addHandler) return; const slotHandlers = this._remoteHandlers[slotName]; if (slotHandlers && slotHandlers[param]) return; const remoteHandler = requestData => new Promise((resolve, reject) => { // If the channel is not ready, reject immediately // TODO think of a better (buffering...) solution in the future if (!this._channelReady) { return reject(new Error(`${ERRORS.CHANNEL_NOT_READY} on ${slotName}`)); } // Keep a reference to the pending promise's // resolution and rejection callbacks const id = getId(); this._pendingRequests[slotName] = this._pendingRequests[slotName] || {}; this._pendingRequests[slotName][param] = this._pendingRequests[slotName][param] || {}; this._pendingRequests[slotName][param][id] = { resolve, reject }; // Send a request to the far end this._channel.send({ type: 'request', id, slotName, param, data: requestData }); // Handle request timeout if needed setTimeout(() => { const slotHandlers = this._pendingRequests[slotName] || {}; const paramHandlers = slotHandlers[param] || {}; const request = paramHandlers[id]; if (request) { const error = new Error(`${ERRORS.TIMED_OUT} on ${slotName} with param ${param}`); request.reject(error); delete this._pendingRequests[slotName][param][id]; } }, this._channel.timeout); }); this._remoteHandlers[slotName] = this._remoteHandlers[slotName] || {}; this._remoteHandlers[slotName][param] = remoteHandler; addHandler(param, remoteHandler); } _unregisterRemoteHandler({ slotName, param }) { const unregisterRemoteHandler = this._remoteHandlerDeletionCallbacks[slotName]; const slotHandlers = this._remoteHandlers[slotName]; if (!slotHandlers) return; const remoteHandler = slotHandlers[param]; if (remoteHandler && unregisterRemoteHandler) { unregisterRemoteHandler(param, remoteHandler); delete this._remoteHandlers[slotName][param]; } } _unregisterAllRemoteHandlers() { Object.keys(this._remoteHandlerDeletionCallbacks).forEach(slotName => { const slotHandlers = this._remoteHandlers[slotName]; if (!slotHandlers) return; const params = Object.keys(slotHandlers).filter(param => slotHandlers[param]); params.forEach(param => this._unregisterRemoteHandler({ slotName, param })); }); } _rejectAllPendingRequests(e) { Object.keys(this._pendingRequests).forEach(slotName => { Object.keys(this._pendingRequests[slotName]).forEach(param => { Object.keys(this._pendingRequests[slotName][param]).forEach(id => { this._pendingRequests[slotName][param][id].reject(e); }); }); this._pendingRequests[slotName] = {}; }); } addRemoteHandlerRegistrationCallback(slotName, addLocalHandler) { if (!this._remoteHandlerRegistrationCallbacks[slotName]) { this._remoteHandlerRegistrationCallbacks[slotName] = addLocalHandler; } } addRemoteHandlerUnregistrationCallback(slotName, removeHandler) { if (!this._remoteHandlerDeletionCallbacks[slotName]) { this._remoteHandlerDeletionCallbacks[slotName] = removeHandler; } } addRemoteEventListChangedListener(slotName, eventListChangedListener) { if (!this._remoteIgnoredEventsCallbacks[slotName]) { this._remoteIgnoredEventsCallbacks[slotName] = eventListChangedListener; } } /** * Called when a local handler is registered, to send a `handler_registered` * message to the far end. */ registerHandler(slotName, param, handler) { this._localHandlers[slotName] = this._localHandlers[slotName] || {}; this._localHandlers[slotName][param] = this._localHandlers[slotName][param] || []; this._localHandlers[slotName][param].push(handler); /** * We notify the far end when adding the first handler only, as they * only need to know if at least one handler is connected. */ if (this._localHandlers[slotName][param].length === 1) { const registrationMessage = { type: 'handler_registered', param, slotName }; this._localHandlerRegistrations[param] = this._localHandlerRegistrations[param] || []; this._localHandlerRegistrations[param].push(registrationMessage); if (this._channelReady) { this._channel.send(registrationMessage); } } } /** * Called when a local handler is unregistered, to send a `handler_unregistered` * message to the far end. */ unregisterHandler(slotName, param, handler) { const slotLocalHandlers = this._localHandlers[slotName]; if (slotLocalHandlers && slotLocalHandlers[param]) { const ix = slotLocalHandlers[param].indexOf(handler); if (ix > -1) { slotLocalHandlers[param].splice(ix, 1); /** * We notify the far end when removing the last handler only, as they * only need to know if at least one handler is connected. */ if (slotLocalHandlers[param].length === 0) { const unregistrationMessage = { type: 'handler_unregistered', param, slotName }; if (this._channelReady) { this._channel.send(unregistrationMessage); } } } } } /** * Allows to know the transport status and to perform a reconnection * * @returns {boolean} Transport's channel connection status, true if disconnected, otherwise false */ isDisconnected() { return !this._channelReady; } /** * Auto-reconnect the channel * see Slot.trigger function for usage * * @returns {Promise} A promise resolving when the connection is established */ autoReconnect() { if (this.isDisconnected() && this._channel.autoReconnect) { const promise = new Promise(resolve => { this._channel.onConnect(() => { return resolve(); }); }); this._channel.autoReconnect(); return promise; } return Promise.resolve(); } } exports.Transport = Transport;