moleculer-api
Version:
A dynamic API Gateway for MoleculerJS which updates REST endpoints and aggregated GraphQL schema, access control policy for each action calls from metadata of remote services schema without restart or deployment.
167 lines • 8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServerWebSocketApplication = void 0;
const tslib_1 = require("tslib");
const _ = tslib_1.__importStar(require("lodash"));
const ws_1 = tslib_1.__importDefault(require("ws"));
const url_1 = tslib_1.__importDefault(require("url"));
const qs_1 = tslib_1.__importDefault(require("qs"));
const context_1 = require("../../context");
const component_1 = require("../component");
const route_1 = require("./route");
class ServerWebSocketApplication extends component_1.ServerApplicationComponent {
constructor(props, opts) {
super(props);
this.Route = route_1.WebSocketRoute;
this.routeHandlerConnectionHandlersMap = new Map();
this.opts = _.defaultsDeep(opts || {}, {
perMessageDeflate: false,
clientTracking: true,
contextCreationTimeout: 100,
pingPongCheckInterval: 5000,
});
const _a = this.opts, { contextCreationTimeout, pingPongCheckInterval } = _a, serverOpts = tslib_1.__rest(_a, ["contextCreationTimeout", "pingPongCheckInterval"]);
// create WebSocket.Server without http.Server instance
const server = new ws_1.default.Server(Object.assign(Object.assign({}, serverOpts), { noServer: true }));
// attach upgrade handler which will be mounted as server protocols "upgrade" event handler
const upgradeEventHandler = (req, tcpSocket, head) => {
// handle upgrade with ws module and emit connection to web socket
server.handleUpgrade(req, tcpSocket, head, socket => {
// proxy socket error to server
socket.on("error", error => {
if (server.listenerCount("error") > 0) {
server.emit("error", error, socket, req);
}
else {
this.props.logger.error(error);
}
});
// emit CONNECTION
server.emit("connection", socket, req);
if (socket.readyState !== socket.OPEN) { // close by middleware or somewhere
return;
}
// trick: if context not being created yet, assume it there are no matched handler
if (context_1.APIRequestContext.isCreating(req)) {
// route matched, start ping-pong
socket.__isAlive = true;
socket.on("pong", () => {
socket.__isAlive = true;
});
}
else {
// route not matched throw error
const error = new Error("not found websocket route"); // TODO: normalize error
if (server.listenerCount("error") > 0) {
server.emit("error", error, socket, req);
}
else {
this.props.logger.error(error);
}
socket.close();
}
});
// TERMINATE dangling sockets
if (this.pingPongCheckIntervalTimer) {
clearInterval(this.pingPongCheckIntervalTimer);
}
this.pingPongCheckIntervalTimer = setInterval(() => {
server.clients.forEach(socket => {
if (socket.__isAlive === true) {
socket.__isAlive = false;
socket.ping();
}
else if (socket.__isAlive === false) {
socket.terminate();
}
else {
// do nothing when __isAlive is undefined yet
}
});
}, pingPongCheckInterval);
};
this.module = Object.assign(server, { upgradeEventHandler });
this.module.setMaxListeners(100); // TODO: this number is quite arbitrary...
}
start() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
// ...
});
}
stop() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (this.pingPongCheckIntervalTimer) {
clearInterval(this.pingPongCheckIntervalTimer);
}
this.module.removeAllListeners();
this.routeHandlerConnectionHandlersMap.clear();
});
}
mountRoutes(routes, pathPrefixes, createContext) {
// create new array to store connection handlers
const connectionHandlers = [];
// create routeHandlerMap for this routes
const routeHandlerMap = new Map();
// link routeHandlerMap to express.Router for the time to unmount
this.routeHandlerConnectionHandlersMap.set(routeHandlerMap, connectionHandlers);
// mount each routes
for (const route of routes) {
// internal handler should extract context and pass context to external handler
const pathRegExps = route.getPathRegExps(pathPrefixes);
// tslint:disable-next-line:no-shadowed-variable
const routeHandler = (socket, req) => tslib_1.__awaiter(this, void 0, void 0, function* () {
const { pathname, query } = url_1.default.parse(req.url || "/");
for (const regExp of pathRegExps) {
const match = regExp.exec(pathname);
if (match) {
// create context
try {
const context = yield createContext(req);
socket.once("close", () => context.clear());
// req.params
req.params = route.paramKeys.reduce((obj, key, i) => {
obj[key.name] = match[i + 1];
return obj;
}, {});
// req.path
req.path = pathname;
// req.query
// use qs module to be along with http component: https://github.com/expressjs/express/blob/3ed5090ca91f6a387e66370d57ead94d886275e1/lib/middleware/query.js#L34
req.query = query ? qs_1.default.parse(query, { allowPrototypes: true }) : {};
// call handler
route.handler(context, socket, req);
}
catch (error) {
socket.emit("error", error);
}
break;
}
}
});
// mount handler to ws.Server
this.module.on("connection", routeHandler);
this.props.logger.debug(`${route} mounted on ${pathPrefixes.join(", ")}`);
// store connection handler to unmount later
connectionHandlers.push(routeHandler);
// store route and handler to map
routeHandlerMap.set(route, routeHandler);
}
return routeHandlerMap;
}
unmountRoutes(routeHandlerMap) {
const connectionHandlers = this.routeHandlerConnectionHandlersMap.get(routeHandlerMap);
if (!connectionHandlers) {
this.props.logger.error(`cannot find io.connectionHandlers matched for given RouteHandlerMap`, routeHandlerMap); // TODO: normalize error for all logger.error
return;
}
// unmount connection handlers
for (const handler of connectionHandlers) {
this.module.removeListener("connection", handler);
}
// forget the routeHandlerMap
this.routeHandlerConnectionHandlersMap.delete(routeHandlerMap);
}
}
exports.ServerWebSocketApplication = ServerWebSocketApplication;
ServerWebSocketApplication.key = "ws";
//# sourceMappingURL=component.js.map