UNPKG

@minimaltech/node-infra

Version:

Minimal Technology NodeJS Infrastructure - Loopback 4 Framework

320 lines 14.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SocketIOServerHelper = void 0; const redis_adapter_1 = require("@socket.io/redis-adapter"); const redis_emitter_1 = require("@socket.io/redis-emitter"); const socket_io_1 = require("socket.io"); const constants_1 = require("../../../components/socket-io/common/constants"); const helpers_1 = require("../../../helpers"); const utilities_1 = require("../../../utilities"); const isEmpty_1 = __importDefault(require("lodash/isEmpty")); const CLIENT_AUTHENTICATE_TIMEOUT = 10 * 1000; // ------------------------------------------------------------------------------------------------------------- class SocketIOServerHelper { constructor(opts) { var _a, _b, _c; this.serverOptions = {}; this.logger = helpers_1.LoggerFactory.getLogger([SocketIOServerHelper.name]); this.clients = {}; this.identifier = opts.identifier; this.serverOptions = (_a = opts === null || opts === void 0 ? void 0 : opts.serverOptions) !== null && _a !== void 0 ? _a : {}; this.redisConnection = opts.redisConnection; this.authenticateFn = opts.authenticateFn; this.onClientConnected = opts.clientConnectedFn; this.authenticateTimeout = (_b = opts.authenticateTimeout) !== null && _b !== void 0 ? _b : CLIENT_AUTHENTICATE_TIMEOUT; this.defaultRooms = (_c = opts.defaultRooms) !== null && _c !== void 0 ? _c : [ constants_1.SocketIOConstants.ROOM_DEFAULT, constants_1.SocketIOConstants.ROOM_NOTIFICATION, ]; if (!opts.server) { throw (0, utilities_1.getError)({ statusCode: 500, message: '[SocketIOServerHelper] Invalid server and lb-application to initialize io-socket server!', }); } this.server = opts.server; // Establish redis connection if (!this.redisConnection) { throw (0, utilities_1.getError)({ statusCode: 500, message: 'Invalid redis connection to config socket.io adapter!', }); } this.configure(); } // ------------------------------------------------------------------------------------------------------------- getIOServer() { return this.io; } // ------------------------------------------------------------------------------------------------------------- getClients(opts) { if (opts === null || opts === void 0 ? void 0 : opts.id) { return this.clients[opts.id]; } return this.clients; } // ------------------------------------------------------------------------------------------------------------- on(opts) { const { topic, handler } = opts; if (!topic || !handler) { throw (0, utilities_1.getError)({ message: '[on] Invalid topic or event handler!' }); } if (!this.io) { throw (0, utilities_1.getError)({ message: '[on] IOServer is not initialized yet!' }); } this.io.on(topic, handler); } // ------------------------------------------------------------------------------------------------------------- configure() { var _a, _b, _c, _d; this.logger.info('[configure][%s] Configuring IO Server', this.identifier); if (!this.server) { throw (0, utilities_1.getError)({ statusCode: 500, message: '[DANGER] Invalid server instance to init Socket.io server!', }); } this.io = new socket_io_1.Server(this.server, this.serverOptions); // Config socket.io redis adapter this.io.adapter((0, redis_adapter_1.createAdapter)(this.redisConnection.getClient().duplicate(), // Redis PUB Client this.redisConnection.getClient().duplicate())); this.logger.info('[configure] SocketIO Server initialized Redis Adapter'); // Config socket.io redis emitter this.emitter = new redis_emitter_1.Emitter(this.redisConnection.getClient().duplicate()); this.emitter.redisClient.on('error', (error) => { this.logger.error('[configure][Emitter] On Error: %j', error); }); this.logger.info('[configure] SocketIO Server initialized Redis Emitter!'); // Handle socket.io new connection this.io.on(constants_1.SocketIOConstants.EVENT_CONNECT, (socket) => { this.onClientConnect({ socket }); }); this.logger.info('[configure] SocketIO Server READY | Path: %s | Address: %j', (_b = (_a = this.serverOptions) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '', (_c = this.server) === null || _c === void 0 ? void 0 : _c.address()); this.logger.debug('[configure] Whether http listening: %s', (_d = this.server) === null || _d === void 0 ? void 0 : _d.listening); } // ------------------------------------------------------------------------------------------------------------- onClientConnect(opts) { const { socket } = opts; if (!socket) { this.logger.info('[onClientConnect] Invalid new socket connection!'); return; } // Validate user identifier const { id, handshake } = socket; const { headers } = handshake; if (this.clients[id]) { this.logger.info('[onClientConnect] Socket client already existed: %j', { id, headers, }); return; } this.logger.info('[onClientConnect] New connection request with options: %j', { id, headers }); this.clients[id] = { id, socket, state: 'unauthorized', authenticateTimeout: setTimeout(() => { var _a; if (((_a = this.clients[id]) === null || _a === void 0 ? void 0 : _a.state) === 'authenticated') { return; } this.disconnect({ socket }); }, this.authenticateTimeout), }; socket.on(constants_1.SocketIOConstants.EVENT_AUTHENTICATE, () => { this.clients[id].state = 'authenticating'; this.authenticateFn(handshake) .then(rs => { this.logger.info('[onClientAuthenticate] Socket: %s | Authenticate result: %s', id, rs); // Valid connection if (rs) { this.onClientAuthenticated({ socket }); return; } // Invalid connection this.clients[id].state = 'unauthorized'; this.send({ destination: socket.id, payload: { topic: constants_1.SocketIOConstants.EVENT_UNAUTHENTICATE, data: { message: 'Invalid token token authenticate! Please login again!', time: new Date().toISOString(), }, }, cb: () => { this.disconnect({ socket }); }, }); }) .catch(error => { // Unexpected error while authenticating connection this.clients[id].state = 'unauthorized'; this.logger.error('[onClientConnect] Connection: %s | Failed to authenticate new socket connection | Error: %s', id, error); this.send({ destination: socket.id, payload: { topic: constants_1.SocketIOConstants.EVENT_UNAUTHENTICATE, data: { message: 'Failed to authenticate connection! Please login again!', time: new Date().toISOString(), }, }, doLog: true, cb: () => { this.disconnect({ socket }); }, }); }); }); } // ------------------------------------------------------------------------------------------------------------- onClientAuthenticated(opts) { var _a, _b; const { socket } = opts; if (!socket) { this.logger.info('[onClientAuthenticated] Invalid new socket connection!'); return; } // Validate user identifier const { id } = socket; if (!this.clients[id]) { this.logger.info('[onClientAuthenticated] Unknown client id %s to continue!', id); this.disconnect({ socket }); return; } this.clients[id].state = 'authenticated'; this.ping({ socket, doIgnoreAuth: true }); // Valid connection this.logger.info('[onClientAuthenticated] Connection: %s | Identifier: %s | CONNECTED | Time: %s', id, this.identifier, new Date().toISOString()); Promise.all(this.defaultRooms.map((room) => socket.join(room))) .then(() => { this.logger.info('[onClientAuthenticated] Connection %s joined all defaultRooms %s', id, this.defaultRooms); }) .catch(error => { this.logger.error('[onClientAuthenticated] Connection %s failed to join defaultRooms %s | Error: %s', id, this.defaultRooms, error); }); // Handle events socket.on(constants_1.SocketIOConstants.EVENT_DISCONNECT, () => { this.disconnect({ socket }); }); socket.on(constants_1.SocketIOConstants.EVENT_JOIN, (payload) => { const { rooms = [] } = payload || {}; if (!(rooms === null || rooms === void 0 ? void 0 : rooms.length)) { return; } Promise.all(rooms.map((room) => socket.join(room))) .then(() => { this.logger.info('[%s] Connection: %s joined all rooms %s', constants_1.SocketIOConstants.EVENT_JOIN, id, rooms); }) .catch(error => { this.logger.error('[%s] Connection %s failed to join rooms %s | Error: %s', constants_1.SocketIOConstants.EVENT_JOIN, id, rooms, error); }); this.logger.info('[%s] Connection: %s | JOIN Rooms: %j', constants_1.SocketIOConstants.EVENT_JOIN, id, rooms); }); socket.on(constants_1.SocketIOConstants.EVENT_LEAVE, (payload) => { const { rooms = [] } = payload || { room: [] }; if (!(rooms === null || rooms === void 0 ? void 0 : rooms.length)) { return; } Promise.all(rooms.map((room) => socket.leave(room))) .then(() => { this.logger.info('[%s] Connection %s left all rooms %s', constants_1.SocketIOConstants.EVENT_LEAVE, id, rooms); }) .catch(error => { this.logger.error('[%s] Connection %s failed to leave rooms %s | Error: %s', constants_1.SocketIOConstants.EVENT_LEAVE, id, rooms, error); }); this.logger.info('[%s] Connection: %s | LEAVE Rooms: %j', constants_1.SocketIOConstants.EVENT_LEAVE, id, rooms); }); this.clients[id].interval = setInterval(() => { this.ping({ socket, doIgnoreAuth: true }); }, 30000); this.send({ destination: socket.id, payload: { topic: constants_1.SocketIOConstants.EVENT_AUTHENTICATED, data: { id: socket.id, time: new Date().toISOString(), }, }, // log: true, }); (_b = (_a = this.onClientConnected) === null || _a === void 0 ? void 0 : _a.call(this, { socket })) === null || _b === void 0 ? void 0 : _b.then(() => { }).catch(error => { this.logger.error('[onClientConnected][Handler] Error: %s', error); }); } // ------------------------------------------------------------------------------------------------------------- ping(opts) { const { socket, doIgnoreAuth } = opts; if (!socket) { this.logger.info('[ping] Socket is undefined to PING!'); return; } const client = this.clients[socket.id]; if (!doIgnoreAuth && client.state !== 'authenticated') { this.logger.info('[ping] Socket client is not authenticated | Authenticated: %s', client.state); this.disconnect({ socket }); return; } this.send({ destination: socket.id, payload: { topic: constants_1.SocketIOConstants.EVENT_PING, data: { time: new Date().toISOString(), }, }, // log: true, }); } // ------------------------------------------------------------------------------------------------------------- disconnect(opts) { const { socket } = opts; if (!socket) { return; } const { id } = socket; if (this.clients[id]) { const { interval, authenticateTimeout } = this.clients[id]; if (interval) { clearInterval(interval); } if (authenticateTimeout) { clearTimeout(authenticateTimeout); } delete this.clients[id]; } this.logger.info('[disconnect] Connection: %s | DISCONNECT | Time: %s', id, new Date().toISOString()); socket.disconnect(); } // ------------------------------------------------------------------------------------------------------------- send(opts) { const { destination, payload, doLog, cb } = opts; if (!payload) { return; } const { topic, data } = payload; if (!topic || !data) { return; } const sender = this.emitter.compress(true); if (destination && !(0, isEmpty_1.default)(destination)) { sender.to(destination).emit(topic, data); } else { sender.emit(topic, data); } cb === null || cb === void 0 ? void 0 : cb(); if (!doLog) { return; } this.logger.info(`[send] Message has emitted! To: ${destination} | Topic: ${topic} | Message: ${JSON.stringify(data)}`); } } exports.SocketIOServerHelper = SocketIOServerHelper; //# sourceMappingURL=socket-io-server.helper.js.map