initial commit (app returns google/startpage results)

This commit is contained in:
odweta
2026-09-07 15:05:50 +02:00
commit 1a7df49411
1975 changed files with 269318 additions and 0 deletions
+676
View File
@@ -0,0 +1,676 @@
'use strict'
const {
safeHTTPMethods,
pathHasQueryOrFragment,
hasSafeIterator,
isValidHTTPToken
} = require('../core/util')
const { serializePathWithQuery } = require('../core/util')
const MAX_DELTA_SECONDS = 2147483647
const RESTRICTIVE_DIRECTIVE_NAMES = ['no-store', 'private', 'no-cache']
const kInvalidCacheControlDirectives = Symbol('invalid cache-control directives')
function trimOWS (value) {
return value.replace(/^[\t ]+|[\t ]+$/g, '')
}
function arrayIncludes (array, value) {
for (let i = 0; i < array.length; i++) {
if (array[i] === value) {
return true
}
}
return false
}
function trimOWSStart (value) {
return value.replace(/^[\t ]+/, '')
}
function trimOWSEnd (value) {
return value.replace(/[\t ]+$/, '')
}
function findUnescapedQuote (value, start) {
let escaped = false
for (let i = start; i < value.length; i++) {
if (escaped) {
escaped = false
} else if (value[i] === '\\') {
escaped = true
} else if (value[i] === '"') {
return i
}
}
return -1
}
function splitCacheControlHeaderValue (value) {
const directives = []
let start = 0
let quoteStart = -1
let inQuote = false
let escaped = false
for (let i = 0; i < value.length; i++) {
if (inQuote) {
if (escaped) {
escaped = false
} else if (value[i] === '\\') {
escaped = true
} else if (value[i] === '"') {
inQuote = false
quoteStart = -1
}
} else if (value[i] === '"') {
inQuote = true
quoteStart = i
} else if (value[i] === ',') {
directives.push({ value: value.substring(start, i), fromMalformedQuote: false })
start = i + 1
}
}
if (!inQuote) {
directives.push({ value: value.substring(start), fromMalformedQuote: false })
return directives
}
const tail = value.substring(start)
const quoteOffset = quoteStart - start
let tailStart = 0
for (let i = 0; i < tail.length; i++) {
if (tail[i] === ',') {
directives.push({
value: tail.substring(tailStart, i),
fromMalformedQuote: tailStart > quoteOffset
})
tailStart = i + 1
}
}
directives.push({
value: tail.substring(tailStart),
fromMalformedQuote: tailStart > quoteOffset
})
return directives
}
function markInvalidCacheControlDirective (directives, key) {
let invalidDirectives = directives[kInvalidCacheControlDirectives]
if (invalidDirectives === undefined) {
invalidDirectives = new Set()
Object.defineProperty(directives, kInvalidCacheControlDirectives, {
value: invalidDirectives
})
}
invalidDirectives.add(key)
}
function hasInvalidCacheControlDirective (directives, key) {
return directives[kInvalidCacheControlDirectives]?.has(key) === true
}
function getMalformedRestrictiveDirectiveName (key) {
for (const directiveName of RESTRICTIVE_DIRECTIVE_NAMES) {
if (
key.startsWith(directiveName) &&
key.length > directiveName.length &&
!isValidHTTPToken(key[directiveName.length])
) {
return directiveName
}
}
let tokenOnlyKey = ''
let hasInvalidTokenChar = false
for (let i = 0; i < key.length; i++) {
if (isValidHTTPToken(key[i])) {
tokenOnlyKey += key[i]
} else {
hasInvalidTokenChar = true
}
}
if (hasInvalidTokenChar && arrayIncludes(RESTRICTIVE_DIRECTIVE_NAMES, tokenOnlyKey)) {
return tokenOnlyKey
}
}
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts
*/
function makeCacheKey (opts) {
if (!opts.origin) {
throw new Error('opts.origin is undefined')
}
let fullPath = opts.path || '/'
if (opts.query && !pathHasQueryOrFragment(fullPath)) {
fullPath = serializePathWithQuery(fullPath, opts.query)
}
return {
origin: opts.origin.toString(),
method: opts.method,
path: fullPath,
headers: opts.headers
}
}
function appendHeader (headers, key, val) {
const headerName = key.toLowerCase()
const current = headers[headerName]
const values = Array.isArray(val) ? val : [val]
if (current === undefined) {
headers[headerName] = Array.isArray(val) ? val.slice() : val
} else if (Array.isArray(current)) {
current.push(...values)
} else {
headers[headerName] = [current, ...values]
}
}
/**
* @param {Record<string, string[] | string>}
* @returns {Record<string, string[] | string>}
*/
function normalizeHeaders (opts) {
let headers
if (opts.headers == null) {
headers = {}
} else if (typeof opts.headers === 'object') {
headers = {}
if (hasSafeIterator(opts.headers)) {
for (const x of opts.headers) {
if (!Array.isArray(x)) {
throw new Error('opts.headers is not a valid header map')
}
const [key, val] = x
if (typeof key !== 'string' || typeof val !== 'string') {
throw new Error('opts.headers is not a valid header map')
}
appendHeader(headers, key, val)
}
} else {
for (const key of Object.keys(opts.headers)) {
appendHeader(headers, key, opts.headers[key])
}
}
} else {
throw new Error('opts.headers is not an object')
}
return headers
}
/**
* @param {any} key
*/
function assertCacheKey (key) {
if (typeof key !== 'object') {
throw new TypeError(`expected key to be object, got ${typeof key}`)
}
for (const property of ['origin', 'method', 'path']) {
if (typeof key[property] !== 'string') {
throw new TypeError(`expected key.${property} to be string, got ${typeof key[property]}`)
}
}
if (key.headers !== undefined && typeof key.headers !== 'object') {
throw new TypeError(`expected headers to be object, got ${typeof key}`)
}
}
/**
* @param {any} value
*/
function assertCacheValue (value) {
if (typeof value !== 'object') {
throw new TypeError(`expected value to be object, got ${typeof value}`)
}
for (const property of ['statusCode', 'cachedAt', 'staleAt', 'deleteAt']) {
if (typeof value[property] !== 'number') {
throw new TypeError(`expected value.${property} to be number, got ${typeof value[property]}`)
}
}
if (typeof value.statusMessage !== 'string') {
throw new TypeError(`expected value.statusMessage to be string, got ${typeof value.statusMessage}`)
}
if (value.headers != null && typeof value.headers !== 'object') {
throw new TypeError(`expected value.rawHeaders to be object, got ${typeof value.headers}`)
}
if (value.vary !== undefined && typeof value.vary !== 'object') {
throw new TypeError(`expected value.vary to be object, got ${typeof value.vary}`)
}
if (value.etag !== undefined && typeof value.etag !== 'string') {
throw new TypeError(`expected value.etag to be string, got ${typeof value.etag}`)
}
}
/**
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control
* @see https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
* @param {string | string[]} header
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
*/
function parseCacheControlHeader (header) {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
*/
const output = {}
const invalidNumericDirectives = new Set()
const invalidNoArgumentDirectives = new Set()
const directives = splitCacheControlHeaderValue(Array.isArray(header) ? header.join(',') : header)
for (let i = 0; i < directives.length; i++) {
const directiveRecord = directives[i]
const directive = directiveRecord.value.toLowerCase()
const fromMalformedQuote = directiveRecord.fromMalformedQuote
const keyValueDelimiter = directive.indexOf('=')
let key
let value
let keyHasTrailingWhitespace = false
let valueHasLeadingWhitespace = false
if (keyValueDelimiter !== -1) {
const rawKey = directive.substring(0, keyValueDelimiter)
const rawValue = directive.substring(keyValueDelimiter + 1)
keyHasTrailingWhitespace = trimOWSEnd(rawKey) !== rawKey
valueHasLeadingWhitespace = trimOWSStart(rawValue) !== rawValue
key = trimOWS(rawKey)
value = trimOWSStart(rawValue)
} else {
key = trimOWS(directive)
}
const malformedRestrictiveDirectiveName = getMalformedRestrictiveDirectiveName(key)
if (malformedRestrictiveDirectiveName !== undefined) {
output[malformedRestrictiveDirectiveName] = true
continue
}
switch (key) {
case 'min-fresh':
case 'max-stale':
case 'max-age':
case 's-maxage':
case 'stale-while-revalidate':
case 'stale-if-error': {
if (fromMalformedQuote || invalidNumericDirectives.has(key)) {
continue
}
if (value === undefined || keyHasTrailingWhitespace || valueHasLeadingWhitespace) {
delete output[key]
invalidNumericDirectives.add(key)
markInvalidCacheControlDirective(output, key)
continue
}
if (
value.length >= 2 &&
value[0] === '"' &&
value[value.length - 1] === '"'
) {
value = value.substring(1, value.length - 1)
}
if (!/^[0-9]+$/.test(value)) {
delete output[key]
invalidNumericDirectives.add(key)
markInvalidCacheControlDirective(output, key)
continue
}
const parsedValue = Math.min(parseInt(value, 10), MAX_DELTA_SECONDS)
if (key === 'min-fresh') {
if (!(key in output) || output[key] < parsedValue) {
output[key] = parsedValue
}
} else if (!(key in output) || output[key] > parsedValue) {
output[key] = parsedValue
}
break
}
case 'private':
case 'no-cache': {
if (fromMalformedQuote) {
output[key] = true
break
}
if (value !== undefined && value.length === 0) {
output[key] = true
break
}
if (value) {
// The private and no-cache directives can be unqualified (aka just
// `private` or `no-cache`) or qualified (w/ a value). When they're
// qualified, it's a list of headers like `no-cache=header1`,
// `no-cache="header1"`, or `no-cache="header1, header2"`
// If we're given multiple headers, the comma messes us up since
// we split the full header by commas. So, let's loop through the
// remaining parts in front of us until we find one that contains a
// closing quote. We can then skip the consumed quoted-list fragments and
// continue parsing like normal.
// https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2
if (value[0] === '"') {
// Something like `no-cache="some-header"` OR `no-cache="some-header, another-header"`.
value = trimOWSEnd(value)
let fieldList = ''
let lastQuotedPart = i
let foundEndingQuote = false
const closingQuote = findUnescapedQuote(value, 1)
if (closingQuote !== -1) {
fieldList = value.substring(1, closingQuote)
foundEndingQuote = true
} else {
// Something like `no-cache="some-header, another-header"`
// This can still be something invalid, e.g. `no-cache="some-header, ...`
const fieldListParts = [value.substring(1)]
for (let j = i + 1; j < directives.length; j++) {
const nextPart = trimOWS(directives[j].value)
const closingQuote = findUnescapedQuote(nextPart, 0)
lastQuotedPart = j
if (closingQuote !== -1) {
fieldListParts.push(nextPart.substring(0, closingQuote))
foundEndingQuote = true
break
}
fieldListParts.push(nextPart)
}
fieldList = fieldListParts.join(',')
}
if (!foundEndingQuote) {
output[key] = true
break
}
i = lastQuotedPart
const headers = fieldList.split(',')
let validFieldNames = true
for (let j = 0; j < headers.length; j++) {
headers[j] = trimOWS(headers[j])
if (!isValidHTTPToken(headers[j])) {
validFieldNames = false
}
}
if (!validFieldNames) {
output[key] = true
} else if (output[key] !== true) {
if (key in output) {
output[key] = output[key].concat(headers)
} else {
output[key] = headers
}
}
} else {
// Something like `no-cache=some-header`
const fieldName = trimOWS(value)
if (!isValidHTTPToken(fieldName)) {
output[key] = true
} else if (output[key] !== true) {
if (key in output) {
output[key] = output[key].concat(fieldName)
} else {
output[key] = [fieldName]
}
}
}
break
}
}
// eslint-disable-next-line no-fallthrough
case 'public':
case 'must-revalidate':
case 'proxy-revalidate':
case 'immutable':
case 'no-transform':
case 'must-understand':
case 'only-if-cached':
if (fromMalformedQuote || invalidNoArgumentDirectives.has(key)) {
continue
}
if (value !== undefined) {
// These are qualified (something like `public=...`) when they aren't
// allowed to be, skip all instances of the malformed directive.
delete output[key]
invalidNoArgumentDirectives.add(key)
continue
}
output[key] = true
break
case 'no-store':
output[key] = true
break
default:
// Ignore unknown directives as per https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.3-1
continue
}
}
return output
}
/**
* @param {string | string[]} varyHeader Vary header from the server
* @returns {string[]}
*/
function splitVaryHeader (varyHeader) {
const values = Array.isArray(varyHeader) ? varyHeader : [varyHeader]
const output = []
for (let i = 0; i < values.length; i++) {
const parts = values[i].split(',')
for (let j = 0; j < parts.length; j++) {
output.push(parts[j])
}
}
return output
}
/**
* @param {string | string[]} varyHeader Vary header from the server
* @returns {boolean}
*/
function hasVaryStar (varyHeader) {
const values = splitVaryHeader(varyHeader)
for (let i = 0; i < values.length; i++) {
if (trimOWS(values[i]).indexOf('*') !== -1) {
return true
}
}
return false
}
/**
* @param {string | string[]} varyHeader Vary header from the server
* @param {Record<string, string | string[]>} headers Request headers
* @returns {Record<string, string | string[] | null> | undefined}
*/
function parseVaryHeader (varyHeader, headers) {
if (hasVaryStar(varyHeader)) {
return headers
}
const output = /** @type {Record<string, string | string[] | null>} */ ({})
const varyingHeaders = splitVaryHeader(varyHeader)
for (const header of varyingHeaders) {
const trimmedHeader = trimOWS(header).toLowerCase()
if (trimmedHeader.length === 0) {
continue
}
if (!isValidHTTPToken(trimmedHeader)) {
return undefined
}
const headerValue = headers[trimmedHeader]
output[trimmedHeader] = Array.isArray(headerValue) ? headerValue.slice() : headerValue ?? null
}
return output
}
/**
* @param {string | string[]} varyHeader Vary header from the server
* @returns {boolean}
*/
function isInvalidOrWildcardVaryHeader (varyHeader) {
return hasVaryStar(varyHeader) || parseVaryHeader(varyHeader, {}) === undefined
}
/**
* Note: this deviates from the spec a little. Empty etags ("", W/"") are valid,
* however, including them in cached resposnes serves little to no purpose.
*
* @see https://www.rfc-editor.org/rfc/rfc9110.html#name-etag
*
* @param {string} etag
* @returns {boolean}
*/
function isEtagUsable (etag) {
if (etag.length <= 2) {
// Shortest an etag can be is two chars (just ""). This is where we deviate
// from the spec requiring a min of 3 chars however
return false
}
if (etag[0] === '"' && etag[etag.length - 1] === '"') {
// ETag: ""asd123"" or ETag: "W/"asd123"", kinda undefined behavior in the
// spec. Some servers will accept these while others don't.
// ETag: "asd123"
return !(etag[1] === '"' || etag.startsWith('"W/'))
}
if (etag.startsWith('W/"') && etag[etag.length - 1] === '"') {
// ETag: W/"", also where we deviate from the spec & require a min of 3
// chars
// ETag: for W/"", W/"asd123"
return etag.length !== 4
}
// Anything else
return false
}
/**
* @param {unknown} store
* @returns {asserts store is import('../../types/cache-interceptor.d.ts').default.CacheStore}
*/
function assertCacheStore (store, name = 'CacheStore') {
if (typeof store !== 'object' || store === null) {
throw new TypeError(`expected type of ${name} to be a CacheStore, got ${store === null ? 'null' : typeof store}`)
}
for (const fn of ['get', 'createWriteStream', 'delete']) {
if (typeof store[fn] !== 'function') {
throw new TypeError(`${name} needs to have a \`${fn}()\` function`)
}
}
}
/**
* @param {unknown} methods
* @returns {asserts methods is import('../../types/cache-interceptor.d.ts').default.CacheMethods[]}
*/
function assertCacheMethods (methods, name = 'CacheMethods') {
if (!Array.isArray(methods)) {
throw new TypeError(`expected type of ${name} needs to be an array, got ${methods === null ? 'null' : typeof methods}`)
}
if (methods.length === 0) {
throw new TypeError(`${name} needs to have at least one method`)
}
for (const method of methods) {
if (!arrayIncludes(safeHTTPMethods, method)) {
throw new TypeError(`element of ${name}-array needs to be one of following values: ${safeHTTPMethods.join(', ')}, got ${method}`)
}
}
}
/**
* Creates a string key for request deduplication purposes.
* This key is used to identify in-flight requests that can be shared.
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
* @param {Set<string>} [excludeHeaders] Set of lowercase header names to exclude from the key
* @returns {string}
*/
function makeDeduplicationKey (cacheKey, excludeHeaders) {
// Use JSON.stringify to produce a collision-resistant key.
// Previous format used `:` and `=` delimiters without escaping, which
// allowed different header sets to produce identical keys (e.g.
// {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012
const headers = {}
if (cacheKey.headers) {
const sortedHeaders = Object.keys(cacheKey.headers).sort()
for (const header of sortedHeaders) {
// Skip excluded headers
if (excludeHeaders?.has(header.toLowerCase())) {
continue
}
headers[header] = cacheKey.headers[header]
}
}
return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers])
}
module.exports = {
makeCacheKey,
normalizeHeaders,
assertCacheKey,
assertCacheValue,
parseCacheControlHeader,
hasInvalidCacheControlDirective,
parseVaryHeader,
hasVaryStar,
isInvalidOrWildcardVaryHeader,
isEtagUsable,
assertCacheMethods,
assertCacheStore,
makeDeduplicationKey
}
+670
View File
@@ -0,0 +1,670 @@
'use strict'
/**
* @see https://www.rfc-editor.org/rfc/rfc9110.html#name-date-time-formats
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseHttpDate (date) {
// Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
// Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
// Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
switch (date[3]) {
case ',': return parseImfDate(date)
case ' ': return parseAscTimeDate(date)
default: return parseRfc850Date(date)
}
}
function makeDate (year, monthIdx, day, hour, minute, second, weekday) {
const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
// Date.UTC treats years 0-99 as 1900-1999. Reset the full year so component
// checks below validate the HTTP date as written.
if (year >= 0 && year <= 99) {
result.setUTCFullYear(year)
}
return result.getUTCFullYear() === year &&
result.getUTCMonth() === monthIdx &&
result.getUTCDate() === day &&
result.getUTCHours() === hour &&
result.getUTCMinutes() === minute &&
result.getUTCSeconds() === second &&
result.getUTCDay() === weekday
? result
: undefined
}
/**
* @see https://httpwg.org/specs/rfc9110.html#preferred.date.format
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseImfDate (date) {
if (
date.length !== 29 ||
date[4] !== ' ' ||
date[7] !== ' ' ||
date[11] !== ' ' ||
date[16] !== ' ' ||
date[19] !== ':' ||
date[22] !== ':' ||
date[25] !== ' ' ||
date[26] !== 'G' ||
date[27] !== 'M' ||
date[28] !== 'T'
) {
return undefined
}
let weekday = -1
if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday
weekday = 0
} else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday
weekday = 1
} else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday
weekday = 2
} else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday
weekday = 3
} else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday
weekday = 4
} else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday
weekday = 5
} else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday
weekday = 6
} else {
return undefined // Not a valid day of the week
}
let day = 0
if (date[5] === '0') {
// Single digit day, e.g. "Sun Nov 6 08:49:37 1994"
const code = date.charCodeAt(6)
if (code < 49 || code > 57) {
return undefined // Not a digit
}
day = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(5)
if (code1 < 49 || code1 > 51) {
return undefined // Not a digit between 1 and 3
}
const code2 = date.charCodeAt(6)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let monthIdx = -1
if (
(date[8] === 'J' && date[9] === 'a' && date[10] === 'n')
) {
monthIdx = 0 // Jan
} else if (
(date[8] === 'F' && date[9] === 'e' && date[10] === 'b')
) {
monthIdx = 1 // Feb
} else if (
(date[8] === 'M' && date[9] === 'a')
) {
if (date[10] === 'r') {
monthIdx = 2 // Mar
} else if (date[10] === 'y') {
monthIdx = 4 // May
} else {
return undefined // Invalid month
}
} else if (
(date[8] === 'J')
) {
if (date[9] === 'a' && date[10] === 'n') {
monthIdx = 0 // Jan
} else if (date[9] === 'u') {
if (date[10] === 'n') {
monthIdx = 5 // Jun
} else if (date[10] === 'l') {
monthIdx = 6 // Jul
} else {
return undefined // Invalid month
}
} else {
return undefined // Invalid month
}
} else if (
(date[8] === 'A')
) {
if (date[9] === 'p' && date[10] === 'r') {
monthIdx = 3 // Apr
} else if (date[9] === 'u' && date[10] === 'g') {
monthIdx = 7 // Aug
} else {
return undefined // Invalid month
}
} else if (
(date[8] === 'S' && date[9] === 'e' && date[10] === 'p')
) {
monthIdx = 8 // Sep
} else if (
(date[8] === 'O' && date[9] === 'c' && date[10] === 't')
) {
monthIdx = 9 // Oct
} else if (
(date[8] === 'N' && date[9] === 'o' && date[10] === 'v')
) {
monthIdx = 10 // Nov
} else if (
(date[8] === 'D' && date[9] === 'e' && date[10] === 'c')
) {
monthIdx = 11 // Dec
} else {
// Not a valid month
return undefined
}
const yearDigit1 = date.charCodeAt(12)
if (yearDigit1 < 48 || yearDigit1 > 57) {
return undefined // Not a digit
}
const yearDigit2 = date.charCodeAt(13)
if (yearDigit2 < 48 || yearDigit2 > 57) {
return undefined // Not a digit
}
const yearDigit3 = date.charCodeAt(14)
if (yearDigit3 < 48 || yearDigit3 > 57) {
return undefined // Not a digit
}
const yearDigit4 = date.charCodeAt(15)
if (yearDigit4 < 48 || yearDigit4 > 57) {
return undefined // Not a digit
}
const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48)
let hour = 0
if (date[17] === '0') {
const code = date.charCodeAt(18)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
hour = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(17)
if (code1 < 48 || code1 > 50) {
return undefined // Not a digit between 0 and 2
}
const code2 = date.charCodeAt(18)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
if (code1 === 50 && code2 > 51) {
return undefined // Hour cannot be greater than 23
}
hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let minute = 0
if (date[20] === '0') {
const code = date.charCodeAt(21)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
minute = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(20)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(21)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let second = 0
if (date[23] === '0') {
const code = date.charCodeAt(24)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
second = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(23)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(24)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
return makeDate(year, monthIdx, day, hour, minute, second, weekday)
}
/**
* @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseAscTimeDate (date) {
// This is assumed to be in UTC
if (
date.length !== 24 ||
date[7] !== ' ' ||
date[10] !== ' ' ||
date[19] !== ' '
) {
return undefined
}
let weekday = -1
if (date[0] === 'S' && date[1] === 'u' && date[2] === 'n') { // Sunday
weekday = 0
} else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n') { // Monday
weekday = 1
} else if (date[0] === 'T' && date[1] === 'u' && date[2] === 'e') { // Tuesday
weekday = 2
} else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd') { // Wednesday
weekday = 3
} else if (date[0] === 'T' && date[1] === 'h' && date[2] === 'u') { // Thursday
weekday = 4
} else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i') { // Friday
weekday = 5
} else if (date[0] === 'S' && date[1] === 'a' && date[2] === 't') { // Saturday
weekday = 6
} else {
return undefined // Not a valid day of the week
}
let monthIdx = -1
if (
(date[4] === 'J' && date[5] === 'a' && date[6] === 'n')
) {
monthIdx = 0 // Jan
} else if (
(date[4] === 'F' && date[5] === 'e' && date[6] === 'b')
) {
monthIdx = 1 // Feb
} else if (
(date[4] === 'M' && date[5] === 'a')
) {
if (date[6] === 'r') {
monthIdx = 2 // Mar
} else if (date[6] === 'y') {
monthIdx = 4 // May
} else {
return undefined // Invalid month
}
} else if (
(date[4] === 'J')
) {
if (date[5] === 'a' && date[6] === 'n') {
monthIdx = 0 // Jan
} else if (date[5] === 'u') {
if (date[6] === 'n') {
monthIdx = 5 // Jun
} else if (date[6] === 'l') {
monthIdx = 6 // Jul
} else {
return undefined // Invalid month
}
} else {
return undefined // Invalid month
}
} else if (
(date[4] === 'A')
) {
if (date[5] === 'p' && date[6] === 'r') {
monthIdx = 3 // Apr
} else if (date[5] === 'u' && date[6] === 'g') {
monthIdx = 7 // Aug
} else {
return undefined // Invalid month
}
} else if (
(date[4] === 'S' && date[5] === 'e' && date[6] === 'p')
) {
monthIdx = 8 // Sep
} else if (
(date[4] === 'O' && date[5] === 'c' && date[6] === 't')
) {
monthIdx = 9 // Oct
} else if (
(date[4] === 'N' && date[5] === 'o' && date[6] === 'v')
) {
monthIdx = 10 // Nov
} else if (
(date[4] === 'D' && date[5] === 'e' && date[6] === 'c')
) {
monthIdx = 11 // Dec
} else {
// Not a valid month
return undefined
}
let day = 0
if (date[8] === ' ') {
// Single digit day, e.g. "Sun Nov 6 08:49:37 1994"
const code = date.charCodeAt(9)
if (code < 49 || code > 57) {
return undefined // Not a digit
}
day = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(8)
if (code1 < 49 || code1 > 51) {
return undefined // Not a digit between 1 and 3
}
const code2 = date.charCodeAt(9)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let hour = 0
if (date[11] === '0') {
const code = date.charCodeAt(12)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
hour = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(11)
if (code1 < 48 || code1 > 50) {
return undefined // Not a digit between 0 and 2
}
const code2 = date.charCodeAt(12)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
if (code1 === 50 && code2 > 51) {
return undefined // Hour cannot be greater than 23
}
hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let minute = 0
if (date[14] === '0') {
const code = date.charCodeAt(15)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
minute = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(14)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(15)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let second = 0
if (date[17] === '0') {
const code = date.charCodeAt(18)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
second = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(17)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(18)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
const yearDigit1 = date.charCodeAt(20)
if (yearDigit1 < 48 || yearDigit1 > 57) {
return undefined // Not a digit
}
const yearDigit2 = date.charCodeAt(21)
if (yearDigit2 < 48 || yearDigit2 > 57) {
return undefined // Not a digit
}
const yearDigit3 = date.charCodeAt(22)
if (yearDigit3 < 48 || yearDigit3 > 57) {
return undefined // Not a digit
}
const yearDigit4 = date.charCodeAt(23)
if (yearDigit4 < 48 || yearDigit4 > 57) {
return undefined // Not a digit
}
const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48)
return makeDate(year, monthIdx, day, hour, minute, second, weekday)
}
/**
* @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
*
* @param {string} date
* @returns {Date | undefined}
*/
function parseRfc850Date (date) {
let commaIndex = -1
let weekday = -1
if (date[0] === 'S') {
if (date[1] === 'u' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') {
weekday = 0 // Sunday
commaIndex = 6
} else if (date[1] === 'a' && date[2] === 't' && date[3] === 'u' && date[4] === 'r' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') {
weekday = 6 // Saturday
commaIndex = 8
}
} else if (date[0] === 'M' && date[1] === 'o' && date[2] === 'n' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') {
weekday = 1 // Monday
commaIndex = 6
} else if (date[0] === 'T') {
if (date[1] === 'u' && date[2] === 'e' && date[3] === 's' && date[4] === 'd' && date[5] === 'a' && date[6] === 'y') {
weekday = 2 // Tuesday
commaIndex = 7
} else if (date[1] === 'h' && date[2] === 'u' && date[3] === 'r' && date[4] === 's' && date[5] === 'd' && date[6] === 'a' && date[7] === 'y') {
weekday = 4 // Thursday
commaIndex = 8
}
} else if (date[0] === 'W' && date[1] === 'e' && date[2] === 'd' && date[3] === 'n' && date[4] === 'e' && date[5] === 's' && date[6] === 'd' && date[7] === 'a' && date[8] === 'y') {
weekday = 3 // Wednesday
commaIndex = 9
} else if (date[0] === 'F' && date[1] === 'r' && date[2] === 'i' && date[3] === 'd' && date[4] === 'a' && date[5] === 'y') {
weekday = 5 // Friday
commaIndex = 6
} else {
// Not a valid day name
return undefined
}
if (
date[commaIndex] !== ',' ||
(date.length - commaIndex - 1) !== 23 ||
date[commaIndex + 1] !== ' ' ||
date[commaIndex + 4] !== '-' ||
date[commaIndex + 8] !== '-' ||
date[commaIndex + 11] !== ' ' ||
date[commaIndex + 14] !== ':' ||
date[commaIndex + 17] !== ':' ||
date[commaIndex + 20] !== ' ' ||
date[commaIndex + 21] !== 'G' ||
date[commaIndex + 22] !== 'M' ||
date[commaIndex + 23] !== 'T'
) {
return undefined
}
let day = 0
if (date[commaIndex + 2] === '0') {
// Single digit day, e.g. "Sun Nov 6 08:49:37 1994"
const code = date.charCodeAt(commaIndex + 3)
if (code < 49 || code > 57) {
return undefined // Not a digit
}
day = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 2)
if (code1 < 49 || code1 > 51) {
return undefined // Not a digit between 1 and 3
}
const code2 = date.charCodeAt(commaIndex + 3)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
day = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let monthIdx = -1
if (
(date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'n')
) {
monthIdx = 0 // Jan
} else if (
(date[commaIndex + 5] === 'F' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'b')
) {
monthIdx = 1 // Feb
} else if (
(date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'r')
) {
monthIdx = 2 // Mar
} else if (
(date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'p' && date[commaIndex + 7] === 'r')
) {
monthIdx = 3 // Apr
} else if (
(date[commaIndex + 5] === 'M' && date[commaIndex + 6] === 'a' && date[commaIndex + 7] === 'y')
) {
monthIdx = 4 // May
} else if (
(date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'n')
) {
monthIdx = 5 // Jun
} else if (
(date[commaIndex + 5] === 'J' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'l')
) {
monthIdx = 6 // Jul
} else if (
(date[commaIndex + 5] === 'A' && date[commaIndex + 6] === 'u' && date[commaIndex + 7] === 'g')
) {
monthIdx = 7 // Aug
} else if (
(date[commaIndex + 5] === 'S' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'p')
) {
monthIdx = 8 // Sep
} else if (
(date[commaIndex + 5] === 'O' && date[commaIndex + 6] === 'c' && date[commaIndex + 7] === 't')
) {
monthIdx = 9 // Oct
} else if (
(date[commaIndex + 5] === 'N' && date[commaIndex + 6] === 'o' && date[commaIndex + 7] === 'v')
) {
monthIdx = 10 // Nov
} else if (
(date[commaIndex + 5] === 'D' && date[commaIndex + 6] === 'e' && date[commaIndex + 7] === 'c')
) {
monthIdx = 11 // Dec
} else {
// Not a valid month
return undefined
}
const yearDigit1 = date.charCodeAt(commaIndex + 9)
if (yearDigit1 < 48 || yearDigit1 > 57) {
return undefined // Not a digit
}
const yearDigit2 = date.charCodeAt(commaIndex + 10)
if (yearDigit2 < 48 || yearDigit2 > 57) {
return undefined // Not a digit
}
let year = (yearDigit1 - 48) * 10 + (yearDigit2 - 48) // Convert ASCII codes to number
// RFC 6265 states that the year is in the range 1970-2069.
// @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1
//
// 3. If the year-value is greater than or equal to 70 and less than or
// equal to 99, increment the year-value by 1900.
// 4. If the year-value is greater than or equal to 0 and less than or
// equal to 69, increment the year-value by 2000.
year += year < 70 ? 2000 : 1900
let hour = 0
if (date[commaIndex + 12] === '0') {
const code = date.charCodeAt(commaIndex + 13)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
hour = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 12)
if (code1 < 48 || code1 > 50) {
return undefined // Not a digit between 0 and 2
}
const code2 = date.charCodeAt(commaIndex + 13)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
if (code1 === 50 && code2 > 51) {
return undefined // Hour cannot be greater than 23
}
hour = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let minute = 0
if (date[commaIndex + 15] === '0') {
const code = date.charCodeAt(commaIndex + 16)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
minute = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 15)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(commaIndex + 16)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
minute = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
let second = 0
if (date[commaIndex + 18] === '0') {
const code = date.charCodeAt(commaIndex + 19)
if (code < 48 || code > 57) {
return undefined // Not a digit
}
second = code - 48 // Convert ASCII code to number
} else {
const code1 = date.charCodeAt(commaIndex + 18)
if (code1 < 48 || code1 > 53) {
return undefined // Not a digit between 0 and 5
}
const code2 = date.charCodeAt(commaIndex + 19)
if (code2 < 48 || code2 > 57) {
return undefined // Not a digit
}
second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number
}
return makeDate(year, monthIdx, day, hour, minute, second, weekday)
}
module.exports = {
parseHttpDate
}
+28
View File
@@ -0,0 +1,28 @@
'use strict'
/**
* @template {*} T
* @typedef {Object} DeferredPromise
* @property {Promise<T>} promise
* @property {(value?: T) => void} resolve
* @property {(reason?: any) => void} reject
*/
/**
* @template {*} T
* @returns {DeferredPromise<T>} An object containing a promise and its resolve/reject methods.
*/
function createDeferredPromise () {
let res
let rej
const promise = new Promise((resolve, reject) => {
res = resolve
rej = reject
})
return { promise, resolve: res, reject: rej }
}
module.exports = {
createDeferredPromise
}
+124
View File
@@ -0,0 +1,124 @@
'use strict'
/** @typedef {`node:${string}`} NodeModuleName */
/** @type {Record<NodeModuleName, () => any>} */
const lazyLoaders = {
__proto__: null,
'node:crypto': () => require('node:crypto'),
'node:sqlite': () => require('node:sqlite'),
'node:worker_threads': () => require('node:worker_threads'),
'node:zlib': () => require('node:zlib')
}
/**
* @param {NodeModuleName} moduleName
* @returns {boolean}
*/
function detectRuntimeFeatureByNodeModule (moduleName) {
try {
lazyLoaders[moduleName]()
return true
} catch (err) {
if (err.code !== 'ERR_UNKNOWN_BUILTIN_MODULE' && err.code !== 'ERR_NO_CRYPTO') {
throw err
}
return false
}
}
/**
* @param {NodeModuleName} moduleName
* @param {string} property
* @returns {boolean}
*/
function detectRuntimeFeatureByExportedProperty (moduleName, property) {
const module = lazyLoaders[moduleName]()
return typeof module[property] !== 'undefined'
}
const runtimeFeaturesByExportedProperty = /** @type {const} */ (['markAsUncloneable', 'zstd'])
/** @type {Record<RuntimeFeatureByExportedProperty, [NodeModuleName, string]>} */
const exportedPropertyLookup = {
markAsUncloneable: ['node:worker_threads', 'markAsUncloneable'],
zstd: ['node:zlib', 'createZstdDecompress']
}
/** @typedef {typeof runtimeFeaturesByExportedProperty[number]} RuntimeFeatureByExportedProperty */
const runtimeFeaturesAsNodeModule = /** @type {const} */ (['crypto', 'sqlite'])
/** @typedef {typeof runtimeFeaturesAsNodeModule[number]} RuntimeFeatureByNodeModule */
const features = /** @type {const} */ ([
...runtimeFeaturesAsNodeModule,
...runtimeFeaturesByExportedProperty
])
/** @typedef {typeof features[number]} Feature */
/**
* @param {Feature} feature
* @returns {boolean}
*/
function detectRuntimeFeature (feature) {
if (runtimeFeaturesAsNodeModule.includes(/** @type {RuntimeFeatureByNodeModule} */ (feature))) {
return detectRuntimeFeatureByNodeModule(`node:${feature}`)
} else if (runtimeFeaturesByExportedProperty.includes(/** @type {RuntimeFeatureByExportedProperty} */ (feature))) {
const [moduleName, property] = exportedPropertyLookup[feature]
return detectRuntimeFeatureByExportedProperty(moduleName, property)
}
throw new TypeError(`unknown feature: ${feature}`)
}
/**
* @class
* @name RuntimeFeatures
*/
class RuntimeFeatures {
/** @type {Map<Feature, boolean>} */
#map = new Map()
/**
* Clears all cached feature detections.
*/
clear () {
this.#map.clear()
}
/**
* @param {Feature} feature
* @returns {boolean}
*/
has (feature) {
return (
this.#map.get(feature) ?? this.#detectRuntimeFeature(feature)
)
}
/**
* @param {Feature} feature
* @param {boolean} value
*/
set (feature, value) {
if (features.includes(feature) === false) {
throw new TypeError(`unknown feature: ${feature}`)
}
this.#map.set(feature, value)
}
/**
* @param {Feature} feature
* @returns {boolean}
*/
#detectRuntimeFeature (feature) {
const result = detectRuntimeFeature(feature)
this.#map.set(feature, result)
return result
}
}
const instance = new RuntimeFeatures()
module.exports.runtimeFeatures = instance
module.exports.default = instance
+32
View File
@@ -0,0 +1,32 @@
'use strict'
const {
kConnected,
kPending,
kRunning,
kSize,
kFree,
kQueued
} = require('../core/symbols')
class ClientStats {
constructor (client) {
this.connected = client[kConnected]
this.pending = client[kPending]
this.running = client[kRunning]
this.size = client[kSize]
}
}
class PoolStats {
constructor (pool) {
this.connected = pool[kConnected]
this.free = pool[kFree]
this.pending = pool[kPending]
this.queued = pool[kQueued]
this.running = pool[kRunning]
this.size = pool[kSize]
}
}
module.exports = { ClientStats, PoolStats }
+425
View File
@@ -0,0 +1,425 @@
'use strict'
/**
* This module offers an optimized timer implementation designed for scenarios
* where high precision is not critical.
*
* The timer achieves faster performance by using a low-resolution approach,
* with an accuracy target of within 500ms. This makes it particularly useful
* for timers with delays of 1 second or more, where exact timing is less
* crucial.
*
* It's important to note that Node.js timers are inherently imprecise, as
* delays can occur due to the event loop being blocked by other operations.
* Consequently, timers may trigger later than their scheduled time.
*/
/**
* The fastNow variable contains the internal fast timer clock value.
*
* @type {number}
*/
let fastNow = 0
/**
* RESOLUTION_MS represents the target resolution time in milliseconds.
*
* @type {number}
* @default 1000
*/
const RESOLUTION_MS = 1e3
/**
* TICK_MS defines the desired interval in milliseconds between each tick.
* The target value is set to half the resolution time, minus 1 ms, to account
* for potential event loop overhead.
*
* @type {number}
* @default 499
*/
const TICK_MS = (RESOLUTION_MS >> 1) - 1
/**
* fastNowTimeout is a Node.js timer used to manage and process
* the FastTimers stored in the `fastTimers` array.
*
* @type {NodeJS.Timeout}
*/
let fastNowTimeout
/**
* The kFastTimer symbol is used to identify FastTimer instances.
*
* @type {Symbol}
*/
const kFastTimer = Symbol('kFastTimer')
/**
* The fastTimers array contains all active FastTimers.
*
* @type {FastTimer[]}
*/
const fastTimers = []
/**
* These constants represent the various states of a FastTimer.
*/
/**
* The `NOT_IN_LIST` constant indicates that the FastTimer is not included
* in the `fastTimers` array. Timers with this status will not be processed
* during the next tick by the `onTick` function.
*
* A FastTimer can be re-added to the `fastTimers` array by invoking the
* `refresh` method on the FastTimer instance.
*
* @type {-2}
*/
const NOT_IN_LIST = -2
/**
* The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled
* for removal from the `fastTimers` array. A FastTimer in this state will
* be removed in the next tick by the `onTick` function and will no longer
* be processed.
*
* This status is also set when the `clear` method is called on the FastTimer instance.
*
* @type {-1}
*/
const TO_BE_CLEARED = -1
/**
* The `PENDING` constant signifies that the FastTimer is awaiting processing
* in the next tick by the `onTick` function. Timers with this status will have
* their `_idleStart` value set and their status updated to `ACTIVE` in the next tick.
*
* @type {0}
*/
const PENDING = 0
/**
* The `ACTIVE` constant indicates that the FastTimer is active and waiting
* for its timer to expire. During the next tick, the `onTick` function will
* check if the timer has expired, and if so, it will execute the associated callback.
*
* @type {1}
*/
const ACTIVE = 1
/**
* The onTick function processes the fastTimers array.
*
* @returns {void}
*/
function onTick () {
/**
* Increment the fastNow value by the TICK_MS value, despite the actual time
* that has passed since the last tick. This approach ensures independence
* from the system clock and delays caused by a blocked event loop.
*
* @type {number}
*/
fastNow += TICK_MS
/**
* The `idx` variable is used to iterate over the `fastTimers` array.
* Expired timers are removed by replacing them with the last element in the array.
* Consequently, `idx` is only incremented when the current element is not removed.
*
* @type {number}
*/
let idx = 0
/**
* The len variable will contain the length of the fastTimers array
* and will be decremented when a FastTimer should be removed from the
* fastTimers array.
*
* @type {number}
*/
let len = fastTimers.length
while (idx < len) {
/**
* @type {FastTimer}
*/
const timer = fastTimers[idx]
// If the timer is in the ACTIVE state and the timer has expired, it will
// be processed in the next tick.
if (timer._state === PENDING) {
// Set the _idleStart value to the fastNow value minus the TICK_MS value
// to account for the time the timer was in the PENDING state.
timer._idleStart = fastNow - TICK_MS
timer._state = ACTIVE
} else if (
timer._state === ACTIVE &&
fastNow >= timer._idleStart + timer._idleTimeout
) {
timer._state = TO_BE_CLEARED
timer._idleStart = -1
timer._onTimeout(timer._timerArg)
}
if (timer._state === TO_BE_CLEARED) {
timer._state = NOT_IN_LIST
// Move the last element to the current index and decrement len if it is
// not the only element in the array.
if (--len !== 0) {
fastTimers[idx] = fastTimers[len]
}
} else {
++idx
}
}
// Set the length of the fastTimers array to the new length and thus
// removing the excess FastTimers elements from the array.
fastTimers.length = len
// If there are still active FastTimers in the array, refresh the Timer.
// If there are no active FastTimers, the timer will be refreshed again
// when a new FastTimer is instantiated.
if (fastTimers.length !== 0) {
refreshTimeout()
}
}
function refreshTimeout () {
// If the fastNowTimeout is already set and the Timer has the refresh()-
// method available, call it to refresh the timer.
// Some timer objects returned by setTimeout may not have a .refresh()
// method (e.g. mocked timers in tests).
if (fastNowTimeout?.refresh) {
fastNowTimeout.refresh()
// fastNowTimeout is not instantiated yet or refresh is not availabe,
// create a new Timer.
} else {
clearTimeout(fastNowTimeout)
fastNowTimeout = setTimeout(onTick, TICK_MS)
// If the Timer has an unref method, call it to allow the process to exit,
// if there are no other active handles. When using fake timers or mocked
// environments (like Jest), .unref() may not be defined,
fastNowTimeout?.unref()
}
}
/**
* The `FastTimer` class is a data structure designed to store and manage
* timer information.
*/
class FastTimer {
[kFastTimer] = true
/**
* The state of the timer, which can be one of the following:
* - NOT_IN_LIST (-2)
* - TO_BE_CLEARED (-1)
* - PENDING (0)
* - ACTIVE (1)
*
* @type {-2|-1|0|1}
* @private
*/
_state = NOT_IN_LIST
/**
* The number of milliseconds to wait before calling the callback.
*
* @type {number}
* @private
*/
_idleTimeout = -1
/**
* The time in milliseconds when the timer was started. This value is used to
* calculate when the timer should expire.
*
* @type {number}
* @default -1
* @private
*/
_idleStart = -1
/**
* The function to be executed when the timer expires.
* @type {Function}
* @private
*/
_onTimeout
/**
* The argument to be passed to the callback when the timer expires.
*
* @type {*}
* @private
*/
_timerArg
/**
* @constructor
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should wait
* before the specified function or code is executed.
* @param {*} arg
*/
constructor (callback, delay, arg) {
this._onTimeout = callback
this._idleTimeout = delay
this._timerArg = arg
this.refresh()
}
/**
* Sets the timer's start time to the current time, and reschedules the timer
* to call its callback at the previously specified duration adjusted to the
* current time.
* Using this on a timer that has already called its callback will reactivate
* the timer.
*
* @returns {void}
*/
refresh () {
// In the special case that the timer is not in the list of active timers,
// add it back to the array to be processed in the next tick by the onTick
// function.
if (this._state === NOT_IN_LIST) {
fastTimers.push(this)
}
// If the timer is the only active timer, refresh the fastNowTimeout for
// better resolution.
if (!fastNowTimeout || fastTimers.length === 1) {
refreshTimeout()
}
// Setting the state to PENDING will cause the timer to be reset in the
// next tick by the onTick function.
this._state = PENDING
}
/**
* The `clear` method cancels the timer, preventing it from executing.
*
* @returns {void}
* @private
*/
clear () {
// Set the state to TO_BE_CLEARED to mark the timer for removal in the next
// tick by the onTick function.
this._state = TO_BE_CLEARED
// Reset the _idleStart value to -1 to indicate that the timer is no longer
// active.
this._idleStart = -1
}
}
/**
* This module exports a setTimeout and clearTimeout function that can be
* used as a drop-in replacement for the native functions.
*/
module.exports = {
/**
* The setTimeout() method sets a timer which executes a function once the
* timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {NodeJS.Timeout|FastTimer}
*/
setTimeout (callback, delay, arg) {
// If the delay is less than or equal to the RESOLUTION_MS value return a
// native Node.js Timer instance.
return delay <= RESOLUTION_MS
? setTimeout(callback, delay, arg)
: new FastTimer(callback, delay, arg)
},
/**
* The clearTimeout method cancels an instantiated Timer previously created
* by calling setTimeout.
*
* @param {NodeJS.Timeout|FastTimer} timeout
*/
clearTimeout (timeout) {
// If the timeout is a FastTimer, call its own clear method.
if (timeout[kFastTimer]) {
/**
* @type {FastTimer}
*/
timeout.clear()
// Otherwise it is an instance of a native NodeJS.Timeout, so call the
// Node.js native clearTimeout function.
} else {
clearTimeout(timeout)
}
},
/**
* The setFastTimeout() method sets a fastTimer which executes a function once
* the timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {FastTimer}
*/
setFastTimeout (callback, delay, arg) {
return new FastTimer(callback, delay, arg)
},
/**
* The clearTimeout method cancels an instantiated FastTimer previously
* created by calling setFastTimeout.
*
* @param {FastTimer} timeout
*/
clearFastTimeout (timeout) {
timeout.clear()
},
/**
* The now method returns the value of the internal fast timer clock.
*
* @returns {number}
*/
now () {
return fastNow
},
/**
* Trigger the onTick function to process the fastTimers array.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
* @param {number} [delay=0] The delay in milliseconds to add to the now value.
*/
tick (delay = 0) {
fastNow += delay - RESOLUTION_MS + 1
onTick()
onTick()
},
/**
* Reset FastTimers.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
reset () {
fastNow = 0
fastTimers.length = 0
clearTimeout(fastNowTimeout)
fastNowTimeout = null
},
/**
* Exporting for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
kFastTimer
}