diff --git a/lib/core/util.js b/lib/core/util.js index 714f3874314..56c895cf25d 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -28,6 +28,10 @@ class BodyAsyncIterable { } } +/** + * @param {*} body + * @returns {*} + */ function wrapRequestBody (body) { if (isStream(body)) { // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp @@ -67,11 +71,19 @@ function wrapRequestBody (body) { } } +/** + * @param {*} obj + * @returns {obj is import('node:stream').Stream} + */ function isStream (obj) { return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' } -// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) +/** + * @param {*} object + * @returns {object is Blob} + * based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) + */ function isBlobLike (object) { if (object === null) { return false @@ -108,6 +120,10 @@ function serializePathWithQuery (url, queryParams) { return url } +/** + * @param {number|string|undefined} port + * @returns {boolean} + */ function isValidPort (port) { const value = parseInt(port, 10) return ( @@ -117,6 +133,12 @@ function isValidPort (port) { ) } +/** + * Check if the value is a valid http or https prefixed string. + * + * @param {string} value + * @returns {boolean} + */ function isHttpOrHttpsPrefixed (value) { return ( value != null && @@ -134,8 +156,15 @@ function isHttpOrHttpsPrefixed (value) { ) } +/** + * @param {string|URL|Record} url + * @returns {URL} + */ function parseURL (url) { if (typeof url === 'string') { + /** + * @type {URL} + */ url = new URL(url) if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { @@ -205,6 +234,10 @@ function parseURL (url) { return url } +/** + * @param {string|URL|Record} url + * @returns {URL} + */ function parseOrigin (url) { url = parseURL(url) @@ -215,6 +248,10 @@ function parseOrigin (url) { return url } +/** + * @param {string} host + * @returns {string} + */ function getHostname (host) { if (host[0] === '[') { const idx = host.indexOf(']') @@ -229,8 +266,12 @@ function getHostname (host) { return host.substring(0, idx) } -// IP addresses are not valid server names per RFC6066 -// > Currently, the only server names supported are DNS hostnames +/** + * IP addresses are not valid server names per RFC6066 + * Currently, the only server names supported are DNS hostnames + * @param {string|null} host + * @returns {string|null} + */ function getServerName (host) { if (!host) { return null @@ -246,18 +287,34 @@ function getServerName (host) { return servername } +/** + * @param {*} obj + * @returns {*} + */ function deepClone (obj) { return JSON.parse(JSON.stringify(obj)) } +/** + * @param {*} obj + * @returns {obj is AsyncIterable} + */ function isAsyncIterable (obj) { return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function') } +/** + * @param {*} obj + * @returns {obj is Iterable} + */ function isIterable (obj) { return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) } +/** + * @param {Blob|Buffer|import ('stream').Stream} body + * @returns {number|null} + */ function bodyLength (body) { if (body == null) { return 0 @@ -275,10 +332,19 @@ function bodyLength (body) { return null } +/** + * @param {import ('stream').Stream} body + * @returns {boolean} + */ function isDestroyed (body) { return body && !!(body.destroyed || body[kDestroyed] || (stream.isDestroyed?.(body))) } +/** + * @param {import ('stream').Stream} stream + * @param {Error} [err] + * @returns + */ function destroy (stream, err) { if (stream == null || !isStream(stream) || isDestroyed(stream)) { return @@ -303,8 +369,12 @@ function destroy (stream, err) { } const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ +/** + * @param {string} val + * @returns {number | null} + */ function parseKeepAliveTimeout (val) { - const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR) + const m = val.match(KEEPALIVE_TIMEOUT_EXPR) return m ? parseInt(m[1], 10) * 1000 : null } @@ -329,12 +399,13 @@ function bufferToLowerCasedHeaderName (value) { } /** - * @param {Record | (Buffer | string | (Buffer | string)[])[]} headers + * @param {(Buffer | string)[]} headers * @param {Record} [obj] * @returns {Record} */ function parseHeaders (headers, obj) { if (obj === undefined) obj = {} + for (let i = 0; i < headers.length; i += 2) { const key = headerNameToString(headers[i]) let val = obj[key] @@ -363,9 +434,16 @@ function parseHeaders (headers, obj) { return obj } +/** + * @param {Buffer[]} headers + * @returns {string[]} + */ function parseRawHeaders (headers) { - const len = headers.length - const ret = new Array(len) + const headersLength = headers.length + /** + * @type {string[]} + */ + const ret = new Array(headersLength) let hasContentLength = false let contentDispositionIdx = -1 @@ -373,7 +451,7 @@ function parseRawHeaders (headers) { let val let kLen = 0 - for (let n = 0; n < headers.length; n += 2) { + for (let n = 0; n < headersLength; n += 2) { key = headers[n] val = headers[n + 1] @@ -439,13 +517,33 @@ function validateHandler (handler, method, upgrade) { } } -// A body is disturbed if it has been read from and it cannot -// be re-used without losing state or data. +/** + * A body is disturbed if it has been read from and it cannot be re-used without + * losing state or data. + * @param {import('node:stream').Readable} body + * @returns {boolean} + */ function isDisturbed (body) { // TODO (fix): Why is body[kBodyUsed] needed? return !!(body && (stream.isDisturbed(body) || body[kBodyUsed])) } +/** + * @typedef {object} SocketInfo + * @property {string} [localAddress] + * @property {number} [localPort] + * @property {string} [remoteAddress] + * @property {number} [remotePort] + * @property {string} [remoteFamily] + * @property {number} [timeout] + * @property {number} bytesWritten + * @property {number} bytesRead + */ + +/** + * @param {import('net').Socket} socket + * @returns {SocketInfo} + */ function getSocketInfo (socket) { return { localAddress: socket.localAddress, @@ -459,7 +557,9 @@ function getSocketInfo (socket) { } } -/** @type {globalThis['ReadableStream']} */ +/** + * @returns {globalThis['ReadableStream']} + */ function ReadableStreamFrom (iterable) { // We cannot use ReadableStream.from here because it does not return a byte stream. @@ -484,7 +584,7 @@ function ReadableStreamFrom (iterable) { } return controller.desiredSize > 0 }, - async cancel (reason) { + async cancel () { await iterator.return() }, type: 'bytes' @@ -492,8 +592,12 @@ function ReadableStreamFrom (iterable) { ) } -// The chunk should be a FormData instance and contains -// all the required methods. +/** + * The object should be a FormData instance and contains all the required + * methods. + * @param {*} object + * @returns {object is FormData} + */ function isFormDataLike (object) { return ( object && @@ -538,6 +642,7 @@ function isUSVString (val) { /** * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 * @param {number} c + * @returns {boolean} */ function isTokenCharCode (c) { switch (c) { @@ -568,6 +673,7 @@ function isTokenCharCode (c) { /** * @param {string} characters + * @returns {boolean} */ function isValidHTTPToken (characters) { if (characters.length === 0) { @@ -594,17 +700,24 @@ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ /** * @param {string} characters + * @returns {boolean} */ function isValidHeaderValue (characters) { return !headerCharRegex.test(characters) } -// Parsed accordingly to RFC 9110 -// https://www.rfc-editor.org/rfc/rfc9110#field.content-range +const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/ + +/** + * Parse accordingly to RFC 9110 + * @see https://www.rfc-editor.org/rfc/rfc9110#field.content-range + * @param {string} [range] + * @returns + */ function parseRangeHeader (range) { if (range == null || range === '') return { start: 0, end: null, size: null } - const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null + const m = range ? range.match(rangeHeaderRegex) : null return m ? { start: parseInt(m[1]), @@ -614,6 +727,11 @@ function parseRangeHeader (range) { : null } +/** + * @param {Record} obj + * @param {string} name + * @param {Function} listener + */ function addListener (obj, name, listener) { const listeners = (obj[kListeners] ??= []) listeners.push([name, listener]) @@ -621,13 +739,23 @@ function addListener (obj, name, listener) { return obj } +/** + * @param {Record} obj + */ function removeAllListeners (obj) { - for (const [name, listener] of obj[kListeners] ?? []) { - obj.removeListener(name, listener) + if (obj[kListeners] != null) { + for (const [name, listener] of obj[kListeners]) { + obj.removeListener(name, listener) + } + obj[kListeners] = null } - obj[kListeners] = null } +/** + * @param {import ('../dispatcher/client')} client + * @param {import ('../core/request')} request + * @param {Error} err + */ function errorRequest (client, request, err) { try { request.onError(err)