UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

390 lines (389 loc) 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.App = void 0; const number_1 = require("@valkyriestudios/utils/number"); const object_1 = require("@valkyriestudios/utils/object"); const Logger_1 = require("./modules/Logger"); const util_1 = require("./modules/Logger/util"); const Router_1 = require("./routing/Router"); const Runtime_1 = require("./runtimes/Runtime"); const Memory_1 = require("./storage/Memory"); const Lazy_1 = require("./utils/Lazy"); const Tree_1 = require("./routing/Tree"); const mount_1 = require("./modules/JSX/style/mount"); const mount_2 = require("./modules/JSX/script/mount"); const Als_1 = require("./utils/Als"); const Generic_1 = require("./utils/Generic"); const RGX_RID = /^[a-z0-9-]{8,64}$/i; class App extends Router_1.Router { /** * MARK: Private Vars */ /* Runtime instance */ runtime = null; /* Logger instance */ logger = null; /* Logger options */ exporters = null; /* Request ID config */ requestId = { inbound: ['x-request-id', 'cf-ray'], outbound: 'x-request-id', validate: val => RGX_RID.test(val), }; /* Whether or not the runtime has been started or not */ running = false; /* Global cookie defaults */ cookies; /* Global cache */ cache; /* Client-css instance */ css = null; /* Client-script instance */ script = null; /* Passed Environment */ env; /** * MARK: Constructor */ constructor(options = {}) { /* Verify that the options passed are in the form of an object */ if (!(0, object_1.isObject)(options)) throw new Error('TriFrost@ctor: options should be an object'); super({ path: '', timeout: options.timeout === null || (0, number_1.isIntGt)(options.timeout, 0) ? options.timeout : 30_000, rateLimit: options.rateLimit ? new Lazy_1.Lazy(options.rateLimit) : null, tree: new Tree_1.RouteTree(), middleware: [], bodyParser: null, }); /* Set runtime if provided */ if (options.runtime) this.runtime = options.runtime; /* Configure provided env, take note runtime-specifics will be added by runtime */ this.env = ((0, object_1.isObject)(options.env) ? options.env : {}); /* Extract domain and configure cookie options */ this.cookies = { config: ((0, object_1.isObject)(options.cookies) ? { ...options.cookies } : { path: '/', secure: true, httpOnly: true, sameSite: 'Strict' }), }; /* Cache */ this.cache = new Lazy_1.Lazy(options.cache || (() => new Memory_1.MemoryCache())); /* Request ID */ if (options.tracing?.requestId) this.requestId = options.tracing.requestId; else if (options.tracing?.requestId === null) this.requestId = null; /* Exporters */ if (options.tracing?.exporters) this.exporters = options.tracing.exporters; /* Add script route */ if (options.client?.script) { (0, mount_2.mount)(this, '/__atomics__/client.js', options.client.script); this.script = options.client?.script; } /* Add css route */ if (options.client?.css) { (0, mount_1.mount)(this, '/__atomics__/client.css', options.client.css); this.css = options.client.css; } } /** * MARK: Getters */ /** * Whether or not the server is running (started) or not */ get isRunning() { return this.running; } /** * MARK: Methods */ /** * Boot the app, configure the runtime and any runtime-specific bits * (such as listening for traffic, configuring workerd, etc) */ async boot(options = {}) { if (this.running) return this; try { if (!this.runtime) this.runtime = await (0, Runtime_1.getRuntime)(); this.logger = new Logger_1.TriFrostRootLogger({ runtime: this.runtime, exporters: (opts) => { const resolved = this.exporters?.(opts); /* Use provided exporters */ const is_arr = Array.isArray(resolved); if (is_arr && resolved.length) return resolved; else if (!is_arr && resolved) return [resolved]; else return [this.runtime.defaultExporter(opts.env)]; /* Fallback to default exporter if none provided */ }, }); this.running = true; /* Triage handler */ const runTriage = async (path, ctx) => { /* User might have forgotten to end ... */ if (ctx.statusCode >= 200 && ctx.statusCode < 400) return ctx.end(); if (ctx.statusCode === 404) { /* Maybe end-user has a specific notfound handler for this */ const notfound = this.tree.matchNotFound(path); if (notfound) { if (Reflect.get(notfound.route.fn, util_1.Sym_TriFrostSpan)) { await notfound.route.fn(ctx); } else { await ctx.logger.span(notfound.route.name, async () => notfound.route.fn(ctx)); } } /* End it if still not locked */ if (!ctx.isLocked) return ctx.end(); } else if (ctx.statusCode >= 400) { /* Ok something's off ... let's see if we have a triage registered */ const error = this.tree.matchError(path); if (error) { if (Reflect.get(error.route.fn, util_1.Sym_TriFrostSpan)) { await error.route.fn(ctx); } else { await ctx.logger.span(error.route.name, async () => error.route.fn(ctx)); } } /* End it if still not locked */ if (!ctx.isLocked) return ctx.end(); } }; /* Start the runtime */ await this.runtime.boot({ logger: this.logger, cfg: { cookies: this.cookies.config, cache: this.cache, requestId: this.requestId, env: this.env, timeout: this.timeout, ...(options?.port && { port: options.port }), ...(this.css !== null && { css: this.css }), ...(this.script !== null && { script: this.script }), }, onIncoming: (async (ctx) => { const ctx_id = ctx.logger.traceId || (0, Generic_1.hexId)(8); return (0, Als_1.activateCtx)(ctx_id, ctx, async () => { const { path, method } = ctx; this.logger.debug('onIncoming', { method, path }); /* Get matching route */ let match = this.tree.match(method, path); try { /* If we have no match check the notfound handlers */ if (!match) { match = this.tree.matchNotFound(path); if (match) ctx.setStatus(404); } /* Generic 404 response if we still dont have anything */ if (!match) return ctx.status(404); /* Add route meta */ if (match.route.meta) ctx.logger.setAttributes(match.route.meta); /* Add attributes to tracer */ ctx.logger.setAttributes({ 'http.host': ctx.host, 'http.method': method, 'http.target': path, 'http.route': match.route.path, 'http.status_code': 200, 'otel.status_code': 'OK', 'user_agent.original': ctx.headers['user-agent'] ?? '', }); /* Initialize Timeout */ ctx.setTimeout(match.route.timeout); /* Initialize context with matched route data, check if triage is necessary (eg payload too large) */ await ctx.init(match); if (ctx.statusCode >= 400) return await runTriage(path, ctx); /* Run chain */ for (let i = 0; i < match.route.middleware.length; i++) { const el = match.route.middleware[i]; if (Reflect.get(el.handler, util_1.Sym_TriFrostSpan)) { await el.handler(ctx); } else { await ctx.logger.span(el.name, async () => el.handler(ctx)); } /* If context is locked at this point, return as the route has been handled */ if (ctx.isLocked) return; /* Check if triage is necessary */ if (ctx.statusCode >= 400) return await runTriage(path, ctx); } /* Run handler */ if (Reflect.get(match.route.fn, util_1.Sym_TriFrostSpan)) { await match.route.fn(ctx); } else { await ctx.logger.span(match.route.name, async () => match.route.fn(ctx)); } /* Let's run triage if context is not locked */ if (!ctx.isLocked) await runTriage(path, ctx); } catch (err) { ctx.logger.error(err); /* Ensure status code is set as 500 if not >= 400, this ensures proper triaging */ if (!ctx.isAborted && ctx.statusCode < 400) ctx.setStatus(500); if (!ctx.isLocked) await runTriage(path, ctx); } finally { /* Flush logger last */ ctx.addAfter(() => ctx.logger.flush()); /* Run ctx cleanup */ ctx.runAfter(); } }); }), }); /* Morph app class with runtime-specific exports, eg: workerd requires fetch globally defined */ if (this.runtime.exports) { Object.defineProperties(this, Object.getOwnPropertyDescriptors(this.runtime.exports)); } } catch (err) { if (this.logger) this.logger.error('boot: Runtime boot failed', { msg: err.message }); this.running = false; } return this; } /** * Stop the runtime and shutdown the app instance */ shutdown() { if (!this.running) return false; try { this.runtime.shutdown(); if (this.logger) this.logger.info('Server closed'); this.running = false; return true; } catch { if (this.logger) this.logger.info('Failed to close server'); return false; } } /** * MARK: Routing */ /** * Add a router or middleware to the router */ use(val) { super.use(val); return this; } /** * Attach a rate limit to the middleware chain */ limit(limit) { super.limit(limit); return this; } /** * Configure body parser options */ bodyParser(options) { super.bodyParser(options); return this; } /** * Add a subrouter with dynamic path handling. */ group(path, handler) { super.group(path, handler); return this; } /** * Add a subroute with a builder approach */ route(path, handler) { super.route(path, handler); return this; } /** * Configure a catch-all not found handler for subroutes of this router * * @param {Handler} handler - Handler to run */ onNotFound(handler) { super.onNotFound(handler); return this; } /** * Configure a catch-all error handler for subroutes of this router * * @param {Handler} handler - Handler to run */ onError(handler) { super.onError(handler); return this; } /** * Configure a HTTP Get route */ get(path, handler) { super.get(path, handler); return this; } /** * Configure a HTTP Post route */ post(path, handler) { super.post(path, handler); return this; } /** * Configure a HTTP Patch route */ patch(path, handler) { super.patch(path, handler); return this; } /** * Configure a HTTP Put route */ put(path, handler) { super.put(path, handler); return this; } /** * Configure a HTTP Delete route */ del(path, handler) { super.del(path, handler); return this; } /** * Configure a health route */ health(path, handler) { super.health(path, handler); return this; } } exports.App = App; exports.default = App;