ts-event-bus
Version:
Distributed messaging in Typescript
298 lines (239 loc) • 11.5 kB
JavaScript
;
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
});
}