require("dotenv").config() import path from "node:path" import EventEmitter from "@foxify/events" import { Observable } from "@gullerya/object-observer" import IPCRouter from "linebridge/dist/classes/IPCRouter" import { onExit } from "signal-exit" import chalk from "chalk" import treeKill from "tree-kill" import scanServices from "./utils/scanServices" import RELP from "./repl" import comtyAscii from "./ascii" import pkg from "../package.json" import ServiceManager from "./services/manager" import Service from "./services/service" import * as Managers from "./managers" const isProduction = process.env.NODE_ENV === "production" const sslKey = path.resolve(process.cwd(), ".ssl", "privkey.pem") const sslCert = path.resolve(process.cwd(), ".ssl", "cert.pem") /** * Gateway class - Main entry point for the service orchestrator * Manages service discovery, spawning, and communication */ export default class Gateway { static gatewayMode = process.env.GATEWAY_MODE ?? "http_proxy" eventBus = new EventEmitter() state = { proxyPort: process.env.GATEWAY_LISTEN_PORT ?? 9000, internalIp: "0.0.0.0", allReady: false, } selectedService = null serviceManager = new ServiceManager() services = [] serviceRegistry = Observable.from({}) gateway = null ipcRouter = null /** * Creates service registry entries based on discovered services */ async createServicesRegistry() { for await (const servicePath of this.services) { const instanceFile = path.basename(servicePath) const instanceBasePath = path.dirname(servicePath) const servicePkg = require( path.resolve(instanceBasePath, "package.json"), ) this.serviceRegistry[servicePkg.name] = { index: this.services.indexOf(servicePath), id: servicePkg.name, version: servicePkg.version, file: instanceFile, cwd: instanceBasePath, ready: false, } } } /** * Creates and initializes all service instances */ async createServiceInstances() { for await (const servicePath of this.services) { const instanceBasePath = path.dirname(servicePath) const servicePkg = require( path.resolve(instanceBasePath, "package.json"), ) const serviceId = servicePkg.name const serviceConfig = { id: serviceId, version: servicePkg.version, path: servicePath, cwd: instanceBasePath, isProduction, internalIp: this.state.internalIp, } // Create service instance const service = new Service(serviceConfig, { onReady: this.onServiceReady.bind(this), onIPCData: this.onServiceIPCData.bind(this), onServiceExit: this.onServiceExit.bind(this), }) // Add to service manager this.serviceManager.addService(service) // Initialize service await service.initialize() console.log(`šŸ“¦ [${serviceId}] Service initialized`) } } /** * Handler for service ready event * @param {Service} service - Service that is ready */ onServiceReady(service) { const serviceId = service.id this.serviceRegistry[serviceId].initialized = true this.serviceRegistry[serviceId].ready = true console.log( `āœ… [${serviceId}][${this.serviceRegistry[serviceId].index}] Ready`, ) // Check if all services are ready this.checkAllServicesReady() } /** * Handler for service IPC data * @param {Service} service - Service sending the data * @param {object} data - IPC data received */ async onServiceIPCData(service, data) { const id = service.id if (data.type === "log") { console.log(`[${id}] ${data.message}`) } if (data.status === "ready") { this.onServiceReady(service) } if (data.type === "service:register") { await this.handleServiceRegistration(service, data) } } /** * Handler for service exit * @param {Service} service - Service that exited * @param {number} code - Exit code * @param {Error} error - Error if any */ onServiceExit(service, code, error) { const id = service.id this.serviceRegistry[id].initialized = true this.serviceRegistry[id].ready = false console.log(`[${id}] Exit with code ${code}`) if (error) { console.error(error) } if (typeof this.gateway.unregisterAllFromService === "function") { this.gateway.unregisterAllFromService(id) } } /** * Handle both router and websocket registration requests from services * @param {Service} service - Service registering a route or websocket * @param {object} msg - Registration message * @param {boolean} isWebsocket - Whether this is a websocket registration */ async handleServiceRegistration(service, data) { const { id } = service const { namespace, http, websocket, listen } = data.register if (http && http.enabled === true && Array.isArray(http.paths)) { for (const path of http.paths) { await this.gateway.register({ serviceId: id, path: path, target: `${http.proto}://${listen.ip}:${listen.port}${path}`, }) } } if (websocket && websocket.enabled === true) { await this.gateway.register({ serviceId: id, websocket: true, path: websocket.path, target: `${http.proto}://${listen.ip}:${listen.port}${websocket.path}`, }) } if (this.state.allReady) { if (typeof this.gateway.applyConfiguration === "function") { await this.gateway.applyConfiguration() } if (typeof this.gateway.reload === "function") { await this.gateway.reload() } } } /** * Check if all services are ready and trigger the ready event */ checkAllServicesReady() { if (this.state.allReady) return const allServicesInitialized = Object.values( this.serviceRegistry, ).every((service) => service.initialized) if (allServicesInitialized) { this.state.allReady = true this.onAllServicesReady() } } /** * Reload all services */ reloadAllServices = () => { this.serviceManager.reloadAllServices() } /** * Handle when all services are ready */ onAllServicesReady = async () => { //console.clear() //console.log(comtyAscii) console.log("\n\n\n") console.log(`šŸŽ‰ All services[${this.services.length}] ready!\n`) console.log(`USE: select , reload, exit`) if (typeof this.gateway.applyConfiguration === "function") { await this.gateway.applyConfiguration() } if (typeof this.gateway.start === "function") { await this.gateway.start() } } /** * Clean up resources on gateway exit */ onGatewayExit = () => { //console.clear() console.log(`\nšŸ›‘ Preparing to exit...`) if (typeof this.gateway.stop === "function") { console.log(`Stopping gateway...`) this.gateway.stop() } console.log(`Stopping all services...`) this.serviceManager.stopAllServices() treeKill(process.pid) } /** * Initialize the gateway and start all services */ async initialize() { if (!Managers[this.constructor.gatewayMode]) { console.error( `āŒ Gateway mode [${this.constructor.gatewayMode}] not supported`, ) return 0 } onExit(this.onGatewayExit) // Increase limits to handle many services process.stdout.setMaxListeners(150) process.stderr.setMaxListeners(150) this.services = await scanServices() this.ipcRouter = new IPCRouter() if (this.services.length === 0) { console.error("āŒ No services found") return process.exit(1) } // Initialize gateway this.gateway = new Managers[this.constructor.gatewayMode]({ port: this.state.proxyPort, internalIp: this.state.internalIp, cert_file_name: sslCert, key_file_name: sslKey, }) if (typeof this.gateway.initialize === "function") { await this.gateway.initialize() } // Make key components available globally global.eventBus = this.eventBus global.ipcRouter = this.ipcRouter global.proxy = this.proxy //console.clear() console.log(comtyAscii) console.log( `\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${this.state.internalIp} | ${isProduction ? "production" : "development"} \n\n\n`, ) console.log(`šŸ“¦ Found ${this.services.length} service(s)`) // Watch for service state changes Observable.observe(this.serviceRegistry, (changes) => { this.checkAllServicesReady() }) await this.createServicesRegistry() await this.createServiceInstances() // WARNING: Starting relp makes uwebsockets unable to work properly, surging some bugs from nodejs (domain.enter) // use another alternative to parse commands, like stdin reading or something... //this.startRELP() } startRELP() { // Initialize REPL interface new RELP({ attachAllServicesSTD: () => this.serviceManager.attachAllServicesStd(), detachAllServicesSTD: () => this.serviceManager.detachAllServicesStd(), attachServiceSTD: (id) => this.serviceManager.attachServiceStd(id), dettachServiceSTD: (id) => this.serviceManager.detachServiceStd(id), reloadService: () => { const selectedService = this.serviceManager.getSelectedService() if (!selectedService) { console.error(`No service selected`) return false } if (selectedService === "all") { return this.reloadAllServices() } return selectedService.reload() }, onAllServicesReady: this.onAllServicesReady, }) } }