rexuws
Version:
An express-like framework built on top of uWebsocket.js aims at simple codebase and high performance
469 lines (468 loc) • 19.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const statuses_1 = __importDefault(require("statuses"));
const fs_1 = __importStar(require("fs"));
const mime_types_1 = __importStar(require("mime-types"));
const cookie_1 = require("cookie");
const content_disposition_1 = __importDefault(require("content-disposition"));
const path_1 = require("path");
const utils_1 = require("./utils");
const symbol_1 = require("./utils/symbol");
const CONTENT_TYPE = {
JSON: 'application/json; charset=utf-8',
PLAIN: 'text/plain; charset=utf-8',
HTML: 'text/html; charset=utf-8',
OCTET: 'application/octet-stream',
};
class Response {
constructor(res, opts, logger) {
this._headers = [];
this.locals = {};
this.maxReadFileSize = 102400; // 100KB
this._cookies = [];
this.originalRes = res;
this[symbol_1.WRITE_HEADER] = this.originalRes.writeHeader.bind(res);
this[symbol_1.WRITE_STATUS] = this.originalRes.writeStatus.bind(res);
this[symbol_1.ON_WRITABLE] = this.originalRes.onWritable.bind(res);
this[symbol_1.GET_PROXIED_ADDR] = this.originalRes.getProxiedRemoteAddressAsText.bind(res);
this[symbol_1.GET_REMOTE_ADDR] = this.originalRes.getRemoteAddressAsText.bind(res);
this[symbol_1.GET_WRITE_OFFSET] = this.originalRes.getWriteOffset.bind(res);
this[symbol_1.ON_ABORTED] = this.originalRes.onAborted.bind(res);
this[symbol_1.ON_WRITABLE] = this.originalRes.onWritable.bind(res);
this[symbol_1.CORK] = this.originalRes.cork.bind(res);
this[symbol_1.END] = this.originalRes.end.bind(res);
this[symbol_1.TRY_END] = this.originalRes.tryEnd.bind(res);
// Set epress setHeader method
this.set = this.setHeader;
this.header = this.setHeader;
this.getHeader = this.get;
// Set express type method
this.type = this.setType;
this.contentType = this.setType;
this.debug = logger;
this.debug = logger || {
...utils_1.colorConsole,
deprecate() {
return utils_1.colorConsole.warn.bind(utils_1.colorConsole, '[DEPRECATED]');
},
};
if (opts) {
const { hasAsync = false, maxReadFileSize = 102400 } = opts;
this[symbol_1.HAS_ASYNC] = hasAsync;
// if (this[HAS_ASYNC]) {
// res.onAborted(() => {
// res.originalRes.aborted = true;
// });
// }
if (isNaN(maxReadFileSize))
this.maxReadFileSize = -1;
}
this.attachAbortHandler();
}
// GETTER
get preStatus() {
return this._statusCode ? +this._statusCode : undefined;
}
get preHeader() {
return this._headers;
}
// ***************************************************************
// PRIVATE METHOD
// ***************************************************************
attachAbortHandler(calledByMethod = false) {
if (calledByMethod) {
if (this[symbol_1.HAS_ASYNC])
return;
this[symbol_1.ON_ABORTED](() => {
this.originalRes.aborted = true;
if (this[symbol_1.READ_STREAM]) {
if (this.originalRes.id === -1) {
// console.log(
// "ERROR! onAbortedOrFinishedResponse called twice for the same res!"
// );
}
else {
// console.log("Stream was closed, openStreams: " + --openStreams);
// console.timeEnd(res.id);
this[symbol_1.READ_STREAM].destroy();
}
/* Mark this response already accounted for */
this.originalRes.id = -1;
}
});
return;
}
if (this[symbol_1.HAS_ASYNC]) {
this[symbol_1.ON_ABORTED](() => {
this.originalRes.aborted = true;
if (this[symbol_1.READ_STREAM]) {
if (this.originalRes.id === -1) {
// console.log(
// "ERROR! onAbortedOrFinishedResponse called twice for the same res!"
// );
}
else {
// console.log("Stream was closed, openStreams: " + --openStreams);
// console.timeEnd(res.id);
this[symbol_1.READ_STREAM].destroy();
}
/* Mark this response already accounted for */
this.originalRes.id = -1;
}
});
}
}
setHeaderAndStatusByNativeMethod() {
if (this._statusCode)
this[symbol_1.WRITE_STATUS](this._statusCode);
for (let i = 0; i < this._headers.length; i++) {
this[symbol_1.WRITE_HEADER](this._headers[i].name, this._headers[i].value);
}
}
setHeader(field, val) {
if (typeof field === 'string') {
const lowerCaseField = field.toLowerCase();
if (lowerCaseField === 'content-type' && Array.isArray(val)) {
throw new TypeError('Content-Type cannot be set to an Array');
}
if (typeof val === 'string') {
this._headers.push({
name: lowerCaseField,
value: val,
});
return this;
}
if (Array.isArray(val)) {
val.forEach((v) => {
this._headers.push({
name: lowerCaseField,
value: v,
});
});
return this;
}
return this;
}
Object.entries(field).forEach(([key, value]) => {
const lowerCaseKey = key.toLowerCase();
if (typeof value === 'string') {
this._headers.push({ name: lowerCaseKey, value });
return;
}
if (Array.isArray(value))
value.forEach((v) => {
this._headers.push({
name: lowerCaseKey,
value: v,
});
});
});
return this;
}
setType(type) {
const ct = type.indexOf('/') === -1 ? mime_types_1.default.lookup(type) : type;
return this.set('Content-Type', ct);
}
get(field) {
return this._headers.find((h) => h.name === field.toLowerCase())?.value;
}
getHeaders() {
return this._headers;
}
status(code) {
this._statusCode = `${code}`;
return this;
}
sendStatus(code) {
const body = `${statuses_1.default(code)}` || `${this._statusCode}`;
this._statusCode = `${code}`;
this.type('txt');
this.send(body);
}
send(body) {
const type = this.get('Content-Type');
if (typeof body === 'string') {
if (!type) {
this.set('Content-Type', CONTENT_TYPE.HTML);
}
return this.end(body);
}
// Check if body is Buffer
if (Buffer.isBuffer(body)) {
if (type)
this.type(CONTENT_TYPE.OCTET);
return this.end(body);
}
return this.json(body);
}
json(body) {
if (!this.get('Content-Type'))
this.type(CONTENT_TYPE.JSON);
// this._headers.push({
// name: 'content-type',
// value: 'application/json;charset=utf-8',
// });
this.end(body && JSON.stringify(body));
}
location(url) {
if (url === 'back') {
const loc = this[symbol_1.FROM_REQ].get('Referrer');
if (!loc)
return this.set('Location', '/');
return this.set('Location', encodeURI(loc));
}
return this.set('Location', encodeURI(url));
}
redirect(urlOrStatus, url) {
let statusCode = 302;
if (arguments.length === 2) {
if (typeof urlOrStatus === 'number')
statusCode = urlOrStatus;
this.status(statusCode);
this.location(url);
const body = `<p>${statuses_1.default[statusCode]}. Redirecting to <a href=${this.get('location')}>${this.get('location')}</a></p>`;
this.end(body);
return;
}
this.status(statusCode);
this.location(urlOrStatus);
const body = `<p>${statuses_1.default[statusCode]}. Redirecting to <a href=${this.get('location')}>${this.get('location')}</a></p>`;
this.set('Content-Length', `${Buffer.byteLength(body)}`);
this.end(body);
}
// TODO handle when there is a range in req header
sendFile(path, options, cb) {
// Serve buffer data to client
const ct = this.get('Content-Disposition');
if (Buffer.isBuffer(path)) {
if (typeof options !== 'object' || !options.mime) {
this.debug.trace('Missing Content-Type when serving file directly by buffer');
this.status(404).end();
return;
}
if (!ct) {
this.set('Content-Type', options.mime);
this.set('Last-Modified', options.lastModified);
this.set('Cache-Control', options.maxAge
? `public, max-age=${options.maxAge}`
: 'no-cache, no-store, must-revalidate');
}
this.end(path);
return;
}
// Read file from path
if (typeof path === 'string') {
const fileName = path_1.basename(path);
let totalSize = -1;
let mimeType = '';
let isDir = false;
let lastModified = '';
try {
if (typeof options === 'object') {
if (options.mime) {
mimeType = options.mime;
}
else {
mimeType = mime_types_1.contentType(fileName) || 'application/octet-stream';
}
if (options.fileSize) {
totalSize = options.fileSize;
}
if (options.lastModified) {
lastModified = options.lastModified;
}
else {
const stat = fs_1.default.statSync(path);
totalSize = stat.size;
isDir = stat.isDirectory();
lastModified = stat.mtime.toUTCString();
}
}
else {
mimeType = mime_types_1.contentType(fileName) || 'application/octet-stream';
const stat = fs_1.default.statSync(path);
totalSize = stat.size;
isDir = stat.isDirectory();
lastModified = stat.mtime.toUTCString();
}
if (isDir) {
this.status(404).end();
return;
}
if (totalSize !== -1 && totalSize <= this.maxReadFileSize) {
const fileBuffer = fs_1.default.readFileSync(path);
if (!ct) {
this.set('Content-Type', mimeType);
this.set('Last-Modified', lastModified);
this.set('Cache-Control', options && typeof options === 'object' && options.maxAge
? `public, max-age=${options.maxAge}`
: 'no-cache, no-store, must-revalidate');
}
this.end(fileBuffer);
return;
}
const stream = fs_1.createReadStream(path);
this[symbol_1.READ_STREAM] = stream;
this.attachAbortHandler(true);
if (!ct)
this[symbol_1.WRITE_HEADER]('Content-Type', mimeType);
else
this[symbol_1.WRITE_HEADER]('Content-Disposition', ct);
/**
* This code was taken from uWebSockets.js examples
*
* @see https://github.com/uNetworking/uWebSockets.js/blob/master/examples/VideoStreamer.js
*/
stream
.on('data', (chunk) => {
/* We only take standard V8 units of data */
const ab = utils_1.toArrayBuffer(chunk);
/* Store where we are, globally, in our response */
const lastOffset = this[symbol_1.GET_WRITE_OFFSET]();
/* Streaming a chunk returns whether that chunk was sent, and if that chunk was last */
const [ok, done] = this[symbol_1.TRY_END](ab, totalSize);
/* Did we successfully send last chunk? */
if (done) {
if (this.originalRes.id === -1) {
this.debug.error('ERROR! onAbortedOrFinishedResponse called twice for the same res!');
}
else {
stream.destroy();
}
/* Mark this response already accounted for */
this.originalRes.id = -1;
}
else if (!ok) {
/* If we could not send this chunk, pause */
stream.pause();
/* Save unsent chunk for when we can send it */
this.originalRes.ab = ab;
this.originalRes.ab.abOffset = lastOffset;
/* Register async handlers for drainage */
this[symbol_1.ON_WRITABLE]((offset) => {
/* Here the timeout is off, we can spend as much time before calling tryEnd we want to */
/* On failure the timeout will start */
const [ok, done] = this[symbol_1.TRY_END](this.originalRes.ab.slice(offset - this.originalRes.abOffset), totalSize);
if (done) {
if (this.originalRes.id === -1) {
this.debug.error('ERROR! onAbortedOrFinishedResponse called twice for the same res!');
}
else {
stream.destroy();
}
/* Mark this response already accounted for */
this.originalRes.id = -1;
}
else if (ok) {
/* We sent a chunk and it was not the last one, so let's resume reading.
* Timeout is still disabled, so we can spend any amount of time waiting
* for more chunks to send. */
stream.resume();
}
/* We always have to return true/false in onWritable.
* If you did not send anything, return true for success. */
return ok;
});
}
})
.on('error', (err) => {
this.debug.trace(err);
this.status(404);
this[symbol_1.END]();
});
return;
}
catch (err) {
this.debug.trace(err);
this.status(404);
this.end();
return;
}
}
this.debug.trace('Invalid agurments', arguments);
this.status(404);
this.end();
}
cookie(name, val, options) {
const opts = typeof options === 'object' ? options : {};
const str = typeof val === 'object' ? `j:${JSON.stringify(val)}` : `${val}`;
if (opts.maxAge) {
opts.expires = new Date(Date.now() + opts.maxAge);
opts.maxAge /= 1000;
}
if (opts.path == null) {
opts.path = '/';
}
this.set('set-cookie', cookie_1.serialize(name, str, opts));
return this;
}
download(path, fileName, options = {}) {
this.setHeader('Content-Disposition', content_disposition_1.default(path || fileName)).sendFile(path);
}
render(view, options, callback) {
let cb = callback;
let opts = options;
if (typeof options === 'function') {
cb = options;
opts = {};
}
if (!cb) {
// eslint-disable-next-line consistent-return
cb = (err, html) => {
if (err) {
this.debug.trace(err);
if (err instanceof ReferenceError) {
this.status(404);
return this.send(utils_1.toHtml(`${err.stack
?.toString()
.replace(/</g, '<')
.replace(/>/g, '>')}`));
}
return this.status(500).json(err);
}
this.set('Content-Type', CONTENT_TYPE.HTML).end(html);
};
}
const { render } = this[symbol_1.FROM_APP];
if (!render) {
this.status(500).end('Missing view render method');
return;
}
render(view, opts, cb);
}
end(body = '', hasAsync) {
if (hasAsync || this[symbol_1.HAS_ASYNC]) {
if (!this.originalRes.aborted)
this[symbol_1.CORK](() => {
this.setHeaderAndStatusByNativeMethod();
this[symbol_1.END](body);
});
return;
}
this.setHeaderAndStatusByNativeMethod();
this[symbol_1.END](body);
}
}
exports.default = Response;