@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
211 lines (210 loc) • 7.22 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeContext = void 0;
const Context_1 = require("../../Context");
const constants_1 = require("../../types/constants");
const Uint8Array_1 = require("../../utils/BodyParser/Uint8Array");
const types_1 = require("../../utils/BodyParser/types");
const encoder = new TextEncoder();
/**
* Utility to load request body
*
* @param {IncomingMessage} req - Incoming Request
*/
async function loadBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', chunk => chunks.push(Buffer.from(chunk)));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
class NodeContext extends Context_1.Context {
/* Node Apis */
node;
/* Incoming Message */
node_req;
/* Outgoing Response */
node_res;
constructor(cfg, logger, nodeApis, req, res) {
/* Hydrate headers */
const headers = {};
for (const key in req.headers) {
/* Node automatically lower cases headers */
if (req.headers[key])
headers[key] = req.headers[key];
}
/* Determine path and query */
const [path, query = ''] = (req.url || '/').split('?', 2);
super(logger, cfg, {
path,
method: constants_1.HttpMethodToNormal[req.method],
headers,
query,
});
this.node = nodeApis;
this.node_req = req;
this.node_res = res;
}
/**
* Initializes the context, this happens when a route is matched and tied to this context.
*/
async init(val) {
await super.init(val, async () => {
const raw_body = await loadBody(this.node_req);
return (0, Uint8Array_1.parseBody)(this, raw_body, val.route.bodyParser || types_1.DEFAULT_BODY_PARSER_OPTIONS);
});
}
/**
* Get a stream for a particular path
*
* @param {string} path - Path to the file
*/
async getStream(path) {
try {
const stat = this.node.statSync(path);
if (!stat || stat.size <= 0)
return null;
const stream = this.node.createReadStream(path);
return { stream, size: stat.size };
}
catch (err) {
this.logger.error('NodeContext@getStream: Failed to create stream', { msg: err.message, path });
return null;
}
}
/**
* Stream a response from a read stream
*
* @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;
/* Coerce to a pipe-compatible Node Readable if needed */
if (typeof stream?.pipe !== 'function') {
if (stream instanceof ReadableStream) {
const reader = stream.getReader();
stream = new this.node.Readable({
async read() {
const { value, done } = await reader.read();
if (done)
return this.push(null);
this.push(value);
},
});
}
else if (typeof stream === 'string' ||
stream instanceof Uint8Array ||
stream instanceof ArrayBuffer ||
stream instanceof Blob) {
stream = this.node.Readable.from(stream);
}
else {
const type = Object.prototype.toString.call(stream);
throw new Error(`NodeContext@stream: Unsupported stream type (${type})`);
}
}
super.stream(stream, size);
/* Write headers */
this.node_res.writeHead(this.res_code, this.res_headers);
/* Write cookies */
this.writeCookies();
switch (this.method) {
case constants_1.HttpMethods.HEAD:
this.node_res.end();
stream.destroy?.();
break;
default: {
this.node
.pipeline(stream, this.node_res)
.catch(err => {
switch (err.code) {
case 'ERR_STREAM_PREMATURE_CLOSE': /* Stream closed by client (eg: browser refresh) */
case 'ERR_STREAM_DESTROYED': /* Stream destroyed manually */
case 'ECONNRESET': /* Unexpected socket close, usually by client */
case 'EPIPE' /* Client closed connection mid-stream */:
this.logger.debug('NodeContext@stream: Stream aborted', { msg: err.message });
break;
default: {
this.logger.error('NodeContext@stream: Failed to stream', { msg: err.message });
this.node_res.destroy(err);
}
}
});
break;
}
}
}
/**
* Abort the request
*
* @param {HttpStatusCode?} status - Status to abort with (defaults to 503)
*/
abort(status) {
if (this.isLocked)
return;
super.abort(status);
/* Write Cookies */
this.writeCookies();
/* Write other headers and status */
this.node_res.writeHead(this.res_code, this.res_headers).end();
}
/**
* End the request and respond to callee
*/
end() {
if (this.isLocked)
return;
super.end();
/* Write Cookies */
this.writeCookies();
switch (this.method) {
case constants_1.HttpMethods.HEAD:
this.res_headers['content-length'] = typeof this.res_body === 'string' ? '' + encoder.encode(this.res_body).length : '0';
this.node_res.writeHead(this.res_code, this.res_headers).end();
break;
default:
this.node_res.writeHead(this.res_code, this.res_headers).end(typeof this.res_body === 'string' ? this.res_body : undefined);
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.node_req.connection?.socket?.remoteAddress ?? this.node_req.socket?.remoteAddress ?? null;
}
/**
* MARK: Private
*/
writeCookies() {
if (!this.$cookies)
return;
const outgoing = this.$cookies.outgoing;
if (!outgoing.length)
return;
this.node_res.setHeader('set-cookie', outgoing);
}
}
exports.NodeContext = NodeContext;