ts-event-bus
Version:
Distributed messaging in Typescript
444 lines (364 loc) • 12.9 kB
JavaScript
"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;