UNPKG

@intentjs/hyper-express

Version:

A fork of hyper-express to suit IntentJS requirements. High performance Node.js webserver with a simple-to-use API powered by uWebsockets.js under the hood.

1,026 lines (883 loc) 41.2 kB
'use strict'; const crypto = require('crypto'); const cookie = require('cookie'); const signature = require('cookie-signature'); const status_codes = require('http').STATUS_CODES; const mime_types = require('mime-types'); const stream = require('stream'); const NodeResponse = require('../compatibility/NodeResponse.js'); const ExpressResponse = require('../compatibility/ExpressResponse.js'); const { inherit_prototype } = require('../../shared/operators.js'); const FilePool = {}; const LiveFile = require('../plugins/LiveFile.js'); const SSEventStream = require('../plugins/SSEventStream.js'); class Response { _sse; _locals; route = null; _corked = false; _streaming = false; _middleware_cursor; _wrapped_request = null; _upgrade_socket = null; _raw_response = null; /** * Returns the custom HTTP underlying status code of the response. * @private * @type {Number=} */ _status_code; /** * Returns the custom HTTP underlying status code message of the response. * @private * @type {String=} */ _status_message; /** * Contains underlying headers for the response. * @private * @type {Record<string, string|string[]} */ _headers = {}; /** * Contains underlying cookies for the response. * @private * @type {Record<string, string>} */ _cookies; /** * Underlying lazy initialized writable body stream. * @private */ _writable = null; /** * Whether this response needs to cork before sending. * @private */ _cork = false; /** * Alias of aborted property as they both represent the same request state in terms of inaccessibility. * @returns {Boolean} */ completed = false; /** * Returns whether response has been initiated by writing the HTTP status code and headers. * Note! No changes can be made to the HTTP status code or headers after a response has been initiated. * @returns {Boolean} */ initiated = false; /** * Creates a new HyperExpress response instance that wraps a uWS.HttpResponse instance. * * @param {import('uWebSockets.js').HttpResponse} raw_response */ constructor(raw_response) { this._raw_response = raw_response; // Bind the abort handler as required by uWebsockets.js for each uWS.HttpResponse to allow for async processing raw_response.onAborted(() => { // If this request has already been completed then this request cannot be aborted again if (this.completed) return; this.completed = true; // Decrement the pending request count this.route.app._resolve_pending_request(); // Stop the body parser from accepting any more data this._wrapped_request._body_parser_stop(); // Ensure we have a writable/emitter instance to emit over if (this._writable) { // Emit an 'abort' event to signify that the client aborted the request this.emit('abort', this._wrapped_request, this); // Emit an 'close' event to signify that the client has disconnected this.emit('close', this._wrapped_request, this); } }); } /* HyperExpress Methods */ /** * Tracks middleware cursor position over a request's lifetime. * This is so we can detect any double middleware iterations and throw an error. * @private * @param {Number} position - Cursor position */ _track_middleware_cursor(position) { // Track and ensure each middleware cursor value is greater than previously tracked value for sequential progression if (this._middleware_cursor === undefined || position > this._middleware_cursor) return (this._middleware_cursor = position); // If position is not greater than last cursor then we likely have a double middleware execution this.throw( new Error( 'ERR_DOUBLE_MIDDLEWARE_EXEUCTION_DETECTED: Please ensure you are not calling the next() iterator inside of an ASYNC middleware. You must only call next() ONCE per middleware inside of SYNCHRONOUS middlewares only!', ), ); } /* Response Methods/Operators */ /** * Alias of `uWS.HttpResponse.cork()` which allows for manual corking of the response. * This is required by `uWebsockets.js` to maximize network performance with batched writes. * * @param {Function} handler * @returns {Response} Response (Chainable) */ atomic(handler) { // Cork the provided handler if the response is not finished yet if (!this.completed) this._raw_response.cork(handler); // Make this chainable return this; } /** * This method is used to set a custom response code. * * @param {Number} code Example: response.status(403) * @param {String=} message Example: response.status(403, 'Forbidden') * @returns {Response} Response (Chainable) */ status(code, message) { // Set the numeric status code. Status text is appended before writing status to uws this._status_code = code; this._status_message = message; return this; } /** * This method is used to set the response content type header based on the provided mime type. Example: type('json') * * @param {String} mime_type Mime type * @returns {Response} Response (Chainable) */ type(mime_type) { // Remove leading dot from mime type if present if (mime_type[0] === '.') mime_type = mime_type.substring(1); // Determine proper mime type and send response this.header('content-type', mime_types.contentType(mime_type) || 'text/plain'); return this; } /** * This method can be used to write a response header and supports chaining. * * @param {String} name Header Name * @param {String|String[]} value Header Value * @param {Boolean=} overwrite If true, overwrites existing header value with same name * @returns {Response} Response (Chainable) */ header(name, value, overwrite) { // Enforce lowercase for header name name = name.toLowerCase(); // Determine if this operation is an overwrite onto any existing header values if (overwrite) { // Overwrite the header value this._headers[name] = value; // Check if some value(s) already exist for this header name } else if (this._headers[name]) { // Check if there are multiple current values for this header name if (Array.isArray(this._headers[name])) { // Check if the provided value is an array if (Array.isArray(value)) { // Concatenate the current and provided header values this._headers[name] = this._headers[name].concat(value); } else { // Push the provided header value to the current header values array this._headers[name].push(value); } } else { // Convert the current header value to an array this._headers[name] = [this._headers[name], value]; } } else { // Write the header value this._headers[name] = value; } // Make chainable return this; } /** * @typedef {Object} CookieOptions * @property {String} domain * @property {String} path * @property {Number} maxAge * @property {Boolean} secure * @property {Boolean} httpOnly * @property {Boolean|'none'|'lax'|'strict'} sameSite * @property {String} secret */ /** * This method is used to write a cookie to incoming request. * To delete a cookie, set the value to null. * * @param {String} name Cookie Name * @param {String|null} value Cookie Value * @param {Number=} expiry In milliseconds * @param {CookieOptions=} options Cookie Options * @param {Boolean=} sign_cookie Enables/Disables Cookie Signing * @returns {Response} Response (Chainable) */ cookie(name, value, expiry, options, sign_cookie = true) { // Determine if this is a delete operation and recursively call self with appropriate options if (name && value === null) return this.cookie(name, '', null, { maxAge: 0, }); // If an options object was not provided, shallow copy it to prevent mutation to the original object // If an options object was not provided, create a new object with default options options = options ? { ...options } : { secure: true, sameSite: 'none', path: '/', }; // Determine if a expiry duration was provided in milliseconds if (typeof expiry == 'number') { // Set the expires value of the cookie if one was not already defined options.expires = options.expires || new Date(Date.now() + expiry); // Define a max age if one was not already defined options.maxAge = options.maxAge || Math.round(expiry / 1000); } // Sign cookie value if signing is enabled and a valid secret is provided if (sign_cookie && typeof options.secret == 'string') { options.encode = false; // Turn off encoding to prevent loss of signature structure value = signature.sign(value, options.secret); } // Initialize the cookies holder object if it does not exist if (this._cookies == undefined) this._cookies = {}; // Store the seralized cookie value to be written during response this._cookies[name] = cookie.serialize(name, value, options); return this; } /** * This method is used to upgrade an incoming upgrade HTTP request to a Websocket connection. * @param {Object=} context Store information about the websocket connection */ upgrade(context) { // Do not allow upgrades if request is already completed if (this.completed) return; // Ensure a upgrade_socket exists before upgrading ensuring only upgrade handler requests are handled if (this._upgrade_socket == null) this.throw( new Error( 'HyperExpress: You cannot upgrade a request that does not come from an upgrade handler. No upgrade socket was found.', ), ); // Resume the request in case it was paused this._wrapped_request.resume(); // Cork the response if it has not been corked yet for when this was handled asynchonously if (this._cork && !this._corked) { this._corked = true; return this.atomic(() => this.upgrade(context)); } // Call uWS.Response.upgrade() method with user data, protocol headers and uWS upgrade socket const headers = this._wrapped_request.headers; this._raw_response.upgrade( { context, }, headers['sec-websocket-key'], headers['sec-websocket-protocol'], headers['sec-websocket-extensions'], this._upgrade_socket, ); // Mark request as complete so no more operations can be performed this.completed = true; // Decrement the pending request count this.route.app._resolve_pending_request(); } /** * Initiates response process by writing HTTP status code and then writing the appropriate headers. * @private * @returns {Boolean} */ _initiate_response() { // Halt execution if response has already been initiated or completed if (this.initiated) return false; // Emit the 'prepare' event to allow for any last minute response modifications if (this._writable) this.emit('prepare', this._wrapped_request, this); // Mark the instance as initiated signifying that no more status/header based operations can be performed this.initiated = true; // Resume the request in case it was paused this._wrapped_request.resume(); // Write the appropriate status code to the response along with mapped status code message if (this._status_code || this._status_message) this._raw_response.writeStatus( this._status_code + ' ' + (this._status_message || status_codes[this._status_code]), ); // Iterate through all headers and write them to uWS for (const name in this._headers) { // If this is a custom content-length header, we need to skip it as we will write it later during the response send if (name == 'content-length') continue; // Write the header value to uWS const values = this._headers[name]; if (Array.isArray(values)) { // Write each individual header value to uWS as there are multiple headers for (const value of values) { this._raw_response.writeHeader(name, value); } } else { // Write the single header value to uWS this._raw_response.writeHeader(name, values); } } // Iterate through all cookies and write them to uWS if (this._cookies) { for (const name in this._cookies) { this._raw_response.writeHeader('set-cookie', this._cookies[name]); } } // Signify that the response was successfully initiated return true; } _drain_handler = null; /** * Binds a drain handler which gets called with a byte offset that can be used to try a failed chunk write. * You MUST perform a write call inside the handler for uWS chunking to work properly. * You MUST return a boolean value indicating if the write was successful or not. * Note! This method can only provie drain events to a single handler at any given time which means If you call this method again with a different handler, it will stop providing drain events to the previous handler. * * @param {function(number):boolean} handler Synchronous callback only */ drain(handler) { // Determine if this is the first time the drain handler is being set const is_first_time = this._drain_handler === null; // Store the handler which will be used to provide drain events to uWS this._drain_handler = handler; // Bind a writable handler with a fallback return value to true as uWS expects a Boolean if (is_first_time) this._raw_response.onWritable((offset) => { // Retrieve the write result from the handler const output = this._drain_handler(offset); // Throw an exception if the handler did not return a boolean value as that is an improper implementation if (typeof output !== 'boolean') this.throw( new Error( 'HyperExpress: Response.drain(handler) -> handler must return a boolean value stating if the write was successful or not.', ), ); // Return the boolean value to uWS as required by uWS documentation return output; }); } /** * Writes the provided chunk to the client over uWS with backpressure handling if a callback is provided. * * @private * @param {String|Buffer|ArrayBuffer} chunk * @param {String} encoding * @param {Function} callback */ _write(chunk, encoding, callback) { // Spread the arguments to allow for a single object argument if (chunk.chunk && chunk.encoding) { // Pull out the chunk and encoding from the object argument const temp = chunk; chunk = temp.chunk; encoding = temp.encoding; // Only use the callback from this specific chunk if one is not provided // This is because we want to respect the iteratore callback from the _writev method if (!callback) callback = temp.callback; } // Ensure this request has not been completed yet if (!this.completed) { // If this response has not be marked as an active stream, mark it as one and bind a 'finish' event handler to send response once a piped stream has completed if (!this._streaming) { this._streaming = true; this.once('finish', () => this.send()); } // Attempt to write the chunk to the client with backpressure handling this._stream_chunk(chunk).then(callback).catch(callback); } else { // Trigger callback to flush the chunk as the response has already been completed callback(); } } /** * Writes multiples chunks for the response to the client over uWS with backpressure handling if a callback is provided. * * @private * @param {Array<Buffer>} chunks * @param {Function} callback * @param {number} index */ _writev(chunks, callback, index = 0) { // Serve the chunk at the current index this._write(chunks[index], null, (error) => { // Pass the error to the callback if one was provided if (error) return callback(error); // Trigger the specific callback for the chunk we just served if it was in object format if (typeof chunks[index].callback == 'function') chunks[index].callback(); // Determine if we have more chunks after the chunk we just served if (index < chunks.length - 1) { // Recursively serve the remaining chunks this._writev(chunks, callback, index + 1); } else { // Trigger the callback as all chunks have been served callback(); } }); } /** * This method is used to end the current request and send response with specified body and headers. * * @param {String|Buffer|ArrayBuffer=} body Optional * @param {Boolean=} close_connection * @returns {Response} */ send(body, close_connection) { // Ensure response connection is still active if (!this.completed) { // If this request has a writable stream with some data in it, we must schedule this send() as the last chunk after which the stream will be flushed if (this._writable && this._writable.writableLength) { // If we have some body data, queue it as the last chunk of the body to be written if (body) this._writable.write(body); // Mark the writable stream as ended this._writable.end(); // Return this to make the Router chainable return this; } // If the response has not been corked yet, cork it and wait for the next tick to send the response if (this._cork && !this._corked) { this._corked = true; return this.atomic(() => this.send(body, close_connection)); } // Initiate the response to begin writing the status code and headers this._initiate_response(); // Determine if the request still has not fully received the whole request body yet if (!this._wrapped_request.received) { // Instruct the request to stop accepting any more data as a response is being sent this._wrapped_request._body_parser_stop(); // Wait for the request to fully receive the whole request body before sending the response return this._wrapped_request.once('received', () => // Because 'received' will be emitted asynchronously, we need to cork the response to ensure the response is sent in the correct order this.atomic(() => this.send(body, close_connection)), ); } // If we have no body and are not streaming and have a custom content-length header, we need to send a response without a body with the custom content-length header const custom_length = this._headers['content-length']; if (!(body !== undefined || this._streaming || !custom_length)) { // We can only use one of the content-lengths, so we will use the last one if there are multiple const content_length = typeof custom_length == 'string' ? custom_length : custom_length[custom_length.length - 1]; // Send the response with the uWS.HttpResponse.endWithoutBody() method as we have no body data // NOTE: This method is completely undocumented by uWS but exists in the source code to solve the problem of no body being sent with a custom content-length this._raw_response.endWithoutBody(content_length, close_connection); } else { // Send the response with the uWS.HttpResponse.end(body, close_connection) method as we have some body data this._raw_response.end(body, close_connection); } // Emit the 'finish' event to signify that the response has been sent without streaming if (this._writable && !this._streaming) this.emit('finish', this._wrapped_request, this); // Mark request as completed as it has been sent this.completed = true; // Decrement the pending request count this.route.app._resolve_pending_request(); // Emit the 'close' event to signify that the response has been completed if (this._writable) this.emit('close', this._wrapped_request, this); } // Make chainable return this; } /** * Writes a given chunk to the client over uWS with the appropriate writing method. * Note! This method uses `uWS.tryEnd()` when a `total_size` is provided. * Note! This method uses `uWS.write()` when a `total_size` is not provided. * * @private * @param {Buffer} chunk * @param {Number=} total_size * @returns {Array<Boolean>} [sent, finished] */ _uws_write_chunk(chunk, total_size) { // The specific uWS method to stream the chunk to the client differs depending on if we have a total_size or not let sent, finished; if (total_size) { // Attempt to stream the current chunk using uWS.tryEnd with a total size const [ok, done] = this._raw_response.tryEnd(chunk, total_size); sent = ok; finished = done; } else { // Attempt to stream the current chunk uWS.write() sent = this._raw_response.write(chunk); // Since we are streaming without a total size, we are not finished finished = false; } // Return the sent and finished booleans return [sent, finished]; } /** * Stream an individual chunk to the client with backpressure handling. * Delivers with chunked transfer and without content-length header when no total_size is specified. * Delivers with chunk writes and content-length header when a total_size is specified. * Calls the `callback` once the chunk has been fully sent to the client. * * @private * @param {Buffer} chunk * @param {Number=} total_size * @returns {Promise} */ _stream_chunk(chunk, total_size) { // If the request has already been completed, we can resolve the promise immediately as we cannot write to the client anymore if (this.completed) return Promise.resolve(); // Return a Promise which resolves once the chunk has been fully sent to the client return new Promise((resolve) => this.atomic(() => { // Ensure the client is still connected after the cork if (this.completed) return resolve(); // Initiate the response to ensure status code & headers get written first if they have not been written yet this._initiate_response(); // Remember the initial write offset for future backpressure sliced chunks // Write the chunk to the client using the appropriate uWS chunk writing method const write_offset = this.write_offset; const [sent] = this._uws_write_chunk(chunk, total_size); if (sent) { // The chunk was fully sent, we can resolve the promise resolve(); } else { // Bind a drain handler to relieve backpressure // Note! This callback may be called as many times as neccessary to send a full chunk when using the tryEnd method this.drain((offset) => { // Check if the response has been completed / connection has been closed since we can no longer write to the client // When total_size is not provided, the chunk has been fully sent already via uWS.write() // Only when total_size is provided we need to retry to send the ramining chunk since we have used uWS.tryEnd() and // that does not guarantee that the whole chunk has been sent when stream is drained if (this.completed || !total_size) { resolve(); return true; } // Attempt to write the remaining chunk to the client const remaining = chunk.slice(offset - write_offset); const [flushed] = this._uws_write_chunk(remaining, total_size); if (flushed) resolve(); // Return the flushed boolean as not flushed means we are still waiting for more drain events from uWS return flushed; }); } }), ); } /** * This method is used to serve a readable stream as response body and send response. * By default, this method will use chunked encoding transfer to stream data. * If your use-case requires a content-length header, you must specify the total payload size. * * @param {stream.Readable} readable A Readable stream which will be consumed as response body * @param {Number=} total_size Total size of the Readable stream source in bytes (Optional) * @returns {Promise} a Promise which resolves once the stream has been fully consumed and response has been sent */ async stream(readable, total_size) { // Ensure readable is an instance of a stream.Readable if (!(readable instanceof stream.Readable)) this.throw( new Error('HyperExpress: Response.stream(readable, total_size) -> readable must be a Readable stream.'), ); // Do not allow streaming if response has already been aborted or completed if (!this.completed) { // Bind an 'close' event handler which will destroy the consumed stream if request is closed this.once('close', () => (!readable.destroyed ? readable.destroy() : null)); // Define a while loop to consume chunks from the readable stream until it is fully consumed or the response has been completed while (!this.completed && !(readable.readableEnded || readable.destroyed)) { // Attempt to read a chunk from the readable stream let chunk = readable.read(); if (!chunk) { // Wait for the readable stream to emit a 'readable' event if no chunk was available in our initial read attempt await new Promise((resolve) => { // Bind a 'end' handler in case the readable stream ends before emitting a 'readable' event readable.once('end', resolve); // Bind a 'readable' handler to resolve the promise once a chunk is available to read readable.once('readable', () => { // Unbind the 'end' handler as we have a chunk available to read readable.removeListener('end', resolve); // Resolve the promise to continue the while loop resolve(); }); }); // Attempt to read a chunk from the readable stream again chunk = readable.read(); } // Stream the chunk to the client if (chunk) await this._stream_chunk(chunk, total_size); } // If we had no total size and the response is still not completed, we need to end the response // This is because no total size means we served with chunked encoding and we need to end the response as it is a unbounded stream if (!this.completed) { // Determine if we have a total size or not if (total_size) { // We must call the decrement method as a conventional close would not be detected this.route.app._resolve_pending_request(); } else { // We must manually close the response as this stream operation is unbounded this.send(); } } } } /** * Instantly aborts/closes current request without writing a status response code. * Use this to instantly abort a request where a proper response with an HTTP status code is not neccessary. */ close() { // Ensure request has already not been completed if (!this.completed) { // Mark request as completed this.completed = true; // Decrement the pending request count this.route.app._resolve_pending_request(); // Stop the body parser from accepting any more data this._wrapped_request._body_parser_stop(); // Resume the request in case it was paused this._wrapped_request.resume(); // Close the underlying uWS request this._raw_response.close(); } } /** * This method is used to redirect an incoming request to a different url. * * @param {String} url Redirect URL * @returns {Boolean} Boolean */ redirect(url) { if (!this.completed) return this.status(302).header('location', url).send(); return false; } /** * This method is an alias of send() method except it accepts an object and automatically stringifies the passed payload object. * * @param {Object} body JSON body * @returns {Boolean} Boolean */ json(body) { return this.header('content-type', 'application/json', true).send(JSON.stringify(body)); } /** * This method is an alias of send() method except it accepts an object * and automatically stringifies the passed payload object with a callback name. * Note! This method uses 'callback' query parameter by default but you can specify 'name' to use something else. * * @param {Object} body * @param {String=} name * @returns {Boolean} Boolean */ jsonp(body, name) { let query_parameters = this._wrapped_request.query_parameters; let method_name = query_parameters['callback'] || name; return this.header('content-type', 'application/javascript', true).send( `${method_name}(${JSON.stringify(body)})`, ); } /** * This method is an alias of send() method except it automatically sets * html as the response content type and sends provided html response body. * * @param {String} body * @returns {Boolean} Boolean */ html(body) { return this.header('content-type', 'text/html', true).send(body); } /** * @private * Sends file content with appropriate content-type header based on file extension from LiveFile. * * @param {LiveFile} live_file * @param {function(Object):void} callback */ async _send_file(live_file, callback) { // Wait for LiveFile to be ready before serving if (!live_file.is_ready) await live_file.ready(); // Write appropriate extension type if one has not been written yet this.type(live_file.extension); // Send response with file buffer as body this.send(live_file.buffer); // Execute callback with cache pool, so user can expire as they wish. if (callback) setImmediate(() => callback(FilePool)); } /** * This method is an alias of send() method except it sends the file at specified path. * This method automatically writes the appropriate content-type header if one has not been specified yet. * This method also maintains its own cache pool in memory allowing for fast performance. * Avoid using this method to a send a large file as it will be kept in memory. * * @param {String} path * @param {function(Object):void=} callback Executed after file has been served with the parameter being the cache pool. */ file(path, callback) { // Send file from local cache pool if available if (FilePool[path]) return this._send_file(FilePool[path], callback); // Create new LiveFile instance in local cache pool for new file path FilePool[path] = new LiveFile({ path, }); // Assign error handler to live file FilePool[path].on('error', (error) => this.throw(error)); // Serve file as response this._send_file(FilePool[path], callback); } /** * Writes approriate headers to signify that file at path has been attached. * * @param {String} path * @param {String=} name * @returns {Response} */ attachment(path, name) { // Attach a blank content-disposition header when no filename is defined if (path == undefined) return this.header('Content-Disposition', 'attachment'); // Parses path in to file name and extension to write appropriate attachment headers let chunks = path.split('/'); let final_name = name || chunks[chunks.length - 1]; let name_chunks = final_name.split('.'); let extension = name_chunks[name_chunks.length - 1]; return this.header('content-disposition', `attachment; filename="${final_name}"`).type(extension); } /** * Writes appropriate attachment headers and sends file content for download on user browser. * This method combined Response.attachment() and Response.file() under the hood, so be sure to follow the same guidelines for usage. * * @param {String} path * @param {String=} filename */ download(path, filename) { return this.attachment(path, filename).file(path); } #thrown = false; /** * This method allows you to throw an error which will be caught by the global error handler. * * @param {Error} error * @returns {Response} */ throw(error) { // If we have already thrown an error, ignore further throws if (this.#thrown) return this; this.#thrown = true; // If the error is not an instance of Error, wrap it in an Error object that if (!(error instanceof Error)) error = new Error(`ERR_CAUGHT_NON_ERROR_TYPE: ${error}`); // Trigger the global error handler this.route.app.handlers.on_error(this._wrapped_request, this, error); // Return this response instance return this; } /* HyperExpress Properties */ /** * Returns the request locals for this request. * @returns {Object.<string, any>} */ get locals() { // Initialize locals object if it does not exist if (!this._locals) this._locals = {}; return this._locals; } /** * Returns the underlying raw uWS.Response object. * Note! Utilizing any of uWS.Response's methods after response has been sent will result in an invalid discarded access error. * @returns {import('uWebSockets.js').Response} */ get raw() { return this._raw_response; } /** * Returns the HyperExpress.Server instance this Response object originated from. * * @returns {import('../Server.js')} */ get app() { return this.route.app; } /** * Returns current state of request in regards to whether the source is still connected. * @returns {Boolean} */ get aborted() { return this.completed; } /** * Upgrade socket context for upgrade requests. * @returns {import('uWebSockets.js').ux_socket_context} */ get upgrade_socket() { return this._upgrade_socket; } /** * Returns a "Server-Sent Events" connection object to allow for SSE functionality. * This property will only be available for GET requests as per the SSE specification. * * @returns {SSEventStream=} */ get sse() { // Return a new SSE instance if one has not been created yet if (this._wrapped_request.method === 'GET') { // Initialize the SSE instance if one has not been created yet if (this._sse === undefined) { this._sse = new SSEventStream(); // Provide the response object to the SSE instance this._sse._response = this; } // Return the SSE instance return this._sse; } } /** * Returns the current response body content write offset in bytes. * Use in conjunction with the drain() offset handler to retry writing failed chunks. * Note! This method will return `-1` after the Response has been completed and the connection has been closed. * @returns {Number} */ get write_offset() { return this.completed ? -1 : this._raw_response.getWriteOffset(); } /** * Throws a descriptive error when an unsupported ExpressJS property/method is invocated. * @private * @param {String} name */ _throw_unsupported(name) { throw new Error( `ERR_INCOMPATIBLE_CALL: One of your middlewares or route logic tried to call Response.${name} which is unsupported with HyperExpress.`, ); } text(data) { this.type('text'); this.send(data); return this; } notFound() { this.status(404); return this; } } // Store the descriptors of the original HyperExpress.Response class const descriptors = Object.getOwnPropertyDescriptors(Response.prototype); // Inherit the compatibility classes inherit_prototype({ from: [NodeResponse.prototype, ExpressResponse.prototype], to: Response.prototype, method: (type, name, original) => { // Initialize a passthrough method for each descriptor const passthrough = function () { // Call the original function with the Request scope return original.apply(this, arguments); }; // Return the passthrough function return passthrough; }, }); // Inherit the stream.Writable prototype and lazy initialize the stream on first call to any inherited method inherit_prototype({ from: stream.Writable.prototype, to: Response.prototype, override: (name) => '_super_' + name, // Prefix all overrides with _super_ method: (type, name, original) => { // Initialize a pass through method const passthrough = function () { // Lazy initialize the writable stream on local scope if (this._writable === null) { // Initialize the writable stream this._writable = new stream.Writable(this.route.streaming.writable); // Bind the natively implemented _write and _writev methods // Ensure the Response scope is passed to these methods this._writable._write = descriptors['_write'].value.bind(this); this._writable._writev = descriptors['_writev'].value.bind(this); } // Return the original function with the writable stream as the context return original.apply(this._writable, arguments); }; // Otherwise, simply return the passthrough method return passthrough; }, }); module.exports = Response;