linebridge/src/server/server.js

423 lines
13 KiB
JavaScript
Executable File

import("./patches")
import fs from "node:fs"
import path from "node:path"
import { EventEmitter } from "@foxify/events"
import Endpoint from "./classes/endpoint"
import defaults from "./defaults"
import IPCClient from "./classes/IPCClient"
async function loadEngine(engine) {
const enginesPath = path.resolve(__dirname, "engines")
const selectedEnginePath = path.resolve(enginesPath, engine)
if (!fs.existsSync(selectedEnginePath)) {
throw new Error(`Engine ${engine} not found!`)
}
return require(selectedEnginePath).default
}
class Server {
constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) {
this.isExperimental = defaults.isExperimental ?? false
if (this.isExperimental) {
console.warn("🚧 This version of Linebridge is experimental! 🚧")
}
this.params = {
...defaults.params,
...params.default ?? params,
}
this.controllers = {
...controllers.default ?? controllers,
}
this.middlewares = {
...middlewares.default ?? middlewares,
}
this.headers = {
...defaults.headers,
...headers.default ?? headers,
}
this.valid_http_methods = defaults.valid_http_methods
// fix and fulfill params
this.params.useMiddlewares = this.params.useMiddlewares ?? []
this.params.name = this.constructor.refName ?? this.params.refName
this.params.useEngine = this.constructor.useEngine ?? this.params.useEngine ?? "express"
this.params.listen_ip = this.constructor.listenIp ?? this.constructor.listen_ip ?? this.params.listen_ip ?? "0.0.0.0"
this.params.listen_port = this.constructor.listenPort ?? this.constructor.listen_port ?? this.params.listen_port ?? 3000
this.params.http_protocol = this.params.http_protocol ?? "http"
this.params.http_address = `${this.params.http_protocol}://${defaults.localhost_address}:${this.params.listen_port}`
this.params.disableWebSockets = this.constructor.disableWebSockets ?? this.params.disableWebSockets ?? false
this.params.routesPath = this.constructor.routesPath ?? this.params.routesPath
this.params.wsRoutesPath = this.constructor.wsRoutesPath ?? this.params.wsRoutesPath
return this
}
engine = null
events = null
ipc = null
ipcEvents = null
eventBus = new EventEmitter()
initialize = async () => {
const startHrTime = process.hrtime()
// register events
if (this.events) {
if (this.events.default) {
this.events = this.events.default
}
for (const [eventName, eventHandler] of Object.entries(this.events)) {
this.eventBus.on(eventName, eventHandler)
}
}
const engineParams = {
...this.params,
handleWsAuth: this.handleWsAuth,
handleAuth: this.handleHttpAuth,
requireAuth: this.constructor.requireHttpAuth,
refName: this.constructor.refName ?? this.params.refName,
}
// initialize engine
this.engine = await loadEngine(this.params.useEngine)
this.engine = new this.engine(engineParams)
if (typeof this.engine.init === "function") {
await this.engine.init(engineParams)
}
// create a router map
if (typeof this.engine.router.map !== "object") {
this.engine.router.map = {}
}
// try to execute onInitialize hook
if (typeof this.onInitialize === "function") {
await this.onInitialize()
}
// set server defined headers
this.useDefaultHeaders()
// set server defined middlewares
this.useDefaultMiddlewares()
// register controllers
await this.initializeControllers()
// register routes
await this.initializeRoutes()
// register main index endpoint `/`
await this.registerBaseEndpoints()
// use main router
await this.engine.app.use(this.engine.router)
// initialize websocket init hook if needed
if (this.engine.ws) {
if (typeof this.engine.ws?.initialize == "function") {
await this.engine.ws.initialize({
redisInstance: this.redis
})
}
}
// if is a linebridge service then initialize IPC Channels
if (process.env.lb_service) {
await this.initializeIpc()
}
// try to execute beforeInitialize hook.
if (typeof this.afterInitialize === "function") {
await this.afterInitialize()
}
// listen
await this.engine.listen(engineParams)
// calculate elapsed time on ms, to fixed 2
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1e3 + elapsedHrTime[1] / 1e6
console.info(`🛰 Server ready!\n\t - ${this.params.http_protocol}://${this.params.listen_ip}:${this.params.listen_port} \n\t - Tooks ${elapsedTimeInMs.toFixed(2)}ms`)
}
initializeIpc = async () => {
console.info("🚄 Starting IPC client")
this.ipc = global.ipc = new IPCClient(this, process)
}
useDefaultHeaders = () => {
this.engine.app.use((req, res, next) => {
Object.keys(this.headers).forEach((key) => {
res.setHeader(key, this.headers[key])
})
next()
})
}
useDefaultMiddlewares = async () => {
const middlewares = await this.resolveMiddlewares([
...this.params.useMiddlewares,
...defaults.useMiddlewares,
])
middlewares.forEach((middleware) => {
this.engine.app.use(middleware)
})
}
initializeControllers = async () => {
const controllers = Object.entries(this.controllers)
for await (let [key, controller] of controllers) {
if (typeof controller !== "function") {
throw new Error(`Controller must use the controller class!`)
}
if (controller.disabled) {
console.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`)
continue
}
try {
const ControllerInstance = new controller()
// get endpoints from controller (ComplexController)
const HTTPEndpoints = ControllerInstance.__get_http_endpoints()
const WSEndpoints = ControllerInstance.__get_ws_endpoints()
HTTPEndpoints.forEach((endpoint) => {
this.register.http(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares))
})
// WSEndpoints.forEach((endpoint) => {
// this.registerWSEndpoint(endpoint)
// })
} catch (error) {
console.error(`\n\x1b[41m\x1b[37m🆘 [${controller.refName ?? controller.name}] Controller initialization failed:\x1b[0m ${error.stack} \n`)
}
}
}
initializeRoutes = async (filePath) => {
if (!this.params.routesPath) {
return false
}
const scanPath = filePath ?? this.params.routesPath
const files = fs.readdirSync(scanPath)
for await (const file of files) {
const filePath = `${scanPath}/${file}`
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
await this.initializeRoutes(filePath)
continue
} else if (file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".ts") || file.endsWith(".tsx")) {
let splitedFilePath = filePath.split("/")
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf("routes") + 1)
const method = splitedFilePath[splitedFilePath.length - 1].split(".")[0].toLocaleLowerCase()
splitedFilePath = splitedFilePath.slice(0, splitedFilePath.length - 1)
// parse parametrized routes
const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g
splitedFilePath = splitedFilePath.map((route) => {
if (route.match(parametersRegex)) {
route = route.replace(parametersRegex, ":$1")
}
route = route.replace("[$]", "*")
return route
})
let route = splitedFilePath.join("/")
route = route.replace(".jsx", "")
route = route.replace(".js", "")
route = route.replace(".ts", "")
route = route.replace(".tsx", "")
if (route.endsWith("/index")) {
route = route.replace("/index", "")
}
route = `/${route}`
// import route
let routeFile = require(filePath)
routeFile = routeFile.default ?? routeFile
if (typeof routeFile !== "function") {
if (!routeFile.fn) {
console.warn(`Missing fn handler in [${method}][${route}]`)
continue
}
if (Array.isArray(routeFile.useContext)) {
let contexts = {}
for (const context of routeFile.useContext) {
contexts[context] = this.contexts[context]
}
routeFile.contexts = contexts
routeFile.fn.bind({ contexts })
}
}
new Endpoint(
this,
{
route: route,
enabled: true,
middlewares: routeFile.middlewares,
handlers: {
[method]: routeFile.fn ?? routeFile,
}
}
)
continue
}
}
}
register = {
http: (endpoint, ..._middlewares) => {
// check and fix method
endpoint.method = endpoint.method?.toLowerCase() ?? "get"
if (defaults.fixed_http_methods[endpoint.method]) {
endpoint.method = defaults.fixed_http_methods[endpoint.method]
}
// check if method is supported
if (typeof this.engine.router[endpoint.method] !== "function") {
throw new Error(`Method [${endpoint.method}] is not supported!`)
}
// grab the middlewares
let middlewares = [..._middlewares]
if (endpoint.middlewares) {
middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)]
}
this.engine.router.map[endpoint.route] = {
method: endpoint.method,
path: endpoint.route,
}
// register endpoint to http interface router
this.engine.router[endpoint.method](endpoint.route, ...middlewares, endpoint.fn)
},
ws: (endpoint, ...execs) => {
endpoint.nsp = endpoint.nsp ?? "/main"
this.websocket_instance.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch])
this.websocket_instance.map[endpoint.on] = {
nsp: endpoint.nsp,
channel: endpoint.on,
}
},
}
async registerBaseEndpoints() {
if (this.params.disableBaseEndpoint) {
console.warn("‼️ [disableBaseEndpoint] Base endpoint is disabled! Endpoints mapping will not be available, so linebridge client bridges will not work! ‼️")
return false
}
const scanPath = path.join(__dirname, "baseEndpoints")
const files = fs.readdirSync(scanPath)
for await (const file of files) {
if (file === "index.js") {
continue
}
let endpoint = require(path.join(scanPath, file)).default
new endpoint(this)
}
}
resolveMiddlewares = (requestedMiddlewares) => {
const middlewares = {
...this.middlewares,
...defaults.middlewares,
}
requestedMiddlewares = Array.isArray(requestedMiddlewares) ? requestedMiddlewares : [requestedMiddlewares]
const execs = []
requestedMiddlewares.forEach((middlewareKey) => {
if (typeof middlewareKey === "string") {
if (typeof middlewares[middlewareKey] !== "function") {
throw new Error(`Middleware ${middlewareKey} not found!`)
}
execs.push(middlewares[middlewareKey])
}
if (typeof middlewareKey === "function") {
execs.push(middlewareKey)
}
})
return execs
}
// Utilities
toogleEndpointReachability = (method, route, enabled) => {
if (typeof this.endpoints_map[method] !== "object") {
throw new Error(`Cannot toogle endpoint, method [${method}] not set!`)
}
if (typeof this.endpoints_map[method][route] !== "object") {
throw new Error(`Cannot toogle endpoint [${route}], is not registered!`)
}
this.endpoints_map[method][route].enabled = enabled ?? !this.endpoints_map[method][route].enabled
}
}
module.exports = Server