reimplement initializators

This commit is contained in:
SrGooglo 2024-05-09 21:37:14 +00:00
parent bb2453ad76
commit f2076b5abe
9 changed files with 500 additions and 536 deletions

View File

@ -1,6 +1,6 @@
{
"name": "linebridge",
"version": "0.19.1",
"version": "0.20.0",
"description": "API Framework for RageStudio backends",
"author": "RageStudio",
"main": "./dist/client/index.js",
@ -33,16 +33,18 @@
"axios": "^1.6.7",
"axios-retry": "3.4.0",
"cors": "2.8.5",
"dotenv": "^16.4.4",
"express": "^4.18.3",
"hyper-express": "^6.14.12",
"hyper-express": "^6.16.1",
"ioredis": "^5.3.2",
"md5": "^2.3.0",
"module-alias": "2.2.2",
"morgan": "1.10.0",
"signal-exit": "^4.1.0",
"socket.io": "^4.7.4",
"socket.io-client": "4.5.4",
"uuid": "^9.0.1",
"sucrase": "^3.35.0"
"sucrase": "^3.35.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@ragestudio/hermes": "^0.1.0",

View File

@ -1,83 +1,147 @@
import cluster from "node:cluster"
import redis from "ioredis"
import SocketIO from "socket.io"
import { EventEmitter } from "@foxify/events"
import { createAdapter as createRedisAdapter } from "@socket.io/redis-adapter"
import { createAdapter as createClusterAdapter } from "@socket.io/cluster-adapter"
import { setupWorker } from "@socket.io/sticky"
import { Emitter } from "@socket.io/redis-emitter"
import RedisMap from "../../lib/redis_map"
export default class RTEngineServer {
constructor(params = {}) {
this.params = params
this.io = this.params.io ?? undefined
this.redis = this.params.redis ?? undefined
this.redisEmitter = null
this.clusterMode = !!cluster.isWorker
this.connections = null
this.users = null
this.redisConnParams = {
host: this.params.redisOptions?.host ?? process.env.REDIS_HOST ?? "localhost",
port: this.params.redisOptions?.port ?? process.env.REDIS_PORT ?? 6379,
username: this.params.redisOptions?.username ?? (process.env.REDIS_AUTH && process.env.REDIS_AUTH.split(":")[0]),
password: this.params.redisOptions?.password ?? (process.env.REDIS_AUTH && process.env.REDIS_AUTH.split(":")[1]),
db: this.params.redisOptions?.db ?? process.env.REDIS_DB ?? 0
}
this.redis = params.redis
this.io = params.io
}
worker_id = nanoid()
io = null
redis = null
connections = null
users = null
events = new Map()
async initialize() {
console.log("🌐 Initializing RTEngine server...")
if (!this.io) {
throw new Error("No io provided")
this.io = new SocketIO.Server({
path: this.params.root ?? "/",
})
}
if (!this.redis) {
this.redis = new redis({
host: this.redisConnParams.host,
port: this.redisConnParams.port,
username: this.redisConnParams.username,
password: this.redisConnParams.password,
db: this.redisConnParams.db,
})
}
// create mappers
this.connections = new RedisMap(this.redis, {
refKey: "connections",
worker_id: this.worker_id,
})
this.users = new RedisMap(this.redis, {
refKey: "users",
worker_id: this.worker_id,
})
// register middlewares
if (typeof this.middlewares === "object" && Array.isArray(this.middlewares)) {
for (const middleware of this.middlewares) {
this.io.use(middleware)
}
}
// handle connection
this.io.on("connection", (socket) => {
this.eventHandler(this.onConnect, socket)
})
console.log(`[RTEngine] Listening...`)
console.log(`[RTEngine] Universal worker id [${this.worker_id}]`)
return true
}
close = () => {
console.log(`Cleaning up RTEngine server...`)
// WARN: Do not flush connections pls
if (process.env.NODE_ENV !== "production") {
console.log(`Flushing previus connections... (Only for dev mode)`)
this.connections.flush()
}
if (this.clusterMode) {
this.connections.flush(cluster.worker.id)
}
if (this.io) {
this.io.close()
}
if (this.redis) {
this.redis.quit()
}
}
onConnect = async (socket) => {
console.log(`🤝 New client connected on socket id [${socket.id}]`)
console.log(`[RTEngine] new:client | id [${socket.id}]`)
socket.eventEmitter = new EventEmitter()
// create eventBus
socket.eventBus = new EventEmitter()
socket.pendingTimeouts = new Set()
// register events
if (typeof this.events === "object") {
for (const event in this.events) {
socket.on(event, (...args) => {
this.eventHandler(this.events[event], socket, ...args)
for (const [key, handler] of this.events.entries()) {
socket.on(key, (...args) => {
this.eventHandler(handler, socket, ...args)
})
}
}
socket.on("disconnect", (_socket) => {
// handle ping
socket.on("ping", () => {
socket.emit("pong")
})
// handle disconnect
socket.on("disconnect", () => {
this.eventHandler(this.onDisconnect, socket)
})
const conn_obj = {
id: socket.id,
}
if (this.clusterMode) {
conn_obj.worker_id = cluster.worker.id
conn_obj._remote = true
this.redisEmitter.serverSideEmit(`redis:conn:set`, conn_obj)
}
await this.connections.set(conn_obj.id, conn_obj)
console.log(`⚙️ Awaiting authentication for client [${socket.id}]`)
await this.connections.set(socket.id, socket)
if (this.params.requireAuth) {
await this.authenticateClient(socket, null, (this.params.handleAuth ?? this.handleAuth))
await this.onAuth(socket, null, (this.params.handleAuth ?? this.handleAuth))
} else if (socket.handshake.auth.token ?? socket.handshake.query.auth) {
await this.authenticateClient(socket, (socket.handshake.auth.token ?? socket.handshake.query.auth), (this.params.handleAuth ?? this.handleAuth))
}
if (process.env.NODE_ENV === "development") {
const connected_size = await this.connections.size()
console.log(`Total connected clients: ${connected_size}`)
await this.onAuth(socket, (socket.handshake.auth.token ?? socket.handshake.query.auth), (this.params.handleAuth ?? this.handleAuth))
}
}
onDisconnect = async (socket,) => {
console.log(`👋 Client disconnected on socket id [${socket.id}]`)
console.log(`[RTEngine] disconnect:client | id [${socket.id}]`)
if (socket.eventEmitter.emit) {
socket.eventEmitter.emit("disconnect")
if (socket.eventBus.emit) {
socket.eventBus.emit("disconnect")
} else {
console.warn(`[${socket.id}][@${socket.userData.username}] Cannot emit disconnect event`)
}
@ -91,15 +155,11 @@ export default class RTEngineServer {
}
await this.connections.del(socket.id)
const connected_size = await this.connections.size()
console.log(`Total connected clients: ${connected_size}`)
}
authenticateClient = async (socket, token, handleAuth) => {
onAuth = async (socket, token, handleAuth) => {
if (typeof handleAuth !== "function") {
console.warn(`Skipping authentication for client [${socket.id}] due no auth handler provided`)
console.log(`[RTEngine] [${socket.id}] No auth handler provided`)
return false
}
@ -113,7 +173,7 @@ export default class RTEngineServer {
}
function err(code, message) {
console.error(`🛑 Disconecting client [${socket.id}] cause an auth error >`, code, message)
console.log(`[RTEngine] [${socket.id}] Auth error: ${code} >`, message)
socket.emit("response:error", {
code,
@ -132,7 +192,7 @@ export default class RTEngineServer {
const authResult = await handleAuth(socket, token, err)
if (authResult) {
const conn = await this.connections.update(socket.id, authResult)
const conn = await this.connections.has(socket.id)
// check if connection update is valid to avoid race condition(When user disconnect before auth verification is completed)
if (!conn) {
@ -140,14 +200,29 @@ export default class RTEngineServer {
return false
}
this.users.set(authResult.user_id, {
this.users.set(authResult.user_id.toString(), {
socket_id: socket.id,
...authResult,
})
socket.emit("response:auth:ok")
console.log(`✅ Authenticated client [${socket.id}] as [@${authResult.username}]`)
console.log(`[RTEngine] client:authenticated | socket_id [${socket.id}] | user_id [${authResult.user_id}] | username [@${authResult.username}]`)
}
}
eventHandler = async (fn, socket, payload) => {
try {
await fn(socket, payload, this)
} catch (error) {
console.error(error)
if (typeof socket.emit === "function") {
socket.emit("response:error", {
code: 500,
message: error.message,
})
}
}
}
@ -167,144 +242,18 @@ export default class RTEngineServer {
userById: async (user_id) => {
const user = await this.users.get(user_id)
console.log(user)
return user
}
}
},
socketByUserId: async (user_id) => {
const user = await this.users.get(user_id)
eventHandler = async (fn, socket, ...args) => {
try {
await fn(socket, ...args)
} catch (error) {
console.error(error)
if (socket.emit) {
socket.emit("response:error", {
code: 500,
message: error.message,
})
if (!user) {
return null
}
}
}
registerBaseEndpoints = (socket) => {
if (!socket) {
const socket = await this.connections.get(user.socket_id)
return socket
}
socket.on("ping", () => {
socket.emit("pong")
})
return socket
}
async initialize({ host, port, username, password, db } = {}) {
console.log("🌐 Initializing RTEngine server...")
process.on("exit", this.cleanUp)
process.on("SIGINT", this.cleanUp)
process.on("SIGTERM", this.cleanUp)
process.on("SIGBREAK", this.cleanUp)
process.on("SIGHUP", this.cleanUp)
// fullfill args
if (typeof host === "undefined") {
host = this.params.redis?.host ?? process.env.REDIS_HOST ?? "localhost"
}
if (typeof port === "undefined") {
port = this.params.redis?.port ?? process.env.REDIS_PORT ?? 6379
}
if (typeof username === "undefined") {
username = this.params.redis?.username ?? process.env.REDIS_USERNAME ?? (process.env.REDIS_AUTH && process.env.REDIS_AUTH.split(":")[0])
}
if (typeof password === "undefined") {
password = this.params.redis?.password ?? process.env.REDIS_PASSWORD ?? (process.env.REDIS_AUTH && process.env.REDIS_AUTH.split(":")[1])
}
if (typeof db === "undefined") {
db = this.params.redis?.db ?? process.env.REDIS_DB ?? 0
}
// create default servers
if (typeof this.redis === "undefined") {
this.redis = new redis({
host,
port,
username: username,
password: password,
db: db,
})
}
// create mappers
this.connections = new RedisMap(this.redis, {
refKey: "connections",
})
this.users = new RedisMap(this.redis, {
refKey: "users",
})
// setup clustered mode
if (this.clusterMode) {
console.log(`Connecting to redis as cluster worker id [${cluster.worker.id}]`)
this.io.adapter(createClusterAdapter())
const subClient = this.redis.duplicate()
this.io.adapter(createRedisAdapter(this.redis, subClient))
setupWorker(this.io)
this.redisEmitter = new Emitter(this.redis)
}
// WARN: Do not flush connections pls
if (process.env.NODE_ENV !== "production") {
console.log(`Flushing previus connections... (Only for dev mode)`)
await this.connections.flush()
}
// register middlewares
if (typeof this.middlewares === "object" && Array.isArray(this.middlewares)) {
for (const middleware of this.middlewares) {
this.io.use(middleware)
}
}
for (const event in this._redisEvents) {
this.io.on(event, this._redisEvents[event])
}
this.io.on("connection", (socket) => {
this.registerBaseEndpoints(socket)
this.eventHandler(this.onConnect, socket)
})
if (typeof this.onInit === "function") {
await this.onInit()
}
console.log(`✅ RTEngine server is running on port [${this.params.listen_port}] ${this.clusterMode ? `on clustered mode [${cluster.worker.id}]` : ""}`)
return true
}
cleanUp = async () => {
console.log(`Cleaning up RTEngine server...`)
if (this.clusterMode) {
this.connections.flush(cluster.worker.id)
}
if (this.io) {
this.io.close()
}
}
}

View File

@ -1,6 +1,5 @@
import he from "hyper-express"
import rtengine from "../../classes/rtengine"
import SocketIO from "socket.io"
export default class Engine {
constructor(params) {
@ -13,11 +12,14 @@ export default class Engine {
router = new he.Router()
io = null
ws = null
init = async (params) => {
initialize = async (params) => {
// create a router map
if (typeof this.router.map !== "object") {
this.router.map = {}
}
// register 404
await this.router.any("*", (req, res) => {
return res.status(404).json({
@ -26,6 +28,14 @@ export default class Engine {
})
})
this.app.use((req, res, next) => {
if (req.method === "OPTIONS") {
return res.status(204).end()
}
next()
})
// register body parser
await this.app.use(async (req, res, next) => {
if (req.headers["content-type"]) {
@ -37,17 +47,15 @@ export default class Engine {
})
if (!params.disableWebSockets) {
this.io = new SocketIO.Server({
path: `/${params.refName}`,
})
this.io.attachApp(this.app.uws_instance)
this.ws = global.rtengine = new rtengine({
this.ws = global.websocket = new rtengine({
...params,
handleAuth: params.handleWsAuth,
io: this.io,
root: `/${params.refName}`
})
this.ws.initialize()
await this.ws.io.attachApp(this.app.uws_instance)
}
}
@ -106,10 +114,16 @@ export default class Engine {
}
close = async () => {
if (this.io) {
this.io.close()
if (this.ws.events) {
this.ws.clear()
}
await this.app.close()
if (typeof this.ws?.close === "function") {
await this.ws.close()
}
if (typeof this.app?.close === "function") {
await this.app.close()
}
}
}

View File

@ -0,0 +1,17 @@
import fs from "node:fs"
import path from "node:path"
export default async (ctx) => {
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(ctx)
}
}

View File

@ -0,0 +1,79 @@
import fs from "node:fs"
import Endpoint from "../../classes/endpoint"
import RecursiveRegister from "../../lib/recursiveRegister"
const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g
export default async (startDir, engine, ctx) => {
if (!fs.existsSync(startDir)) {
return engine
}
await RecursiveRegister({
start: startDir,
match: async (filePath) => {
return filePath.endsWith(".js") || filePath.endsWith(".ts")
},
onMatch: async ({ absolutePath, relativePath }) => {
const paths = relativePath.split("/")
let method = paths[paths.length - 1].split(".")[0].toLocaleLowerCase()
let route = paths.slice(0, paths.length - 1).join("/")
// parse parametrized routes
route = route.replace(parametersRegex, ":$1")
route = route.replace("[$]", "*")
// clean up
route = route.replace(".js", "")
route = route.replace(".ts", "")
// check if route ends with index
if (route.endsWith("/index")) {
route = route.replace("/index", "")
}
// add leading slash
route = `/${route}`
// import route
let fn = require(absolutePath)
fn = fn.default ?? fn
if (typeof fn !== "function") {
if (!fn.fn) {
console.warn(`Missing fn handler in [${method}][${route}]`)
return false
}
if (Array.isArray(fn.useContext)) {
let contexts = {}
for (const context of fn.useContext) {
contexts[context] = ctx.contexts[context]
}
fn.contexts = contexts
fn.fn.bind({ contexts })
}
}
new Endpoint(
ctx,
{
route: route,
enabled: true,
middlewares: fn.middlewares,
handlers: {
[method]: fn.fn ?? fn,
}
}
)
}
})
return engine
}

View File

@ -0,0 +1,36 @@
import fs from "node:fs"
import RecursiveRegister from "../../lib/recursiveRegister"
export default async (startDir, engine) => {
if (!engine.ws) {
return engine
}
if (!fs.existsSync(startDir)) {
return engine
}
await RecursiveRegister({
start: startDir,
match: async (filePath) => {
return filePath.endsWith(".js") || filePath.endsWith(".ts")
},
onMatch: async ({ absolutePath, relativePath }) => {
let eventName = relativePath.split("/").join(":")
eventName = eventName.replace(".js", "")
eventName = eventName.replace(".ts", "")
let fn = require(absolutePath)
fn = fn.default ?? fn
console.log(`[WEBSOCKET] register event : ${eventName} >`, fn)
engine.ws.events.set(eventName, fn)
}
})
return engine
}

View File

@ -0,0 +1,37 @@
import path from "node:path"
import fs from "node:fs"
export default async ({
start,
match,
onMatch,
}) => {
const filterFrom = start.split("/").pop()
async function registerPath(_path) {
const files = await fs.promises.readdir(_path)
for await (const file of files) {
const filePath = path.join(_path, file)
const stat = await fs.promises.stat(filePath)
if (stat.isDirectory()) {
await registerPath(filePath)
continue
} else {
const isMatch = await match(filePath)
if (isMatch) {
await onMatch({
absolutePath: filePath,
relativePath: filePath.split("/").slice(filePath.split("/").indexOf(filterFrom) + 1).join("/"),
})
}
}
}
}
await registerPath(start)
}

View File

@ -4,39 +4,50 @@ export default class RedisMap {
throw new Error("redis client is required")
}
if (!params.refKey) {
throw new Error("refKey is required")
}
if (!params.worker_id) {
throw new Error("worker_id is required")
}
this.redis = redis
this.params = params
this.refKey = this.params.refKey
if (!this.refKey) {
throw new Error("refKey is required")
}
this.worker_id = this.params.worker_id
}
localMap = new Map()
set = async (key, value) => {
if (!key) {
console.warn(`[redis:${this.refKey}] Failed to set entry with no key`)
console.warn(`[redismap] (${this.refKey}) Failed to set entry with no key`)
return
}
if (!value) {
console.warn(`[redis:${this.refKey}] Failed to set entry [${key}] with no value`)
console.warn(`[redismap] (${this.refKey}) Failed to set entry [${key}] with no value`)
return
}
const redisKey = `${this.refKey}:${key}`
//console.log(`[redis:${this.refKey}] Setting entry [${key}]`,)
this.localMap.set(key, value)
await this.redis.hset(redisKey, value)
// console.log(`[redismap] (${this.refKey}) Set entry [${key}] to [${value}]`)
await this.redis.hset(redisKey, {
worker_id: this.worker_id,
})
return value
}
get = async (key, value) => {
if (!key) {
console.warn(`[redis:${this.refKey}] Failed to get entry with no key`)
console.warn(`[redismap] (${this.refKey}) Failed to get entry with no key`)
return
}
@ -44,58 +55,24 @@ export default class RedisMap {
let result = null
if (value) {
result = await this.redis.hget(redisKey, value)
if (this.localMap.has(key)) {
result = this.localMap.get(key)
} else {
result = await this.redis.hgetall(redisKey)
}
const remoteWorkerID = await this.redis.hget(redisKey, value)
if (Object.keys(result).length === 0) {
result = null
if (!remoteWorkerID) {
return null
}
throw new Error("Redis stream data, not implemented...")
}
return result
}
getMany = async (keys) => {
if (!keys) {
console.warn(`[redis:${this.refKey}] Failed to get entry with no key`)
return
}
const redisKeys = keys.map((key) => `${this.refKey}:${key}`)
const pipeline = this.redis.pipeline()
for (const redisKey of redisKeys) {
pipeline.hgetall(redisKey)
}
let results = await pipeline.exec()
results = results.map((result) => {
return result[1]
})
// delete null or empty objects
results = results.filter((result) => {
if (result === null) {
return false
}
if (Object.keys(result).length === 0) {
return false
}
return true
})
return results
}
del = async (key) => {
if (!key) {
console.warn(`[redis:${this.refKey}] Failed to delete entry with no key`)
console.warn(`[redismap] (${this.refKey}) Failed to delete entry with no key`)
return false
}
@ -107,37 +84,18 @@ export default class RedisMap {
return false
}
await this.redis.hdel(redisKey, Object.keys(data))
if (this.localMap.has(key)) {
this.localMap.delete(key)
}
await this.redis.hdel(redisKey, ["worker_id"])
return true
}
getAll = async () => {
let map = []
let nextIndex = 0
do {
const [nextIndexAsStr, results] = await this.redis.scan(
nextIndex,
"MATCH",
`${this.refKey}:*`,
"COUNT",
100
)
nextIndex = parseInt(nextIndexAsStr, 10)
map = map.concat(results)
} while (nextIndex !== 0)
return map
}
update = async (key, data) => {
if (!key) {
console.warn(`[redis:${this.refKey}] Failed to update entry with no key`)
console.warn(`[redismap] (${this.refKey}) Failed to update entry with no key`)
return
}
@ -146,7 +104,7 @@ export default class RedisMap {
let new_data = await this.get(key)
if (!new_data) {
console.warn(`[redis:${this.refKey}] Object [${key}] not exist, nothing to update`)
console.warn(`[redismap] (${this.refKey}) Object [${key}] not exist, nothing to update`)
return false
}
@ -156,68 +114,93 @@ export default class RedisMap {
...data,
}
await this.redis.hset(redisKey, new_data)
//console.log(`[redismap] (${this.refKey}) Object [${key}] updated`)
this.localMap.set(key, new_data)
await this.redis.hset(redisKey, {
worker_id: this.worker_id,
})
return new_data
}
flush = async (worker_id) => {
let nextIndex = 0
has = async (key) => {
if (!key) {
console.warn(`[redismap] (${this.refKey}) Failed to check entry with no key`)
return false
}
do {
const [nextIndexAsStr, results] = await this.redis.scan(
nextIndex,
"MATCH",
`${this.refKey}:*`,
"COUNT",
100
)
const redisKey = `${this.refKey}:${key}`
nextIndex = parseInt(nextIndexAsStr, 10)
if (this.localMap.has(key)) {
return true
}
const pipeline = this.redis.pipeline()
if (await this.redis.hget(redisKey, "worker_id")) {
return true
}
for await (const key of results) {
const key_id = key.split(this.refKey + ":")[1]
const data = await this.get(key_id)
if (!data) {
continue
}
if (worker_id) {
if (data.worker_id !== worker_id) {
continue
}
}
pipeline.hdel(key, Object.keys(data))
}
await pipeline.exec()
} while (nextIndex !== 0)
return false
}
size = async () => {
let count = 0
// flush = async (worker_id) => {
// let nextIndex = 0
let nextIndex = 0
// do {
// const [nextIndexAsStr, results] = await this.redis.scan(
// nextIndex,
// "MATCH",
// `${this.refKey}:*`,
// "COUNT",
// 100
// )
do {
const [nextIndexAsStr, results] = await this.redis.scan(
nextIndex,
"MATCH",
`${this.refKey}:*`,
"COUNT",
100
)
// nextIndex = parseInt(nextIndexAsStr, 10)
nextIndex = parseInt(nextIndexAsStr, 10)
// const pipeline = this.redis.pipeline()
count = count + results.length
} while (nextIndex !== 0)
// for await (const key of results) {
// const key_id = key.split(this.refKey + ":")[1]
return count
}
// const data = await this.get(key_id)
// if (!data) {
// continue
// }
// if (worker_id) {
// if (data.worker_id !== worker_id) {
// continue
// }
// }
// pipeline.hdel(key, Object.keys(data))
// }
// await pipeline.exec()
// } while (nextIndex !== 0)
// }
// size = async () => {
// let count = 0
// let nextIndex = 0
// do {
// const [nextIndexAsStr, results] = await this.redis.scan(
// nextIndex,
// "MATCH",
// `${this.refKey}:*`,
// "COUNT",
// 100
// )
// nextIndex = parseInt(nextIndexAsStr, 10)
// count = count + results.length
// } while (nextIndex !== 0)
// return count
// }
}

View File

@ -3,13 +3,16 @@ import("./patches")
import fs from "node:fs"
import path from "node:path"
import { EventEmitter } from "@foxify/events"
import Endpoint from "./classes/endpoint"
import { onExit } from "signal-exit"
import defaults from "./defaults"
import IPCClient from "./classes/IPCClient"
import registerBaseEndpoints from "./initializators/registerBaseEndpoints"
import registerWebsocketsEvents from "./initializators/registerWebsocketsEvents"
import registerHttpRoutes from "./initializators/registerHttpRoutes"
async function loadEngine(engine) {
const enginesPath = path.resolve(__dirname, "engines")
@ -27,7 +30,8 @@ class Server {
this.isExperimental = defaults.isExperimental ?? false
if (this.isExperimental) {
console.warn("🚧 This version of Linebridge is experimental! 🚧")
console.warn("\n🚧 This version of Linebridge is experimental! 🚧")
console.warn(`Version: ${defaults.version}\n`)
}
this.params = {
@ -53,15 +57,15 @@ class Server {
// 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.useEngine = this.constructor.useEngine ?? this.params.useEngine ?? "hyper-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
this.params.routesPath = this.constructor.routesPath ?? this.params.routesPath ?? path.resolve(process.cwd(), "routes")
this.params.wsRoutesPath = this.constructor.wsRoutesPath ?? this.params.wsRoutesPath ?? path.resolve(process.cwd(), "routes_ws")
return this
}
@ -77,6 +81,11 @@ class Server {
eventBus = new EventEmitter()
initialize = async () => {
onExit((code, signal) => {
this.engine.close()
process.exit(code)
})
const startHrTime = process.hrtime()
// register events
@ -103,47 +112,48 @@ class Server {
this.engine = new this.engine(engineParams)
if (typeof this.engine.init === "function") {
await this.engine.init(engineParams)
if (typeof this.engine.initialize === "function") {
await this.engine.initialize(engineParams)
}
// create a router map
if (typeof this.engine.router.map !== "object") {
this.engine.router.map = {}
// check if ws events are defined
if (typeof this.wsEvents !== "undefined") {
if (!this.engine.ws) {
console.warn("`wsEvents` detected, but Websockets are not enabled! Ignoring...")
} else {
for (const [eventName, eventHandler] of Object.entries(this.wsEvents)) {
this.engine.ws.events.set(eventName, eventHandler)
}
}
}
// try to execute onInitialize hook
if (typeof this.onInitialize === "function") {
await this.onInitialize()
try {
await this.onInitialize()
}
catch (err) {
console.error(err)
process.exit(1)
}
}
// set server defined headers
// set defaults
this.useDefaultHeaders()
// set server defined middlewares
this.useDefaultMiddlewares()
// register controllers
await this.initializeControllers()
// register http & ws routes
this.engine = await registerHttpRoutes(this.params.routesPath, this.engine, this)
this.engine = await registerWebsocketsEvents(this.params.wsRoutesPath, this.engine)
// register routes
await this.initializeRoutes()
// register main index endpoint `/`
await this.registerBaseEndpoints()
// register base endpoints if enabled
if (!this.params.disableBaseEndpoint) {
await registerBaseEndpoints(this)
}
// 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()
@ -183,6 +193,7 @@ class Server {
useDefaultMiddlewares = async () => {
const middlewares = await this.resolveMiddlewares([
...this.params.useMiddlewares,
...this.useMiddlewares ?? [],
...defaults.useMiddlewares,
])
@ -191,133 +202,6 @@ class Server {
})
}
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
@ -336,6 +220,10 @@ class Server {
let middlewares = [..._middlewares]
if (endpoint.middlewares) {
if (!Array.isArray(endpoint.middlewares)) {
endpoint.middlewares = [endpoint.middlewares]
}
middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)]
}
@ -347,36 +235,6 @@ class Server {
// 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) => {
@ -385,7 +243,9 @@ class Server {
...defaults.middlewares,
}
requestedMiddlewares = Array.isArray(requestedMiddlewares) ? requestedMiddlewares : [requestedMiddlewares]
if (typeof requestedMiddlewares === "string") {
requestedMiddlewares = [requestedMiddlewares]
}
const execs = []
@ -405,19 +265,6 @@ class Server {
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