@minimaltech/node-infra
Version:
Minimal Technology NodeJS Infrastructure - Loopback 4 Framework
320 lines • 14.7 kB
JavaScript
"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