UNPKG

@trifrost/core

Version:

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

170 lines (169 loc) 5.03 kB
/// <reference types="bun-types" /> import { Context } from '../../Context'; import { HttpMethods, HttpMethodToNormal } from '../../types/constants'; import { parseBody } from '../../utils/BodyParser/Request'; import { extractPartsFromUrl } from '../../utils/Http'; import { DEFAULT_BODY_PARSER_OPTIONS } from '../../utils/BodyParser/types'; import { verifyFileStream } from '../../utils/Stream'; const encoder = new TextEncoder(); export class BunContext extends Context { /* Bun Apis */ bun; /* Bun Request instance */ bun_req; /* Internal Response instance */ res = null; constructor(cfg, logger, bunApis, req) { /* Extract path and query */ const { path, query } = extractPartsFromUrl(req.url); /* Hydrate headers */ const headers = {}; /* eslint-disable-next-line */ /* @ts-ignore */ req.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; }); super(logger, cfg, { path, method: HttpMethodToNormal[req.method], headers, query, }); this.bun = bunApis; this.bun_req = req; } /** * Getter for the final response */ get response() { return this.res; } /** * Initializes the context, this happens when a route is matched and tied to this context. */ async init(val) { await super.init(val, async () => parseBody(this, this.bun_req, val.route.bodyParser || DEFAULT_BODY_PARSER_OPTIONS)); } /** * Get a stream for a particular path * * @param {string} path - Path to the file */ async getStream(path) { try { const file_obj = this.bun.file(path); if (!file_obj) { this.logger.warn('BunContext@getStream: File not found', { path }); return null; } return { stream: file_obj.stream(), size: file_obj.size, }; } catch (err) { this.logger.error('BunContext@getStream: Failed to create stream', { msg: err.message, path }); return null; } } /** * Stream a file-like response in Bun * * @param {unknown} stream - Stream to respond with * @param {number|null} size - Size of the stream */ stream(stream, size = null) { /* If already locked do nothing */ if (this.isLocked) return; verifyFileStream(stream); super.stream(stream, size); /* Set response with stream */ this.res = new Response(stream, { status: this.res_code, headers: this.res_headers, }); /* Write cookies */ this.writeCookies(); } /** * Abort the request * * @param {HttpStatusCode?} status - Status to abort with (defaults to 503) */ abort(status) { if (this.isLocked) return; super.abort(status); /* Set response */ this.res = new Response(null, { status: this.res_code, headers: this.res_headers, }); /* Write cookies */ this.writeCookies(); } /** * End the request and respond to callee */ end() { if (this.isLocked) return; super.end(); switch (this.method) { case HttpMethods.HEAD: { this.res_headers['content-length'] = typeof this.res_body === 'string' ? '' + encoder.encode(this.res_body).length : '0'; this.res = new Response(null, { status: this.res_code, headers: this.res_headers, }); /* Write cookies */ this.writeCookies(); break; } default: /* Set response */ this.res = new Response(this.res_body, { status: this.res_code, headers: this.res_headers, }); /* Write cookies */ this.writeCookies(); break; } } /** * Run jobs after the response has gone out */ runAfter() { const hooks = this.afterHooks; if (!hooks.length) return; queueMicrotask(() => { for (let i = 0; i < hooks.length; i++) { try { hooks[i](this); } catch { /* No-Op */ } } }); } /** * MARK: Protected */ getIP() { return this.bun_req.socket?.remoteAddress ?? null; } /** * MARK: Private */ writeCookies() { if (!this.$cookies) return; const outgoing = this.$cookies.outgoing; for (let i = 0; i < outgoing.length; i++) this.res.headers.append('set-cookie', outgoing[i]); } }