@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
390 lines (389 loc) • 14.7 kB
JavaScript
"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;