Merge from local

This commit is contained in:
SrGooglo 2025-04-14 14:54:18 +00:00
parent ff38d45b96
commit 11d8a862b4
58 changed files with 1320 additions and 1300 deletions

View File

@ -1,182 +0,0 @@
#!/usr/bin/env node
require("dotenv").config()
require("sucrase/register")
const path = require("node:path")
const Module = require("node:module")
const { Buffer } = require("node:buffer")
const { webcrypto: crypto } = require("node:crypto")
const { InfisicalClient } = require("@infisical/sdk")
const moduleAlias = require("module-alias")
const { onExit } = require("signal-exit")
// Override file execution arg
process.argv.splice(1, 1)
process.argv[1] = path.resolve(process.argv[1])
// Expose boot function to global
global.Boot = Boot
global.isProduction = process.env.NODE_ENV === "production"
global["__root"] = path.resolve(process.cwd())
global["__src"] = path.resolve(globalThis["__root"], path.dirname(process.argv[1]))
global["aliases"] = {
"root": global["__root"],
"src": global["__src"],
// expose shared resources
"@db": path.resolve(process.cwd(), "db_models"),
"@db_models": path.resolve(process.cwd(), "db_models"),
"@shared-utils": path.resolve(process.cwd(), "utils"),
"@shared-classes": path.resolve(process.cwd(), "classes"),
"@shared-lib": path.resolve(process.cwd(), "lib"),
"@shared-middlewares": path.resolve(process.cwd(), "middlewares"),
// expose internal resources
"@lib": path.resolve(global["__src"], "lib"),
"@middlewares": path.resolve(global["__src"], "middlewares"),
"@controllers": path.resolve(global["__src"], "controllers"),
"@config": path.resolve(global["__src"], "config"),
"@shared": path.resolve(global["__src"], "shared"),
"@classes": path.resolve(global["__src"], "classes"),
"@models": path.resolve(global["__src"], "models"),
"@services": path.resolve(global["__src"], "services"),
"@utils": path.resolve(global["__src"], "utils"),
}
function registerBaseAliases(fromPath, customAliases = {}) {
if (typeof fromPath === "undefined") {
if (module.parent.filename.includes("dist")) {
fromPath = path.resolve(process.cwd(), "dist")
} else {
fromPath = path.resolve(process.cwd(), "src")
}
}
moduleAlias.addAliases({
...customAliases,
"@controllers": path.resolve(fromPath, "controllers"),
"@middlewares": path.resolve(fromPath, "middlewares"),
"@models": path.resolve(fromPath, "models"),
"@classes": path.resolve(fromPath, "classes"),
"@lib": path.resolve(fromPath, "lib"),
"@utils": path.resolve(fromPath, "utils"),
})
}
function registerPatches() {
global.b64Decode = (data) => {
return Buffer.from(data, "base64").toString("utf-8")
}
global.b64Encode = (data) => {
return Buffer.from(data, "utf-8").toString("base64")
}
global.nanoid = (t = 21) => crypto.getRandomValues(new Uint8Array(t)).reduce(((t, e) => t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e > 62 ? "-" : "_"), "");
Array.prototype.updateFromObjectKeys = function (obj) {
this.forEach((value, index) => {
if (obj[value] !== undefined) {
this[index] = obj[value]
}
})
return this
}
global.ToBoolean = (value) => {
if (typeof value === "boolean") {
return value
}
if (typeof value === "string") {
return value.toLowerCase() === "true"
}
return false
}
}
function registerAliases() {
registerBaseAliases(global["__src"], global["aliases"])
}
async function injectEnvFromInfisical() {
const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev"
console.log(`[BOOT] 🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`)
const client = new InfisicalClient({
auth: {
universalAuth: {
clientId: process.env.INFISICAL_CLIENT_ID,
clientSecret: process.env.INFISICAL_CLIENT_SECRET,
}
},
})
const secrets = await client.listSecrets({
environment: envMode,
path: process.env.INFISICAL_PATH ?? "/",
projectId: process.env.INFISICAL_PROJECT_ID ?? null,
includeImports: false,
})
//inject to process.env
secrets.forEach((secret) => {
if (!(process.env[secret.secretKey])) {
process.env[secret.secretKey] = secret.secretValue
}
})
}
async function Boot(main) {
if (!main) {
throw new Error("main class is not defined")
}
console.log(`[BOOT] Booting in [${global.isProduction ? "production" : "development"}] mode...`)
if (process.env.INFISICAL_CLIENT_ID && process.env.INFISICAL_CLIENT_SECRET) {
console.log(`[BOOT] INFISICAL Credentials found, injecting env variables from INFISICAL...`)
await injectEnvFromInfisical()
}
const instance = new main()
onExit((code, signal) => {
console.log(`[BOOT] Cleaning up...`)
if (typeof instance.onClose === "function") {
instance.onClose()
}
instance.engine.close()
}, {
alwaysLast: true,
})
await instance.initialize()
if (process.env.lb_service && process.send) {
process.send({
status: "ready"
})
}
return instance
}
try {
// Apply patches
registerPatches()
// Apply aliases
registerAliases()
// execute main
Module.runMain()
} catch (error) {
console.error("[BOOT] ❌ Boot error: ", error)
}

49
server/bootloader/bin Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env node
const path = require("node:path")
const childProcess = require("node:child_process")
const startFileWatcher = require("./startFileWatcher.js")
const bootloaderPath = path.resolve(__dirname, "boot.js")
const mainModulePath = process.argv[2]
const mainModuleSrc = path.resolve(process.cwd(), path.dirname(mainModulePath))
let childProcessInstance = null
let reloadTimeout = null
function selfReload() {
if (!childProcessInstance) {
console.error(
"[BOOT] Cannot self-reload. Missing childProcessInstance.",
)
return process.exit(0)
}
console.log("[BOOT] Reloading...")
childProcessInstance.kill()
boot()
}
function selfReloadDebounce() {
if (reloadTimeout) {
clearTimeout(reloadTimeout)
}
reloadTimeout = setTimeout(selfReload, 300)
}
function boot() {
childProcessInstance = childProcess.fork(bootloaderPath, [mainModulePath], {
stdio: "inherit",
})
}
// if --watch flag exist, start file watcher
if (process.argv.includes("--watch")) {
startFileWatcher(mainModuleSrc, {
onReload: selfReloadDebounce,
})
}
boot()

55
server/bootloader/boot.js Normal file
View File

@ -0,0 +1,55 @@
require("dotenv").config()
require("sucrase/register")
const path = require("node:path")
const Module = require("node:module")
const registerBaseAliases = require("./registerBaseAliases")
// Override file execution arg
process.argv.splice(1, 1)
process.argv[1] = path.resolve(process.argv[1])
// Expose to global
global.paths = {
root: process.cwd(),
__src: path.resolve(process.cwd(), path.dirname(process.argv[1])),
}
global["aliases"] = {
// expose src
"@": global.paths.__src,
// expose shared resources
"@db": path.resolve(process.cwd(), "db_models"),
"@db_models": path.resolve(process.cwd(), "db_models"),
"@shared-utils": path.resolve(process.cwd(), "utils"),
"@shared-classes": path.resolve(process.cwd(), "classes"),
"@shared-lib": path.resolve(process.cwd(), "lib"),
"@shared-middlewares": path.resolve(process.cwd(), "middlewares"),
// expose internal resources
"@routes": path.resolve(paths.__src, "routes"),
"@models": path.resolve(paths.__src, "models"),
"@middlewares": path.resolve(paths.__src, "middlewares"),
"@classes": path.resolve(paths.__src, "classes"),
"@services": path.resolve(paths.__src, "services"),
"@config": path.resolve(paths.__src, "config"),
"@utils": path.resolve(paths.__src, "utils"),
"@lib": path.resolve(paths.__src, "lib"),
}
// expose bootwrapper to global
global.Boot = require("./bootWrapper")
try {
// apply patches
require("./patches.js")
// Apply aliases
registerBaseAliases(global.paths.__src, global["aliases"])
// execute main
Module.runMain()
} catch (error) {
console.error("[BOOT] ❌ Boot error: ", error)
}

View File

@ -0,0 +1,48 @@
const { onExit } = require("signal-exit")
const injectEnvFromInfisical = require("./injectEnvFromInfisical")
module.exports = async function Boot(main) {
if (!main) {
throw new Error("main class is not defined")
}
console.log(
`[BOOT] Booting in [${global.isProduction ? "production" : "development"}] mode...`,
)
if (
process.env.INFISICAL_CLIENT_ID &&
process.env.INFISICAL_CLIENT_SECRET
) {
console.log(
`[BOOT] INFISICAL Credentials found, injecting env variables from INFISICAL...`,
)
await injectEnvFromInfisical()
}
const instance = new main()
process.on("exit", (code) => {
console.log(`[BOOT] Closing ...`)
instance._fireClose()
})
process.on("SIGTERM", () => {
process.exit(0)
})
process.on("SIGINT", () => {
process.exit(0)
})
await instance.initialize()
if (process.env.lb_service && process.send) {
process.send({
status: "ready",
})
}
return instance
}

View File

@ -0,0 +1,32 @@
const { InfisicalClient } = require("@infisical/sdk")
module.exports = async function injectEnvFromInfisical() {
const envMode = (global.FORCE_ENV ?? global.isProduction) ? "prod" : "dev"
console.log(
`[BOOT] 🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`,
)
const client = new InfisicalClient({
auth: {
universalAuth: {
clientId: process.env.INFISICAL_CLIENT_ID,
clientSecret: process.env.INFISICAL_CLIENT_SECRET,
},
},
})
const secrets = await client.listSecrets({
environment: envMode,
path: process.env.INFISICAL_PATH ?? "/",
projectId: process.env.INFISICAL_PROJECT_ID ?? null,
includeImports: false,
})
//inject to process.env
secrets.forEach((secret) => {
if (!process.env[secret.secretKey]) {
process.env[secret.secretKey] = secret.secretValue
}
})
}

View File

@ -0,0 +1,49 @@
const { webcrypto: crypto } = require("node:crypto")
const { Buffer } = require("node:buffer")
global.isProduction = process.env.NODE_ENV === "production"
global.b64Decode = (data) => {
return Buffer.from(data, "base64").toString("utf-8")
}
global.b64Encode = (data) => {
return Buffer.from(data, "utf-8").toString("base64")
}
global.nanoid = (t = 21) =>
crypto
.getRandomValues(new Uint8Array(t))
.reduce(
(t, e) =>
(t +=
(e &= 63) < 36
? e.toString(36)
: e < 62
? (e - 26).toString(36).toUpperCase()
: e > 62
? "-"
: "_"),
"",
)
Array.prototype.updateFromObjectKeys = function (obj) {
this.forEach((value, index) => {
if (obj[value] !== undefined) {
this[index] = obj[value]
}
})
return this
}
global.ToBoolean = (value) => {
if (typeof value === "boolean") {
return value
}
if (typeof value === "string") {
return value.toLowerCase() === "true"
}
return false
}

View File

@ -0,0 +1,14 @@
const path = require("node:path")
const moduleAlias = require("module-alias")
module.exports = function registerBaseAliases(fromPath, customAliases = {}) {
if (typeof fromPath === "undefined") {
if (module.parent.filename.includes("dist")) {
fromPath = path.resolve(process.cwd(), "dist")
} else {
fromPath = path.resolve(process.cwd(), "src")
}
}
moduleAlias.addAliases(customAliases)
}

View File

@ -0,0 +1,29 @@
const chokidar = require("chokidar")
const { minimatch } = require("minimatch")
const defaultIgnored = [
"**/.cache/**",
"**/node_modules/**",
"**/dist/**",
"**/build/**",
]
module.exports = async (fromPath, { onReload }) => {
console.log("[WATCHER] Starting watching path >", fromPath)
global._watcher = chokidar.watch(fromPath, {
ignored: (path) =>
defaultIgnored.some((pattern) => minimatch(path, pattern)),
persistent: true,
ignoreInitial: true,
awaitWriteFinish: true,
})
global._watcher.on("all", (event, filePath) => {
console.log(`[WATCHER] Event [${event}] > ${filePath}`)
if (typeof onReload === "function") {
onReload()
}
})
}

View File

@ -1,6 +1,6 @@
{
"name": "linebridge",
"version": "0.26.0",
"version": "1.0.0",
"description": "Multiproposal framework to build fast, scalable, and secure servers.",
"author": "RageStudio <support@ragestudio.net>",
"bugs": {
@ -9,7 +9,7 @@
"license": "MIT",
"main": "./dist/index.js",
"bin": {
"linebridge-boot": "./bin/boot.js"
"linebridge-boot": "./bootloader/bin"
},
"publishConfig": {
"access": "public"
@ -20,32 +20,26 @@
"./package.json"
],
"scripts": {
"start": "hermes-node ./src/bin/server.js",
"build": "hermes build --parallel --clean",
"test": "mocha"
},
"dependencies": {
"@foxify/events": "^2.1.0",
"@gullerya/object-observer": "^6.1.3",
"@infisical/sdk": "^2.1.8",
"@socket.io/cluster-adapter": "^0.2.2",
"@socket.io/redis-adapter": "^8.2.1",
"@socket.io/redis-emitter": "^5.1.0",
"@socket.io/sticky": "^1.0.4",
"axios": "^1.6.7",
"axios-retry": "3.4.0",
"cors": "2.8.5",
"dotenv": "^16.4.4",
"axios": "^1.8.4",
"chokidar": "^4.0.3",
"dotenv": "^16.5.0",
"hyper-express": "^6.17.3",
"ioredis": "^5.3.2",
"md5": "^2.3.0",
"module-alias": "2.2.2",
"morgan": "1.10.0",
"ioredis": "^5.6.1",
"minimatch": "^10.0.1",
"module-alias": "^2.2.2",
"signal-exit": "^4.1.0",
"socket.io": "^4.8.1",
"socket.io-client": "^4.5.4",
"sucrase": "^3.35.0",
"uuid": "^9.0.1"
"sucrase": "^3.35.0"
},
"devDependencies": {
"@ragestudio/hermes": "^1.0.0",

View File

@ -1,21 +0,0 @@
import path from "node:path"
import Endpoint from "../classes/endpoint"
import defaults from "../defaults"
const projectPkg = require(path.resolve(process.cwd(), "package.json"))
export default class MainEndpoint extends Endpoint {
route = "/"
get = async (req, res) => {
return {
name: globalThis._linebridge.name ?? "unknown",
version: projectPkg.version ?? "unknown",
engine: globalThis._linebridge.useEngine ?? "unknown",
request_time: new Date().getTime(),
lb_version: defaults.version ?? "unknown",
experimental: defaults.isExperimental.toString() ?? "unknown",
}
}
}

View File

@ -1,24 +0,0 @@
import Endpoint from "../classes/endpoint"
export default class MainEndpoint extends Endpoint {
route = "/_map"
get = async (req, res) => {
const httpMap = Object.entries(this.server.engine.router.map).reduce((acc, [route, { method, path }]) => {
if (!acc[method]) {
acc[method] = []
}
acc[method].push({
route: path
})
return acc
}, {})
return res.json({
http: httpMap,
websocket: []
})
}
}

20
server/src/baseRoutes/main.js Executable file
View File

@ -0,0 +1,20 @@
import path from "node:path"
import Route from "../classes/Route"
import Vars from "../vars"
export default class MainRoute extends Route {
static route = "/"
static useContexts = ["server"]
get = async (req, res, ctx) => {
return {
name: ctx.server.params.refName ?? "unknown",
version: Vars.projectPkg.version ?? "unknown",
engine: ctx.server.params.useEngine ?? "unknown",
lb_version: Vars.libPkg.version ?? "unknown",
experimental: ctx.server.isExperimental ?? "unknown",
request_time: new Date().getTime(),
}
}
}

27
server/src/baseRoutes/map.js Executable file
View File

@ -0,0 +1,27 @@
import Route from "../classes/Route"
export default class MapRoute extends Route {
static route = "/_map"
get = async (req, res) => {
const httpMap = Array.from(this.server.engine.map.entries()).reduce(
(acc, [route, { method, path }]) => {
if (!acc[method]) {
acc[method] = []
}
acc[method].push({
route: path,
})
return acc
},
{},
)
return res.json({
http: httpMap,
websocket: [],
})
}
}

View File

@ -0,0 +1,17 @@
import { HttpRequestHandler } from "../Handler"
export default class Endpoint {
static _constructed = false
static _class = true
constructor(method, context) {
this._constructed = true
this.context = context
if (typeof method === "function") {
this.run = method
}
this.handler = new HttpRequestHandler(this.run, this.context)
}
}

View File

@ -0,0 +1,55 @@
export class Handler {
constructor(fn, ctx) {
this.fn = fn ?? (() => Promise.resolve())
this.ctx = ctx ?? {}
this.fn = this.fn.bind({
contexts: this.ctx,
})
}
}
export class HttpRequestHandler extends Handler {
constructor(fn, ctx) {
super(fn, ctx)
return this.exec
}
exec = async (req, res) => {
try {
req.ctx = this.ctx
const result = await this.fn(req, res, this.ctx)
if (result) {
return res.json(result)
}
} catch (error) {
// handle if is a operation error
if (error instanceof OperationError) {
return res.status(error.code).json({
error: error.message,
})
}
// if is not a operation error, that is a exception.
// gonna handle like a generic 500 error
console.error({
message: "Unhandled route error:",
description: error.stack,
})
return res.status(500).json({
error: error.message,
})
}
}
}
// TODO: Implement MiddlewareHandler
export class MiddlewareHandler extends Handler {}
// TODO: Implement WebsocketRequestHandler
export class WebsocketRequestHandler extends Handler {}
export default Handler

View File

@ -1,159 +1,162 @@
import { EventEmitter } from "@foxify/events"
export default class IPCClient {
constructor(self, _process) {
this.self = self
this.process = _process
constructor(self, _process) {
this.self = self
this.process = _process
this.process.on("message", (msg) => {
if (typeof msg !== "object") {
// not an IPC message, ignore
return false
}
this.process.on("message", (msg) => {
if (typeof msg !== "object") {
// not an IPC message, ignore
return false
}
const { event, payload } = msg
const { event, payload } = msg
if (!event || !event.startsWith("ipc:")) {
return false
}
if (!event || !event.startsWith("ipc:")) {
return false
}
//console.log(`[IPC:CLIENT] Received event [${event}] from [${payload.from}]`)
//console.log(`[IPC:CLIENT] Received event [${event}] from [${payload.from}]`)
if (event.startsWith("ipc:exec")) {
return this.handleExecution(payload)
}
if (event.startsWith("ipc:exec")) {
return this.handleExecution(payload)
}
if (event.startsWith("ipc:akn")) {
return this.handleAcknowledgement(payload)
}
})
}
if (event.startsWith("ipc:akn")) {
return this.handleAcknowledgement(payload)
}
})
}
eventBus = new EventEmitter()
eventBus = new EventEmitter()
handleExecution = async (payload) => {
let { id, command, args, from } = payload
handleExecution = async (payload) => {
if (typeof this.self.ipcEvents !== "object") {
return null
}
let fn = this.self.ipcEvents[command]
let { id, command, args, from } = payload
if (!fn) {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
let fn = this.self.ipcEvents[command]
id: id,
error: `IPC: Command [${command}] not found`,
}
})
if (typeof fn !== "function") {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.params.refName,
return false
}
id: id,
error: `IPC: Command [${command}] not found`,
},
})
try {
let result = await fn(this.self.contexts, ...args)
return false
}
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
try {
let result = await fn(this.self.contexts, ...args)
id: id,
result: result,
}
})
} catch (error) {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.params.refName,
id: id,
error: error.message,
}
})
}
}
id: id,
result: result,
},
})
} catch (error) {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.params.refName,
handleAcknowledgement = async (payload) => {
let { id, result, error } = payload
id: id,
error: error.message,
},
})
}
}
this.eventBus.emit(`ipc:akn:${id}`, {
id: id,
result: result,
error: error,
})
}
handleAcknowledgement = async (payload) => {
let { id, result, error } = payload
// call a command on a remote service, and waits to get a response from akn (async)
call = async (to_service_id, command, ...args) => {
const remote_call_id = Date.now()
this.eventBus.emit(`ipc:akn:${id}`, {
id: id,
result: result,
error: error,
})
}
//console.debug(`[IPC:CLIENT] Invoking command [${command}] on service [${to_service_id}]`)
// call a command on a remote service, and waits to get a response from akn (async)
call = async (to_service_id, command, ...args) => {
const remote_call_id = Date.now()
const response = await new Promise((resolve, reject) => {
try {
//console.debug(`[IPC:CLIENT] Invoking command [${command}] on service [${to_service_id}]`)
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.constructor.refName,
const response = await new Promise((resolve, reject) => {
try {
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.params.refName,
id: remote_call_id,
command,
args,
}
})
id: remote_call_id,
command,
args,
},
})
this.eventBus.once(`ipc:akn:${remote_call_id}`, resolve)
} catch (error) {
console.error(error)
this.eventBus.once(`ipc:akn:${remote_call_id}`, resolve)
} catch (error) {
console.error(error)
reject(error)
}
}).catch((error) => {
return {
error: error
}
})
reject(error)
}
}).catch((error) => {
return {
error: error,
}
})
if (response.error) {
throw new OperationError(500, response.error)
}
if (response.error) {
throw new OperationError(500, response.error)
}
return response.result
}
return response.result
}
// call a command on a remote service, but return it immediately
invoke = async (to_service_id, command, ...args) => {
const remote_call_id = Date.now()
// call a command on a remote service, but return it immediately
invoke = async (to_service_id, command, ...args) => {
const remote_call_id = Date.now()
try {
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.constructor.refName,
try {
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.params.refName,
id: remote_call_id,
command,
args,
}
})
id: remote_call_id,
command,
args,
},
})
return {
id: remote_call_id
}
} catch (error) {
console.error(error)
return {
id: remote_call_id,
}
} catch (error) {
console.error(error)
return {
error: error
}
}
}
}
return {
error: error,
}
}
}
}

View File

@ -0,0 +1,58 @@
import Endpoint from "../Endpoint"
export default class Route {
constructor(server, params = {}) {
if (!server) {
throw new Error("server is not defined")
}
this.server = server
this.params = {
route: this.constructor.route ?? "/",
useContexts: this.constructor.useContexts ?? [],
useMiddlewares: this.constructor.useMiddlewares ?? [],
...params,
}
if (typeof this.params.handlers === "object") {
for (const method of global._linebridge.params.httpMethods) {
if (typeof this.params.handlers[method] !== "function") {
continue
}
this[method] = this.params.handlers[method]
}
}
if (this.server.contexts && Array.isArray(this.params.useContexts)) {
for (const key of this.params.useContexts) {
this.ctx[key] = this.server.contexts[key]
}
}
}
ctx = {}
register = () => {
for (const method of global._linebridge.params.httpMethods) {
if (typeof this[method] === "undefined") {
continue
}
if (!(this[method] instanceof Endpoint)) {
if (this[method]._class && !this[method]._constructed) {
this[method] = new this[method](undefined, this.ctx)
} else {
this[method] = new Endpoint(this[method], this.ctx)
}
}
this.server.register.http({
method: method,
route: this.params.route,
middlewares: this.params.useMiddlewares,
fn: this[method].handler,
})
}
}
}

View File

@ -167,7 +167,7 @@ class RTEngineNG {
}
}
attach = async (engine) => {
attach = (engine) => {
this.engine = engine
this.engine.app.ws(this.config.path ?? `/`, this.handleConnection)

View File

@ -1,87 +0,0 @@
export default class Endpoint {
constructor(server, params = {}, ctx = {}) {
this.server = server
this.params = params
this.ctx = ctx
if (!server) {
throw new Error("Server is not defined")
}
this.route = this.route ?? this.constructor.route ?? this.params.route
this.enabled = this.enabled ?? this.constructor.enabled ?? this.params.enabled ?? true
this.middlewares = [
...this.middlewares ?? [],
...this.params.middlewares ?? [],
]
if (this.params.handlers) {
for (const method of globalThis._linebridge.validHttpMethods) {
if (typeof this.params.handlers[method] === "function") {
this[method] = this.params.handlers[method]
}
}
}
this.selfRegister()
if (Array.isArray(this.params.useContexts)) {
for (const contextRef of this.params.useContexts) {
this.endpointContext[contextRef] = this.server.contexts[contextRef]
}
}
return this
}
endpointContext = {}
createHandler(fn) {
fn = fn.bind(this.server)
return async (req, res) => {
try {
const result = await fn(req, res, this.endpointContext)
if (result) {
return res.json(result)
}
} catch (error) {
if (error instanceof OperationError) {
return res.status(error.code).json({
"error": error.message
})
}
console.error({
message: "Unhandled route error:",
description: error.stack,
})
return res.status(500).json({
"error": error.message
})
}
}
}
selfRegister = async () => {
for await (const method of globalThis._linebridge.validHttpMethods) {
const methodHandler = this[method]
if (typeof methodHandler !== "undefined") {
const fn = this.createHandler(this[method].fn ?? this[method])
this.server.register.http(
{
method,
route: this.route,
middlewares: this.middlewares,
fn: fn,
},
)
}
}
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
Controller: require("./controller"),
Endpoint: require("./endpoint"),
RTEngine: require("./rtengine"),
}

View File

@ -1,65 +0,0 @@
const path = require("path")
const fs = require("fs")
const os = require("os")
const packageJSON = require(path.resolve(module.path, "../package.json"))
function getHostAddress() {
const interfaces = os.networkInterfaces()
for (const key in interfaces) {
const iface = interfaces[key]
for (let index = 0; index < iface.length; index++) {
const alias = iface[index]
if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal) {
return alias.address
}
}
}
return "0.0.0.0"
}
export default {
isExperimental: fs.existsSync(path.resolve(module.path, "../.experimental")),
version: packageJSON.version,
localhost_address: getHostAddress() ?? "localhost",
params: {
urlencoded: true,
engine: "express",
http_protocol: "http",
ws_protocol: "ws",
},
headers: {
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE, DEL",
"Access-Control-Allow-Credentials": "true",
},
middlewares: {
cors: require("./middlewares/cors").default,
logs: require("./middlewares/logger").default,
},
useMiddlewares: [
//"cors",
"logs",
],
controllers: [],
fixed_http_methods: {
"del": "delete",
},
valid_http_methods: [
"get",
"post",
"put",
"patch",
"del",
"delete",
"trace",
"head",
"any",
"options",
"ws",
],
}

View File

@ -0,0 +1,80 @@
import he from "hyper-express"
import rtengine from "./rtengine"
export default class Engine {
constructor(server) {
this.server = server
}
static heDefaultParams = {
max_body_length: 50 * 1024 * 1024, //50MB in bytes,
}
app = null
ws = null
router = new he.Router()
map = new Map()
initialize = async () => {
this.app = new he.Server({
...Engine.heDefaultParams,
key_file_name: this.server.ssl?.key ?? undefined,
cert_file_name: this.server.ssl?.cert ?? undefined,
})
this.router.any("*", this.defaultResponse)
this.app.use(this.mainMiddleware)
this.app.use(this.router)
if (this.server.params.websockets === true) {
this.ws = new rtengine({
requireAuth: this.server.constructor.requiredWsAuth,
handleAuth: this.server.handleWsAuth,
root: `/${this.server.params.refName}`,
})
this.ws.initialize()
global.websockets = this.ws
await this.ws.io.attachApp(this.app.uws_instance)
}
}
mainMiddleware = async (req, res, next) => {
if (req.method === "OPTIONS") {
return res.status(204).end()
}
// register body parser
if (req.headers["content-type"]) {
if (
!req.headers["content-type"].startsWith("multipart/form-data")
) {
req.body = await req.urlencoded()
req.body = await req.json(req.body)
}
}
}
defaultResponse = (req, res) => {
return res.status(404).json({
error: "Not found",
})
}
listen = async () => {
await this.app.listen(this.server.params.listenPort)
}
// close must be synchronous
close = () => {
if (this.ws && typeof this.ws.close === "function") {
this.ws.close()
}
if (this.app && typeof this.app.close === "function") {
this.app.close()
}
}
}

View File

@ -3,7 +3,7 @@ import redis from "ioredis"
import SocketIO from "socket.io"
import { EventEmitter } from "@foxify/events"
import RedisMap from "../../lib/redis_map"
import RedisMap from "./redis_map.js"
export default class RTEngineServer {
constructor(params = {}) {

81
server/src/engines/he/index.js Executable file
View File

@ -0,0 +1,81 @@
import he from "hyper-express"
import RtEngine from "../../classes/RtEngine"
export default class Engine {
constructor(server) {
this.server = server
}
static heDefaultParams = {
max_body_length: 50 * 1024 * 1024, //50MB in bytes,
}
app = null
ws = null
router = new he.Router()
map = new Map()
initialize = async () => {
this.app = new he.Server({
...Engine.heDefaultParams,
key_file_name: this.server.ssl?.key ?? undefined,
cert_file_name: this.server.ssl?.cert ?? undefined,
})
this.router.any("*", this.defaultResponse)
this.app.use(this.mainMiddleware)
this.app.use(this.router)
if (this.server.params.websockets === true) {
this.ws = new RtEngine({
path:
this.server.params.wsPath ??
`/${this.server.params.refName}`,
onUpgrade: this.server.handleWsUpgrade,
onConnection: this.server.handleWsConnection,
onDisconnect: this.server.handleWsDisconnect,
})
global.websockets = this.ws
this.ws.attach(this)
}
}
mainMiddleware = async (req, res, next) => {
if (req.method === "OPTIONS") {
return res.status(204).end()
}
// register body parser
if (req.headers["content-type"]) {
if (
!req.headers["content-type"].startsWith("multipart/form-data")
) {
req.body = await req.urlencoded()
req.body = await req.json(req.body)
}
}
}
defaultResponse = (req, res) => {
return res.status(404).json({
error: "Not found",
})
}
listen = async () => {
await this.app.listen(this.server.params.listenPort)
}
// close must be synchronous
close = () => {
if (this.ws && typeof this.ws.close === "function") {
this.ws.close()
}
if (this.app && typeof this.app.close === "function") {
this.app.close()
}
}
}

View File

@ -1,100 +0,0 @@
import he from "hyper-express"
import rtengineng from "../../classes/rtengineng"
export default class HyperExpressEngineNG {
constructor(ctx) {
this.ctx = ctx
}
app = null
ws = null
router = null
initialize = async () => {
console.warn(
`hyper-express-ng is a experimental engine, some features may not be available or work properly!`,
)
const appParams = {
max_body_length: 50 * 1024 * 1024, //50MB in bytes,
}
if (this.ctx.ssl) {
appParams.key_file_name = this.ctx.ssl?.key ?? null
appParams.cert_file_name = this.ctx.ssl?.cert ?? null
}
this.app = new he.Server(appParams)
this.router = new he.Router()
// create a router map
if (typeof this.router.map !== "object") {
this.router.map = {}
}
await this.router.any("*", (req, res) => {
return res.status(404).json({
code: 404,
message: "Not found",
})
})
await this.app.use(async (req, res, next) => {
if (req.method === "OPTIONS") {
// handle cors
if (this.ctx.constructor.ignoreCors) {
res.setHeader("Access-Control-Allow-Methods", "*")
res.setHeader("Access-Control-Allow-Origin", "*")
res.setHeader("Access-Control-Allow-Headers", "*")
}
return res.status(204).end()
}
// register body parser
if (req.headers["content-type"]) {
if (
!req.headers["content-type"].startsWith(
"multipart/form-data",
)
) {
req.body = await req.urlencoded()
req.body = await req.json(req.body)
}
}
})
if (this.ctx.constructor.enableWebsockets === true) {
this.ws = global.websocket = new rtengineng({
path:
this.ctx.constructor.wsPath ??
`/${this.ctx.constructor.refName}`,
onUpgrade: this.ctx.handleWsUpgrade,
onConnection: this.ctx.handleWsConnection,
onDisconnect: this.ctx.handleWsDisconnect,
})
await this.ws.attach(this)
}
}
listen = async () => {
await this.app.listen(this.ctx.constructor.listen_port)
}
// close must be synchronous
close = () => {
if (this.ws && typeof this.ws.close === "function") {
this.ws.close()
}
if (typeof this.app.close === "function") {
this.app.close()
}
if (typeof this.ctx.onClose === "function") {
this.ctx.onClose()
}
}
}

View File

@ -1,99 +0,0 @@
import he from "hyper-express"
import rtengine from "../../classes/rtengine"
export default class Engine {
constructor(ctx) {
this.ctx = ctx
}
app = null
router = null
ws = null
initialize = async () => {
const serverParams = {
max_body_length: 50 * 1024 * 1024, //50MB in bytes,
}
if (this.ctx.ssl) {
serverParams.key_file_name = this.ctx.ssl?.key ?? null
serverParams.cert_file_name = this.ctx.ssl?.cert ?? null
}
this.app = new he.Server(serverParams)
this.router = new he.Router()
// create a router map
if (typeof this.router.map !== "object") {
this.router.map = {}
}
await this.router.any("*", (req, res) => {
return res.status(404).json({
code: 404,
message: "Not found",
})
})
await this.app.use(async (req, res, next) => {
if (req.method === "OPTIONS") {
// handle cors
if (this.ctx.constructor.ignoreCors) {
res.setHeader("Access-Control-Allow-Methods", "*")
res.setHeader("Access-Control-Allow-Origin", "*")
res.setHeader("Access-Control-Allow-Headers", "*")
}
return res.status(204).end()
}
// register body parser
if (req.headers["content-type"]) {
if (
!req.headers["content-type"].startsWith(
"multipart/form-data",
)
) {
req.body = await req.urlencoded()
req.body = await req.json(req.body)
}
}
})
if (this.ctx.constructor.enableWebsockets) {
this.ws = global.websocket = new rtengine({
requireAuth: this.ctx.constructor.requiredWsAuth,
handleAuth: this.ctx.handleWsAuth,
root: `/${this.ctx.constructor.refName}`,
})
this.ws.initialize()
await this.ws.io.attachApp(this.app.uws_instance)
}
}
listen = async () => {
await this.app.listen(this.ctx.constructor.listen_port)
}
// close should be synchronous
close = () => {
if (this.ws) {
this.ws.clear()
if (typeof this.ws?.close === "function") {
this.ws.close()
}
}
if (typeof this.app?.close === "function") {
this.app.close()
}
if (typeof this.ctx.onClose === "function") {
this.ctx.onClose()
}
}
}

View File

@ -1,9 +1,9 @@
import HyperExpress from "./hyper-express"
import HyperExpressNG from "./hyper-express-ng"
import HeLegacy from "./he-legacy"
import He from "./he"
import Worker from "./worker"
export default {
"hyper-express": HyperExpress,
"hyper-express-ng": HyperExpressNG,
"he-legacy": HeLegacy,
he: He,
worker: Worker,
}

View File

@ -1,126 +1,118 @@
import { EventEmitter } from "@foxify/events"
class WorkerEngineRouter {
routes = []
routes = []
get = (path, ...execs) => {
get = (path, ...execs) => {}
}
post = (path, ...execs) => {}
post = (path, ...execs) => {
delete = (path, ...execs) => {}
}
put = (path, ...execs) => {}
delete = (path, ...execs) => {
patch = (path, ...execs) => {}
}
head = (path, ...execs) => {}
put = (path, ...execs) => {
options = (path, ...execs) => {}
}
any = (path, ...execs) => {}
patch = (path, ...execs) => {
}
head = (path, ...execs) => {
}
options = (path, ...execs) => {
}
any = (path, ...execs) => {
}
use = (path, ...execs) => {
}
use = (path, ...execs) => {}
}
class WorkerEngine {
static ipcPrefix = "rail:"
static ipcPrefix = "rail:"
selfId = process.env.lb_service.id
selfId = process.env.lb_service.id
router = new WorkerEngineRouter()
router = new WorkerEngineRouter()
eventBus = new EventEmitter()
eventBus = new EventEmitter()
perExecTail = []
perExecTail = []
initialize = async () => {
console.error(`[WorkerEngine] Worker engine its not implemented yet...`)
initialize = async () => {
console.error(`[WorkerEngine] Worker engine its not implemented yet...`)
process.on("message", this.handleIPCMessage)
}
process.on("message", this.handleIPCMessage)
}
listen = async () => {
console.log(`Sending Rail Register`)
listen = async () => {
console.log(`Sending Rail Register`)
process.send({
type: "rail:register",
data: {
id: process.env.lb_service.id,
pid: process.pid,
routes: this.router.routes,
}
})
}
process.send({
type: "rail:register",
data: {
id: process.env.lb_service.id,
pid: process.pid,
routes: this.router.routes,
},
})
}
handleIPCMessage = async (msg) => {
if (typeof msg !== "object") {
// ignore, its not for us
return false
}
handleIPCMessage = async (msg) => {
if (typeof msg !== "object") {
// ignore, its not for us
return false
}
if (!msg.event || !msg.event.startsWith(WorkerEngine.ipcPrefix)) {
return false
}
if (!msg.event || !msg.event.startsWith(WorkerEngine.ipcPrefix)) {
return false
}
const { event, payload } = msg
const { event, payload } = msg
switch (event) {
case "rail:request": {
const { req } = payload
switch (event) {
case "rail:request": {
const { req } = payload
break
}
case "rail:response": {
break
}
case "rail:response": {
}
}
}
}
}
}
use = (fn) => {
if (fn instanceof WorkerEngineRouter) {
this.router = fn
return
}
use = (fn) => {
if (fn instanceof WorkerEngineRouter) {
this.router = fn
return
}
if (fn instanceof Function) {
this.perExecTail.push(fn)
return
}
}
if (fn instanceof Function) {
this.perExecTail.push(fn)
return
}
}
}
export default class Engine {
constructor(params) {
this.params = params
}
constructor(params) {
this.params = params
}
app = new WorkerEngine()
app = null
router = new WorkerEngineRouter()
map = new Map()
router = new WorkerEngineRouter()
initialize = async () => {
if (
!process.env.lb_service ||
process.env.lb_service?.type !== "worker"
) {
throw new Error(
"No worker environment detected!\nThis engine is only meant to be used in a worker environment\n",
)
}
init = async () => {
await this.app.initialize()
}
this.app = new WorkerEngine()
listen = async () => {
await this.app.listen()
}
}
await this.app.initialize()
}
listen = async () => {
await this.app.listen()
}
}

View File

@ -1,6 +1,6 @@
module.exports = {
Server: require("./server.js"),
Endpoint: require("./classes/endpoint"),
registerBaseAliases: require("./registerAliases"),
version: require("../package.json").version,
Server: require("./server"),
Route: require("./classes/Route"),
registerBaseAliases: require("./registerAliases"),
version: require("../package.json").version,
}

View File

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

View File

@ -1,79 +0,0 @@
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, server) => {
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] = server.contexts[context]
}
fn.contexts = contexts
fn.fn.bind({ contexts })
}
}
new Endpoint(
server,
{
route: route,
enabled: true,
middlewares: fn.middlewares,
handlers: {
[method]: fn.fn ?? fn,
}
}
)
}
})
return engine
}

View File

@ -1,28 +0,0 @@
import fs from "node:fs"
import getRouteredFunctions from "../../utils/getRouteredFunctions"
import flatRouteredFunctions from "../../utils/flatRouteredFunctions"
export default async (startDir, engine) => {
if (!engine.ws || !fs.existsSync(startDir)) {
return engine
}
let events = await getRouteredFunctions(startDir)
events = flatRouteredFunctions(events)
if (typeof events !== "object") {
return engine
}
if (typeof engine.ws.registerEvents === "function") {
await engine.ws.registerEvents(events)
} else {
for (const eventKey of Object.keys(events)) {
engine.ws.events.set(eventKey, events[eventKey])
}
}
return engine
}

View File

@ -1,18 +0,0 @@
const { Controller } = require("../../classes/controller")
const generateEndpointsFromDir = require("../generateEndpointsFromDir")
function generateControllerFromEndpointsDir(dir, controllerName) {
const endpoints = generateEndpointsFromDir(dir)
return class extends Controller {
static refName = controllerName
get = endpoints.get
post = endpoints.post
put = endpoints.put
patch = endpoints.patch
delete = endpoints.delete
}
}
module.exports = generateControllerFromEndpointsDir

View File

@ -1,23 +0,0 @@
const loadEndpointsFromDir = require("../loadEndpointsFromDir")
function generateEndpointsFromDir(dir) {
const loadedEndpoints = loadEndpointsFromDir(dir)
// filter by methods
const endpointsByMethods = Object()
for (const endpointKey in loadedEndpoints) {
const endpoint = loadedEndpoints[endpointKey]
const method = endpoint.method.toLowerCase()
if (!endpointsByMethods[method]) {
endpointsByMethods[method] = {}
}
endpointsByMethods[method][endpoint.route] = loadedEndpoints[endpointKey]
}
return endpointsByMethods
}
module.exports = generateEndpointsFromDir

View File

@ -1,41 +0,0 @@
const fs = require("node:fs")
const path = require("node:path")
function loadEndpointsFromDir(dir) {
if (!dir) {
throw new Error("No directory provided")
}
if (!fs.existsSync(dir)) {
throw new Error(`Directory [${dir}] does not exist`)
}
// scan the directory for files
const files = fs.readdirSync(dir)
// create an object to store the endpoints
const endpoints = {}
// loop through the files
for (const file of files) {
// get the full path of the file
const filePath = path.join(dir, file)
// get the file stats
const stats = fs.statSync(filePath)
// if the file is a directory, recursively call this function
if (stats.isDirectory()) {
endpoints[file] = loadEndpointsFromDir(filePath)
}
// if the file is a javascript file, require it and add it to the endpoints object
if (stats.isFile() && path.extname(filePath) === ".js") {
endpoints[path.basename(file, ".js")] = require(filePath).default
}
}
return endpoints
}
module.exports = loadEndpointsFromDir

View File

@ -1,8 +0,0 @@
import cors from "cors"
export default cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"],
preflightContinue: false,
optionsSuccessStatus: 204,
})

View File

@ -1,3 +1,15 @@
import OperationError from "./classes/operation_error"
import OperationError from "./classes/OperationError"
import Endpoint from "./classes/Endpoint"
import {
Handler,
HttpRequestHandler,
MiddlewareHandler,
WebsocketRequestHandler,
} from "./classes/Handler"
global.OperationError = OperationError
global.OperationError = OperationError
global.Endpoint = Endpoint
global.Handler = Handler
global.HttpRequestHandler = HttpRequestHandler
global.MiddlewareHandler = MiddlewareHandler
global.WebsocketRequestHandler = WebsocketRequestHandler

View File

@ -0,0 +1,22 @@
import Vars from "../vars"
export default (server) => {
if (!server || !server.headers || !server.engine || !server.engine.app) {
return null
}
let headers = {
...server.headers,
...Vars.baseHeaders,
}
headers = Object.entries(headers)
server.engine.app.use((req, res, next) => {
for (let i = 0; i < headers.length; i++) {
res.setHeader(headers[i][0], headers[i][1])
}
next()
})
}

View File

@ -0,0 +1,14 @@
import composeMiddlewares from "../utils/composeMiddlewares"
import Vars from "../vars"
export default async (server) => {
const middlewares = composeMiddlewares(
{ ...server.middlewares, ...Vars.baseMiddlewares },
server.params.useMiddlewares,
"/*",
)
middlewares.forEach((middleware) => {
server.engine.app.use(middleware)
})
}

View File

@ -0,0 +1,21 @@
import fs from "node:fs"
import path from "node:path"
import Vars from "../vars"
export default async (server) => {
const scanPath = path.resolve(Vars.libPath, "baseRoutes")
const files = fs.readdirSync(scanPath)
for await (const file of files) {
if (file === "index.js") {
continue
}
let RouteModule = await import(path.join(scanPath, file))
RouteModule = RouteModule.default
new RouteModule(server).register()
}
}

View File

@ -0,0 +1,6 @@
export default (server) => {
server.headers["Access-Control-Allow-Origin"] = "*"
server.headers["Access-Control-Allow-Methods"] = "*"
server.headers["Access-Control-Allow-Headers"] = "*"
server.headers["Access-Control-Allow-Credentials"] = "true"
}

View File

@ -0,0 +1,64 @@
import fs from "node:fs"
import Route from "../classes/Route"
import RecursiveRegister from "../utils/recursiveRegister"
const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g
export default async (startDir, server) => {
if (!fs.existsSync(startDir)) {
return null
}
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 endpoint
let fileObj = await import(absolutePath)
fileObj = fileObj.default ?? fileObj
if (typeof fileObj !== "function") {
if (typeof fileObj.fn !== "function") {
console.warn(`Missing fn handler in [${method}][${route}]`)
return false
}
}
new Route(server, {
route: route,
useMiddlewares: fileObj.useMiddlewares,
useContexts: fileObj.useContexts,
handlers: {
[method]: fileObj.fn ?? fileObj,
},
}).register()
},
})
}

View File

@ -0,0 +1,47 @@
export default async (server) => {
if (!process.env.lb_service || !process.send) {
console.error("IPC not available")
return null
}
// get only the root paths
let paths = Array.from(server.engine.map.keys()).map((key) => {
const root = key.split("/")[1]
return "/" + root
})
// remove duplicates
paths = [...new Set(paths)]
// remove "" and _map
paths = paths.filter((key) => {
if (key === "/" || key === "/_map") {
return false
}
return true
})
process.send({
type: "service:register",
id: process.env.lb_service.id,
index: process.env.lb_service.index,
register: {
namespace: server.params.refName,
http: {
enabled: true,
paths: paths,
proto: server.hasSSL ? "https" : "http",
},
websocket: {
enabled: server.params.websockets,
path: server.params.refName ?? `/${server.params.refName}`,
},
listen: {
ip: server.params.listenIp,
port: server.params.listenPort,
},
},
})
}

View File

@ -0,0 +1,28 @@
import fs from "node:fs"
import getRouteredFunctions from "../utils/getRouteredFunctions"
import flatRouteredFunctions from "../utils/flatRouteredFunctions"
export default async (startDir, server) => {
if (!server.engine.ws || !fs.existsSync(startDir)) {
return null
}
let events = await getRouteredFunctions(startDir)
events = flatRouteredFunctions(events)
if (typeof events !== "object") {
return null
}
if (typeof server.engine.ws.registerEvents === "function") {
await server.engine.ws.registerEvents(events)
} else {
for (const eventKey of Object.keys(events)) {
server.engine.ws.events.set(eventKey, events[eventKey])
}
}
return server
}

View File

@ -1,136 +1,126 @@
import("./patches")
import fs from "node:fs"
import path from "node:path"
import { EventEmitter } from "@foxify/events"
import defaults from "./defaults"
import IPCClient from "./classes/IPCClient"
import Endpoint from "./classes/endpoint"
import Route from "./classes/Route"
import registerBaseEndpoints from "./initializators/registerBaseEndpoints"
import registerWebsocketsEvents from "./initializators/registerWebsocketsEvents"
import registerHttpRoutes from "./initializators/registerHttpRoutes"
import registerBaseRoutes from "./registers/baseRoutes"
import registerBaseMiddlewares from "./registers/baseMiddlewares"
import registerBaseHeaders from "./registers/baseHeaders"
import registerWebsocketsFileEvents from "./registers/websocketFileEvents"
import registerHttpFileRoutes from "./registers/httpFileRoutes"
import registerServiceToIPC from "./registers/ipcService"
import bypassCorsHeaders from "./registers/bypassCorsHeaders"
import isExperimental from "./utils/isExperimental"
import getHostAddress from "./utils/getHostAddress"
import composeMiddlewares from "./utils/composeMiddlewares"
import Vars from "./vars"
import Engines from "./engines"
class Server {
constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) {
this.isExperimental = defaults.isExperimental ?? false
constructor(params = {}) {
if (this.isExperimental) {
console.warn("\n🚧 This version of Linebridge is experimental! 🚧")
console.warn(`Version: ${defaults.version}\n`)
console.warn(`Version: ${Vars.libPkg.version}\n`)
}
this.params = {
...defaults.params,
...(params.default ?? params),
...Vars.defaultParams,
...params,
}
this.controllers = {
...(controllers.default ?? controllers),
// overrides some params with constructor values
if (typeof this.constructor.refName === "string") {
this.params.refName = this.constructor.refName
}
this.middlewares = {
...(middlewares.default ?? middlewares),
if (typeof this.constructor.useEngine === "string") {
this.params.useEngine = this.constructor.useEngine
}
this.headers = {
...defaults.headers,
...(headers.default ?? headers),
if (typeof this.constructor.listenIp === "string") {
this.params.listenIp = this.constructor.listenIp
}
// fix and fulfill params
this.params.useMiddlewares = this.params.useMiddlewares ?? []
if (
typeof this.constructor.listenPort === "string" ||
typeof this.constructor.listenPort === "number"
) {
this.params.listenPort = this.constructor.listenPort
}
this.params.name = this.constructor.refName ?? this.params.refName
if (typeof this.constructor.websockets === "boolean") {
this.params.websockets = this.constructor.websockets
}
this.params.useEngine =
this.constructor.useEngine ??
this.params.useEngine ??
"hyper-express"
if (typeof this.constructor.bypassCors === "boolean") {
this.params.bypassCors = this.constructor.bypassCors
}
this.params.listen_ip =
this.constructor.listenIp ??
this.constructor.listen_ip ??
this.params.listen_ip ??
"0.0.0.0"
if (typeof this.constructor.baseRoutes === "boolean") {
this.params.baseRoutes = this.constructor.baseRoutes
}
this.params.listen_port =
this.constructor.listenPort ??
this.constructor.listen_port ??
this.params.listen_port ??
3000
if (typeof this.constructor.routesPath === "string") {
this.params.routesPath = this.constructor.routesPath
}
this.params.http_protocol = this.params.http_protocol ?? "http"
if (typeof this.constructor.wsRoutesPath === "string") {
this.params.wsRoutesPath = this.constructor.wsRoutesPath
}
this.params.http_address = `${this.params.http_protocol}://${defaults.localhost_address}:${this.params.listen_port}`
if (typeof this.constructor.useMiddlewares !== "undefined") {
if (!Array.isArray(this.constructor.useMiddlewares)) {
this.constructor.useMiddlewares = [
this.constructor.useMiddlewares,
]
}
this.params.enableWebsockets =
this.constructor.enableWebsockets ??
this.params.enableWebsockets ??
false
this.params.useMiddlewares = this.constructor.useMiddlewares
}
this.params.ignoreCors =
this.constructor.ignoreCors ?? this.params.ignoreCors ?? true
this.params.disableBaseEndpoints =
this.constructor.disableBaseEndpoints ??
this.params.disableBaseEndpoints ??
false
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")
globalThis._linebridge = {
name: this.params.name,
useEngine: this.params.useEngine,
listenIp: this.params.listen_ip,
listenPort: this.params.listen_port,
httpProtocol: this.params.http_protocol,
httpAddress: this.params.http_address,
enableWebsockets: this.params.enableWebsockets,
ignoreCors: this.params.ignoreCors,
routesPath: this.params.routesPath,
validHttpMethods: defaults.valid_http_methods,
global._linebridge = {
vars: Vars,
params: this.params,
}
return this
}
eventBus = new EventEmitter()
middlewares = {}
headers = {}
events = {}
contexts = {}
engine = null
events = null
get hasSSL() {
if (!this.ssl) {
return false
}
ipc = null
return this.ssl.key && this.ssl.cert
}
ipcEvents = null
eventBus = new EventEmitter()
get isExperimental() {
return isExperimental()
}
initialize = async () => {
const startHrTime = process.hrtime()
// register events
if (this.events) {
if (this.events.default) {
this.events = this.events.default
}
// resolve current local private address of the host
this.localAddress = getHostAddress()
for (const [eventName, eventHandler] of Object.entries(
this.events,
)) {
this.eventBus.on(eventName, eventHandler)
}
this.contexts["server"] = this
// register declared events to eventBus
for (const [eventName, eventHandler] of Object.entries(this.events)) {
this.eventBus.on(eventName, eventHandler)
}
// initialize engine
@ -140,28 +130,19 @@ class Server {
throw new Error(`Engine ${this.params.useEngine} not found`)
}
// construct engine instance
// important, pass this instance to the engine constructor
this.engine = new this.engine(this)
// fire engine initialization
if (typeof this.engine.initialize === "function") {
await this.engine.initialize()
}
// 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)
}
}
}
// at this point, we wanna to pass to onInitialize hook,
// a simple base context, without any registers extra
// try to execute onInitialize hook
// fire onInitialize hook
if (typeof this.onInitialize === "function") {
try {
await this.onInitialize()
@ -171,211 +152,123 @@ class Server {
}
}
// set defaults
this.useDefaultHeaders()
this.useDefaultMiddlewares()
// Now gonna initialize the final steps & registers
if (this.routes) {
// bypassCors if needed
if (this.params.bypassCors) {
bypassCorsHeaders(this)
}
// register base headers & middlewares
registerBaseHeaders(this)
registerBaseMiddlewares(this)
// if websocket enabled, lets do some work
if (typeof this.engine.ws === "object") {
// register declared ws events
if (typeof this.wsEvents === "object") {
for (const [eventName, eventHandler] of Object.entries(
this.wsEvents,
)) {
this.engine.ws.events.set(eventName, eventHandler)
}
}
}
// now, initialize declared routes with Endpoint class
if (typeof this.routes === "object") {
for (const [route, endpoint] of Object.entries(this.routes)) {
this.engine.router.map[route] = new Endpoint(this, {
new Route(this, {
...endpoint,
route: route,
handlers: {
[endpoint.method.toLowerCase()]: endpoint.fn,
},
})
}).register()
}
}
// register http & ws routes
this.engine = await registerHttpRoutes(
this.params.routesPath,
this.engine,
this,
)
this.engine = await registerWebsocketsEvents(
this.params.wsRoutesPath,
this.engine,
)
// register http file routes
await registerHttpFileRoutes(this.params.routesPath, this)
// register base endpoints if enabled
if (!this.params.disableBaseEndpoints) {
await registerBaseEndpoints(this)
// register ws file routes
await registerWebsocketsFileEvents(this.params.wsRoutesPath, this)
// register base routes if enabled
if (this.params.baseRoutes == true) {
await registerBaseRoutes(this)
}
// use main router
await this.engine.app.use(this.engine.router)
// if is a linebridge service then initialize IPC Channels
// if is a linebridge service, then initialize IPC Channels
if (process.env.lb_service) {
await this.initializeIpc()
await this.registerServiceToIPC()
}
console.info("🚄 Starting IPC client")
this.ipc = global.ipc = new IPCClient(this, process)
// try to execute beforeInitialize hook.
if (typeof this.afterInitialize === "function") {
await this.afterInitialize()
await registerServiceToIPC(this)
}
// listen
await this.engine.listen()
// execute afterInitialize hook.
if (typeof this.afterInitialize === "function") {
await this.afterInitialize()
}
// 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 \n\t - Websocket: ${this.engine.ws ? "Enabled" : "Disabled"}`,
`🛰 Server ready!\n\t - ${this.hasSSL ? "https" : "http"}://${this.params.listenIp}:${this.params.listenPort} \n\t - Websocket: ${this.engine.ws ? "Enabled" : "Disabled"} \n\t - Routes: ${this.engine.map.size} \n\t - Tooks: ${elapsedTimeInMs.toFixed(2)}ms \n\t `,
)
}
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,
...(this.useMiddlewares ?? []),
...defaults.useMiddlewares,
])
middlewares.forEach((middleware) => {
this.engine.app.use(middleware)
})
}
register = {
http: (endpoint, ..._middlewares) => {
http: (obj) => {
// check and fix method
endpoint.method = endpoint.method?.toLowerCase() ?? "get"
obj.method = obj.method?.toLowerCase() ?? "get"
if (defaults.fixed_http_methods[endpoint.method]) {
endpoint.method = defaults.fixed_http_methods[endpoint.method]
if (Vars.fixedHttpMethods[obj.method]) {
obj.method = Vars.fixedHttpMethods[obj.method]
}
// check if method is supported
if (typeof this.engine.router[endpoint.method] !== "function") {
throw new Error(`Method [${endpoint.method}] is not supported!`)
if (typeof this.engine.router[obj.method] !== "function") {
throw new Error(`Method [${obj.method}] is not supported!`)
}
// grab the middlewares
let middlewares = [..._middlewares]
// compose the middlewares
obj.middlewares = composeMiddlewares(
{ ...this.middlewares, ...Vars.baseMiddlewares },
obj.middlewares,
`[${obj.method.toUpperCase()}] ${obj.route}`,
)
if (endpoint.middlewares) {
if (!Array.isArray(endpoint.middlewares)) {
endpoint.middlewares = [endpoint.middlewares]
}
middlewares = [
...middlewares,
...this.resolveMiddlewares(endpoint.middlewares),
]
}
this.engine.router.map[endpoint.route] = {
method: endpoint.method,
path: endpoint.route,
}
// set to the endpoints map, used by _map
this.engine.map.set(obj.route, {
method: obj.method,
path: obj.route,
})
// register endpoint to http interface router
this.engine.router[endpoint.method](
endpoint.route,
...middlewares,
endpoint.fn,
this.engine.router[obj.method](
obj.route,
...obj.middlewares,
obj.fn,
)
},
ws: (wsEndpointObj) => {},
}
resolveMiddlewares = (requestedMiddlewares) => {
const middlewares = {
...this.middlewares,
...defaults.middlewares,
_fireClose = () => {
if (typeof this.onClose === "function") {
this.onClose()
}
if (typeof requestedMiddlewares === "string") {
requestedMiddlewares = [requestedMiddlewares]
if (typeof this.engine.close === "function") {
this.engine.close()
}
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
}
registerServiceToIPC = () => {
if (!process.env.lb_service || !process.send) {
console.error("IPC not available")
return null
}
// get only the root paths
let paths = Object.keys(this.engine.router.map).map((key) => {
const root = key.split("/")[1]
return "/" + root
})
// remove duplicates
paths = [...new Set(paths)]
// remove "" and _map
paths = paths.filter((key) => {
if (key === "/" || key === "/_map") {
return false
}
return true
})
process.send({
type: "service:register",
id: process.env.lb_service.id,
index: process.env.lb_service.index,
register: {
namespace: this.constructor.refName,
http: {
enabled: true,
paths: paths,
proto: this.ssl?.key && this.ssl?.cert ? "https" : "http",
},
websocket: {
enabled: this.constructor.enableWebsockets,
path:
this.constructor.wsPath ??
`/${this.constructor.refName}`,
},
listen: {
ip: this.params.listen_ip,
port: this.params.listen_port,
},
},
})
}
}

View File

@ -0,0 +1,29 @@
export default (middlewares, selectors, endpointRef) => {
if (!middlewares || !selectors) {
return []
}
if (typeof selectors === "string") {
selectors = [selectors]
}
const execs = []
selectors.forEach((middlewareKey) => {
if (typeof middlewareKey === "string") {
if (typeof middlewares[middlewareKey] !== "function") {
throw new Error(
`Required middleware [${middlewareKey}] not found!\n\t- Required by endpoint > ${endpointRef}\n\n`,
)
}
execs.push(middlewares[middlewareKey])
}
if (typeof middlewareKey === "function") {
execs.push(middlewareKey)
}
})
return execs
}

View File

@ -0,0 +1,23 @@
import os from "node:os"
export default function getHostAddress() {
const interfaces = os.networkInterfaces()
for (const key in interfaces) {
const iface = interfaces[key]
for (let index = 0; index < iface.length; index++) {
const alias = iface[index]
if (
alias.family === "IPv4" &&
alias.address !== "127.0.0.1" &&
!alias.internal
) {
return alias.address
}
}
}
return "0.0.0.0"
}

View File

@ -1 +0,0 @@
export { default as Schematized } from "./schematized"

View File

@ -0,0 +1,8 @@
import fs from "node:fs"
import path from "node:path"
import Vars from "../vars"
export default () => {
return fs.existsSync(path.resolve(Vars.rootLibPath, ".experimental"))
}

48
server/src/vars.js Executable file
View File

@ -0,0 +1,48 @@
import path from "node:path"
const rootLibPath = path.resolve(__dirname, "../")
const packageJSON = require(rootLibPath, "../package.json")
const projectPkg = require(path.resolve(process.cwd(), "package.json"))
export default {
libPath: __dirname,
rootLibPath: rootLibPath,
libPkg: packageJSON,
projectCwd: process.cwd(),
projectPkg: projectPkg,
defaultParams: {
refName: "linebridge",
listenIp: "0.0.0.0",
listenPort: 3000,
useEngine: "he",
websockets: false,
bypassCors: false,
baseRoutes: true,
routesPath: path.resolve(process.cwd(), "routes"),
wsRoutesPath: path.resolve(process.cwd(), "ws_routes"),
useMiddlewares: [],
httpMethods: [
"get",
"post",
"put",
"patch",
"del",
"delete",
"trace",
"head",
"any",
"options",
"ws",
],
},
baseHeaders: {
server: "linebridge",
"lb-version": packageJSON.version,
},
baseMiddlewares: {
logs: require("./middlewares/logger").default,
},
fixedHttpMethods: {
del: "delete",
},
}