UNPKG

@soketi/soketi

Version:

Just another simple, fast, and resilient open-source WebSockets server.

537 lines (536 loc) 21.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Server = void 0; const dot = require("dot-wild"); const adapters_1 = require("./adapters"); const app_managers_1 = require("./app-managers"); const cache_manager_1 = require("./cache-managers/cache-manager"); const http_handler_1 = require("./http-handler"); const log_1 = require("./log"); const metrics_1 = require("./metrics"); const queue_1 = require("./queues/queue"); const rate_limiter_1 = require("./rate-limiters/rate-limiter"); const uuid_1 = require("uuid"); const webhook_sender_1 = require("./webhook-sender"); const ws_handler_1 = require("./ws-handler"); const Discover = require('node-discover'); const queryString = require('query-string'); const uWS = require('uWebSockets.js'); class Server { constructor(options = {}) { this.options = { adapter: { driver: 'local', redis: { requestsTimeout: 5000, prefix: '', redisPubOptions: {}, redisSubOptions: {}, clusterMode: false, }, cluster: { requestsTimeout: 5000, }, nats: { requestsTimeout: 5000, prefix: '', servers: ['127.0.0.1:4222'], user: null, pass: null, token: null, timeout: 10000, nodesNumber: null, }, }, appManager: { driver: 'array', cache: { enabled: false, ttl: -1, }, array: { apps: [ { id: 'app-id', key: 'app-key', secret: 'app-secret', maxConnections: -1, enableClientMessages: false, enabled: true, maxBackendEventsPerSecond: -1, maxClientEventsPerSecond: -1, maxReadRequestsPerSecond: -1, webhooks: [], }, ], }, dynamodb: { table: 'apps', region: 'us-east-1', endpoint: null, }, mysql: { table: 'apps', version: '8.0', useMysql2: false, }, postgres: { table: 'apps', version: '13.3', }, }, cache: { driver: 'memory', redis: { redisOptions: {}, clusterMode: false, }, }, channelLimits: { maxNameLength: 200, cacheTtl: 3600, }, cluster: { hostname: '0.0.0.0', helloInterval: 500, checkInterval: 500, nodeTimeout: 2000, masterTimeout: 2000, port: 11002, prefix: '', ignoreProcess: true, broadcast: '255.255.255.255', unicast: null, multicast: null, }, cors: { credentials: true, origin: ['*'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: [ 'Origin', 'Content-Type', 'X-Auth-Token', 'X-Requested-With', 'Accept', 'Authorization', 'X-CSRF-TOKEN', 'XSRF-TOKEN', 'X-Socket-Id', ], }, database: { mysql: { host: '127.0.0.1', port: 3306, user: 'root', password: 'password', database: 'main', }, postgres: { host: '127.0.0.1', port: 5432, user: 'postgres', password: 'password', database: 'main', }, redis: { host: '127.0.0.1', port: 6379, db: 0, username: null, password: null, keyPrefix: '', sentinels: null, sentinelPassword: null, name: 'mymaster', clusterNodes: [], }, }, databasePooling: { enabled: false, min: 0, max: 7, }, debug: false, eventLimits: { maxChannelsAtOnce: 100, maxNameLength: 200, maxPayloadInKb: 100, maxBatchSize: 10, }, host: '0.0.0.0', httpApi: { requestLimitInMb: 100, acceptTraffic: { memoryThreshold: 85, }, }, instance: { process_id: process.pid || (0, uuid_1.v4)(), }, metrics: { enabled: false, driver: 'prometheus', host: '0.0.0.0', prometheus: { prefix: 'soketi_', }, port: 9601, }, mode: 'full', port: 6001, pathPrefix: '', presence: { maxMembersPerChannel: 100, maxMemberSizeInKb: 2, }, queue: { driver: 'sync', redis: { concurrency: 1, redisOptions: {}, clusterMode: false, }, sqs: { region: 'us-east-1', endpoint: null, clientOptions: {}, consumerOptions: {}, queueUrl: '', processBatch: false, batchSize: 1, pollingWaitTimeMs: 0, }, }, rateLimiter: { driver: 'local', redis: { redisOptions: {}, clusterMode: false, }, }, shutdownGracePeriod: 3000, ssl: { certPath: '', keyPath: '', passphrase: '', caPath: '', }, userAuthenticationTimeout: 30000, webhooks: { batching: { enabled: false, duration: 50, }, }, }; this.closing = false; this.pm2 = false; this.nodes = new Map(); this.setOptions(options); } static async start(options = {}, callback) { return (new Server(options)).start(callback); } async start(callback) { log_1.Log.br(); this.configureDiscovery().then(() => { this.initializeDrivers().then(() => { if (this.options.debug) { console.dir(this.options, { depth: 100 }); } this.wsHandler = new ws_handler_1.WsHandler(this); this.httpHandler = new http_handler_1.HttpHandler(this); if (this.options.debug) { log_1.Log.info('📡 soketi initialization....'); log_1.Log.info('⚡ Initializing the HTTP API & Websockets Server...'); } let server = this.shouldConfigureSsl() ? uWS.SSLApp({ key_file_name: this.options.ssl.keyPath, cert_file_name: this.options.ssl.certPath, passphrase: this.options.ssl.passphrase, ca_file_name: this.options.ssl.caPath, }) : uWS.App(); let metricsServer = uWS.App(); if (this.options.debug) { log_1.Log.info('⚡ Initializing the Websocket listeners and channels...'); } this.configureWebsockets(server).then(server => { if (this.options.debug) { log_1.Log.info('⚡ Initializing the HTTP webserver...'); } this.configureHttp(server).then(server => { this.configureMetricsServer(metricsServer).then(metricsServer => { metricsServer.listen(this.options.metrics.host, this.options.metrics.port, metricsServerProcess => { this.metricsServerProcess = metricsServerProcess; server.listen(this.options.host, this.options.port, serverProcess => { this.serverProcess = serverProcess; log_1.Log.successTitle('🎉 Server is up and running!'); log_1.Log.successTitle(`📡 The Websockets server is available at 127.0.0.1:${this.options.port}`); log_1.Log.successTitle(`🔗 The HTTP API server is available at http://127.0.0.1:${this.options.port}`); log_1.Log.successTitle(`🎊 The /usage endpoint is available on port ${this.options.metrics.port}.`); if (this.options.metrics.enabled) { log_1.Log.successTitle(`🌠 Prometheus /metrics endpoint is available on port ${this.options.metrics.port}.`); } log_1.Log.br(); if (callback) { callback(this); } }); }); }); }); }); }); }); } stop() { this.closing = true; log_1.Log.br(); log_1.Log.warning('🚫 New users cannot connect to this instance anymore. Preparing for signaling...'); log_1.Log.warning('⚡ The server is closing and signaling the existing connections to terminate.'); log_1.Log.br(); return this.wsHandler.closeAllLocalSockets().then(() => { return new Promise(resolve => { if (this.options.debug) { log_1.Log.warningTitle('⚡ All sockets were closed. Now closing the server.'); } setTimeout(() => { Promise.all([ this.metricsManager.clear(), this.queueManager.disconnect(), this.rateLimiter.disconnect(), this.cacheManager.disconnect(), ]).then(() => { this.adapter.disconnect().then(() => { if (this.serverProcess) { uWS.us_listen_socket_close(this.serverProcess); } if (this.metricsServerProcess) { uWS.us_listen_socket_close(this.metricsServerProcess); } }).then(() => resolve()); }); }, this.options.shutdownGracePeriod); }); }); } setOptions(options) { for (let optionKey in options) { if (optionKey.match("^appManager.array.apps.\\d+.id")) { if (Number.isInteger(options[optionKey])) { options[optionKey] = options[optionKey].toString(); } } this.options = dot.set(this.options, optionKey, options[optionKey]); } } initializeDrivers() { return Promise.all([ this.setAppManager(new app_managers_1.AppManager(this)), this.setAdapter(new adapters_1.Adapter(this)), this.setMetricsManager(new metrics_1.Metrics(this)), this.setRateLimiter(new rate_limiter_1.RateLimiter(this)), this.setQueueManager(new queue_1.Queue(this)), this.setCacheManager(new cache_manager_1.CacheManager(this)), this.setWebhookSender(), ]); } setAppManager(instance) { this.appManager = instance; } setAdapter(instance) { return new Promise(resolve => { instance.init().then(() => { this.adapter = instance; resolve(); }); }); } setMetricsManager(instance) { return new Promise(resolve => { this.metricsManager = instance; resolve(); }); } setRateLimiter(instance) { return new Promise(resolve => { this.rateLimiter = instance; resolve(); }); } setQueueManager(instance) { return new Promise(resolve => { this.queueManager = instance; resolve(); }); } setCacheManager(instance) { return new Promise(resolve => { this.cacheManager = instance; resolve(); }); } setWebhookSender() { return new Promise(resolve => { this.webhookSender = new webhook_sender_1.WebhookSender(this); resolve(); }); } url(path) { return this.options.pathPrefix + path; } clusterPrefix(channel) { if (this.options.cluster.prefix) { channel = this.options.cluster.prefix + '#' + channel; } return channel; } configureDiscovery() { return new Promise(resolve => { this.discover = Discover(this.options.cluster, () => { this.nodes.set('self', this.discover.me); this.discover.on('promotion', () => { this.nodes.set('self', this.discover.me); if (this.options.debug) { log_1.Log.discoverTitle('Promoted from node to master.'); log_1.Log.discover(this.discover.me); } }); this.discover.on('demotion', () => { this.nodes.set('self', this.discover.me); if (this.options.debug) { log_1.Log.discoverTitle('Demoted from master to node.'); log_1.Log.discover(this.discover.me); } }); this.discover.on('added', (node) => { this.nodes.set('self', this.discover.me); this.nodes.set(node.id, node); if (this.options.debug) { log_1.Log.discoverTitle('New node added.'); log_1.Log.discover(node); } }); this.discover.on('removed', (node) => { this.nodes.set('self', this.discover.me); this.nodes.delete(node.id); if (this.options.debug) { log_1.Log.discoverTitle('Node removed.'); log_1.Log.discover(node); } }); this.discover.on('master', (node) => { this.nodes.set('self', this.discover.me); this.nodes.set(node.id, node); if (this.options.debug) { log_1.Log.discoverTitle('New master.'); log_1.Log.discover(node); } }); resolve(); }); }); } configureWebsockets(server) { return new Promise(resolve => { if (this.canProcessRequests()) { server = server.ws(this.url('/app/:id'), { idleTimeout: 120, maxBackpressure: 1024 * 1024, maxPayloadLength: 100 * 1024 * 1024, message: (ws, message, isBinary) => this.wsHandler.onMessage(ws, message, isBinary), open: (ws) => this.wsHandler.onOpen(ws), close: (ws, code, message) => this.wsHandler.onClose(ws, code, message), upgrade: (res, req, context) => this.wsHandler.handleUpgrade(res, req, context), }); } resolve(server); }); } configureHttp(server) { return new Promise(resolve => { server.get(this.url('/'), (res, req) => this.httpHandler.healthCheck(res)); server.get(this.url('/ready'), (res, req) => this.httpHandler.ready(res)); if (this.canProcessRequests()) { server.get(this.url('/accept-traffic'), (res, req) => this.httpHandler.acceptTraffic(res)); server.get(this.url('/apps/:appId/channels'), (res, req) => { res.params = { appId: req.getParameter(0) }; res.query = queryString.parse(req.getQuery()); res.method = req.getMethod().toUpperCase(); res.url = req.getUrl(); return this.httpHandler.channels(res); }); server.get(this.url('/apps/:appId/channels/:channelName'), (res, req) => { res.params = { appId: req.getParameter(0), channel: req.getParameter(1) }; res.query = queryString.parse(req.getQuery()); res.method = req.getMethod().toUpperCase(); res.url = req.getUrl(); return this.httpHandler.channel(res); }); server.get(this.url('/apps/:appId/channels/:channelName/users'), (res, req) => { res.params = { appId: req.getParameter(0), channel: req.getParameter(1) }; res.query = queryString.parse(req.getQuery()); res.method = req.getMethod().toUpperCase(); res.url = req.getUrl(); return this.httpHandler.channelUsers(res); }); server.post(this.url('/apps/:appId/events'), (res, req) => { res.params = { appId: req.getParameter(0) }; res.query = queryString.parse(req.getQuery()); res.method = req.getMethod().toUpperCase(); res.url = req.getUrl(); return this.httpHandler.events(res); }); server.post(this.url('/apps/:appId/batch_events'), (res, req) => { res.params = { appId: req.getParameter(0) }; res.query = queryString.parse(req.getQuery()); res.method = req.getMethod().toUpperCase(); res.url = req.getUrl(); return this.httpHandler.batchEvents(res); }); server.post(this.url('/apps/:appId/users/:userId/terminate_connections'), (res, req) => { res.params = { appId: req.getParameter(0), userId: req.getParameter(1) }; res.query = queryString.parse(req.getQuery()); res.method = req.getMethod().toUpperCase(); res.url = req.getUrl(); return this.httpHandler.terminateUserConnections(res); }); } server.any(this.url('/*'), (res, req) => { return this.httpHandler.notFound(res); }); resolve(server); }); } configureMetricsServer(metricsServer) { return new Promise(resolve => { log_1.Log.info('🕵️‍♂️ Initiating metrics endpoints...'); log_1.Log.br(); metricsServer.get(this.url('/'), (res, req) => this.httpHandler.healthCheck(res)); metricsServer.get(this.url('/ready'), (res, req) => this.httpHandler.ready(res)); metricsServer.get(this.url('/usage'), (res, req) => this.httpHandler.usage(res)); if (this.options.metrics.enabled) { metricsServer.get(this.url('/metrics'), (res, req) => { res.query = queryString.parse(req.getQuery()); return this.httpHandler.metrics(res); }); } resolve(metricsServer); }); } shouldConfigureSsl() { return this.options.ssl.certPath !== '' || this.options.ssl.keyPath !== ''; } canProcessQueues() { return ['worker', 'full'].includes(this.options.mode); } canProcessRequests() { return ['server', 'full'].includes(this.options.mode); } } exports.Server = Server;