aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/undici/lib
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
commit76cb9c2a39d477a64824a985ade40507e3bbade1 (patch)
tree41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/undici/lib
parent819a39a21ac992b1393244a4c283bbb125208c69 (diff)
downloadneko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/undici/lib')
-rw-r--r--vanilla/node_modules/undici/lib/api/abort-signal.js59
-rw-r--r--vanilla/node_modules/undici/lib/api/api-connect.js110
-rw-r--r--vanilla/node_modules/undici/lib/api/api-pipeline.js252
-rw-r--r--vanilla/node_modules/undici/lib/api/api-request.js214
-rw-r--r--vanilla/node_modules/undici/lib/api/api-stream.js209
-rw-r--r--vanilla/node_modules/undici/lib/api/api-upgrade.js111
-rw-r--r--vanilla/node_modules/undici/lib/api/index.js7
-rw-r--r--vanilla/node_modules/undici/lib/api/readable.js580
-rw-r--r--vanilla/node_modules/undici/lib/cache/memory-cache-store.js234
-rw-r--r--vanilla/node_modules/undici/lib/cache/sqlite-cache-store.js461
-rw-r--r--vanilla/node_modules/undici/lib/core/connect.js137
-rw-r--r--vanilla/node_modules/undici/lib/core/constants.js143
-rw-r--r--vanilla/node_modules/undici/lib/core/diagnostics.js225
-rw-r--r--vanilla/node_modules/undici/lib/core/errors.js448
-rw-r--r--vanilla/node_modules/undici/lib/core/request.js412
-rw-r--r--vanilla/node_modules/undici/lib/core/symbols.js74
-rw-r--r--vanilla/node_modules/undici/lib/core/tree.js160
-rw-r--r--vanilla/node_modules/undici/lib/core/util.js957
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/agent.js158
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/balanced-pool.js216
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/client-h1.js1606
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/client-h2.js990
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/client.js647
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/dispatcher-base.js165
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/dispatcher.js48
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js146
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/fixed-queue.js135
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/h2c-client.js51
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/pool-base.js214
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/pool.js118
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/proxy-agent.js287
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/retry-agent.js35
-rw-r--r--vanilla/node_modules/undici/lib/dispatcher/round-robin-pool.js137
-rw-r--r--vanilla/node_modules/undici/lib/encoding/index.js33
-rw-r--r--vanilla/node_modules/undici/lib/global.js50
-rw-r--r--vanilla/node_modules/undici/lib/handler/cache-handler.js561
-rw-r--r--vanilla/node_modules/undici/lib/handler/cache-revalidation-handler.js124
-rw-r--r--vanilla/node_modules/undici/lib/handler/decorator-handler.js67
-rw-r--r--vanilla/node_modules/undici/lib/handler/deduplication-handler.js216
-rw-r--r--vanilla/node_modules/undici/lib/handler/redirect-handler.js237
-rw-r--r--vanilla/node_modules/undici/lib/handler/retry-handler.js394
-rw-r--r--vanilla/node_modules/undici/lib/handler/unwrap-handler.js96
-rw-r--r--vanilla/node_modules/undici/lib/handler/wrap-handler.js95
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/cache.js495
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/decompress.js259
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/deduplicate.js107
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/dns.js474
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/dump.js112
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/redirect.js21
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/response-error.js95
-rw-r--r--vanilla/node_modules/undici/lib/interceptor/retry.js19
-rw-r--r--vanilla/node_modules/undici/lib/llhttp/.gitkeep0
-rw-r--r--vanilla/node_modules/undici/lib/llhttp/constants.d.ts195
-rw-r--r--vanilla/node_modules/undici/lib/llhttp/constants.js531
-rw-r--r--vanilla/node_modules/undici/lib/llhttp/llhttp-wasm.js15
-rw-r--r--vanilla/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js15
-rw-r--r--vanilla/node_modules/undici/lib/llhttp/utils.d.ts2
-rw-r--r--vanilla/node_modules/undici/lib/llhttp/utils.js12
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-agent.js232
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-call-history.js248
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-client.js68
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-errors.js29
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-interceptor.js209
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-pool.js68
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-symbols.js31
-rw-r--r--vanilla/node_modules/undici/lib/mock/mock-utils.js480
-rw-r--r--vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js43
-rw-r--r--vanilla/node_modules/undici/lib/mock/snapshot-agent.js353
-rw-r--r--vanilla/node_modules/undici/lib/mock/snapshot-recorder.js588
-rw-r--r--vanilla/node_modules/undici/lib/mock/snapshot-utils.js158
-rw-r--r--vanilla/node_modules/undici/lib/util/cache.js405
-rw-r--r--vanilla/node_modules/undici/lib/util/date.js653
-rw-r--r--vanilla/node_modules/undici/lib/util/promise.js28
-rw-r--r--vanilla/node_modules/undici/lib/util/runtime-features.js124
-rw-r--r--vanilla/node_modules/undici/lib/util/stats.js32
-rw-r--r--vanilla/node_modules/undici/lib/util/timers.js425
-rw-r--r--vanilla/node_modules/undici/lib/web/cache/cache.js864
-rw-r--r--vanilla/node_modules/undici/lib/web/cache/cachestorage.js152
-rw-r--r--vanilla/node_modules/undici/lib/web/cache/util.js45
-rw-r--r--vanilla/node_modules/undici/lib/web/cookies/constants.js12
-rw-r--r--vanilla/node_modules/undici/lib/web/cookies/index.js199
-rw-r--r--vanilla/node_modules/undici/lib/web/cookies/parse.js322
-rw-r--r--vanilla/node_modules/undici/lib/web/cookies/util.js282
-rw-r--r--vanilla/node_modules/undici/lib/web/eventsource/eventsource-stream.js399
-rw-r--r--vanilla/node_modules/undici/lib/web/eventsource/eventsource.js501
-rw-r--r--vanilla/node_modules/undici/lib/web/eventsource/util.js29
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/LICENSE21
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/body.js509
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/constants.js131
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/data-url.js596
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js575
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/formdata.js259
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/global.js40
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/headers.js719
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/index.js2372
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/request.js1115
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/response.js641
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/util.js1520
-rw-r--r--vanilla/node_modules/undici/lib/web/infra/index.js229
-rw-r--r--vanilla/node_modules/undici/lib/web/subresource-integrity/Readme.md9
-rw-r--r--vanilla/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js307
-rw-r--r--vanilla/node_modules/undici/lib/web/webidl/index.js1003
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/connection.js329
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/constants.js126
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/events.js331
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/frame.js133
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js70
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/receiver.js444
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/sender.js109
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js104
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js497
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/util.js339
-rw-r--r--vanilla/node_modules/undici/lib/web/websocket/websocket.js739
113 files changed, 34217 insertions, 0 deletions
diff --git a/vanilla/node_modules/undici/lib/api/abort-signal.js b/vanilla/node_modules/undici/lib/api/abort-signal.js
new file mode 100644
index 0000000..608170b
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/abort-signal.js
@@ -0,0 +1,59 @@
+'use strict'
+
+const { addAbortListener } = require('../core/util')
+const { RequestAbortedError } = require('../core/errors')
+
+const kListener = Symbol('kListener')
+const kSignal = Symbol('kSignal')
+
+function abort (self) {
+ if (self.abort) {
+ self.abort(self[kSignal]?.reason)
+ } else {
+ self.reason = self[kSignal]?.reason ?? new RequestAbortedError()
+ }
+ removeSignal(self)
+}
+
+function addSignal (self, signal) {
+ self.reason = null
+
+ self[kSignal] = null
+ self[kListener] = null
+
+ if (!signal) {
+ return
+ }
+
+ if (signal.aborted) {
+ abort(self)
+ return
+ }
+
+ self[kSignal] = signal
+ self[kListener] = () => {
+ abort(self)
+ }
+
+ addAbortListener(self[kSignal], self[kListener])
+}
+
+function removeSignal (self) {
+ if (!self[kSignal]) {
+ return
+ }
+
+ if ('removeEventListener' in self[kSignal]) {
+ self[kSignal].removeEventListener('abort', self[kListener])
+ } else {
+ self[kSignal].removeListener('abort', self[kListener])
+ }
+
+ self[kSignal] = null
+ self[kListener] = null
+}
+
+module.exports = {
+ addSignal,
+ removeSignal
+}
diff --git a/vanilla/node_modules/undici/lib/api/api-connect.js b/vanilla/node_modules/undici/lib/api/api-connect.js
new file mode 100644
index 0000000..c8b86dd
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/api-connect.js
@@ -0,0 +1,110 @@
+'use strict'
+
+const assert = require('node:assert')
+const { AsyncResource } = require('node:async_hooks')
+const { InvalidArgumentError, SocketError } = require('../core/errors')
+const util = require('../core/util')
+const { addSignal, removeSignal } = require('./abort-signal')
+
+class ConnectHandler extends AsyncResource {
+ constructor (opts, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ const { signal, opaque, responseHeaders } = opts
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ super('UNDICI_CONNECT')
+
+ this.opaque = opaque || null
+ this.responseHeaders = responseHeaders || null
+ this.callback = callback
+ this.abort = null
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ if (this.reason) {
+ abort(this.reason)
+ return
+ }
+
+ assert(this.callback)
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders () {
+ throw new SocketError('bad connect', null)
+ }
+
+ onUpgrade (statusCode, rawHeaders, socket) {
+ const { callback, opaque, context } = this
+
+ removeSignal(this)
+
+ this.callback = null
+
+ let headers = rawHeaders
+ // Indicates is an HTTP2Session
+ if (headers != null) {
+ headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ }
+
+ this.runInAsyncScope(callback, null, null, {
+ statusCode,
+ headers,
+ socket,
+ opaque,
+ context
+ })
+ }
+
+ onError (err) {
+ const { callback, opaque } = this
+
+ removeSignal(this)
+
+ if (callback) {
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+ }
+}
+
+function connect (opts, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ connect.call(this, opts, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ const connectHandler = new ConnectHandler(opts, callback)
+ const connectOptions = { ...opts, method: 'CONNECT' }
+
+ this.dispatch(connectOptions, connectHandler)
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts?.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = connect
diff --git a/vanilla/node_modules/undici/lib/api/api-pipeline.js b/vanilla/node_modules/undici/lib/api/api-pipeline.js
new file mode 100644
index 0000000..77f3520
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/api-pipeline.js
@@ -0,0 +1,252 @@
+'use strict'
+
+const {
+ Readable,
+ Duplex,
+ PassThrough
+} = require('node:stream')
+const assert = require('node:assert')
+const { AsyncResource } = require('node:async_hooks')
+const {
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError
+} = require('../core/errors')
+const util = require('../core/util')
+const { addSignal, removeSignal } = require('./abort-signal')
+
+function noop () {}
+
+const kResume = Symbol('resume')
+
+class PipelineRequest extends Readable {
+ constructor () {
+ super({ autoDestroy: true })
+
+ this[kResume] = null
+ }
+
+ _read () {
+ const { [kResume]: resume } = this
+
+ if (resume) {
+ this[kResume] = null
+ resume()
+ }
+ }
+
+ _destroy (err, callback) {
+ this._read()
+
+ callback(err)
+ }
+}
+
+class PipelineResponse extends Readable {
+ constructor (resume) {
+ super({ autoDestroy: true })
+ this[kResume] = resume
+ }
+
+ _read () {
+ this[kResume]()
+ }
+
+ _destroy (err, callback) {
+ if (!err && !this._readableState.endEmitted) {
+ err = new RequestAbortedError()
+ }
+
+ callback(err)
+ }
+}
+
+class PipelineHandler extends AsyncResource {
+ constructor (opts, handler) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ if (typeof handler !== 'function') {
+ throw new InvalidArgumentError('invalid handler')
+ }
+
+ const { signal, method, opaque, onInfo, responseHeaders } = opts
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ if (method === 'CONNECT') {
+ throw new InvalidArgumentError('invalid method')
+ }
+
+ if (onInfo && typeof onInfo !== 'function') {
+ throw new InvalidArgumentError('invalid onInfo callback')
+ }
+
+ super('UNDICI_PIPELINE')
+
+ this.opaque = opaque || null
+ this.responseHeaders = responseHeaders || null
+ this.handler = handler
+ this.abort = null
+ this.context = null
+ this.onInfo = onInfo || null
+
+ this.req = new PipelineRequest().on('error', noop)
+
+ this.ret = new Duplex({
+ readableObjectMode: opts.objectMode,
+ autoDestroy: true,
+ read: () => {
+ const { body } = this
+
+ if (body?.resume) {
+ body.resume()
+ }
+ },
+ write: (chunk, encoding, callback) => {
+ const { req } = this
+
+ if (req.push(chunk, encoding) || req._readableState.destroyed) {
+ callback()
+ } else {
+ req[kResume] = callback
+ }
+ },
+ destroy: (err, callback) => {
+ const { body, req, res, ret, abort } = this
+
+ if (!err && !ret._readableState.endEmitted) {
+ err = new RequestAbortedError()
+ }
+
+ if (abort && err) {
+ abort()
+ }
+
+ util.destroy(body, err)
+ util.destroy(req, err)
+ util.destroy(res, err)
+
+ removeSignal(this)
+
+ callback(err)
+ }
+ }).on('prefinish', () => {
+ const { req } = this
+
+ // Node < 15 does not call _final in same tick.
+ req.push(null)
+ })
+
+ this.res = null
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ const { res } = this
+
+ if (this.reason) {
+ abort(this.reason)
+ return
+ }
+
+ assert(!res, 'pipeline cannot be retried')
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders (statusCode, rawHeaders, resume) {
+ const { opaque, handler, context } = this
+
+ if (statusCode < 200) {
+ if (this.onInfo) {
+ const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ this.onInfo({ statusCode, headers })
+ }
+ return
+ }
+
+ this.res = new PipelineResponse(resume)
+
+ let body
+ try {
+ this.handler = null
+ const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ body = this.runInAsyncScope(handler, null, {
+ statusCode,
+ headers,
+ opaque,
+ body: this.res,
+ context
+ })
+ } catch (err) {
+ this.res.on('error', noop)
+ throw err
+ }
+
+ if (!body || typeof body.on !== 'function') {
+ throw new InvalidReturnValueError('expected Readable')
+ }
+
+ body
+ .on('data', (chunk) => {
+ const { ret, body } = this
+
+ if (!ret.push(chunk) && body.pause) {
+ body.pause()
+ }
+ })
+ .on('error', (err) => {
+ const { ret } = this
+
+ util.destroy(ret, err)
+ })
+ .on('end', () => {
+ const { ret } = this
+
+ ret.push(null)
+ })
+ .on('close', () => {
+ const { ret } = this
+
+ if (!ret._readableState.ended) {
+ util.destroy(ret, new RequestAbortedError())
+ }
+ })
+
+ this.body = body
+ }
+
+ onData (chunk) {
+ const { res } = this
+ return res.push(chunk)
+ }
+
+ onComplete (trailers) {
+ const { res } = this
+ res.push(null)
+ }
+
+ onError (err) {
+ const { ret } = this
+ this.handler = null
+ util.destroy(ret, err)
+ }
+}
+
+function pipeline (opts, handler) {
+ try {
+ const pipelineHandler = new PipelineHandler(opts, handler)
+ this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler)
+ return pipelineHandler.ret
+ } catch (err) {
+ return new PassThrough().destroy(err)
+ }
+}
+
+module.exports = pipeline
diff --git a/vanilla/node_modules/undici/lib/api/api-request.js b/vanilla/node_modules/undici/lib/api/api-request.js
new file mode 100644
index 0000000..f6d15f7
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/api-request.js
@@ -0,0 +1,214 @@
+'use strict'
+
+const assert = require('node:assert')
+const { AsyncResource } = require('node:async_hooks')
+const { Readable } = require('./readable')
+const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
+const util = require('../core/util')
+
+function noop () {}
+
+class RequestHandler extends AsyncResource {
+ constructor (opts, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ const { signal, method, opaque, body, onInfo, responseHeaders, highWaterMark } = opts
+
+ try {
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
+ throw new InvalidArgumentError('invalid highWaterMark')
+ }
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ if (method === 'CONNECT') {
+ throw new InvalidArgumentError('invalid method')
+ }
+
+ if (onInfo && typeof onInfo !== 'function') {
+ throw new InvalidArgumentError('invalid onInfo callback')
+ }
+
+ super('UNDICI_REQUEST')
+ } catch (err) {
+ if (util.isStream(body)) {
+ util.destroy(body.on('error', noop), err)
+ }
+ throw err
+ }
+
+ this.method = method
+ this.responseHeaders = responseHeaders || null
+ this.opaque = opaque || null
+ this.callback = callback
+ this.res = null
+ this.abort = null
+ this.body = body
+ this.trailers = {}
+ this.context = null
+ this.onInfo = onInfo || null
+ this.highWaterMark = highWaterMark
+ this.reason = null
+ this.removeAbortListener = null
+
+ if (signal?.aborted) {
+ this.reason = signal.reason ?? new RequestAbortedError()
+ } else if (signal) {
+ this.removeAbortListener = util.addAbortListener(signal, () => {
+ this.reason = signal.reason ?? new RequestAbortedError()
+ if (this.res) {
+ util.destroy(this.res.on('error', noop), this.reason)
+ } else if (this.abort) {
+ this.abort(this.reason)
+ }
+ })
+ }
+ }
+
+ onConnect (abort, context) {
+ if (this.reason) {
+ abort(this.reason)
+ return
+ }
+
+ assert(this.callback)
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
+ const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this
+
+ const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+
+ if (statusCode < 200) {
+ if (this.onInfo) {
+ this.onInfo({ statusCode, headers })
+ }
+ return
+ }
+
+ const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
+ const contentType = parsedHeaders['content-type']
+ const contentLength = parsedHeaders['content-length']
+ const res = new Readable({
+ resume,
+ abort,
+ contentType,
+ contentLength: this.method !== 'HEAD' && contentLength
+ ? Number(contentLength)
+ : null,
+ highWaterMark
+ })
+
+ if (this.removeAbortListener) {
+ res.on('close', this.removeAbortListener)
+ this.removeAbortListener = null
+ }
+
+ this.callback = null
+ this.res = res
+ if (callback !== null) {
+ try {
+ this.runInAsyncScope(callback, null, null, {
+ statusCode,
+ statusText: statusMessage,
+ headers,
+ trailers: this.trailers,
+ opaque,
+ body: res,
+ context
+ })
+ } catch (err) {
+ // If the callback throws synchronously, we need to handle it
+ // Remove reference to res to allow res being garbage collected
+ this.res = null
+
+ // Destroy the response stream
+ util.destroy(res.on('error', noop), err)
+
+ // Use queueMicrotask to re-throw the error so it reaches uncaughtException
+ queueMicrotask(() => {
+ throw err
+ })
+ }
+ }
+ }
+
+ onData (chunk) {
+ return this.res.push(chunk)
+ }
+
+ onComplete (trailers) {
+ util.parseHeaders(trailers, this.trailers)
+ this.res.push(null)
+ }
+
+ onError (err) {
+ const { res, callback, body, opaque } = this
+
+ if (callback) {
+ // TODO: Does this need queueMicrotask?
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+
+ if (res) {
+ this.res = null
+ // Ensure all queued handlers are invoked before destroying res.
+ queueMicrotask(() => {
+ util.destroy(res.on('error', noop), err)
+ })
+ }
+
+ if (body) {
+ this.body = null
+
+ if (util.isStream(body)) {
+ body.on('error', noop)
+ util.destroy(body, err)
+ }
+ }
+
+ if (this.removeAbortListener) {
+ this.removeAbortListener()
+ this.removeAbortListener = null
+ }
+ }
+}
+
+function request (opts, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ request.call(this, opts, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ const handler = new RequestHandler(opts, callback)
+
+ this.dispatch(opts, handler)
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts?.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = request
+module.exports.RequestHandler = RequestHandler
diff --git a/vanilla/node_modules/undici/lib/api/api-stream.js b/vanilla/node_modules/undici/lib/api/api-stream.js
new file mode 100644
index 0000000..5d0b3fb
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/api-stream.js
@@ -0,0 +1,209 @@
+'use strict'
+
+const assert = require('node:assert')
+const { finished } = require('node:stream')
+const { AsyncResource } = require('node:async_hooks')
+const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors')
+const util = require('../core/util')
+const { addSignal, removeSignal } = require('./abort-signal')
+
+function noop () {}
+
+class StreamHandler extends AsyncResource {
+ constructor (opts, factory, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ const { signal, method, opaque, body, onInfo, responseHeaders } = opts
+
+ try {
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('invalid factory')
+ }
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ if (method === 'CONNECT') {
+ throw new InvalidArgumentError('invalid method')
+ }
+
+ if (onInfo && typeof onInfo !== 'function') {
+ throw new InvalidArgumentError('invalid onInfo callback')
+ }
+
+ super('UNDICI_STREAM')
+ } catch (err) {
+ if (util.isStream(body)) {
+ util.destroy(body.on('error', noop), err)
+ }
+ throw err
+ }
+
+ this.responseHeaders = responseHeaders || null
+ this.opaque = opaque || null
+ this.factory = factory
+ this.callback = callback
+ this.res = null
+ this.abort = null
+ this.context = null
+ this.trailers = null
+ this.body = body
+ this.onInfo = onInfo || null
+
+ if (util.isStream(body)) {
+ body.on('error', (err) => {
+ this.onError(err)
+ })
+ }
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ if (this.reason) {
+ abort(this.reason)
+ return
+ }
+
+ assert(this.callback)
+
+ this.abort = abort
+ this.context = context
+ }
+
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
+ const { factory, opaque, context, responseHeaders } = this
+
+ const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+
+ if (statusCode < 200) {
+ if (this.onInfo) {
+ this.onInfo({ statusCode, headers })
+ }
+ return
+ }
+
+ this.factory = null
+
+ if (factory === null) {
+ return
+ }
+
+ const res = this.runInAsyncScope(factory, null, {
+ statusCode,
+ headers,
+ opaque,
+ context
+ })
+
+ if (
+ !res ||
+ typeof res.write !== 'function' ||
+ typeof res.end !== 'function' ||
+ typeof res.on !== 'function'
+ ) {
+ throw new InvalidReturnValueError('expected Writable')
+ }
+
+ // TODO: Avoid finished. It registers an unnecessary amount of listeners.
+ finished(res, { readable: false }, (err) => {
+ const { callback, res, opaque, trailers, abort } = this
+
+ this.res = null
+ if (err || !res?.readable) {
+ util.destroy(res, err)
+ }
+
+ this.callback = null
+ this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
+
+ if (err) {
+ abort()
+ }
+ })
+
+ res.on('drain', resume)
+
+ this.res = res
+
+ const needDrain = res.writableNeedDrain !== undefined
+ ? res.writableNeedDrain
+ : res._writableState?.needDrain
+
+ return needDrain !== true
+ }
+
+ onData (chunk) {
+ const { res } = this
+
+ return res ? res.write(chunk) : true
+ }
+
+ onComplete (trailers) {
+ const { res } = this
+
+ removeSignal(this)
+
+ if (!res) {
+ return
+ }
+
+ this.trailers = util.parseHeaders(trailers)
+
+ res.end()
+ }
+
+ onError (err) {
+ const { res, callback, opaque, body } = this
+
+ removeSignal(this)
+
+ this.factory = null
+
+ if (res) {
+ this.res = null
+ util.destroy(res, err)
+ } else if (callback) {
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+
+ if (body) {
+ this.body = null
+ util.destroy(body, err)
+ }
+ }
+}
+
+function stream (opts, factory, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ stream.call(this, opts, factory, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ const handler = new StreamHandler(opts, factory, callback)
+
+ this.dispatch(opts, handler)
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts?.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = stream
diff --git a/vanilla/node_modules/undici/lib/api/api-upgrade.js b/vanilla/node_modules/undici/lib/api/api-upgrade.js
new file mode 100644
index 0000000..2b03f20
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/api-upgrade.js
@@ -0,0 +1,111 @@
+'use strict'
+
+const { InvalidArgumentError, SocketError } = require('../core/errors')
+const { AsyncResource } = require('node:async_hooks')
+const assert = require('node:assert')
+const util = require('../core/util')
+const { kHTTP2Stream } = require('../core/symbols')
+const { addSignal, removeSignal } = require('./abort-signal')
+
+class UpgradeHandler extends AsyncResource {
+ constructor (opts, callback) {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('invalid opts')
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ const { signal, opaque, responseHeaders } = opts
+
+ if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
+ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
+ }
+
+ super('UNDICI_UPGRADE')
+
+ this.responseHeaders = responseHeaders || null
+ this.opaque = opaque || null
+ this.callback = callback
+ this.abort = null
+ this.context = null
+
+ addSignal(this, signal)
+ }
+
+ onConnect (abort, context) {
+ if (this.reason) {
+ abort(this.reason)
+ return
+ }
+
+ assert(this.callback)
+
+ this.abort = abort
+ this.context = null
+ }
+
+ onHeaders () {
+ throw new SocketError('bad upgrade', null)
+ }
+
+ onUpgrade (statusCode, rawHeaders, socket) {
+ assert(socket[kHTTP2Stream] === true ? statusCode === 200 : statusCode === 101)
+
+ const { callback, opaque, context } = this
+
+ removeSignal(this)
+
+ this.callback = null
+ const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
+ this.runInAsyncScope(callback, null, null, {
+ headers,
+ socket,
+ opaque,
+ context
+ })
+ }
+
+ onError (err) {
+ const { callback, opaque } = this
+
+ removeSignal(this)
+
+ if (callback) {
+ this.callback = null
+ queueMicrotask(() => {
+ this.runInAsyncScope(callback, null, err, { opaque })
+ })
+ }
+ }
+}
+
+function upgrade (opts, callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ upgrade.call(this, opts, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ try {
+ const upgradeHandler = new UpgradeHandler(opts, callback)
+ const upgradeOpts = {
+ ...opts,
+ method: opts.method || 'GET',
+ upgrade: opts.protocol || 'Websocket'
+ }
+
+ this.dispatch(upgradeOpts, upgradeHandler)
+ } catch (err) {
+ if (typeof callback !== 'function') {
+ throw err
+ }
+ const opaque = opts?.opaque
+ queueMicrotask(() => callback(err, { opaque }))
+ }
+}
+
+module.exports = upgrade
diff --git a/vanilla/node_modules/undici/lib/api/index.js b/vanilla/node_modules/undici/lib/api/index.js
new file mode 100644
index 0000000..8983a5e
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/index.js
@@ -0,0 +1,7 @@
+'use strict'
+
+module.exports.request = require('./api-request')
+module.exports.stream = require('./api-stream')
+module.exports.pipeline = require('./api-pipeline')
+module.exports.upgrade = require('./api-upgrade')
+module.exports.connect = require('./api-connect')
diff --git a/vanilla/node_modules/undici/lib/api/readable.js b/vanilla/node_modules/undici/lib/api/readable.js
new file mode 100644
index 0000000..cdede95
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/api/readable.js
@@ -0,0 +1,580 @@
+'use strict'
+
+const assert = require('node:assert')
+const { Readable } = require('node:stream')
+const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require('../core/errors')
+const util = require('../core/util')
+const { ReadableStreamFrom } = require('../core/util')
+
+const kConsume = Symbol('kConsume')
+const kReading = Symbol('kReading')
+const kBody = Symbol('kBody')
+const kAbort = Symbol('kAbort')
+const kContentType = Symbol('kContentType')
+const kContentLength = Symbol('kContentLength')
+const kUsed = Symbol('kUsed')
+const kBytesRead = Symbol('kBytesRead')
+
+const noop = () => {}
+
+/**
+ * @class
+ * @extends {Readable}
+ * @see https://fetch.spec.whatwg.org/#body
+ */
+class BodyReadable extends Readable {
+ /**
+ * @param {object} opts
+ * @param {(this: Readable, size: number) => void} opts.resume
+ * @param {() => (void | null)} opts.abort
+ * @param {string} [opts.contentType = '']
+ * @param {number} [opts.contentLength]
+ * @param {number} [opts.highWaterMark = 64 * 1024]
+ */
+ constructor ({
+ resume,
+ abort,
+ contentType = '',
+ contentLength,
+ highWaterMark = 64 * 1024 // Same as nodejs fs streams.
+ }) {
+ super({
+ autoDestroy: true,
+ read: resume,
+ highWaterMark
+ })
+
+ this._readableState.dataEmitted = false
+
+ this[kAbort] = abort
+
+ /** @type {Consume | null} */
+ this[kConsume] = null
+
+ /** @type {number} */
+ this[kBytesRead] = 0
+
+ /** @type {ReadableStream|null} */
+ this[kBody] = null
+
+ /** @type {boolean} */
+ this[kUsed] = false
+
+ /** @type {string} */
+ this[kContentType] = contentType
+
+ /** @type {number|null} */
+ this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
+
+ /**
+ * Is stream being consumed through Readable API?
+ * This is an optimization so that we avoid checking
+ * for 'data' and 'readable' listeners in the hot path
+ * inside push().
+ *
+ * @type {boolean}
+ */
+ this[kReading] = false
+ }
+
+ /**
+ * @param {Error|null} err
+ * @param {(error:(Error|null)) => void} callback
+ * @returns {void}
+ */
+ _destroy (err, callback) {
+ if (!err && !this._readableState.endEmitted) {
+ err = new RequestAbortedError()
+ }
+
+ if (err) {
+ this[kAbort]()
+ }
+
+ // Workaround for Node "bug". If the stream is destroyed in same
+ // tick as it is created, then a user who is waiting for a
+ // promise (i.e micro tick) for installing an 'error' listener will
+ // never get a chance and will always encounter an unhandled exception.
+ if (!this[kUsed]) {
+ setImmediate(callback, err)
+ } else {
+ callback(err)
+ }
+ }
+
+ /**
+ * @param {string|symbol} event
+ * @param {(...args: any[]) => void} listener
+ * @returns {this}
+ */
+ on (event, listener) {
+ if (event === 'data' || event === 'readable') {
+ this[kReading] = true
+ this[kUsed] = true
+ }
+ return super.on(event, listener)
+ }
+
+ /**
+ * @param {string|symbol} event
+ * @param {(...args: any[]) => void} listener
+ * @returns {this}
+ */
+ addListener (event, listener) {
+ return this.on(event, listener)
+ }
+
+ /**
+ * @param {string|symbol} event
+ * @param {(...args: any[]) => void} listener
+ * @returns {this}
+ */
+ off (event, listener) {
+ const ret = super.off(event, listener)
+ if (event === 'data' || event === 'readable') {
+ this[kReading] = (
+ this.listenerCount('data') > 0 ||
+ this.listenerCount('readable') > 0
+ )
+ }
+ return ret
+ }
+
+ /**
+ * @param {string|symbol} event
+ * @param {(...args: any[]) => void} listener
+ * @returns {this}
+ */
+ removeListener (event, listener) {
+ return this.off(event, listener)
+ }
+
+ /**
+ * @param {Buffer|null} chunk
+ * @returns {boolean}
+ */
+ push (chunk) {
+ if (chunk) {
+ this[kBytesRead] += chunk.length
+ if (this[kConsume]) {
+ consumePush(this[kConsume], chunk)
+ return this[kReading] ? super.push(chunk) : true
+ }
+ }
+
+ return super.push(chunk)
+ }
+
+ /**
+ * Consumes and returns the body as a string.
+ *
+ * @see https://fetch.spec.whatwg.org/#dom-body-text
+ * @returns {Promise<string>}
+ */
+ text () {
+ return consume(this, 'text')
+ }
+
+ /**
+ * Consumes and returns the body as a JavaScript Object.
+ *
+ * @see https://fetch.spec.whatwg.org/#dom-body-json
+ * @returns {Promise<unknown>}
+ */
+ json () {
+ return consume(this, 'json')
+ }
+
+ /**
+ * Consumes and returns the body as a Blob
+ *
+ * @see https://fetch.spec.whatwg.org/#dom-body-blob
+ * @returns {Promise<Blob>}
+ */
+ blob () {
+ return consume(this, 'blob')
+ }
+
+ /**
+ * Consumes and returns the body as an Uint8Array.
+ *
+ * @see https://fetch.spec.whatwg.org/#dom-body-bytes
+ * @returns {Promise<Uint8Array>}
+ */
+ bytes () {
+ return consume(this, 'bytes')
+ }
+
+ /**
+ * Consumes and returns the body as an ArrayBuffer.
+ *
+ * @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
+ * @returns {Promise<ArrayBuffer>}
+ */
+ arrayBuffer () {
+ return consume(this, 'arrayBuffer')
+ }
+
+ /**
+ * Not implemented
+ *
+ * @see https://fetch.spec.whatwg.org/#dom-body-formdata
+ * @throws {NotSupportedError}
+ */
+ async formData () {
+ // TODO: Implement.
+ throw new NotSupportedError()
+ }
+
+ /**
+ * Returns true if the body is not null and the body has been consumed.
+ * Otherwise, returns false.
+ *
+ * @see https://fetch.spec.whatwg.org/#dom-body-bodyused
+ * @readonly
+ * @returns {boolean}
+ */
+ get bodyUsed () {
+ return util.isDisturbed(this)
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#dom-body-body
+ * @readonly
+ * @returns {ReadableStream}
+ */
+ get body () {
+ if (!this[kBody]) {
+ this[kBody] = ReadableStreamFrom(this)
+ if (this[kConsume]) {
+ // TODO: Is this the best way to force a lock?
+ this[kBody].getReader() // Ensure stream is locked.
+ assert(this[kBody].locked)
+ }
+ }
+ return this[kBody]
+ }
+
+ /**
+ * Dumps the response body by reading `limit` number of bytes.
+ * @param {object} opts
+ * @param {number} [opts.limit = 131072] Number of bytes to read.
+ * @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump.
+ * @returns {Promise<null>}
+ */
+ dump (opts) {
+ const signal = opts?.signal
+
+ if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
+ return Promise.reject(new InvalidArgumentError('signal must be an AbortSignal'))
+ }
+
+ const limit = opts?.limit && Number.isFinite(opts.limit)
+ ? opts.limit
+ : 128 * 1024
+
+ if (signal?.aborted) {
+ return Promise.reject(signal.reason ?? new AbortError())
+ }
+
+ if (this._readableState.closeEmitted) {
+ return Promise.resolve(null)
+ }
+
+ return new Promise((resolve, reject) => {
+ if (
+ (this[kContentLength] && (this[kContentLength] > limit)) ||
+ this[kBytesRead] > limit
+ ) {
+ this.destroy(new AbortError())
+ }
+
+ if (signal) {
+ const onAbort = () => {
+ this.destroy(signal.reason ?? new AbortError())
+ }
+ signal.addEventListener('abort', onAbort)
+ this
+ .on('close', function () {
+ signal.removeEventListener('abort', onAbort)
+ if (signal.aborted) {
+ reject(signal.reason ?? new AbortError())
+ } else {
+ resolve(null)
+ }
+ })
+ } else {
+ this.on('close', resolve)
+ }
+
+ this
+ .on('error', noop)
+ .on('data', () => {
+ if (this[kBytesRead] > limit) {
+ this.destroy()
+ }
+ })
+ .resume()
+ })
+ }
+
+ /**
+ * @param {BufferEncoding} encoding
+ * @returns {this}
+ */
+ setEncoding (encoding) {
+ if (Buffer.isEncoding(encoding)) {
+ this._readableState.encoding = encoding
+ }
+ return this
+ }
+}
+
+/**
+ * @see https://streams.spec.whatwg.org/#readablestream-locked
+ * @param {BodyReadable} bodyReadable
+ * @returns {boolean}
+ */
+function isLocked (bodyReadable) {
+ // Consume is an implicit lock.
+ return bodyReadable[kBody]?.locked === true || bodyReadable[kConsume] !== null
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#body-unusable
+ * @param {BodyReadable} bodyReadable
+ * @returns {boolean}
+ */
+function isUnusable (bodyReadable) {
+ return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
+}
+
+/**
+ * @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType
+ */
+
+/**
+ * @template {ConsumeType} T
+ * @typedef {T extends 'text' ? string :
+ * T extends 'json' ? unknown :
+ * T extends 'blob' ? Blob :
+ * T extends 'arrayBuffer' ? ArrayBuffer :
+ * T extends 'bytes' ? Uint8Array :
+ * never
+ * } ConsumeReturnType
+ */
+/**
+ * @typedef {object} Consume
+ * @property {ConsumeType} type
+ * @property {BodyReadable} stream
+ * @property {((value?: any) => void)} resolve
+ * @property {((err: Error) => void)} reject
+ * @property {number} length
+ * @property {Buffer[]} body
+ */
+
+/**
+ * @template {ConsumeType} T
+ * @param {BodyReadable} stream
+ * @param {T} type
+ * @returns {Promise<ConsumeReturnType<T>>}
+ */
+function consume (stream, type) {
+ assert(!stream[kConsume])
+
+ return new Promise((resolve, reject) => {
+ if (isUnusable(stream)) {
+ const rState = stream._readableState
+ if (rState.destroyed && rState.closeEmitted === false) {
+ stream
+ .on('error', reject)
+ .on('close', () => {
+ reject(new TypeError('unusable'))
+ })
+ } else {
+ reject(rState.errored ?? new TypeError('unusable'))
+ }
+ } else {
+ queueMicrotask(() => {
+ stream[kConsume] = {
+ type,
+ stream,
+ resolve,
+ reject,
+ length: 0,
+ body: []
+ }
+
+ stream
+ .on('error', function (err) {
+ consumeFinish(this[kConsume], err)
+ })
+ .on('close', function () {
+ if (this[kConsume].body !== null) {
+ consumeFinish(this[kConsume], new RequestAbortedError())
+ }
+ })
+
+ consumeStart(stream[kConsume])
+ })
+ }
+ })
+}
+
+/**
+ * @param {Consume} consume
+ * @returns {void}
+ */
+function consumeStart (consume) {
+ if (consume.body === null) {
+ return
+ }
+
+ const { _readableState: state } = consume.stream
+
+ if (state.bufferIndex) {
+ const start = state.bufferIndex
+ const end = state.buffer.length
+ for (let n = start; n < end; n++) {
+ consumePush(consume, state.buffer[n])
+ }
+ } else {
+ for (const chunk of state.buffer) {
+ consumePush(consume, chunk)
+ }
+ }
+
+ if (state.endEmitted) {
+ consumeEnd(this[kConsume], this._readableState.encoding)
+ } else {
+ consume.stream.on('end', function () {
+ consumeEnd(this[kConsume], this._readableState.encoding)
+ })
+ }
+
+ consume.stream.resume()
+
+ while (consume.stream.read() != null) {
+ // Loop
+ }
+}
+
+/**
+ * @param {Buffer[]} chunks
+ * @param {number} length
+ * @param {BufferEncoding} [encoding='utf8']
+ * @returns {string}
+ */
+function chunksDecode (chunks, length, encoding) {
+ if (chunks.length === 0 || length === 0) {
+ return ''
+ }
+ const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length)
+ const bufferLength = buffer.length
+
+ // Skip BOM.
+ const start =
+ bufferLength > 2 &&
+ buffer[0] === 0xef &&
+ buffer[1] === 0xbb &&
+ buffer[2] === 0xbf
+ ? 3
+ : 0
+ if (!encoding || encoding === 'utf8' || encoding === 'utf-8') {
+ return buffer.utf8Slice(start, bufferLength)
+ } else {
+ return buffer.subarray(start, bufferLength).toString(encoding)
+ }
+}
+
+/**
+ * @param {Buffer[]} chunks
+ * @param {number} length
+ * @returns {Uint8Array}
+ */
+function chunksConcat (chunks, length) {
+ if (chunks.length === 0 || length === 0) {
+ return new Uint8Array(0)
+ }
+ if (chunks.length === 1) {
+ // fast-path
+ return new Uint8Array(chunks[0])
+ }
+ const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer)
+
+ let offset = 0
+ for (let i = 0; i < chunks.length; ++i) {
+ const chunk = chunks[i]
+ buffer.set(chunk, offset)
+ offset += chunk.length
+ }
+
+ return buffer
+}
+
+/**
+ * @param {Consume} consume
+ * @param {BufferEncoding} encoding
+ * @returns {void}
+ */
+function consumeEnd (consume, encoding) {
+ const { type, body, resolve, stream, length } = consume
+
+ try {
+ if (type === 'text') {
+ resolve(chunksDecode(body, length, encoding))
+ } else if (type === 'json') {
+ resolve(JSON.parse(chunksDecode(body, length, encoding)))
+ } else if (type === 'arrayBuffer') {
+ resolve(chunksConcat(body, length).buffer)
+ } else if (type === 'blob') {
+ resolve(new Blob(body, { type: stream[kContentType] }))
+ } else if (type === 'bytes') {
+ resolve(chunksConcat(body, length))
+ }
+
+ consumeFinish(consume)
+ } catch (err) {
+ stream.destroy(err)
+ }
+}
+
+/**
+ * @param {Consume} consume
+ * @param {Buffer} chunk
+ * @returns {void}
+ */
+function consumePush (consume, chunk) {
+ consume.length += chunk.length
+ consume.body.push(chunk)
+}
+
+/**
+ * @param {Consume} consume
+ * @param {Error} [err]
+ * @returns {void}
+ */
+function consumeFinish (consume, err) {
+ if (consume.body === null) {
+ return
+ }
+
+ if (err) {
+ consume.reject(err)
+ } else {
+ consume.resolve()
+ }
+
+ // Reset the consume object to allow for garbage collection.
+ consume.type = null
+ consume.stream = null
+ consume.resolve = null
+ consume.reject = null
+ consume.length = 0
+ consume.body = null
+}
+
+module.exports = {
+ Readable: BodyReadable,
+ chunksDecode
+}
diff --git a/vanilla/node_modules/undici/lib/cache/memory-cache-store.js b/vanilla/node_modules/undici/lib/cache/memory-cache-store.js
new file mode 100644
index 0000000..dba29ae
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/cache/memory-cache-store.js
@@ -0,0 +1,234 @@
+'use strict'
+
+const { Writable } = require('node:stream')
+const { EventEmitter } = require('node:events')
+const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
+
+/**
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheValue} CacheValue
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.GetResult} GetResult
+ */
+
+/**
+ * @implements {CacheStore}
+ * @extends {EventEmitter}
+ */
+class MemoryCacheStore extends EventEmitter {
+ #maxCount = 1024
+ #maxSize = 104857600 // 100MB
+ #maxEntrySize = 5242880 // 5MB
+
+ #size = 0
+ #count = 0
+ #entries = new Map()
+ #hasEmittedMaxSizeEvent = false
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
+ */
+ constructor (opts) {
+ super()
+ if (opts) {
+ if (typeof opts !== 'object') {
+ throw new TypeError('MemoryCacheStore options must be an object')
+ }
+
+ if (opts.maxCount !== undefined) {
+ if (
+ typeof opts.maxCount !== 'number' ||
+ !Number.isInteger(opts.maxCount) ||
+ opts.maxCount < 0
+ ) {
+ throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
+ }
+ this.#maxCount = opts.maxCount
+ }
+
+ if (opts.maxSize !== undefined) {
+ if (
+ typeof opts.maxSize !== 'number' ||
+ !Number.isInteger(opts.maxSize) ||
+ opts.maxSize < 0
+ ) {
+ throw new TypeError('MemoryCacheStore options.maxSize must be a non-negative integer')
+ }
+ this.#maxSize = opts.maxSize
+ }
+
+ if (opts.maxEntrySize !== undefined) {
+ if (
+ typeof opts.maxEntrySize !== 'number' ||
+ !Number.isInteger(opts.maxEntrySize) ||
+ opts.maxEntrySize < 0
+ ) {
+ throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
+ }
+ this.#maxEntrySize = opts.maxEntrySize
+ }
+ }
+ }
+
+ /**
+ * Get the current size of the cache in bytes
+ * @returns {number} The current size of the cache in bytes
+ */
+ get size () {
+ return this.#size
+ }
+
+ /**
+ * Check if the cache is full (either max size or max count reached)
+ * @returns {boolean} True if the cache is full, false otherwise
+ */
+ isFull () {
+ return this.#size >= this.#maxSize || this.#count >= this.#maxCount
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
+ * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
+ */
+ get (key) {
+ assertCacheKey(key)
+
+ const topLevelKey = `${key.origin}:${key.path}`
+
+ const now = Date.now()
+ const entries = this.#entries.get(topLevelKey)
+
+ const entry = entries ? findEntry(key, entries, now) : null
+
+ return entry == null
+ ? undefined
+ : {
+ statusMessage: entry.statusMessage,
+ statusCode: entry.statusCode,
+ headers: entry.headers,
+ body: entry.body,
+ vary: entry.vary ? entry.vary : undefined,
+ etag: entry.etag,
+ cacheControlDirectives: entry.cacheControlDirectives,
+ cachedAt: entry.cachedAt,
+ staleAt: entry.staleAt,
+ deleteAt: entry.deleteAt
+ }
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val
+ * @returns {Writable | undefined}
+ */
+ createWriteStream (key, val) {
+ assertCacheKey(key)
+ assertCacheValue(val)
+
+ const topLevelKey = `${key.origin}:${key.path}`
+
+ const store = this
+ const entry = { ...key, ...val, body: [], size: 0 }
+
+ return new Writable({
+ write (chunk, encoding, callback) {
+ if (typeof chunk === 'string') {
+ chunk = Buffer.from(chunk, encoding)
+ }
+
+ entry.size += chunk.byteLength
+
+ if (entry.size >= store.#maxEntrySize) {
+ this.destroy()
+ } else {
+ entry.body.push(chunk)
+ }
+
+ callback(null)
+ },
+ final (callback) {
+ let entries = store.#entries.get(topLevelKey)
+ if (!entries) {
+ entries = []
+ store.#entries.set(topLevelKey, entries)
+ }
+ const previousEntry = findEntry(key, entries, Date.now())
+ if (previousEntry) {
+ const index = entries.indexOf(previousEntry)
+ entries.splice(index, 1, entry)
+ store.#size -= previousEntry.size
+ } else {
+ entries.push(entry)
+ store.#count += 1
+ }
+
+ store.#size += entry.size
+
+ // Check if cache is full and emit event if needed
+ if (store.#size > store.#maxSize || store.#count > store.#maxCount) {
+ // Emit maxSizeExceeded event if we haven't already
+ if (!store.#hasEmittedMaxSizeEvent) {
+ store.emit('maxSizeExceeded', {
+ size: store.#size,
+ maxSize: store.#maxSize,
+ count: store.#count,
+ maxCount: store.#maxCount
+ })
+ store.#hasEmittedMaxSizeEvent = true
+ }
+
+ // Perform eviction
+ for (const [key, entries] of store.#entries) {
+ for (const entry of entries.splice(0, entries.length / 2)) {
+ store.#size -= entry.size
+ store.#count -= 1
+ }
+ if (entries.length === 0) {
+ store.#entries.delete(key)
+ }
+ }
+
+ // Reset the event flag after eviction
+ if (store.#size < store.#maxSize && store.#count < store.#maxCount) {
+ store.#hasEmittedMaxSizeEvent = false
+ }
+ }
+
+ callback(null)
+ }
+ })
+ }
+
+ /**
+ * @param {CacheKey} key
+ */
+ delete (key) {
+ if (typeof key !== 'object') {
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
+ }
+
+ const topLevelKey = `${key.origin}:${key.path}`
+
+ for (const entry of this.#entries.get(topLevelKey) ?? []) {
+ this.#size -= entry.size
+ this.#count -= 1
+ }
+ this.#entries.delete(topLevelKey)
+ }
+}
+
+function findEntry (key, entries, now) {
+ return entries.find((entry) => (
+ entry.deleteAt > now &&
+ entry.method === key.method &&
+ (entry.vary == null || Object.keys(entry.vary).every(headerName => {
+ if (entry.vary[headerName] === null) {
+ return key.headers[headerName] === undefined
+ }
+
+ return entry.vary[headerName] === key.headers[headerName]
+ }))
+ ))
+}
+
+module.exports = MemoryCacheStore
diff --git a/vanilla/node_modules/undici/lib/cache/sqlite-cache-store.js b/vanilla/node_modules/undici/lib/cache/sqlite-cache-store.js
new file mode 100644
index 0000000..7cb4aa7
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/cache/sqlite-cache-store.js
@@ -0,0 +1,461 @@
+'use strict'
+
+const { Writable } = require('node:stream')
+const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
+
+let DatabaseSync
+
+const VERSION = 3
+
+// 2gb
+const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
+
+/**
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
+ * @implements {CacheStore}
+ *
+ * @typedef {{
+ * id: Readonly<number>,
+ * body?: Uint8Array
+ * statusCode: number
+ * statusMessage: string
+ * headers?: string
+ * vary?: string
+ * etag?: string
+ * cacheControlDirectives?: string
+ * cachedAt: number
+ * staleAt: number
+ * deleteAt: number
+ * }} SqliteStoreValue
+ */
+module.exports = class SqliteCacheStore {
+ #maxEntrySize = MAX_ENTRY_SIZE
+ #maxCount = Infinity
+
+ /**
+ * @type {import('node:sqlite').DatabaseSync}
+ */
+ #db
+
+ /**
+ * @type {import('node:sqlite').StatementSync}
+ */
+ #getValuesQuery
+
+ /**
+ * @type {import('node:sqlite').StatementSync}
+ */
+ #updateValueQuery
+
+ /**
+ * @type {import('node:sqlite').StatementSync}
+ */
+ #insertValueQuery
+
+ /**
+ * @type {import('node:sqlite').StatementSync}
+ */
+ #deleteExpiredValuesQuery
+
+ /**
+ * @type {import('node:sqlite').StatementSync}
+ */
+ #deleteByUrlQuery
+
+ /**
+ * @type {import('node:sqlite').StatementSync}
+ */
+ #countEntriesQuery
+
+ /**
+ * @type {import('node:sqlite').StatementSync | null}
+ */
+ #deleteOldValuesQuery
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
+ */
+ constructor (opts) {
+ if (opts) {
+ if (typeof opts !== 'object') {
+ throw new TypeError('SqliteCacheStore options must be an object')
+ }
+
+ if (opts.maxEntrySize !== undefined) {
+ if (
+ typeof opts.maxEntrySize !== 'number' ||
+ !Number.isInteger(opts.maxEntrySize) ||
+ opts.maxEntrySize < 0
+ ) {
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
+ }
+
+ if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
+ }
+
+ this.#maxEntrySize = opts.maxEntrySize
+ }
+
+ if (opts.maxCount !== undefined) {
+ if (
+ typeof opts.maxCount !== 'number' ||
+ !Number.isInteger(opts.maxCount) ||
+ opts.maxCount < 0
+ ) {
+ throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')
+ }
+ this.#maxCount = opts.maxCount
+ }
+ }
+
+ if (!DatabaseSync) {
+ DatabaseSync = require('node:sqlite').DatabaseSync
+ }
+ this.#db = new DatabaseSync(opts?.location ?? ':memory:')
+
+ this.#db.exec(`
+ PRAGMA journal_mode = WAL;
+ PRAGMA synchronous = NORMAL;
+ PRAGMA temp_store = memory;
+ PRAGMA optimize;
+
+ CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
+ -- Data specific to us
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ url TEXT NOT NULL,
+ method TEXT NOT NULL,
+
+ -- Data returned to the interceptor
+ body BUF NULL,
+ deleteAt INTEGER NOT NULL,
+ statusCode INTEGER NOT NULL,
+ statusMessage TEXT NOT NULL,
+ headers TEXT NULL,
+ cacheControlDirectives TEXT NULL,
+ etag TEXT NULL,
+ vary TEXT NULL,
+ cachedAt INTEGER NOT NULL,
+ staleAt INTEGER NOT NULL
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_getValuesQuery ON cacheInterceptorV${VERSION}(url, method, deleteAt);
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteByUrlQuery ON cacheInterceptorV${VERSION}(deleteAt);
+ `)
+
+ this.#getValuesQuery = this.#db.prepare(`
+ SELECT
+ id,
+ body,
+ deleteAt,
+ statusCode,
+ statusMessage,
+ headers,
+ etag,
+ cacheControlDirectives,
+ vary,
+ cachedAt,
+ staleAt
+ FROM cacheInterceptorV${VERSION}
+ WHERE
+ url = ?
+ AND method = ?
+ ORDER BY
+ deleteAt ASC
+ `)
+
+ this.#updateValueQuery = this.#db.prepare(`
+ UPDATE cacheInterceptorV${VERSION} SET
+ body = ?,
+ deleteAt = ?,
+ statusCode = ?,
+ statusMessage = ?,
+ headers = ?,
+ etag = ?,
+ cacheControlDirectives = ?,
+ cachedAt = ?,
+ staleAt = ?
+ WHERE
+ id = ?
+ `)
+
+ this.#insertValueQuery = this.#db.prepare(`
+ INSERT INTO cacheInterceptorV${VERSION} (
+ url,
+ method,
+ body,
+ deleteAt,
+ statusCode,
+ statusMessage,
+ headers,
+ etag,
+ cacheControlDirectives,
+ vary,
+ cachedAt,
+ staleAt
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `)
+
+ this.#deleteByUrlQuery = this.#db.prepare(
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`
+ )
+
+ this.#countEntriesQuery = this.#db.prepare(
+ `SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`
+ )
+
+ this.#deleteExpiredValuesQuery = this.#db.prepare(
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`
+ )
+
+ this.#deleteOldValuesQuery = this.#maxCount === Infinity
+ ? null
+ : this.#db.prepare(`
+ DELETE FROM cacheInterceptorV${VERSION}
+ WHERE id IN (
+ SELECT
+ id
+ FROM cacheInterceptorV${VERSION}
+ ORDER BY cachedAt DESC
+ LIMIT ?
+ )
+ `)
+ }
+
+ close () {
+ this.#db.close()
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
+ * @returns {(import('../../types/cache-interceptor.d.ts').default.GetResult & { body?: Buffer }) | undefined}
+ */
+ get (key) {
+ assertCacheKey(key)
+
+ const value = this.#findValue(key)
+ return value
+ ? {
+ body: value.body ? Buffer.from(value.body.buffer, value.body.byteOffset, value.body.byteLength) : undefined,
+ statusCode: value.statusCode,
+ statusMessage: value.statusMessage,
+ headers: value.headers ? JSON.parse(value.headers) : undefined,
+ etag: value.etag ? value.etag : undefined,
+ vary: value.vary ? JSON.parse(value.vary) : undefined,
+ cacheControlDirectives: value.cacheControlDirectives
+ ? JSON.parse(value.cacheControlDirectives)
+ : undefined,
+ cachedAt: value.cachedAt,
+ staleAt: value.staleAt,
+ deleteAt: value.deleteAt
+ }
+ : undefined
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue & { body: null | Buffer | Array<Buffer>}} value
+ */
+ set (key, value) {
+ assertCacheKey(key)
+
+ const url = this.#makeValueUrl(key)
+ const body = Array.isArray(value.body) ? Buffer.concat(value.body) : value.body
+ const size = body?.byteLength
+
+ if (size && size > this.#maxEntrySize) {
+ return
+ }
+
+ const existingValue = this.#findValue(key, true)
+ if (existingValue) {
+ // Updating an existing response, let's overwrite it
+ this.#updateValueQuery.run(
+ body,
+ value.deleteAt,
+ value.statusCode,
+ value.statusMessage,
+ value.headers ? JSON.stringify(value.headers) : null,
+ value.etag ? value.etag : null,
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
+ value.cachedAt,
+ value.staleAt,
+ existingValue.id
+ )
+ } else {
+ this.#prune()
+ // New response, let's insert it
+ this.#insertValueQuery.run(
+ url,
+ key.method,
+ body,
+ value.deleteAt,
+ value.statusCode,
+ value.statusMessage,
+ value.headers ? JSON.stringify(value.headers) : null,
+ value.etag ? value.etag : null,
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
+ value.vary ? JSON.stringify(value.vary) : null,
+ value.cachedAt,
+ value.staleAt
+ )
+ }
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value
+ * @returns {Writable | undefined}
+ */
+ createWriteStream (key, value) {
+ assertCacheKey(key)
+ assertCacheValue(value)
+
+ let size = 0
+ /**
+ * @type {Buffer[] | null}
+ */
+ const body = []
+ const store = this
+
+ return new Writable({
+ decodeStrings: true,
+ write (chunk, encoding, callback) {
+ size += chunk.byteLength
+
+ if (size < store.#maxEntrySize) {
+ body.push(chunk)
+ } else {
+ this.destroy()
+ }
+
+ callback()
+ },
+ final (callback) {
+ store.set(key, { ...value, body })
+ callback()
+ }
+ })
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
+ */
+ delete (key) {
+ if (typeof key !== 'object') {
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
+ }
+
+ this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
+ }
+
+ #prune () {
+ if (Number.isFinite(this.#maxCount) && this.size <= this.#maxCount) {
+ return 0
+ }
+
+ {
+ const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
+ if (removed) {
+ return removed
+ }
+ }
+
+ {
+ const removed = this.#deleteOldValuesQuery?.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
+ if (removed) {
+ return removed
+ }
+ }
+
+ return 0
+ }
+
+ /**
+ * Counts the number of rows in the cache
+ * @returns {Number}
+ */
+ get size () {
+ const { total } = this.#countEntriesQuery.get()
+ return total
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
+ * @returns {string}
+ */
+ #makeValueUrl (key) {
+ return `${key.origin}/${key.path}`
+ }
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
+ * @param {boolean} [canBeExpired=false]
+ * @returns {SqliteStoreValue | undefined}
+ */
+ #findValue (key, canBeExpired = false) {
+ const url = this.#makeValueUrl(key)
+ const { headers, method } = key
+
+ /**
+ * @type {SqliteStoreValue[]}
+ */
+ const values = this.#getValuesQuery.all(url, method)
+
+ if (values.length === 0) {
+ return undefined
+ }
+
+ const now = Date.now()
+ for (const value of values) {
+ if (now >= value.deleteAt && !canBeExpired) {
+ return undefined
+ }
+
+ let matches = true
+
+ if (value.vary) {
+ const vary = JSON.parse(value.vary)
+
+ for (const header in vary) {
+ if (!headerValueEquals(headers[header], vary[header])) {
+ matches = false
+ break
+ }
+ }
+ }
+
+ if (matches) {
+ return value
+ }
+ }
+
+ return undefined
+ }
+}
+
+/**
+ * @param {string|string[]|null|undefined} lhs
+ * @param {string|string[]|null|undefined} rhs
+ * @returns {boolean}
+ */
+function headerValueEquals (lhs, rhs) {
+ if (lhs == null && rhs == null) {
+ return true
+ }
+
+ if ((lhs == null && rhs != null) ||
+ (lhs != null && rhs == null)) {
+ return false
+ }
+
+ if (Array.isArray(lhs) && Array.isArray(rhs)) {
+ if (lhs.length !== rhs.length) {
+ return false
+ }
+
+ return lhs.every((x, i) => x === rhs[i])
+ }
+
+ return lhs === rhs
+}
diff --git a/vanilla/node_modules/undici/lib/core/connect.js b/vanilla/node_modules/undici/lib/core/connect.js
new file mode 100644
index 0000000..a49af91
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/connect.js
@@ -0,0 +1,137 @@
+'use strict'
+
+const net = require('node:net')
+const assert = require('node:assert')
+const util = require('./util')
+const { InvalidArgumentError } = require('./errors')
+
+let tls // include tls conditionally since it is not always available
+
+// TODO: session re-use does not wait for the first
+// connection to resolve the session and might therefore
+// resolve the same servername multiple times even when
+// re-use is enabled.
+
+const SessionCache = class WeakSessionCache {
+ constructor (maxCachedSessions) {
+ this._maxCachedSessions = maxCachedSessions
+ this._sessionCache = new Map()
+ this._sessionRegistry = new FinalizationRegistry((key) => {
+ if (this._sessionCache.size < this._maxCachedSessions) {
+ return
+ }
+
+ const ref = this._sessionCache.get(key)
+ if (ref !== undefined && ref.deref() === undefined) {
+ this._sessionCache.delete(key)
+ }
+ })
+ }
+
+ get (sessionKey) {
+ const ref = this._sessionCache.get(sessionKey)
+ return ref ? ref.deref() : null
+ }
+
+ set (sessionKey, session) {
+ if (this._maxCachedSessions === 0) {
+ return
+ }
+
+ this._sessionCache.set(sessionKey, new WeakRef(session))
+ this._sessionRegistry.register(session, sessionKey)
+ }
+}
+
+function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
+ if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
+ throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
+ }
+
+ const options = { path: socketPath, ...opts }
+ const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
+ timeout = timeout == null ? 10e3 : timeout
+ allowH2 = allowH2 != null ? allowH2 : false
+ return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
+ let socket
+ if (protocol === 'https:') {
+ if (!tls) {
+ tls = require('node:tls')
+ }
+ servername = servername || options.servername || util.getServerName(host) || null
+
+ const sessionKey = servername || hostname
+ assert(sessionKey)
+
+ const session = customSession || sessionCache.get(sessionKey) || null
+
+ port = port || 443
+
+ socket = tls.connect({
+ highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
+ ...options,
+ servername,
+ session,
+ localAddress,
+ ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
+ socket: httpSocket, // upgrade socket connection
+ port,
+ host: hostname
+ })
+
+ socket
+ .on('session', function (session) {
+ // TODO (fix): Can a session become invalid once established? Don't think so?
+ sessionCache.set(sessionKey, session)
+ })
+ } else {
+ assert(!httpSocket, 'httpSocket can only be sent on TLS update')
+
+ port = port || 80
+
+ socket = net.connect({
+ highWaterMark: 64 * 1024, // Same as nodejs fs streams.
+ ...options,
+ localAddress,
+ port,
+ host: hostname
+ })
+ if (useH2c === true) {
+ socket.alpnProtocol = 'h2'
+ }
+ }
+
+ // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
+ if (options.keepAlive == null || options.keepAlive) {
+ const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
+ socket.setKeepAlive(true, keepAliveInitialDelay)
+ }
+
+ const clearConnectTimeout = util.setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
+
+ socket
+ .setNoDelay(true)
+ .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
+ queueMicrotask(clearConnectTimeout)
+
+ if (callback) {
+ const cb = callback
+ callback = null
+ cb(null, this)
+ }
+ })
+ .on('error', function (err) {
+ queueMicrotask(clearConnectTimeout)
+
+ if (callback) {
+ const cb = callback
+ callback = null
+ cb(err)
+ }
+ })
+
+ return socket
+ }
+}
+
+module.exports = buildConnector
diff --git a/vanilla/node_modules/undici/lib/core/constants.js b/vanilla/node_modules/undici/lib/core/constants.js
new file mode 100644
index 0000000..088cf47
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/constants.js
@@ -0,0 +1,143 @@
+'use strict'
+
+/**
+ * @see https://developer.mozilla.org/docs/Web/HTTP/Headers
+ */
+const wellknownHeaderNames = /** @type {const} */ ([
+ 'Accept',
+ 'Accept-Encoding',
+ 'Accept-Language',
+ 'Accept-Ranges',
+ 'Access-Control-Allow-Credentials',
+ 'Access-Control-Allow-Headers',
+ 'Access-Control-Allow-Methods',
+ 'Access-Control-Allow-Origin',
+ 'Access-Control-Expose-Headers',
+ 'Access-Control-Max-Age',
+ 'Access-Control-Request-Headers',
+ 'Access-Control-Request-Method',
+ 'Age',
+ 'Allow',
+ 'Alt-Svc',
+ 'Alt-Used',
+ 'Authorization',
+ 'Cache-Control',
+ 'Clear-Site-Data',
+ 'Connection',
+ 'Content-Disposition',
+ 'Content-Encoding',
+ 'Content-Language',
+ 'Content-Length',
+ 'Content-Location',
+ 'Content-Range',
+ 'Content-Security-Policy',
+ 'Content-Security-Policy-Report-Only',
+ 'Content-Type',
+ 'Cookie',
+ 'Cross-Origin-Embedder-Policy',
+ 'Cross-Origin-Opener-Policy',
+ 'Cross-Origin-Resource-Policy',
+ 'Date',
+ 'Device-Memory',
+ 'Downlink',
+ 'ECT',
+ 'ETag',
+ 'Expect',
+ 'Expect-CT',
+ 'Expires',
+ 'Forwarded',
+ 'From',
+ 'Host',
+ 'If-Match',
+ 'If-Modified-Since',
+ 'If-None-Match',
+ 'If-Range',
+ 'If-Unmodified-Since',
+ 'Keep-Alive',
+ 'Last-Modified',
+ 'Link',
+ 'Location',
+ 'Max-Forwards',
+ 'Origin',
+ 'Permissions-Policy',
+ 'Pragma',
+ 'Proxy-Authenticate',
+ 'Proxy-Authorization',
+ 'RTT',
+ 'Range',
+ 'Referer',
+ 'Referrer-Policy',
+ 'Refresh',
+ 'Retry-After',
+ 'Sec-WebSocket-Accept',
+ 'Sec-WebSocket-Extensions',
+ 'Sec-WebSocket-Key',
+ 'Sec-WebSocket-Protocol',
+ 'Sec-WebSocket-Version',
+ 'Server',
+ 'Server-Timing',
+ 'Service-Worker-Allowed',
+ 'Service-Worker-Navigation-Preload',
+ 'Set-Cookie',
+ 'SourceMap',
+ 'Strict-Transport-Security',
+ 'Supports-Loading-Mode',
+ 'TE',
+ 'Timing-Allow-Origin',
+ 'Trailer',
+ 'Transfer-Encoding',
+ 'Upgrade',
+ 'Upgrade-Insecure-Requests',
+ 'User-Agent',
+ 'Vary',
+ 'Via',
+ 'WWW-Authenticate',
+ 'X-Content-Type-Options',
+ 'X-DNS-Prefetch-Control',
+ 'X-Frame-Options',
+ 'X-Permitted-Cross-Domain-Policies',
+ 'X-Powered-By',
+ 'X-Requested-With',
+ 'X-XSS-Protection'
+])
+
+/** @type {Record<typeof wellknownHeaderNames[number]|Lowercase<typeof wellknownHeaderNames[number]>, string>} */
+const headerNameLowerCasedRecord = {}
+
+// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
+Object.setPrototypeOf(headerNameLowerCasedRecord, null)
+
+/**
+ * @type {Record<Lowercase<typeof wellknownHeaderNames[number]>, Buffer>}
+ */
+const wellknownHeaderNameBuffers = {}
+
+// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
+Object.setPrototypeOf(wellknownHeaderNameBuffers, null)
+
+/**
+ * @param {string} header Lowercased header
+ * @returns {Buffer}
+ */
+function getHeaderNameAsBuffer (header) {
+ let buffer = wellknownHeaderNameBuffers[header]
+
+ if (buffer === undefined) {
+ buffer = Buffer.from(header)
+ }
+
+ return buffer
+}
+
+for (let i = 0; i < wellknownHeaderNames.length; ++i) {
+ const key = wellknownHeaderNames[i]
+ const lowerCasedKey = key.toLowerCase()
+ headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] =
+ lowerCasedKey
+}
+
+module.exports = {
+ wellknownHeaderNames,
+ headerNameLowerCasedRecord,
+ getHeaderNameAsBuffer
+}
diff --git a/vanilla/node_modules/undici/lib/core/diagnostics.js b/vanilla/node_modules/undici/lib/core/diagnostics.js
new file mode 100644
index 0000000..ccd6870
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/diagnostics.js
@@ -0,0 +1,225 @@
+'use strict'
+
+const diagnosticsChannel = require('node:diagnostics_channel')
+const util = require('node:util')
+
+const undiciDebugLog = util.debuglog('undici')
+const fetchDebuglog = util.debuglog('fetch')
+const websocketDebuglog = util.debuglog('websocket')
+
+const channels = {
+ // Client
+ beforeConnect: diagnosticsChannel.channel('undici:client:beforeConnect'),
+ connected: diagnosticsChannel.channel('undici:client:connected'),
+ connectError: diagnosticsChannel.channel('undici:client:connectError'),
+ sendHeaders: diagnosticsChannel.channel('undici:client:sendHeaders'),
+ // Request
+ create: diagnosticsChannel.channel('undici:request:create'),
+ bodySent: diagnosticsChannel.channel('undici:request:bodySent'),
+ bodyChunkSent: diagnosticsChannel.channel('undici:request:bodyChunkSent'),
+ bodyChunkReceived: diagnosticsChannel.channel('undici:request:bodyChunkReceived'),
+ headers: diagnosticsChannel.channel('undici:request:headers'),
+ trailers: diagnosticsChannel.channel('undici:request:trailers'),
+ error: diagnosticsChannel.channel('undici:request:error'),
+ // WebSocket
+ open: diagnosticsChannel.channel('undici:websocket:open'),
+ close: diagnosticsChannel.channel('undici:websocket:close'),
+ socketError: diagnosticsChannel.channel('undici:websocket:socket_error'),
+ ping: diagnosticsChannel.channel('undici:websocket:ping'),
+ pong: diagnosticsChannel.channel('undici:websocket:pong'),
+ // ProxyAgent
+ proxyConnected: diagnosticsChannel.channel('undici:proxy:connected')
+}
+
+let isTrackingClientEvents = false
+
+function trackClientEvents (debugLog = undiciDebugLog) {
+ if (isTrackingClientEvents) {
+ return
+ }
+
+ // Check if any of the channels already have subscribers to prevent duplicate subscriptions
+ // This can happen when both Node.js built-in undici and undici as a dependency are present
+ if (channels.beforeConnect.hasSubscribers || channels.connected.hasSubscribers ||
+ channels.connectError.hasSubscribers || channels.sendHeaders.hasSubscribers) {
+ isTrackingClientEvents = true
+ return
+ }
+
+ isTrackingClientEvents = true
+
+ diagnosticsChannel.subscribe('undici:client:beforeConnect',
+ evt => {
+ const {
+ connectParams: { version, protocol, port, host }
+ } = evt
+ debugLog(
+ 'connecting to %s%s using %s%s',
+ host,
+ port ? `:${port}` : '',
+ protocol,
+ version
+ )
+ })
+
+ diagnosticsChannel.subscribe('undici:client:connected',
+ evt => {
+ const {
+ connectParams: { version, protocol, port, host }
+ } = evt
+ debugLog(
+ 'connected to %s%s using %s%s',
+ host,
+ port ? `:${port}` : '',
+ protocol,
+ version
+ )
+ })
+
+ diagnosticsChannel.subscribe('undici:client:connectError',
+ evt => {
+ const {
+ connectParams: { version, protocol, port, host },
+ error
+ } = evt
+ debugLog(
+ 'connection to %s%s using %s%s errored - %s',
+ host,
+ port ? `:${port}` : '',
+ protocol,
+ version,
+ error.message
+ )
+ })
+
+ diagnosticsChannel.subscribe('undici:client:sendHeaders',
+ evt => {
+ const {
+ request: { method, path, origin }
+ } = evt
+ debugLog('sending request to %s %s%s', method, origin, path)
+ })
+}
+
+let isTrackingRequestEvents = false
+
+function trackRequestEvents (debugLog = undiciDebugLog) {
+ if (isTrackingRequestEvents) {
+ return
+ }
+
+ // Check if any of the channels already have subscribers to prevent duplicate subscriptions
+ // This can happen when both Node.js built-in undici and undici as a dependency are present
+ if (channels.headers.hasSubscribers || channels.trailers.hasSubscribers ||
+ channels.error.hasSubscribers) {
+ isTrackingRequestEvents = true
+ return
+ }
+
+ isTrackingRequestEvents = true
+
+ diagnosticsChannel.subscribe('undici:request:headers',
+ evt => {
+ const {
+ request: { method, path, origin },
+ response: { statusCode }
+ } = evt
+ debugLog(
+ 'received response to %s %s%s - HTTP %d',
+ method,
+ origin,
+ path,
+ statusCode
+ )
+ })
+
+ diagnosticsChannel.subscribe('undici:request:trailers',
+ evt => {
+ const {
+ request: { method, path, origin }
+ } = evt
+ debugLog('trailers received from %s %s%s', method, origin, path)
+ })
+
+ diagnosticsChannel.subscribe('undici:request:error',
+ evt => {
+ const {
+ request: { method, path, origin },
+ error
+ } = evt
+ debugLog(
+ 'request to %s %s%s errored - %s',
+ method,
+ origin,
+ path,
+ error.message
+ )
+ })
+}
+
+let isTrackingWebSocketEvents = false
+
+function trackWebSocketEvents (debugLog = websocketDebuglog) {
+ if (isTrackingWebSocketEvents) {
+ return
+ }
+
+ // Check if any of the channels already have subscribers to prevent duplicate subscriptions
+ // This can happen when both Node.js built-in undici and undici as a dependency are present
+ if (channels.open.hasSubscribers || channels.close.hasSubscribers ||
+ channels.socketError.hasSubscribers || channels.ping.hasSubscribers ||
+ channels.pong.hasSubscribers) {
+ isTrackingWebSocketEvents = true
+ return
+ }
+
+ isTrackingWebSocketEvents = true
+
+ diagnosticsChannel.subscribe('undici:websocket:open',
+ evt => {
+ const {
+ address: { address, port }
+ } = evt
+ debugLog('connection opened %s%s', address, port ? `:${port}` : '')
+ })
+
+ diagnosticsChannel.subscribe('undici:websocket:close',
+ evt => {
+ const { websocket, code, reason } = evt
+ debugLog(
+ 'closed connection to %s - %s %s',
+ websocket.url,
+ code,
+ reason
+ )
+ })
+
+ diagnosticsChannel.subscribe('undici:websocket:socket_error',
+ err => {
+ debugLog('connection errored - %s', err.message)
+ })
+
+ diagnosticsChannel.subscribe('undici:websocket:ping',
+ evt => {
+ debugLog('ping received')
+ })
+
+ diagnosticsChannel.subscribe('undici:websocket:pong',
+ evt => {
+ debugLog('pong received')
+ })
+}
+
+if (undiciDebugLog.enabled || fetchDebuglog.enabled) {
+ trackClientEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog)
+ trackRequestEvents(fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog)
+}
+
+if (websocketDebuglog.enabled) {
+ trackClientEvents(undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog)
+ trackWebSocketEvents(websocketDebuglog)
+}
+
+module.exports = {
+ channels
+}
diff --git a/vanilla/node_modules/undici/lib/core/errors.js b/vanilla/node_modules/undici/lib/core/errors.js
new file mode 100644
index 0000000..4b1a8a1
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/errors.js
@@ -0,0 +1,448 @@
+'use strict'
+
+const kUndiciError = Symbol.for('undici.error.UND_ERR')
+class UndiciError extends Error {
+ constructor (message, options) {
+ super(message, options)
+ this.name = 'UndiciError'
+ this.code = 'UND_ERR'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kUndiciError] === true
+ }
+
+ get [kUndiciError] () {
+ return true
+ }
+}
+
+const kConnectTimeoutError = Symbol.for('undici.error.UND_ERR_CONNECT_TIMEOUT')
+class ConnectTimeoutError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'ConnectTimeoutError'
+ this.message = message || 'Connect Timeout Error'
+ this.code = 'UND_ERR_CONNECT_TIMEOUT'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kConnectTimeoutError] === true
+ }
+
+ get [kConnectTimeoutError] () {
+ return true
+ }
+}
+
+const kHeadersTimeoutError = Symbol.for('undici.error.UND_ERR_HEADERS_TIMEOUT')
+class HeadersTimeoutError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'HeadersTimeoutError'
+ this.message = message || 'Headers Timeout Error'
+ this.code = 'UND_ERR_HEADERS_TIMEOUT'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kHeadersTimeoutError] === true
+ }
+
+ get [kHeadersTimeoutError] () {
+ return true
+ }
+}
+
+const kHeadersOverflowError = Symbol.for('undici.error.UND_ERR_HEADERS_OVERFLOW')
+class HeadersOverflowError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'HeadersOverflowError'
+ this.message = message || 'Headers Overflow Error'
+ this.code = 'UND_ERR_HEADERS_OVERFLOW'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kHeadersOverflowError] === true
+ }
+
+ get [kHeadersOverflowError] () {
+ return true
+ }
+}
+
+const kBodyTimeoutError = Symbol.for('undici.error.UND_ERR_BODY_TIMEOUT')
+class BodyTimeoutError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'BodyTimeoutError'
+ this.message = message || 'Body Timeout Error'
+ this.code = 'UND_ERR_BODY_TIMEOUT'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kBodyTimeoutError] === true
+ }
+
+ get [kBodyTimeoutError] () {
+ return true
+ }
+}
+
+const kInvalidArgumentError = Symbol.for('undici.error.UND_ERR_INVALID_ARG')
+class InvalidArgumentError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'InvalidArgumentError'
+ this.message = message || 'Invalid Argument Error'
+ this.code = 'UND_ERR_INVALID_ARG'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kInvalidArgumentError] === true
+ }
+
+ get [kInvalidArgumentError] () {
+ return true
+ }
+}
+
+const kInvalidReturnValueError = Symbol.for('undici.error.UND_ERR_INVALID_RETURN_VALUE')
+class InvalidReturnValueError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'InvalidReturnValueError'
+ this.message = message || 'Invalid Return Value Error'
+ this.code = 'UND_ERR_INVALID_RETURN_VALUE'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kInvalidReturnValueError] === true
+ }
+
+ get [kInvalidReturnValueError] () {
+ return true
+ }
+}
+
+const kAbortError = Symbol.for('undici.error.UND_ERR_ABORT')
+class AbortError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'AbortError'
+ this.message = message || 'The operation was aborted'
+ this.code = 'UND_ERR_ABORT'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kAbortError] === true
+ }
+
+ get [kAbortError] () {
+ return true
+ }
+}
+
+const kRequestAbortedError = Symbol.for('undici.error.UND_ERR_ABORTED')
+class RequestAbortedError extends AbortError {
+ constructor (message) {
+ super(message)
+ this.name = 'AbortError'
+ this.message = message || 'Request aborted'
+ this.code = 'UND_ERR_ABORTED'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kRequestAbortedError] === true
+ }
+
+ get [kRequestAbortedError] () {
+ return true
+ }
+}
+
+const kInformationalError = Symbol.for('undici.error.UND_ERR_INFO')
+class InformationalError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'InformationalError'
+ this.message = message || 'Request information'
+ this.code = 'UND_ERR_INFO'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kInformationalError] === true
+ }
+
+ get [kInformationalError] () {
+ return true
+ }
+}
+
+const kRequestContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_REQ_CONTENT_LENGTH_MISMATCH')
+class RequestContentLengthMismatchError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'RequestContentLengthMismatchError'
+ this.message = message || 'Request body length does not match content-length header'
+ this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kRequestContentLengthMismatchError] === true
+ }
+
+ get [kRequestContentLengthMismatchError] () {
+ return true
+ }
+}
+
+const kResponseContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
+class ResponseContentLengthMismatchError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'ResponseContentLengthMismatchError'
+ this.message = message || 'Response body length does not match content-length header'
+ this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kResponseContentLengthMismatchError] === true
+ }
+
+ get [kResponseContentLengthMismatchError] () {
+ return true
+ }
+}
+
+const kClientDestroyedError = Symbol.for('undici.error.UND_ERR_DESTROYED')
+class ClientDestroyedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'ClientDestroyedError'
+ this.message = message || 'The client is destroyed'
+ this.code = 'UND_ERR_DESTROYED'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kClientDestroyedError] === true
+ }
+
+ get [kClientDestroyedError] () {
+ return true
+ }
+}
+
+const kClientClosedError = Symbol.for('undici.error.UND_ERR_CLOSED')
+class ClientClosedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'ClientClosedError'
+ this.message = message || 'The client is closed'
+ this.code = 'UND_ERR_CLOSED'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kClientClosedError] === true
+ }
+
+ get [kClientClosedError] () {
+ return true
+ }
+}
+
+const kSocketError = Symbol.for('undici.error.UND_ERR_SOCKET')
+class SocketError extends UndiciError {
+ constructor (message, socket) {
+ super(message)
+ this.name = 'SocketError'
+ this.message = message || 'Socket error'
+ this.code = 'UND_ERR_SOCKET'
+ this.socket = socket
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kSocketError] === true
+ }
+
+ get [kSocketError] () {
+ return true
+ }
+}
+
+const kNotSupportedError = Symbol.for('undici.error.UND_ERR_NOT_SUPPORTED')
+class NotSupportedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'NotSupportedError'
+ this.message = message || 'Not supported error'
+ this.code = 'UND_ERR_NOT_SUPPORTED'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kNotSupportedError] === true
+ }
+
+ get [kNotSupportedError] () {
+ return true
+ }
+}
+
+const kBalancedPoolMissingUpstreamError = Symbol.for('undici.error.UND_ERR_BPL_MISSING_UPSTREAM')
+class BalancedPoolMissingUpstreamError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'MissingUpstreamError'
+ this.message = message || 'No upstream has been added to the BalancedPool'
+ this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kBalancedPoolMissingUpstreamError] === true
+ }
+
+ get [kBalancedPoolMissingUpstreamError] () {
+ return true
+ }
+}
+
+const kHTTPParserError = Symbol.for('undici.error.UND_ERR_HTTP_PARSER')
+class HTTPParserError extends Error {
+ constructor (message, code, data) {
+ super(message)
+ this.name = 'HTTPParserError'
+ this.code = code ? `HPE_${code}` : undefined
+ this.data = data ? data.toString() : undefined
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kHTTPParserError] === true
+ }
+
+ get [kHTTPParserError] () {
+ return true
+ }
+}
+
+const kResponseExceededMaxSizeError = Symbol.for('undici.error.UND_ERR_RES_EXCEEDED_MAX_SIZE')
+class ResponseExceededMaxSizeError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'ResponseExceededMaxSizeError'
+ this.message = message || 'Response content exceeded max size'
+ this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kResponseExceededMaxSizeError] === true
+ }
+
+ get [kResponseExceededMaxSizeError] () {
+ return true
+ }
+}
+
+const kRequestRetryError = Symbol.for('undici.error.UND_ERR_REQ_RETRY')
+class RequestRetryError extends UndiciError {
+ constructor (message, code, { headers, data }) {
+ super(message)
+ this.name = 'RequestRetryError'
+ this.message = message || 'Request retry error'
+ this.code = 'UND_ERR_REQ_RETRY'
+ this.statusCode = code
+ this.data = data
+ this.headers = headers
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kRequestRetryError] === true
+ }
+
+ get [kRequestRetryError] () {
+ return true
+ }
+}
+
+const kResponseError = Symbol.for('undici.error.UND_ERR_RESPONSE')
+class ResponseError extends UndiciError {
+ constructor (message, code, { headers, body }) {
+ super(message)
+ this.name = 'ResponseError'
+ this.message = message || 'Response error'
+ this.code = 'UND_ERR_RESPONSE'
+ this.statusCode = code
+ this.body = body
+ this.headers = headers
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kResponseError] === true
+ }
+
+ get [kResponseError] () {
+ return true
+ }
+}
+
+const kSecureProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_TLS')
+class SecureProxyConnectionError extends UndiciError {
+ constructor (cause, message, options = {}) {
+ super(message, { cause, ...options })
+ this.name = 'SecureProxyConnectionError'
+ this.message = message || 'Secure Proxy Connection failed'
+ this.code = 'UND_ERR_PRX_TLS'
+ this.cause = cause
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kSecureProxyConnectionError] === true
+ }
+
+ get [kSecureProxyConnectionError] () {
+ return true
+ }
+}
+
+const kMaxOriginsReachedError = Symbol.for('undici.error.UND_ERR_MAX_ORIGINS_REACHED')
+class MaxOriginsReachedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'MaxOriginsReachedError'
+ this.message = message || 'Maximum allowed origins reached'
+ this.code = 'UND_ERR_MAX_ORIGINS_REACHED'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kMaxOriginsReachedError] === true
+ }
+
+ get [kMaxOriginsReachedError] () {
+ return true
+ }
+}
+
+module.exports = {
+ AbortError,
+ HTTPParserError,
+ UndiciError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ BodyTimeoutError,
+ RequestContentLengthMismatchError,
+ ConnectTimeoutError,
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError,
+ ClientDestroyedError,
+ ClientClosedError,
+ InformationalError,
+ SocketError,
+ NotSupportedError,
+ ResponseContentLengthMismatchError,
+ BalancedPoolMissingUpstreamError,
+ ResponseExceededMaxSizeError,
+ RequestRetryError,
+ ResponseError,
+ SecureProxyConnectionError,
+ MaxOriginsReachedError
+}
diff --git a/vanilla/node_modules/undici/lib/core/request.js b/vanilla/node_modules/undici/lib/core/request.js
new file mode 100644
index 0000000..7dbf781
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/request.js
@@ -0,0 +1,412 @@
+'use strict'
+
+const {
+ InvalidArgumentError,
+ NotSupportedError
+} = require('./errors')
+const assert = require('node:assert')
+const {
+ isValidHTTPToken,
+ isValidHeaderValue,
+ isStream,
+ destroy,
+ isBuffer,
+ isFormDataLike,
+ isIterable,
+ isBlobLike,
+ serializePathWithQuery,
+ assertRequestHandler,
+ getServerName,
+ normalizedMethodRecords,
+ getProtocolFromUrlString
+} = require('./util')
+const { channels } = require('./diagnostics.js')
+const { headerNameLowerCasedRecord } = require('./constants')
+
+// Verifies that a given path is valid does not contain control chars \x00 to \x20
+const invalidPathRegex = /[^\u0021-\u00ff]/
+
+const kHandler = Symbol('handler')
+
+class Request {
+ constructor (origin, {
+ path,
+ method,
+ body,
+ headers,
+ query,
+ idempotent,
+ blocking,
+ upgrade,
+ headersTimeout,
+ bodyTimeout,
+ reset,
+ expectContinue,
+ servername,
+ throwOnError,
+ maxRedirections
+ }, handler) {
+ if (typeof path !== 'string') {
+ throw new InvalidArgumentError('path must be a string')
+ } else if (
+ path[0] !== '/' &&
+ !(path.startsWith('http://') || path.startsWith('https://')) &&
+ method !== 'CONNECT'
+ ) {
+ throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
+ } else if (invalidPathRegex.test(path)) {
+ throw new InvalidArgumentError('invalid request path')
+ }
+
+ if (typeof method !== 'string') {
+ throw new InvalidArgumentError('method must be a string')
+ } else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) {
+ throw new InvalidArgumentError('invalid request method')
+ }
+
+ if (upgrade && typeof upgrade !== 'string') {
+ throw new InvalidArgumentError('upgrade must be a string')
+ }
+
+ if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError('invalid headersTimeout')
+ }
+
+ if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError('invalid bodyTimeout')
+ }
+
+ if (reset != null && typeof reset !== 'boolean') {
+ throw new InvalidArgumentError('invalid reset')
+ }
+
+ if (expectContinue != null && typeof expectContinue !== 'boolean') {
+ throw new InvalidArgumentError('invalid expectContinue')
+ }
+
+ if (throwOnError != null) {
+ throw new InvalidArgumentError('invalid throwOnError')
+ }
+
+ if (maxRedirections != null && maxRedirections !== 0) {
+ throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor')
+ }
+
+ this.headersTimeout = headersTimeout
+
+ this.bodyTimeout = bodyTimeout
+
+ this.method = method
+
+ this.abort = null
+
+ if (body == null) {
+ this.body = null
+ } else if (isStream(body)) {
+ this.body = body
+
+ const rState = this.body._readableState
+ if (!rState || !rState.autoDestroy) {
+ this.endHandler = function autoDestroy () {
+ destroy(this)
+ }
+ this.body.on('end', this.endHandler)
+ }
+
+ this.errorHandler = err => {
+ if (this.abort) {
+ this.abort(err)
+ } else {
+ this.error = err
+ }
+ }
+ this.body.on('error', this.errorHandler)
+ } else if (isBuffer(body)) {
+ this.body = body.byteLength ? body : null
+ } else if (ArrayBuffer.isView(body)) {
+ this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
+ } else if (body instanceof ArrayBuffer) {
+ this.body = body.byteLength ? Buffer.from(body) : null
+ } else if (typeof body === 'string') {
+ this.body = body.length ? Buffer.from(body) : null
+ } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) {
+ this.body = body
+ } else {
+ throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
+ }
+
+ this.completed = false
+ this.aborted = false
+
+ this.upgrade = upgrade || null
+
+ this.path = query ? serializePathWithQuery(path, query) : path
+
+ // TODO: shall we maybe standardize it to an URL object?
+ this.origin = origin
+
+ this.protocol = getProtocolFromUrlString(origin)
+
+ this.idempotent = idempotent == null
+ ? method === 'HEAD' || method === 'GET'
+ : idempotent
+
+ this.blocking = blocking ?? this.method !== 'HEAD'
+
+ this.reset = reset == null ? null : reset
+
+ this.host = null
+
+ this.contentLength = null
+
+ this.contentType = null
+
+ this.headers = []
+
+ // Only for H2
+ this.expectContinue = expectContinue != null ? expectContinue : false
+
+ if (Array.isArray(headers)) {
+ if (headers.length % 2 !== 0) {
+ throw new InvalidArgumentError('headers array must be even')
+ }
+ for (let i = 0; i < headers.length; i += 2) {
+ processHeader(this, headers[i], headers[i + 1])
+ }
+ } else if (headers && typeof headers === 'object') {
+ if (headers[Symbol.iterator]) {
+ for (const header of headers) {
+ if (!Array.isArray(header) || header.length !== 2) {
+ throw new InvalidArgumentError('headers must be in key-value pair format')
+ }
+ processHeader(this, header[0], header[1])
+ }
+ } else {
+ const keys = Object.keys(headers)
+ for (let i = 0; i < keys.length; ++i) {
+ processHeader(this, keys[i], headers[keys[i]])
+ }
+ }
+ } else if (headers != null) {
+ throw new InvalidArgumentError('headers must be an object or an array')
+ }
+
+ assertRequestHandler(handler, method, upgrade)
+
+ this.servername = servername || getServerName(this.host) || null
+
+ this[kHandler] = handler
+
+ if (channels.create.hasSubscribers) {
+ channels.create.publish({ request: this })
+ }
+ }
+
+ onBodySent (chunk) {
+ if (channels.bodyChunkSent.hasSubscribers) {
+ channels.bodyChunkSent.publish({ request: this, chunk })
+ }
+ if (this[kHandler].onBodySent) {
+ try {
+ return this[kHandler].onBodySent(chunk)
+ } catch (err) {
+ this.abort(err)
+ }
+ }
+ }
+
+ onRequestSent () {
+ if (channels.bodySent.hasSubscribers) {
+ channels.bodySent.publish({ request: this })
+ }
+
+ if (this[kHandler].onRequestSent) {
+ try {
+ return this[kHandler].onRequestSent()
+ } catch (err) {
+ this.abort(err)
+ }
+ }
+ }
+
+ onConnect (abort) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ if (this.error) {
+ abort(this.error)
+ } else {
+ this.abort = abort
+ return this[kHandler].onConnect(abort)
+ }
+ }
+
+ onResponseStarted () {
+ return this[kHandler].onResponseStarted?.()
+ }
+
+ onHeaders (statusCode, headers, resume, statusText) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ if (channels.headers.hasSubscribers) {
+ channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
+ }
+
+ try {
+ return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
+ } catch (err) {
+ this.abort(err)
+ }
+ }
+
+ onData (chunk) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ if (channels.bodyChunkReceived.hasSubscribers) {
+ channels.bodyChunkReceived.publish({ request: this, chunk })
+ }
+ try {
+ return this[kHandler].onData(chunk)
+ } catch (err) {
+ this.abort(err)
+ return false
+ }
+ }
+
+ onUpgrade (statusCode, headers, socket) {
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ return this[kHandler].onUpgrade(statusCode, headers, socket)
+ }
+
+ onComplete (trailers) {
+ this.onFinally()
+
+ assert(!this.aborted)
+ assert(!this.completed)
+
+ this.completed = true
+ if (channels.trailers.hasSubscribers) {
+ channels.trailers.publish({ request: this, trailers })
+ }
+
+ try {
+ return this[kHandler].onComplete(trailers)
+ } catch (err) {
+ // TODO (fix): This might be a bad idea?
+ this.onError(err)
+ }
+ }
+
+ onError (error) {
+ this.onFinally()
+
+ if (channels.error.hasSubscribers) {
+ channels.error.publish({ request: this, error })
+ }
+
+ if (this.aborted) {
+ return
+ }
+ this.aborted = true
+
+ return this[kHandler].onError(error)
+ }
+
+ onFinally () {
+ if (this.errorHandler) {
+ this.body.off('error', this.errorHandler)
+ this.errorHandler = null
+ }
+
+ if (this.endHandler) {
+ this.body.off('end', this.endHandler)
+ this.endHandler = null
+ }
+ }
+
+ addHeader (key, value) {
+ processHeader(this, key, value)
+ return this
+ }
+}
+
+function processHeader (request, key, val) {
+ if (val && (typeof val === 'object' && !Array.isArray(val))) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ } else if (val === undefined) {
+ return
+ }
+
+ let headerName = headerNameLowerCasedRecord[key]
+
+ if (headerName === undefined) {
+ headerName = key.toLowerCase()
+ if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) {
+ throw new InvalidArgumentError('invalid header key')
+ }
+ }
+
+ if (Array.isArray(val)) {
+ const arr = []
+ for (let i = 0; i < val.length; i++) {
+ if (typeof val[i] === 'string') {
+ if (!isValidHeaderValue(val[i])) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ }
+ arr.push(val[i])
+ } else if (val[i] === null) {
+ arr.push('')
+ } else if (typeof val[i] === 'object') {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ } else {
+ arr.push(`${val[i]}`)
+ }
+ }
+ val = arr
+ } else if (typeof val === 'string') {
+ if (!isValidHeaderValue(val)) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ }
+ } else if (val === null) {
+ val = ''
+ } else {
+ val = `${val}`
+ }
+
+ if (request.host === null && headerName === 'host') {
+ if (typeof val !== 'string') {
+ throw new InvalidArgumentError('invalid host header')
+ }
+ // Consumed by Client
+ request.host = val
+ } else if (request.contentLength === null && headerName === 'content-length') {
+ request.contentLength = parseInt(val, 10)
+ if (!Number.isFinite(request.contentLength)) {
+ throw new InvalidArgumentError('invalid content-length header')
+ }
+ } else if (request.contentType === null && headerName === 'content-type') {
+ request.contentType = val
+ request.headers.push(key, val)
+ } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
+ throw new InvalidArgumentError(`invalid ${headerName} header`)
+ } else if (headerName === 'connection') {
+ const value = typeof val === 'string' ? val.toLowerCase() : null
+ if (value !== 'close' && value !== 'keep-alive') {
+ throw new InvalidArgumentError('invalid connection header')
+ }
+
+ if (value === 'close') {
+ request.reset = true
+ }
+ } else if (headerName === 'expect') {
+ throw new NotSupportedError('expect header not supported')
+ } else {
+ request.headers.push(key, val)
+ }
+}
+
+module.exports = Request
diff --git a/vanilla/node_modules/undici/lib/core/symbols.js b/vanilla/node_modules/undici/lib/core/symbols.js
new file mode 100644
index 0000000..fd7af0c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/symbols.js
@@ -0,0 +1,74 @@
+'use strict'
+
+module.exports = {
+ kClose: Symbol('close'),
+ kDestroy: Symbol('destroy'),
+ kDispatch: Symbol('dispatch'),
+ kUrl: Symbol('url'),
+ kWriting: Symbol('writing'),
+ kResuming: Symbol('resuming'),
+ kQueue: Symbol('queue'),
+ kConnect: Symbol('connect'),
+ kConnecting: Symbol('connecting'),
+ kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
+ kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
+ kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
+ kKeepAliveTimeoutValue: Symbol('keep alive timeout'),
+ kKeepAlive: Symbol('keep alive'),
+ kHeadersTimeout: Symbol('headers timeout'),
+ kBodyTimeout: Symbol('body timeout'),
+ kServerName: Symbol('server name'),
+ kLocalAddress: Symbol('local address'),
+ kHost: Symbol('host'),
+ kNoRef: Symbol('no ref'),
+ kBodyUsed: Symbol('used'),
+ kBody: Symbol('abstracted request body'),
+ kRunning: Symbol('running'),
+ kBlocking: Symbol('blocking'),
+ kPending: Symbol('pending'),
+ kSize: Symbol('size'),
+ kBusy: Symbol('busy'),
+ kQueued: Symbol('queued'),
+ kFree: Symbol('free'),
+ kConnected: Symbol('connected'),
+ kClosed: Symbol('closed'),
+ kNeedDrain: Symbol('need drain'),
+ kReset: Symbol('reset'),
+ kDestroyed: Symbol.for('nodejs.stream.destroyed'),
+ kResume: Symbol('resume'),
+ kOnError: Symbol('on error'),
+ kMaxHeadersSize: Symbol('max headers size'),
+ kRunningIdx: Symbol('running index'),
+ kPendingIdx: Symbol('pending index'),
+ kError: Symbol('error'),
+ kClients: Symbol('clients'),
+ kClient: Symbol('client'),
+ kParser: Symbol('parser'),
+ kOnDestroyed: Symbol('destroy callbacks'),
+ kPipelining: Symbol('pipelining'),
+ kSocket: Symbol('socket'),
+ kHostHeader: Symbol('host header'),
+ kConnector: Symbol('connector'),
+ kStrictContentLength: Symbol('strict content length'),
+ kMaxRedirections: Symbol('maxRedirections'),
+ kMaxRequests: Symbol('maxRequestsPerClient'),
+ kProxy: Symbol('proxy agent options'),
+ kCounter: Symbol('socket request counter'),
+ kMaxResponseSize: Symbol('max response size'),
+ kHTTP2Session: Symbol('http2Session'),
+ kHTTP2SessionState: Symbol('http2Session state'),
+ kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
+ kConstruct: Symbol('constructable'),
+ kListeners: Symbol('listeners'),
+ kHTTPContext: Symbol('http context'),
+ kMaxConcurrentStreams: Symbol('max concurrent streams'),
+ kHTTP2InitialWindowSize: Symbol('http2 initial window size'),
+ kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'),
+ kEnableConnectProtocol: Symbol('http2session connect protocol'),
+ kRemoteSettings: Symbol('http2session remote settings'),
+ kHTTP2Stream: Symbol('http2session client stream'),
+ kPingInterval: Symbol('ping interval'),
+ kNoProxyAgent: Symbol('no proxy agent'),
+ kHttpProxyAgent: Symbol('http proxy agent'),
+ kHttpsProxyAgent: Symbol('https proxy agent')
+}
diff --git a/vanilla/node_modules/undici/lib/core/tree.js b/vanilla/node_modules/undici/lib/core/tree.js
new file mode 100644
index 0000000..6eed58a
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/tree.js
@@ -0,0 +1,160 @@
+'use strict'
+
+const {
+ wellknownHeaderNames,
+ headerNameLowerCasedRecord
+} = require('./constants')
+
+class TstNode {
+ /** @type {any} */
+ value = null
+ /** @type {null | TstNode} */
+ left = null
+ /** @type {null | TstNode} */
+ middle = null
+ /** @type {null | TstNode} */
+ right = null
+ /** @type {number} */
+ code
+ /**
+ * @param {string} key
+ * @param {any} value
+ * @param {number} index
+ */
+ constructor (key, value, index) {
+ if (index === undefined || index >= key.length) {
+ throw new TypeError('Unreachable')
+ }
+ const code = this.code = key.charCodeAt(index)
+ // check code is ascii string
+ if (code > 0x7F) {
+ throw new TypeError('key must be ascii string')
+ }
+ if (key.length !== ++index) {
+ this.middle = new TstNode(key, value, index)
+ } else {
+ this.value = value
+ }
+ }
+
+ /**
+ * @param {string} key
+ * @param {any} value
+ * @returns {void}
+ */
+ add (key, value) {
+ const length = key.length
+ if (length === 0) {
+ throw new TypeError('Unreachable')
+ }
+ let index = 0
+ /**
+ * @type {TstNode}
+ */
+ let node = this
+ while (true) {
+ const code = key.charCodeAt(index)
+ // check code is ascii string
+ if (code > 0x7F) {
+ throw new TypeError('key must be ascii string')
+ }
+ if (node.code === code) {
+ if (length === ++index) {
+ node.value = value
+ break
+ } else if (node.middle !== null) {
+ node = node.middle
+ } else {
+ node.middle = new TstNode(key, value, index)
+ break
+ }
+ } else if (node.code < code) {
+ if (node.left !== null) {
+ node = node.left
+ } else {
+ node.left = new TstNode(key, value, index)
+ break
+ }
+ } else if (node.right !== null) {
+ node = node.right
+ } else {
+ node.right = new TstNode(key, value, index)
+ break
+ }
+ }
+ }
+
+ /**
+ * @param {Uint8Array} key
+ * @returns {TstNode | null}
+ */
+ search (key) {
+ const keylength = key.length
+ let index = 0
+ /**
+ * @type {TstNode|null}
+ */
+ let node = this
+ while (node !== null && index < keylength) {
+ let code = key[index]
+ // A-Z
+ // First check if it is bigger than 0x5a.
+ // Lowercase letters have higher char codes than uppercase ones.
+ // Also we assume that headers will mostly contain lowercase characters.
+ if (code <= 0x5a && code >= 0x41) {
+ // Lowercase for uppercase.
+ code |= 32
+ }
+ while (node !== null) {
+ if (code === node.code) {
+ if (keylength === ++index) {
+ // Returns Node since it is the last key.
+ return node
+ }
+ node = node.middle
+ break
+ }
+ node = node.code < code ? node.left : node.right
+ }
+ }
+ return null
+ }
+}
+
+class TernarySearchTree {
+ /** @type {TstNode | null} */
+ node = null
+
+ /**
+ * @param {string} key
+ * @param {any} value
+ * @returns {void}
+ * */
+ insert (key, value) {
+ if (this.node === null) {
+ this.node = new TstNode(key, value, 0)
+ } else {
+ this.node.add(key, value)
+ }
+ }
+
+ /**
+ * @param {Uint8Array} key
+ * @returns {any}
+ */
+ lookup (key) {
+ return this.node?.search(key)?.value ?? null
+ }
+}
+
+const tree = new TernarySearchTree()
+
+for (let i = 0; i < wellknownHeaderNames.length; ++i) {
+ const key = headerNameLowerCasedRecord[wellknownHeaderNames[i]]
+ tree.insert(key, key)
+}
+
+module.exports = {
+ TernarySearchTree,
+ tree
+}
diff --git a/vanilla/node_modules/undici/lib/core/util.js b/vanilla/node_modules/undici/lib/core/util.js
new file mode 100644
index 0000000..be2c1a7
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/core/util.js
@@ -0,0 +1,957 @@
+'use strict'
+
+const assert = require('node:assert')
+const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
+const { IncomingMessage } = require('node:http')
+const stream = require('node:stream')
+const net = require('node:net')
+const { stringify } = require('node:querystring')
+const { EventEmitter: EE } = require('node:events')
+const timers = require('../util/timers')
+const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
+const { headerNameLowerCasedRecord } = require('./constants')
+const { tree } = require('./tree')
+
+const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(v => Number(v))
+
+class BodyAsyncIterable {
+ constructor (body) {
+ this[kBody] = body
+ this[kBodyUsed] = false
+ }
+
+ async * [Symbol.asyncIterator] () {
+ assert(!this[kBodyUsed], 'disturbed')
+ this[kBodyUsed] = true
+ yield * this[kBody]
+ }
+}
+
+function noop () {}
+
+/**
+ * @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
+ // so that it can be dispatched again?
+ // TODO (fix): Do we need 100-expect support to provide a way to do this properly?
+ if (bodyLength(body) === 0) {
+ body
+ .on('data', function () {
+ assert(false)
+ })
+ }
+
+ if (typeof body.readableDidRead !== 'boolean') {
+ body[kBodyUsed] = false
+ EE.prototype.on.call(body, 'data', function () {
+ this[kBodyUsed] = true
+ })
+ }
+
+ return body
+ } else if (body && typeof body.pipeTo === 'function') {
+ // TODO (fix): We can't access ReadableStream internal state
+ // to determine whether or not it has been disturbed. This is just
+ // a workaround.
+ return new BodyAsyncIterable(body)
+ } else if (body && isFormDataLike(body)) {
+ return body
+ } else if (
+ body &&
+ typeof body !== 'string' &&
+ !ArrayBuffer.isView(body) &&
+ isIterable(body)
+ ) {
+ // TODO: Should we allow re-using iterable if !this.opts.idempotent
+ // or through some other flag?
+ return new BodyAsyncIterable(body)
+ } else {
+ return 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'
+}
+
+/**
+ * @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
+ } else if (object instanceof Blob) {
+ return true
+ } else if (typeof object !== 'object') {
+ return false
+ } else {
+ const sTag = object[Symbol.toStringTag]
+
+ return (sTag === 'Blob' || sTag === 'File') && (
+ ('stream' in object && typeof object.stream === 'function') ||
+ ('arrayBuffer' in object && typeof object.arrayBuffer === 'function')
+ )
+ }
+}
+
+/**
+ * @param {string} url The path to check for query strings or fragments.
+ * @returns {boolean} Returns true if the path contains a query string or fragment.
+ */
+function pathHasQueryOrFragment (url) {
+ return (
+ url.includes('?') ||
+ url.includes('#')
+ )
+}
+
+/**
+ * @param {string} url The URL to add the query params to
+ * @param {import('node:querystring').ParsedUrlQueryInput} queryParams The object to serialize into a URL query string
+ * @returns {string} The URL with the query params added
+ */
+function serializePathWithQuery (url, queryParams) {
+ if (pathHasQueryOrFragment(url)) {
+ throw new Error('Query params cannot be passed when url already contains "?" or "#".')
+ }
+
+ const stringified = stringify(queryParams)
+
+ if (stringified) {
+ url += '?' + stringified
+ }
+
+ return url
+}
+
+/**
+ * @param {number|string|undefined} port
+ * @returns {boolean}
+ */
+function isValidPort (port) {
+ const value = parseInt(port, 10)
+ return (
+ value === Number(port) &&
+ value >= 0 &&
+ value <= 65535
+ )
+}
+
+/**
+ * Check if the value is a valid http or https prefixed string.
+ *
+ * @param {string} value
+ * @returns {boolean}
+ */
+function isHttpOrHttpsPrefixed (value) {
+ return (
+ value != null &&
+ value[0] === 'h' &&
+ value[1] === 't' &&
+ value[2] === 't' &&
+ value[3] === 'p' &&
+ (
+ value[4] === ':' ||
+ (
+ value[4] === 's' &&
+ value[5] === ':'
+ )
+ )
+ )
+}
+
+/**
+ * @param {string|URL|Record<string,string>} url
+ * @returns {URL}
+ */
+function parseURL (url) {
+ if (typeof url === 'string') {
+ /**
+ * @type {URL}
+ */
+ url = new URL(url)
+
+ if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
+ return url
+ }
+
+ if (!url || typeof url !== 'object') {
+ throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
+ }
+
+ if (!(url instanceof URL)) {
+ if (url.port != null && url.port !== '' && isValidPort(url.port) === false) {
+ throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
+ }
+
+ if (url.path != null && typeof url.path !== 'string') {
+ throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
+ }
+
+ if (url.pathname != null && typeof url.pathname !== 'string') {
+ throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
+ }
+
+ if (url.hostname != null && typeof url.hostname !== 'string') {
+ throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
+ }
+
+ if (url.origin != null && typeof url.origin !== 'string') {
+ throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
+ }
+
+ if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
+ const port = url.port != null
+ ? url.port
+ : (url.protocol === 'https:' ? 443 : 80)
+ let origin = url.origin != null
+ ? url.origin
+ : `${url.protocol || ''}//${url.hostname || ''}:${port}`
+ let path = url.path != null
+ ? url.path
+ : `${url.pathname || ''}${url.search || ''}`
+
+ if (origin[origin.length - 1] === '/') {
+ origin = origin.slice(0, origin.length - 1)
+ }
+
+ if (path && path[0] !== '/') {
+ path = `/${path}`
+ }
+ // new URL(path, origin) is unsafe when `path` contains an absolute URL
+ // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
+ // If first parameter is a relative URL, second param is required, and will be used as the base URL.
+ // If first parameter is an absolute URL, a given second param will be ignored.
+ return new URL(`${origin}${path}`)
+ }
+
+ if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
+ return url
+}
+
+/**
+ * @param {string|URL|Record<string, string>} url
+ * @returns {URL}
+ */
+function parseOrigin (url) {
+ url = parseURL(url)
+
+ if (url.pathname !== '/' || url.search || url.hash) {
+ throw new InvalidArgumentError('invalid url')
+ }
+
+ return url
+}
+
+/**
+ * @param {string} host
+ * @returns {string}
+ */
+function getHostname (host) {
+ if (host[0] === '[') {
+ const idx = host.indexOf(']')
+
+ assert(idx !== -1)
+ return host.substring(1, idx)
+ }
+
+ const idx = host.indexOf(':')
+ if (idx === -1) return host
+
+ return host.substring(0, idx)
+}
+
+/**
+ * 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
+ }
+
+ assert(typeof host === 'string')
+
+ const servername = getHostname(host)
+ if (net.isIP(servername)) {
+ return ''
+ }
+
+ return servername
+}
+
+/**
+ * @function
+ * @template T
+ * @param {T} obj
+ * @returns {T}
+ */
+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
+ } else if (isStream(body)) {
+ const state = body._readableState
+ return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
+ ? state.length
+ : null
+ } else if (isBlobLike(body)) {
+ return body.size != null ? body.size : null
+ } else if (isBuffer(body)) {
+ return body.byteLength
+ }
+
+ 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 {void}
+ */
+function destroy (stream, err) {
+ if (stream == null || !isStream(stream) || isDestroyed(stream)) {
+ return
+ }
+
+ if (typeof stream.destroy === 'function') {
+ if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
+ // See: https://github.com/nodejs/node/pull/38505/files
+ stream.socket = null
+ }
+
+ stream.destroy(err)
+ } else if (err) {
+ queueMicrotask(() => {
+ stream.emit('error', err)
+ })
+ }
+
+ if (stream.destroyed !== true) {
+ stream[kDestroyed] = true
+ }
+}
+
+const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
+/**
+ * @param {string} val
+ * @returns {number | null}
+ */
+function parseKeepAliveTimeout (val) {
+ const m = val.match(KEEPALIVE_TIMEOUT_EXPR)
+ return m ? parseInt(m[1], 10) * 1000 : null
+}
+
+/**
+ * Retrieves a header name and returns its lowercase value.
+ * @param {string | Buffer} value Header name
+ * @returns {string}
+ */
+function headerNameToString (value) {
+ return typeof value === 'string'
+ ? headerNameLowerCasedRecord[value] ?? value.toLowerCase()
+ : tree.lookup(value) ?? value.toString('latin1').toLowerCase()
+}
+
+/**
+ * Receive the buffer as a string and return its lowercase value.
+ * @param {Buffer} value Header name
+ * @returns {string}
+ */
+function bufferToLowerCasedHeaderName (value) {
+ return tree.lookup(value) ?? value.toString('latin1').toLowerCase()
+}
+
+/**
+ * @param {(Buffer | string)[]} headers
+ * @param {Record<string, string | string[]>} [obj]
+ * @returns {Record<string, string | string[]>}
+ */
+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]
+
+ if (val) {
+ if (typeof val === 'string') {
+ val = [val]
+ obj[key] = val
+ }
+ val.push(headers[i + 1].toString('latin1'))
+ } else {
+ const headersValue = headers[i + 1]
+ if (typeof headersValue === 'string') {
+ obj[key] = headersValue
+ } else {
+ obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('latin1')) : headersValue.toString('latin1')
+ }
+ }
+ }
+
+ return obj
+}
+
+/**
+ * @param {Buffer[]} headers
+ * @returns {string[]}
+ */
+function parseRawHeaders (headers) {
+ const headersLength = headers.length
+ /**
+ * @type {string[]}
+ */
+ const ret = new Array(headersLength)
+
+ let key
+ let val
+
+ for (let n = 0; n < headersLength; n += 2) {
+ key = headers[n]
+ val = headers[n + 1]
+
+ typeof key !== 'string' && (key = key.toString())
+ typeof val !== 'string' && (val = val.toString('latin1'))
+
+ ret[n] = key
+ ret[n + 1] = val
+ }
+
+ return ret
+}
+
+/**
+ * @param {string[]} headers
+ * @param {Buffer[]} headers
+ */
+function encodeRawHeaders (headers) {
+ if (!Array.isArray(headers)) {
+ throw new TypeError('expected headers to be an array')
+ }
+ return headers.map(x => Buffer.from(x))
+}
+
+/**
+ * @param {*} buffer
+ * @returns {buffer is Buffer}
+ */
+function isBuffer (buffer) {
+ // See, https://github.com/mcollina/undici/pull/319
+ return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
+}
+
+/**
+ * Asserts that the handler object is a request handler.
+ *
+ * @param {object} handler
+ * @param {string} method
+ * @param {string} [upgrade]
+ * @returns {asserts handler is import('../api/api-request').RequestHandler}
+ */
+function assertRequestHandler (handler, method, upgrade) {
+ if (!handler || typeof handler !== 'object') {
+ throw new InvalidArgumentError('handler must be an object')
+ }
+
+ if (typeof handler.onRequestStart === 'function') {
+ // TODO (fix): More checks...
+ return
+ }
+
+ if (typeof handler.onConnect !== 'function') {
+ throw new InvalidArgumentError('invalid onConnect method')
+ }
+
+ if (typeof handler.onError !== 'function') {
+ throw new InvalidArgumentError('invalid onError method')
+ }
+
+ if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
+ throw new InvalidArgumentError('invalid onBodySent method')
+ }
+
+ if (upgrade || method === 'CONNECT') {
+ if (typeof handler.onUpgrade !== 'function') {
+ throw new InvalidArgumentError('invalid onUpgrade method')
+ }
+ } else {
+ if (typeof handler.onHeaders !== 'function') {
+ throw new InvalidArgumentError('invalid onHeaders method')
+ }
+
+ if (typeof handler.onData !== 'function') {
+ throw new InvalidArgumentError('invalid onData method')
+ }
+
+ if (typeof handler.onComplete !== 'function') {
+ throw new InvalidArgumentError('invalid onComplete method')
+ }
+ }
+}
+
+/**
+ * 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,
+ localPort: socket.localPort,
+ remoteAddress: socket.remoteAddress,
+ remotePort: socket.remotePort,
+ remoteFamily: socket.remoteFamily,
+ timeout: socket.timeout,
+ bytesWritten: socket.bytesWritten,
+ bytesRead: socket.bytesRead
+ }
+}
+
+/**
+ * @param {Iterable} iterable
+ * @returns {ReadableStream}
+ */
+function ReadableStreamFrom (iterable) {
+ // We cannot use ReadableStream.from here because it does not return a byte stream.
+
+ let iterator
+ return new ReadableStream(
+ {
+ start () {
+ iterator = iterable[Symbol.asyncIterator]()
+ },
+ pull (controller) {
+ return iterator.next().then(({ done, value }) => {
+ if (done) {
+ return queueMicrotask(() => {
+ controller.close()
+ controller.byobRequest?.respond(0)
+ })
+ } else {
+ const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
+ if (buf.byteLength) {
+ return controller.enqueue(new Uint8Array(buf))
+ } else {
+ return this.pull(controller)
+ }
+ }
+ })
+ },
+ cancel () {
+ return iterator.return()
+ },
+ type: 'bytes'
+ }
+ )
+}
+
+/**
+ * The object should be a FormData instance and contains all the required
+ * methods.
+ * @param {*} object
+ * @returns {object is FormData}
+ */
+function isFormDataLike (object) {
+ return (
+ object &&
+ typeof object === 'object' &&
+ typeof object.append === 'function' &&
+ typeof object.delete === 'function' &&
+ typeof object.get === 'function' &&
+ typeof object.getAll === 'function' &&
+ typeof object.has === 'function' &&
+ typeof object.set === 'function' &&
+ object[Symbol.toStringTag] === 'FormData'
+ )
+}
+
+function addAbortListener (signal, listener) {
+ if ('addEventListener' in signal) {
+ signal.addEventListener('abort', listener, { once: true })
+ return () => signal.removeEventListener('abort', listener)
+ }
+ signal.once('abort', listener)
+ return () => signal.removeListener('abort', listener)
+}
+
+const validTokenChars = new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
+ 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32-47 (!"#$%&'()*+,-./)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48-63 (0-9:;<=>?)
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64-79 (@A-O)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80-95 (P-Z[\]^_)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96-111 (`a-o)
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112-127 (p-z{|}~)
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128-143
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144-159
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160-175
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176-191
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192-207
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208-223
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224-239
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240-255
+])
+
+/**
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
+ * @param {number} c
+ * @returns {boolean}
+ */
+function isTokenCharCode (c) {
+ return (validTokenChars[c] === 1)
+}
+
+const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
+
+/**
+ * @param {string} characters
+ * @returns {boolean}
+ */
+function isValidHTTPToken (characters) {
+ if (characters.length >= 12) return tokenRegExp.test(characters)
+ if (characters.length === 0) return false
+
+ for (let i = 0; i < characters.length; i++) {
+ if (validTokenChars[characters.charCodeAt(i)] !== 1) {
+ return false
+ }
+ }
+ return true
+}
+
+// headerCharRegex have been lifted from
+// https://github.com/nodejs/node/blob/main/lib/_http_common.js
+
+/**
+ * Matches if val contains an invalid field-vchar
+ * field-value = *( field-content / obs-fold )
+ * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
+ * field-vchar = VCHAR / obs-text
+ */
+const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
+
+/**
+ * @param {string} characters
+ * @returns {boolean}
+ */
+function isValidHeaderValue (characters) {
+ return !headerCharRegex.test(characters)
+}
+
+const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/
+
+/**
+ * @typedef {object} RangeHeader
+ * @property {number} start
+ * @property {number | null} end
+ * @property {number | null} size
+ */
+
+/**
+ * Parse accordingly to RFC 9110
+ * @see https://www.rfc-editor.org/rfc/rfc9110#field.content-range
+ * @param {string} [range]
+ * @returns {RangeHeader|null}
+ */
+function parseRangeHeader (range) {
+ if (range == null || range === '') return { start: 0, end: null, size: null }
+
+ const m = range ? range.match(rangeHeaderRegex) : null
+ return m
+ ? {
+ start: parseInt(m[1]),
+ end: m[2] ? parseInt(m[2]) : null,
+ size: m[3] ? parseInt(m[3]) : null
+ }
+ : null
+}
+
+/**
+ * @template {import("events").EventEmitter} T
+ * @param {T} obj
+ * @param {string} name
+ * @param {(...args: any[]) => void} listener
+ * @returns {T}
+ */
+function addListener (obj, name, listener) {
+ const listeners = (obj[kListeners] ??= [])
+ listeners.push([name, listener])
+ obj.on(name, listener)
+ return obj
+}
+
+/**
+ * @template {import("events").EventEmitter} T
+ * @param {T} obj
+ * @returns {T}
+ */
+function removeAllListeners (obj) {
+ if (obj[kListeners] != null) {
+ for (const [name, listener] of obj[kListeners]) {
+ obj.removeListener(name, listener)
+ }
+ obj[kListeners] = null
+ }
+ return obj
+}
+
+/**
+ * @param {import ('../dispatcher/client')} client
+ * @param {import ('../core/request')} request
+ * @param {Error} err
+ */
+function errorRequest (client, request, err) {
+ try {
+ request.onError(err)
+ assert(request.aborted)
+ } catch (err) {
+ client.emit('error', err)
+ }
+}
+
+/**
+ * @param {WeakRef<net.Socket>} socketWeakRef
+ * @param {object} opts
+ * @param {number} opts.timeout
+ * @param {string} opts.hostname
+ * @param {number} opts.port
+ * @returns {() => void}
+ */
+const setupConnectTimeout = process.platform === 'win32'
+ ? (socketWeakRef, opts) => {
+ if (!opts.timeout) {
+ return noop
+ }
+
+ let s1 = null
+ let s2 = null
+ const fastTimer = timers.setFastTimeout(() => {
+ // setImmediate is added to make sure that we prioritize socket error events over timeouts
+ s1 = setImmediate(() => {
+ // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
+ s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
+ })
+ }, opts.timeout)
+ return () => {
+ timers.clearFastTimeout(fastTimer)
+ clearImmediate(s1)
+ clearImmediate(s2)
+ }
+ }
+ : (socketWeakRef, opts) => {
+ if (!opts.timeout) {
+ return noop
+ }
+
+ let s1 = null
+ const fastTimer = timers.setFastTimeout(() => {
+ // setImmediate is added to make sure that we prioritize socket error events over timeouts
+ s1 = setImmediate(() => {
+ onConnectTimeout(socketWeakRef.deref(), opts)
+ })
+ }, opts.timeout)
+ return () => {
+ timers.clearFastTimeout(fastTimer)
+ clearImmediate(s1)
+ }
+ }
+
+/**
+ * @param {net.Socket} socket
+ * @param {object} opts
+ * @param {number} opts.timeout
+ * @param {string} opts.hostname
+ * @param {number} opts.port
+ */
+function onConnectTimeout (socket, opts) {
+ // The socket could be already garbage collected
+ if (socket == null) {
+ return
+ }
+
+ let message = 'Connect Timeout Error'
+ if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
+ message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
+ } else {
+ message += ` (attempted address: ${opts.hostname}:${opts.port},`
+ }
+
+ message += ` timeout: ${opts.timeout}ms)`
+
+ destroy(socket, new ConnectTimeoutError(message))
+}
+
+/**
+ * @param {string} urlString
+ * @returns {string}
+ */
+function getProtocolFromUrlString (urlString) {
+ if (
+ urlString[0] === 'h' &&
+ urlString[1] === 't' &&
+ urlString[2] === 't' &&
+ urlString[3] === 'p'
+ ) {
+ switch (urlString[4]) {
+ case ':':
+ return 'http:'
+ case 's':
+ if (urlString[5] === ':') {
+ return 'https:'
+ }
+ }
+ }
+ // fallback if none of the usual suspects
+ return urlString.slice(0, urlString.indexOf(':') + 1)
+}
+
+const kEnumerableProperty = Object.create(null)
+kEnumerableProperty.enumerable = true
+
+const normalizedMethodRecordsBase = {
+ delete: 'DELETE',
+ DELETE: 'DELETE',
+ get: 'GET',
+ GET: 'GET',
+ head: 'HEAD',
+ HEAD: 'HEAD',
+ options: 'OPTIONS',
+ OPTIONS: 'OPTIONS',
+ post: 'POST',
+ POST: 'POST',
+ put: 'PUT',
+ PUT: 'PUT'
+}
+
+const normalizedMethodRecords = {
+ ...normalizedMethodRecordsBase,
+ patch: 'patch',
+ PATCH: 'PATCH'
+}
+
+// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
+Object.setPrototypeOf(normalizedMethodRecordsBase, null)
+Object.setPrototypeOf(normalizedMethodRecords, null)
+
+module.exports = {
+ kEnumerableProperty,
+ isDisturbed,
+ isBlobLike,
+ parseOrigin,
+ parseURL,
+ getServerName,
+ isStream,
+ isIterable,
+ isAsyncIterable,
+ isDestroyed,
+ headerNameToString,
+ bufferToLowerCasedHeaderName,
+ addListener,
+ removeAllListeners,
+ errorRequest,
+ parseRawHeaders,
+ encodeRawHeaders,
+ parseHeaders,
+ parseKeepAliveTimeout,
+ destroy,
+ bodyLength,
+ deepClone,
+ ReadableStreamFrom,
+ isBuffer,
+ assertRequestHandler,
+ getSocketInfo,
+ isFormDataLike,
+ pathHasQueryOrFragment,
+ serializePathWithQuery,
+ addAbortListener,
+ isValidHTTPToken,
+ isValidHeaderValue,
+ isTokenCharCode,
+ parseRangeHeader,
+ normalizedMethodRecordsBase,
+ normalizedMethodRecords,
+ isValidPort,
+ isHttpOrHttpsPrefixed,
+ nodeMajor,
+ nodeMinor,
+ safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']),
+ wrapRequestBody,
+ setupConnectTimeout,
+ getProtocolFromUrlString
+}
diff --git a/vanilla/node_modules/undici/lib/dispatcher/agent.js b/vanilla/node_modules/undici/lib/dispatcher/agent.js
new file mode 100644
index 0000000..939b0ad
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/agent.js
@@ -0,0 +1,158 @@
+'use strict'
+
+const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
+const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
+const DispatcherBase = require('./dispatcher-base')
+const Pool = require('./pool')
+const Client = require('./client')
+const util = require('../core/util')
+
+const kOnConnect = Symbol('onConnect')
+const kOnDisconnect = Symbol('onDisconnect')
+const kOnConnectionError = Symbol('onConnectionError')
+const kOnDrain = Symbol('onDrain')
+const kFactory = Symbol('factory')
+const kOptions = Symbol('options')
+const kOrigins = Symbol('origins')
+
+function defaultFactory (origin, opts) {
+ return opts && opts.connections === 1
+ ? new Client(origin, opts)
+ : new Pool(origin, opts)
+}
+
+class Agent extends DispatcherBase {
+ constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('factory must be a function.')
+ }
+
+ if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
+ throw new InvalidArgumentError('connect must be a function or an object')
+ }
+
+ if (typeof maxOrigins !== 'number' || Number.isNaN(maxOrigins) || maxOrigins <= 0) {
+ throw new InvalidArgumentError('maxOrigins must be a number greater than 0')
+ }
+
+ super()
+
+ if (connect && typeof connect !== 'function') {
+ connect = { ...connect }
+ }
+
+ this[kOptions] = { ...util.deepClone(options), maxOrigins, connect }
+ this[kFactory] = factory
+ this[kClients] = new Map()
+ this[kOrigins] = new Set()
+
+ this[kOnDrain] = (origin, targets) => {
+ this.emit('drain', origin, [this, ...targets])
+ }
+
+ this[kOnConnect] = (origin, targets) => {
+ this.emit('connect', origin, [this, ...targets])
+ }
+
+ this[kOnDisconnect] = (origin, targets, err) => {
+ this.emit('disconnect', origin, [this, ...targets], err)
+ }
+
+ this[kOnConnectionError] = (origin, targets, err) => {
+ this.emit('connectionError', origin, [this, ...targets], err)
+ }
+ }
+
+ get [kRunning] () {
+ let ret = 0
+ for (const { dispatcher } of this[kClients].values()) {
+ ret += dispatcher[kRunning]
+ }
+ return ret
+ }
+
+ [kDispatch] (opts, handler) {
+ let key
+ if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) {
+ key = String(opts.origin)
+ } else {
+ throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
+ }
+
+ if (this[kOrigins].size >= this[kOptions].maxOrigins && !this[kOrigins].has(key)) {
+ throw new MaxOriginsReachedError()
+ }
+
+ const result = this[kClients].get(key)
+ let dispatcher = result && result.dispatcher
+ if (!dispatcher) {
+ const closeClientIfUnused = (connected) => {
+ const result = this[kClients].get(key)
+ if (result) {
+ if (connected) result.count -= 1
+ if (result.count <= 0) {
+ this[kClients].delete(key)
+ if (!result.dispatcher.destroyed) {
+ result.dispatcher.close()
+ }
+ }
+ this[kOrigins].delete(key)
+ }
+ }
+ dispatcher = this[kFactory](opts.origin, this[kOptions])
+ .on('drain', this[kOnDrain])
+ .on('connect', (origin, targets) => {
+ const result = this[kClients].get(key)
+ if (result) {
+ result.count += 1
+ }
+ this[kOnConnect](origin, targets)
+ })
+ .on('disconnect', (origin, targets, err) => {
+ closeClientIfUnused(true)
+ this[kOnDisconnect](origin, targets, err)
+ })
+ .on('connectionError', (origin, targets, err) => {
+ closeClientIfUnused(false)
+ this[kOnConnectionError](origin, targets, err)
+ })
+
+ this[kClients].set(key, { count: 0, dispatcher })
+ this[kOrigins].add(key)
+ }
+
+ return dispatcher.dispatch(opts, handler)
+ }
+
+ [kClose] () {
+ const closePromises = []
+ for (const { dispatcher } of this[kClients].values()) {
+ closePromises.push(dispatcher.close())
+ }
+ this[kClients].clear()
+
+ return Promise.all(closePromises)
+ }
+
+ [kDestroy] (err) {
+ const destroyPromises = []
+ for (const { dispatcher } of this[kClients].values()) {
+ destroyPromises.push(dispatcher.destroy(err))
+ }
+ this[kClients].clear()
+
+ return Promise.all(destroyPromises)
+ }
+
+ get stats () {
+ const allClientStats = {}
+ for (const { dispatcher } of this[kClients].values()) {
+ if (dispatcher.stats) {
+ allClientStats[dispatcher[kUrl].origin] = dispatcher.stats
+ }
+ }
+ return allClientStats
+ }
+}
+
+module.exports = Agent
diff --git a/vanilla/node_modules/undici/lib/dispatcher/balanced-pool.js b/vanilla/node_modules/undici/lib/dispatcher/balanced-pool.js
new file mode 100644
index 0000000..d53a269
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/balanced-pool.js
@@ -0,0 +1,216 @@
+'use strict'
+
+const {
+ BalancedPoolMissingUpstreamError,
+ InvalidArgumentError
+} = require('../core/errors')
+const {
+ PoolBase,
+ kClients,
+ kNeedDrain,
+ kAddClient,
+ kRemoveClient,
+ kGetDispatcher
+} = require('./pool-base')
+const Pool = require('./pool')
+const { kUrl } = require('../core/symbols')
+const { parseOrigin } = require('../core/util')
+const kFactory = Symbol('factory')
+
+const kOptions = Symbol('options')
+const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor')
+const kCurrentWeight = Symbol('kCurrentWeight')
+const kIndex = Symbol('kIndex')
+const kWeight = Symbol('kWeight')
+const kMaxWeightPerServer = Symbol('kMaxWeightPerServer')
+const kErrorPenalty = Symbol('kErrorPenalty')
+
+/**
+ * Calculate the greatest common divisor of two numbers by
+ * using the Euclidean algorithm.
+ *
+ * @param {number} a
+ * @param {number} b
+ * @returns {number}
+ */
+function getGreatestCommonDivisor (a, b) {
+ if (a === 0) return b
+
+ while (b !== 0) {
+ const t = b
+ b = a % b
+ a = t
+ }
+ return a
+}
+
+function defaultFactory (origin, opts) {
+ return new Pool(origin, opts)
+}
+
+class BalancedPool extends PoolBase {
+ constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('factory must be a function.')
+ }
+
+ super()
+
+ this[kOptions] = opts
+ this[kIndex] = -1
+ this[kCurrentWeight] = 0
+
+ this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100
+ this[kErrorPenalty] = this[kOptions].errorPenalty || 15
+
+ if (!Array.isArray(upstreams)) {
+ upstreams = [upstreams]
+ }
+
+ this[kFactory] = factory
+
+ for (const upstream of upstreams) {
+ this.addUpstream(upstream)
+ }
+ this._updateBalancedPoolStats()
+ }
+
+ addUpstream (upstream) {
+ const upstreamOrigin = parseOrigin(upstream).origin
+
+ if (this[kClients].find((pool) => (
+ pool[kUrl].origin === upstreamOrigin &&
+ pool.closed !== true &&
+ pool.destroyed !== true
+ ))) {
+ return this
+ }
+ const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))
+
+ this[kAddClient](pool)
+ pool.on('connect', () => {
+ pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty])
+ })
+
+ pool.on('connectionError', () => {
+ pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
+ this._updateBalancedPoolStats()
+ })
+
+ pool.on('disconnect', (...args) => {
+ const err = args[2]
+ if (err && err.code === 'UND_ERR_SOCKET') {
+ // decrease the weight of the pool.
+ pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
+ this._updateBalancedPoolStats()
+ }
+ })
+
+ for (const client of this[kClients]) {
+ client[kWeight] = this[kMaxWeightPerServer]
+ }
+
+ this._updateBalancedPoolStats()
+
+ return this
+ }
+
+ _updateBalancedPoolStats () {
+ let result = 0
+ for (let i = 0; i < this[kClients].length; i++) {
+ result = getGreatestCommonDivisor(this[kClients][i][kWeight], result)
+ }
+
+ this[kGreatestCommonDivisor] = result
+ }
+
+ removeUpstream (upstream) {
+ const upstreamOrigin = parseOrigin(upstream).origin
+
+ const pool = this[kClients].find((pool) => (
+ pool[kUrl].origin === upstreamOrigin &&
+ pool.closed !== true &&
+ pool.destroyed !== true
+ ))
+
+ if (pool) {
+ this[kRemoveClient](pool)
+ }
+
+ return this
+ }
+
+ getUpstream (upstream) {
+ const upstreamOrigin = parseOrigin(upstream).origin
+
+ return this[kClients].find((pool) => (
+ pool[kUrl].origin === upstreamOrigin &&
+ pool.closed !== true &&
+ pool.destroyed !== true
+ ))
+ }
+
+ get upstreams () {
+ return this[kClients]
+ .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true)
+ .map((p) => p[kUrl].origin)
+ }
+
+ [kGetDispatcher] () {
+ // We validate that pools is greater than 0,
+ // otherwise we would have to wait until an upstream
+ // is added, which might never happen.
+ if (this[kClients].length === 0) {
+ throw new BalancedPoolMissingUpstreamError()
+ }
+
+ const dispatcher = this[kClients].find(dispatcher => (
+ !dispatcher[kNeedDrain] &&
+ dispatcher.closed !== true &&
+ dispatcher.destroyed !== true
+ ))
+
+ if (!dispatcher) {
+ return
+ }
+
+ const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true)
+
+ if (allClientsBusy) {
+ return
+ }
+
+ let counter = 0
+
+ let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain])
+
+ while (counter++ < this[kClients].length) {
+ this[kIndex] = (this[kIndex] + 1) % this[kClients].length
+ const pool = this[kClients][this[kIndex]]
+
+ // find pool index with the largest weight
+ if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) {
+ maxWeightIndex = this[kIndex]
+ }
+
+ // decrease the current weight every `this[kClients].length`.
+ if (this[kIndex] === 0) {
+ // Set the current weight to the next lower weight.
+ this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor]
+
+ if (this[kCurrentWeight] <= 0) {
+ this[kCurrentWeight] = this[kMaxWeightPerServer]
+ }
+ }
+ if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) {
+ return pool
+ }
+ }
+
+ this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]
+ this[kIndex] = maxWeightIndex
+ return this[kClients][maxWeightIndex]
+ }
+}
+
+module.exports = BalancedPool
diff --git a/vanilla/node_modules/undici/lib/dispatcher/client-h1.js b/vanilla/node_modules/undici/lib/dispatcher/client-h1.js
new file mode 100644
index 0000000..ce6b4ee
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/client-h1.js
@@ -0,0 +1,1606 @@
+'use strict'
+
+/* global WebAssembly */
+
+const assert = require('node:assert')
+const util = require('../core/util.js')
+const { channels } = require('../core/diagnostics.js')
+const timers = require('../util/timers.js')
+const {
+ RequestContentLengthMismatchError,
+ ResponseContentLengthMismatchError,
+ RequestAbortedError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ SocketError,
+ InformationalError,
+ BodyTimeoutError,
+ HTTPParserError,
+ ResponseExceededMaxSizeError
+} = require('../core/errors.js')
+const {
+ kUrl,
+ kReset,
+ kClient,
+ kParser,
+ kBlocking,
+ kRunning,
+ kPending,
+ kSize,
+ kWriting,
+ kQueue,
+ kNoRef,
+ kKeepAliveDefaultTimeout,
+ kHostHeader,
+ kPendingIdx,
+ kRunningIdx,
+ kError,
+ kPipelining,
+ kSocket,
+ kKeepAliveTimeoutValue,
+ kMaxHeadersSize,
+ kKeepAliveMaxTimeout,
+ kKeepAliveTimeoutThreshold,
+ kHeadersTimeout,
+ kBodyTimeout,
+ kStrictContentLength,
+ kMaxRequests,
+ kCounter,
+ kMaxResponseSize,
+ kOnError,
+ kResume,
+ kHTTPContext,
+ kClosed
+} = require('../core/symbols.js')
+
+const constants = require('../llhttp/constants.js')
+const EMPTY_BUF = Buffer.alloc(0)
+const FastBuffer = Buffer[Symbol.species]
+const removeAllListeners = util.removeAllListeners
+
+let extractBody
+
+function lazyllhttp () {
+ const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
+
+ let mod
+
+ // We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
+ let useWasmSIMD = process.arch !== 'ppc64'
+ // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
+ if (process.env.UNDICI_NO_WASM_SIMD === '1') {
+ useWasmSIMD = true
+ } else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
+ useWasmSIMD = false
+ }
+
+ if (useWasmSIMD) {
+ try {
+ mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
+ } catch {
+ }
+ }
+
+ if (!mod) {
+ // We could check if the error was caused by the simd option not
+ // being enabled, but the occurring of this other error
+ // * https://github.com/emscripten-core/emscripten/issues/11495
+ // got me to remove that check to avoid breaking Node 12.
+ mod = new WebAssembly.Module(llhttpWasmData || require('../llhttp/llhttp-wasm.js'))
+ }
+
+ return new WebAssembly.Instance(mod, {
+ env: {
+ /**
+ * @param {number} p
+ * @param {number} at
+ * @param {number} len
+ * @returns {number}
+ */
+ wasm_on_url: (p, at, len) => {
+ return 0
+ },
+ /**
+ * @param {number} p
+ * @param {number} at
+ * @param {number} len
+ * @returns {number}
+ */
+ wasm_on_status: (p, at, len) => {
+ assert(currentParser.ptr === p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len))
+ },
+ /**
+ * @param {number} p
+ * @returns {number}
+ */
+ wasm_on_message_begin: (p) => {
+ assert(currentParser.ptr === p)
+ return currentParser.onMessageBegin()
+ },
+ /**
+ * @param {number} p
+ * @param {number} at
+ * @param {number} len
+ * @returns {number}
+ */
+ wasm_on_header_field: (p, at, len) => {
+ assert(currentParser.ptr === p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len))
+ },
+ /**
+ * @param {number} p
+ * @param {number} at
+ * @param {number} len
+ * @returns {number}
+ */
+ wasm_on_header_value: (p, at, len) => {
+ assert(currentParser.ptr === p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len))
+ },
+ /**
+ * @param {number} p
+ * @param {number} statusCode
+ * @param {0|1} upgrade
+ * @param {0|1} shouldKeepAlive
+ * @returns {number}
+ */
+ wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
+ assert(currentParser.ptr === p)
+ return currentParser.onHeadersComplete(statusCode, upgrade === 1, shouldKeepAlive === 1)
+ },
+ /**
+ * @param {number} p
+ * @param {number} at
+ * @param {number} len
+ * @returns {number}
+ */
+ wasm_on_body: (p, at, len) => {
+ assert(currentParser.ptr === p)
+ const start = at - currentBufferPtr + currentBufferRef.byteOffset
+ return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len))
+ },
+ /**
+ * @param {number} p
+ * @returns {number}
+ */
+ wasm_on_message_complete: (p) => {
+ assert(currentParser.ptr === p)
+ return currentParser.onMessageComplete()
+ }
+
+ }
+ })
+}
+
+let llhttpInstance = null
+
+/**
+ * @type {Parser|null}
+ */
+let currentParser = null
+let currentBufferRef = null
+/**
+ * @type {number}
+ */
+let currentBufferSize = 0
+let currentBufferPtr = null
+
+const USE_NATIVE_TIMER = 0
+const USE_FAST_TIMER = 1
+
+// Use fast timers for headers and body to take eventual event loop
+// latency into account.
+const TIMEOUT_HEADERS = 2 | USE_FAST_TIMER
+const TIMEOUT_BODY = 4 | USE_FAST_TIMER
+
+// Use native timers to ignore event loop latency for keep-alive
+// handling.
+const TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER
+
+class Parser {
+ /**
+ * @param {import('./client.js')} client
+ * @param {import('net').Socket} socket
+ * @param {*} llhttp
+ */
+ constructor (client, socket, { exports }) {
+ this.llhttp = exports
+ this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE)
+ this.client = client
+ /**
+ * @type {import('net').Socket}
+ */
+ this.socket = socket
+ this.timeout = null
+ this.timeoutValue = null
+ this.timeoutType = null
+ this.statusCode = 0
+ this.statusText = ''
+ this.upgrade = false
+ this.headers = []
+ this.headersSize = 0
+ this.headersMaxSize = client[kMaxHeadersSize]
+ this.shouldKeepAlive = false
+ this.paused = false
+ this.resume = this.resume.bind(this)
+
+ this.bytesRead = 0
+
+ this.keepAlive = ''
+ this.contentLength = ''
+ this.connection = ''
+ this.maxResponseSize = client[kMaxResponseSize]
+ }
+
+ setTimeout (delay, type) {
+ // If the existing timer and the new timer are of different timer type
+ // (fast or native) or have different delay, we need to clear the existing
+ // timer and set a new one.
+ if (
+ delay !== this.timeoutValue ||
+ (type & USE_FAST_TIMER) ^ (this.timeoutType & USE_FAST_TIMER)
+ ) {
+ // If a timeout is already set, clear it with clearTimeout of the fast
+ // timer implementation, as it can clear fast and native timers.
+ if (this.timeout) {
+ timers.clearTimeout(this.timeout)
+ this.timeout = null
+ }
+
+ if (delay) {
+ if (type & USE_FAST_TIMER) {
+ this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
+ } else {
+ this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
+ this.timeout?.unref()
+ }
+ }
+
+ this.timeoutValue = delay
+ } else if (this.timeout) {
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+
+ this.timeoutType = type
+ }
+
+ resume () {
+ if (this.socket.destroyed || !this.paused) {
+ return
+ }
+
+ assert(this.ptr != null)
+ assert(currentParser === null)
+
+ this.llhttp.llhttp_resume(this.ptr)
+
+ assert(this.timeoutType === TIMEOUT_BODY)
+ if (this.timeout) {
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+
+ this.paused = false
+ this.execute(this.socket.read() || EMPTY_BUF) // Flush parser.
+ this.readMore()
+ }
+
+ readMore () {
+ while (!this.paused && this.ptr) {
+ const chunk = this.socket.read()
+ if (chunk === null) {
+ break
+ }
+ this.execute(chunk)
+ }
+ }
+
+ /**
+ * @param {Buffer} chunk
+ */
+ execute (chunk) {
+ assert(currentParser === null)
+ assert(this.ptr != null)
+ assert(!this.paused)
+
+ const { socket, llhttp } = this
+
+ // Allocate a new buffer if the current buffer is too small.
+ if (chunk.length > currentBufferSize) {
+ if (currentBufferPtr) {
+ llhttp.free(currentBufferPtr)
+ }
+ // Allocate a buffer that is a multiple of 4096 bytes.
+ currentBufferSize = Math.ceil(chunk.length / 4096) * 4096
+ currentBufferPtr = llhttp.malloc(currentBufferSize)
+ }
+
+ new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(chunk)
+
+ // Call `execute` on the wasm parser.
+ // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data,
+ // and finally the length of bytes to parse.
+ // The return value is an error code or `constants.ERROR.OK`.
+ try {
+ let ret
+
+ try {
+ currentBufferRef = chunk
+ currentParser = this
+ ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, chunk.length)
+ } finally {
+ currentParser = null
+ currentBufferRef = null
+ }
+
+ if (ret !== constants.ERROR.OK) {
+ const data = chunk.subarray(llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr)
+
+ if (ret === constants.ERROR.PAUSED_UPGRADE) {
+ this.onUpgrade(data)
+ } else if (ret === constants.ERROR.PAUSED) {
+ this.paused = true
+ socket.unshift(data)
+ } else {
+ const ptr = llhttp.llhttp_get_error_reason(this.ptr)
+ let message = ''
+ if (ptr) {
+ const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0)
+ message =
+ 'Response does not match the HTTP/1.1 protocol (' +
+ Buffer.from(llhttp.memory.buffer, ptr, len).toString() +
+ ')'
+ }
+ throw new HTTPParserError(message, constants.ERROR[ret], data)
+ }
+ }
+ } catch (err) {
+ util.destroy(socket, err)
+ }
+ }
+
+ destroy () {
+ assert(currentParser === null)
+ assert(this.ptr != null)
+
+ this.llhttp.llhttp_free(this.ptr)
+ this.ptr = null
+
+ this.timeout && timers.clearTimeout(this.timeout)
+ this.timeout = null
+ this.timeoutValue = null
+ this.timeoutType = null
+
+ this.paused = false
+ }
+
+ /**
+ * @param {Buffer} buf
+ * @returns {0}
+ */
+ onStatus (buf) {
+ this.statusText = buf.toString()
+ return 0
+ }
+
+ /**
+ * @returns {0|-1}
+ */
+ onMessageBegin () {
+ const { socket, client } = this
+
+ if (socket.destroyed) {
+ return -1
+ }
+
+ const request = client[kQueue][client[kRunningIdx]]
+ if (!request) {
+ return -1
+ }
+ request.onResponseStarted()
+
+ return 0
+ }
+
+ /**
+ * @param {Buffer} buf
+ * @returns {number}
+ */
+ onHeaderField (buf) {
+ const len = this.headers.length
+
+ if ((len & 1) === 0) {
+ this.headers.push(buf)
+ } else {
+ this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
+ }
+
+ this.trackHeader(buf.length)
+
+ return 0
+ }
+
+ /**
+ * @param {Buffer} buf
+ * @returns {number}
+ */
+ onHeaderValue (buf) {
+ let len = this.headers.length
+
+ if ((len & 1) === 1) {
+ this.headers.push(buf)
+ len += 1
+ } else {
+ this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf])
+ }
+
+ const key = this.headers[len - 2]
+ if (key.length === 10) {
+ const headerName = util.bufferToLowerCasedHeaderName(key)
+ if (headerName === 'keep-alive') {
+ this.keepAlive += buf.toString()
+ } else if (headerName === 'connection') {
+ this.connection += buf.toString()
+ }
+ } else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === 'content-length') {
+ this.contentLength += buf.toString()
+ }
+
+ this.trackHeader(buf.length)
+
+ return 0
+ }
+
+ /**
+ * @param {number} len
+ */
+ trackHeader (len) {
+ this.headersSize += len
+ if (this.headersSize >= this.headersMaxSize) {
+ util.destroy(this.socket, new HeadersOverflowError())
+ }
+ }
+
+ /**
+ * @param {Buffer} head
+ */
+ onUpgrade (head) {
+ const { upgrade, client, socket, headers, statusCode } = this
+
+ assert(upgrade)
+ assert(client[kSocket] === socket)
+ assert(!socket.destroyed)
+ assert(!this.paused)
+ assert((headers.length & 1) === 0)
+
+ const request = client[kQueue][client[kRunningIdx]]
+ assert(request)
+ assert(request.upgrade || request.method === 'CONNECT')
+
+ this.statusCode = 0
+ this.statusText = ''
+ this.shouldKeepAlive = false
+
+ this.headers = []
+ this.headersSize = 0
+
+ socket.unshift(head)
+
+ socket[kParser].destroy()
+ socket[kParser] = null
+
+ socket[kClient] = null
+ socket[kError] = null
+
+ removeAllListeners(socket)
+
+ client[kSocket] = null
+ client[kHTTPContext] = null // TODO (fix): This is hacky...
+ client[kQueue][client[kRunningIdx]++] = null
+ client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
+
+ try {
+ request.onUpgrade(statusCode, headers, socket)
+ } catch (err) {
+ util.destroy(socket, err)
+ }
+
+ client[kResume]()
+ }
+
+ /**
+ * @param {number} statusCode
+ * @param {boolean} upgrade
+ * @param {boolean} shouldKeepAlive
+ * @returns {number}
+ */
+ onHeadersComplete (statusCode, upgrade, shouldKeepAlive) {
+ const { client, socket, headers, statusText } = this
+
+ if (socket.destroyed) {
+ return -1
+ }
+
+ const request = client[kQueue][client[kRunningIdx]]
+
+ if (!request) {
+ return -1
+ }
+
+ assert(!this.upgrade)
+ assert(this.statusCode < 200)
+
+ if (statusCode === 100) {
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
+ return -1
+ }
+
+ /* this can only happen if server is misbehaving */
+ if (upgrade && !request.upgrade) {
+ util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket)))
+ return -1
+ }
+
+ assert(this.timeoutType === TIMEOUT_HEADERS)
+
+ this.statusCode = statusCode
+ this.shouldKeepAlive = (
+ shouldKeepAlive ||
+ // Override llhttp value which does not allow keepAlive for HEAD.
+ (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive')
+ )
+
+ if (this.statusCode >= 200) {
+ const bodyTimeout = request.bodyTimeout != null
+ ? request.bodyTimeout
+ : client[kBodyTimeout]
+ this.setTimeout(bodyTimeout, TIMEOUT_BODY)
+ } else if (this.timeout) {
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+
+ if (request.method === 'CONNECT') {
+ assert(client[kRunning] === 1)
+ this.upgrade = true
+ return 2
+ }
+
+ if (upgrade) {
+ assert(client[kRunning] === 1)
+ this.upgrade = true
+ return 2
+ }
+
+ assert((this.headers.length & 1) === 0)
+ this.headers = []
+ this.headersSize = 0
+
+ if (this.shouldKeepAlive && client[kPipelining]) {
+ const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null
+
+ if (keepAliveTimeout != null) {
+ const timeout = Math.min(
+ keepAliveTimeout - client[kKeepAliveTimeoutThreshold],
+ client[kKeepAliveMaxTimeout]
+ )
+ if (timeout <= 0) {
+ socket[kReset] = true
+ } else {
+ client[kKeepAliveTimeoutValue] = timeout
+ }
+ } else {
+ client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout]
+ }
+ } else {
+ // Stop more requests from being dispatched.
+ socket[kReset] = true
+ }
+
+ const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
+
+ if (request.aborted) {
+ return -1
+ }
+
+ if (request.method === 'HEAD') {
+ return 1
+ }
+
+ if (statusCode < 200) {
+ return 1
+ }
+
+ if (socket[kBlocking]) {
+ socket[kBlocking] = false
+ client[kResume]()
+ }
+
+ return pause ? constants.ERROR.PAUSED : 0
+ }
+
+ /**
+ * @param {Buffer} buf
+ * @returns {number}
+ */
+ onBody (buf) {
+ const { client, socket, statusCode, maxResponseSize } = this
+
+ if (socket.destroyed) {
+ return -1
+ }
+
+ const request = client[kQueue][client[kRunningIdx]]
+ assert(request)
+
+ assert(this.timeoutType === TIMEOUT_BODY)
+ if (this.timeout) {
+ if (this.timeout.refresh) {
+ this.timeout.refresh()
+ }
+ }
+
+ assert(statusCode >= 200)
+
+ if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) {
+ util.destroy(socket, new ResponseExceededMaxSizeError())
+ return -1
+ }
+
+ this.bytesRead += buf.length
+
+ if (request.onData(buf) === false) {
+ return constants.ERROR.PAUSED
+ }
+
+ return 0
+ }
+
+ /**
+ * @returns {number}
+ */
+ onMessageComplete () {
+ const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this
+
+ if (socket.destroyed && (!statusCode || shouldKeepAlive)) {
+ return -1
+ }
+
+ if (upgrade) {
+ return 0
+ }
+
+ assert(statusCode >= 100)
+ assert((this.headers.length & 1) === 0)
+
+ const request = client[kQueue][client[kRunningIdx]]
+ assert(request)
+
+ this.statusCode = 0
+ this.statusText = ''
+ this.bytesRead = 0
+ this.contentLength = ''
+ this.keepAlive = ''
+ this.connection = ''
+
+ this.headers = []
+ this.headersSize = 0
+
+ if (statusCode < 200) {
+ return 0
+ }
+
+ if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) {
+ util.destroy(socket, new ResponseContentLengthMismatchError())
+ return -1
+ }
+
+ request.onComplete(headers)
+
+ client[kQueue][client[kRunningIdx]++] = null
+
+ if (socket[kWriting]) {
+ assert(client[kRunning] === 0)
+ // Response completed before request.
+ util.destroy(socket, new InformationalError('reset'))
+ return constants.ERROR.PAUSED
+ } else if (!shouldKeepAlive) {
+ util.destroy(socket, new InformationalError('reset'))
+ return constants.ERROR.PAUSED
+ } else if (socket[kReset] && client[kRunning] === 0) {
+ // Destroy socket once all requests have completed.
+ // The request at the tail of the pipeline is the one
+ // that requested reset and no further requests should
+ // have been queued since then.
+ util.destroy(socket, new InformationalError('reset'))
+ return constants.ERROR.PAUSED
+ } else if (client[kPipelining] == null || client[kPipelining] === 1) {
+ // We must wait a full event loop cycle to reuse this socket to make sure
+ // that non-spec compliant servers are not closing the connection even if they
+ // said they won't.
+ setImmediate(client[kResume])
+ } else {
+ client[kResume]()
+ }
+
+ return 0
+ }
+}
+
+function onParserTimeout (parserWeakRef) {
+ const parser = parserWeakRef.deref()
+ if (!parser) {
+ return
+ }
+
+ const { socket, timeoutType, client, paused } = parser
+
+ if (timeoutType === TIMEOUT_HEADERS) {
+ if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
+ assert(!paused, 'cannot be paused while waiting for headers')
+ util.destroy(socket, new HeadersTimeoutError())
+ }
+ } else if (timeoutType === TIMEOUT_BODY) {
+ if (!paused) {
+ util.destroy(socket, new BodyTimeoutError())
+ }
+ } else if (timeoutType === TIMEOUT_KEEP_ALIVE) {
+ assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
+ util.destroy(socket, new InformationalError('socket idle timeout'))
+ }
+}
+
+/**
+ * @param {import ('./client.js')} client
+ * @param {import('net').Socket} socket
+ * @returns
+ */
+function connectH1 (client, socket) {
+ client[kSocket] = socket
+
+ if (!llhttpInstance) {
+ llhttpInstance = lazyllhttp()
+ }
+
+ if (socket.errored) {
+ throw socket.errored
+ }
+
+ if (socket.destroyed) {
+ throw new SocketError('destroyed')
+ }
+
+ socket[kNoRef] = false
+ socket[kWriting] = false
+ socket[kReset] = false
+ socket[kBlocking] = false
+ socket[kParser] = new Parser(client, socket, llhttpInstance)
+
+ util.addListener(socket, 'error', onHttpSocketError)
+ util.addListener(socket, 'readable', onHttpSocketReadable)
+ util.addListener(socket, 'end', onHttpSocketEnd)
+ util.addListener(socket, 'close', onHttpSocketClose)
+
+ socket[kClosed] = false
+ socket.on('close', onSocketClose)
+
+ return {
+ version: 'h1',
+ defaultPipelining: 1,
+ write (request) {
+ return writeH1(client, request)
+ },
+ resume () {
+ resumeH1(client)
+ },
+ /**
+ * @param {Error|undefined} err
+ * @param {() => void} callback
+ */
+ destroy (err, callback) {
+ if (socket[kClosed]) {
+ queueMicrotask(callback)
+ } else {
+ socket.on('close', callback)
+ socket.destroy(err)
+ }
+ },
+ /**
+ * @returns {boolean}
+ */
+ get destroyed () {
+ return socket.destroyed
+ },
+ /**
+ * @param {import('../core/request.js')} request
+ * @returns {boolean}
+ */
+ busy (request) {
+ if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
+ return true
+ }
+
+ if (request) {
+ if (client[kRunning] > 0 && !request.idempotent) {
+ // Non-idempotent request cannot be retried.
+ // Ensure that no other requests are inflight and
+ // could cause failure.
+ return true
+ }
+
+ if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) {
+ // Don't dispatch an upgrade until all preceding requests have completed.
+ // A misbehaving server might upgrade the connection before all pipelined
+ // request has completed.
+ return true
+ }
+
+ if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 &&
+ (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) {
+ // Request with stream or iterator body can error while other requests
+ // are inflight and indirectly error those as well.
+ // Ensure this doesn't happen by waiting for inflight
+ // to complete before dispatching.
+
+ // Request with stream or iterator body cannot be retried.
+ // Ensure that no other requests are inflight and
+ // could cause failure.
+ return true
+ }
+ }
+
+ return false
+ }
+ }
+}
+
+function onHttpSocketError (err) {
+ assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
+
+ const parser = this[kParser]
+
+ // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
+ // to the user.
+ if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
+ // We treat all incoming data so for as a valid response.
+ parser.onMessageComplete()
+ return
+ }
+
+ this[kError] = err
+
+ this[kClient][kOnError](err)
+}
+
+function onHttpSocketReadable () {
+ this[kParser]?.readMore()
+}
+
+function onHttpSocketEnd () {
+ const parser = this[kParser]
+
+ if (parser.statusCode && !parser.shouldKeepAlive) {
+ // We treat all incoming data so far as a valid response.
+ parser.onMessageComplete()
+ return
+ }
+
+ util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
+}
+
+function onHttpSocketClose () {
+ const parser = this[kParser]
+
+ if (parser) {
+ if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
+ // We treat all incoming data so far as a valid response.
+ parser.onMessageComplete()
+ }
+
+ this[kParser].destroy()
+ this[kParser] = null
+ }
+
+ const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
+
+ const client = this[kClient]
+
+ client[kSocket] = null
+ client[kHTTPContext] = null // TODO (fix): This is hacky...
+
+ if (client.destroyed) {
+ assert(client[kPending] === 0)
+
+ // Fail entire queue.
+ const requests = client[kQueue].splice(client[kRunningIdx])
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ util.errorRequest(client, request, err)
+ }
+ } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
+ // Fail head of pipeline.
+ const request = client[kQueue][client[kRunningIdx]]
+ client[kQueue][client[kRunningIdx]++] = null
+
+ util.errorRequest(client, request, err)
+ }
+
+ client[kPendingIdx] = client[kRunningIdx]
+
+ assert(client[kRunning] === 0)
+
+ client.emit('disconnect', client[kUrl], [client], err)
+
+ client[kResume]()
+}
+
+function onSocketClose () {
+ this[kClosed] = true
+}
+
+/**
+ * @param {import('./client.js')} client
+ */
+function resumeH1 (client) {
+ const socket = client[kSocket]
+
+ if (socket && !socket.destroyed) {
+ if (client[kSize] === 0) {
+ if (!socket[kNoRef] && socket.unref) {
+ socket.unref()
+ socket[kNoRef] = true
+ }
+ } else if (socket[kNoRef] && socket.ref) {
+ socket.ref()
+ socket[kNoRef] = false
+ }
+
+ if (client[kSize] === 0) {
+ if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
+ socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
+ }
+ } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
+ if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
+ const request = client[kQueue][client[kRunningIdx]]
+ const headersTimeout = request.headersTimeout != null
+ ? request.headersTimeout
+ : client[kHeadersTimeout]
+ socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS)
+ }
+ }
+ }
+}
+
+// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
+function shouldSendContentLength (method) {
+ return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
+}
+
+/**
+ * @param {import('./client.js')} client
+ * @param {import('../core/request.js')} request
+ * @returns
+ */
+function writeH1 (client, request) {
+ const { method, path, host, upgrade, blocking, reset } = request
+
+ let { body, headers, contentLength } = request
+
+ // https://tools.ietf.org/html/rfc7231#section-4.3.1
+ // https://tools.ietf.org/html/rfc7231#section-4.3.2
+ // https://tools.ietf.org/html/rfc7231#section-4.3.5
+
+ // Sending a payload body on a request that does not
+ // expect it can cause undefined behavior on some
+ // servers and corrupt connection state. Do not
+ // re-use the connection for further requests.
+
+ const expectsPayload = (
+ method === 'PUT' ||
+ method === 'POST' ||
+ method === 'PATCH' ||
+ method === 'QUERY' ||
+ method === 'PROPFIND' ||
+ method === 'PROPPATCH'
+ )
+
+ if (util.isFormDataLike(body)) {
+ if (!extractBody) {
+ extractBody = require('../web/fetch/body.js').extractBody
+ }
+
+ const [bodyStream, contentType] = extractBody(body)
+ if (request.contentType == null) {
+ headers.push('content-type', contentType)
+ }
+ body = bodyStream.stream
+ contentLength = bodyStream.length
+ } else if (util.isBlobLike(body) && request.contentType == null && body.type) {
+ headers.push('content-type', body.type)
+ }
+
+ if (body && typeof body.read === 'function') {
+ // Try to read EOF in order to get length.
+ body.read(0)
+ }
+
+ const bodyLength = util.bodyLength(body)
+
+ contentLength = bodyLength ?? contentLength
+
+ if (contentLength === null) {
+ contentLength = request.contentLength
+ }
+
+ if (contentLength === 0 && !expectsPayload) {
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
+ // A user agent SHOULD NOT send a Content-Length header field when
+ // the request message does not contain a payload body and the method
+ // semantics do not anticipate such a body.
+
+ contentLength = null
+ }
+
+ // https://github.com/nodejs/undici/issues/2046
+ // A user agent may send a Content-Length header with 0 value, this should be allowed.
+ if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) {
+ if (client[kStrictContentLength]) {
+ util.errorRequest(client, request, new RequestContentLengthMismatchError())
+ return false
+ }
+
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+
+ const socket = client[kSocket]
+
+ /**
+ * @param {Error} [err]
+ * @returns {void}
+ */
+ const abort = (err) => {
+ if (request.aborted || request.completed) {
+ return
+ }
+
+ util.errorRequest(client, request, err || new RequestAbortedError())
+
+ util.destroy(body)
+ util.destroy(socket, new InformationalError('aborted'))
+ }
+
+ try {
+ request.onConnect(abort)
+ } catch (err) {
+ util.errorRequest(client, request, err)
+ }
+
+ if (request.aborted) {
+ return false
+ }
+
+ if (method === 'HEAD') {
+ // https://github.com/mcollina/undici/issues/258
+ // Close after a HEAD request to interop with misbehaving servers
+ // that may send a body in the response.
+
+ socket[kReset] = true
+ }
+
+ if (upgrade || method === 'CONNECT') {
+ // On CONNECT or upgrade, block pipeline from dispatching further
+ // requests on this connection.
+
+ socket[kReset] = true
+ }
+
+ if (reset != null) {
+ socket[kReset] = reset
+ }
+
+ if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) {
+ socket[kReset] = true
+ }
+
+ if (blocking) {
+ socket[kBlocking] = true
+ }
+
+ let header = `${method} ${path} HTTP/1.1\r\n`
+
+ if (typeof host === 'string') {
+ header += `host: ${host}\r\n`
+ } else {
+ header += client[kHostHeader]
+ }
+
+ if (upgrade) {
+ header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n`
+ } else if (client[kPipelining] && !socket[kReset]) {
+ header += 'connection: keep-alive\r\n'
+ } else {
+ header += 'connection: close\r\n'
+ }
+
+ if (Array.isArray(headers)) {
+ for (let n = 0; n < headers.length; n += 2) {
+ const key = headers[n + 0]
+ const val = headers[n + 1]
+
+ if (Array.isArray(val)) {
+ for (let i = 0; i < val.length; i++) {
+ header += `${key}: ${val[i]}\r\n`
+ }
+ } else {
+ header += `${key}: ${val}\r\n`
+ }
+ }
+ }
+
+ if (channels.sendHeaders.hasSubscribers) {
+ channels.sendHeaders.publish({ request, headers: header, socket })
+ }
+
+ if (!body || bodyLength === 0) {
+ writeBuffer(abort, null, client, request, socket, contentLength, header, expectsPayload)
+ } else if (util.isBuffer(body)) {
+ writeBuffer(abort, body, client, request, socket, contentLength, header, expectsPayload)
+ } else if (util.isBlobLike(body)) {
+ if (typeof body.stream === 'function') {
+ writeIterable(abort, body.stream(), client, request, socket, contentLength, header, expectsPayload)
+ } else {
+ writeBlob(abort, body, client, request, socket, contentLength, header, expectsPayload)
+ }
+ } else if (util.isStream(body)) {
+ writeStream(abort, body, client, request, socket, contentLength, header, expectsPayload)
+ } else if (util.isIterable(body)) {
+ writeIterable(abort, body, client, request, socket, contentLength, header, expectsPayload)
+ } else {
+ assert(false)
+ }
+
+ return true
+}
+
+/**
+ * @param {AbortCallback} abort
+ * @param {import('stream').Stream} body
+ * @param {import('./client.js')} client
+ * @param {import('../core/request.js')} request
+ * @param {import('net').Socket} socket
+ * @param {number} contentLength
+ * @param {string} header
+ * @param {boolean} expectsPayload
+ */
+function writeStream (abort, body, client, request, socket, contentLength, header, expectsPayload) {
+ assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
+
+ let finished = false
+
+ const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
+
+ /**
+ * @param {Buffer} chunk
+ * @returns {void}
+ */
+ const onData = function (chunk) {
+ if (finished) {
+ return
+ }
+
+ try {
+ if (!writer.write(chunk) && this.pause) {
+ this.pause()
+ }
+ } catch (err) {
+ util.destroy(this, err)
+ }
+ }
+
+ /**
+ * @returns {void}
+ */
+ const onDrain = function () {
+ if (finished) {
+ return
+ }
+
+ if (body.resume) {
+ body.resume()
+ }
+ }
+
+ /**
+ * @returns {void}
+ */
+ const onClose = function () {
+ // 'close' might be emitted *before* 'error' for
+ // broken streams. Wait a tick to avoid this case.
+ queueMicrotask(() => {
+ // It's only safe to remove 'error' listener after
+ // 'close'.
+ body.removeListener('error', onFinished)
+ })
+
+ if (!finished) {
+ const err = new RequestAbortedError()
+ queueMicrotask(() => onFinished(err))
+ }
+ }
+
+ /**
+ * @param {Error} [err]
+ * @returns
+ */
+ const onFinished = function (err) {
+ if (finished) {
+ return
+ }
+
+ finished = true
+
+ assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1))
+
+ socket
+ .off('drain', onDrain)
+ .off('error', onFinished)
+
+ body
+ .removeListener('data', onData)
+ .removeListener('end', onFinished)
+ .removeListener('close', onClose)
+
+ if (!err) {
+ try {
+ writer.end()
+ } catch (er) {
+ err = er
+ }
+ }
+
+ writer.destroy(err)
+
+ if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) {
+ util.destroy(body, err)
+ } else {
+ util.destroy(body)
+ }
+ }
+
+ body
+ .on('data', onData)
+ .on('end', onFinished)
+ .on('error', onFinished)
+ .on('close', onClose)
+
+ if (body.resume) {
+ body.resume()
+ }
+
+ socket
+ .on('drain', onDrain)
+ .on('error', onFinished)
+
+ if (body.errorEmitted ?? body.errored) {
+ setImmediate(onFinished, body.errored)
+ } else if (body.endEmitted ?? body.readableEnded) {
+ setImmediate(onFinished, null)
+ }
+
+ if (body.closeEmitted ?? body.closed) {
+ setImmediate(onClose)
+ }
+}
+
+/**
+ * @typedef AbortCallback
+ * @type {Function}
+ * @param {Error} [err]
+ * @returns {void}
+ */
+
+/**
+ * @param {AbortCallback} abort
+ * @param {Uint8Array|null} body
+ * @param {import('./client.js')} client
+ * @param {import('../core/request.js')} request
+ * @param {import('net').Socket} socket
+ * @param {number} contentLength
+ * @param {string} header
+ * @param {boolean} expectsPayload
+ * @returns {void}
+ */
+function writeBuffer (abort, body, client, request, socket, contentLength, header, expectsPayload) {
+ try {
+ if (!body) {
+ if (contentLength === 0) {
+ socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
+ } else {
+ assert(contentLength === null, 'no body must not have content length')
+ socket.write(`${header}\r\n`, 'latin1')
+ }
+ } else if (util.isBuffer(body)) {
+ assert(contentLength === body.byteLength, 'buffer body must have content length')
+
+ socket.cork()
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
+ socket.write(body)
+ socket.uncork()
+ request.onBodySent(body)
+
+ if (!expectsPayload && request.reset !== false) {
+ socket[kReset] = true
+ }
+ }
+ request.onRequestSent()
+
+ client[kResume]()
+ } catch (err) {
+ abort(err)
+ }
+}
+
+/**
+ * @param {AbortCallback} abort
+ * @param {Blob} body
+ * @param {import('./client.js')} client
+ * @param {import('../core/request.js')} request
+ * @param {import('net').Socket} socket
+ * @param {number} contentLength
+ * @param {string} header
+ * @param {boolean} expectsPayload
+ * @returns {Promise<void>}
+ */
+async function writeBlob (abort, body, client, request, socket, contentLength, header, expectsPayload) {
+ assert(contentLength === body.size, 'blob body must have content length')
+
+ try {
+ if (contentLength != null && contentLength !== body.size) {
+ throw new RequestContentLengthMismatchError()
+ }
+
+ const buffer = Buffer.from(await body.arrayBuffer())
+
+ socket.cork()
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
+ socket.write(buffer)
+ socket.uncork()
+
+ request.onBodySent(buffer)
+ request.onRequestSent()
+
+ if (!expectsPayload && request.reset !== false) {
+ socket[kReset] = true
+ }
+
+ client[kResume]()
+ } catch (err) {
+ abort(err)
+ }
+}
+
+/**
+ * @param {AbortCallback} abort
+ * @param {Iterable} body
+ * @param {import('./client.js')} client
+ * @param {import('../core/request.js')} request
+ * @param {import('net').Socket} socket
+ * @param {number} contentLength
+ * @param {string} header
+ * @param {boolean} expectsPayload
+ * @returns {Promise<void>}
+ */
+async function writeIterable (abort, body, client, request, socket, contentLength, header, expectsPayload) {
+ assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
+
+ let callback = null
+ function onDrain () {
+ if (callback) {
+ const cb = callback
+ callback = null
+ cb()
+ }
+ }
+
+ const waitForDrain = () => new Promise((resolve, reject) => {
+ assert(callback === null)
+
+ if (socket[kError]) {
+ reject(socket[kError])
+ } else {
+ callback = resolve
+ }
+ })
+
+ socket
+ .on('close', onDrain)
+ .on('drain', onDrain)
+
+ const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
+ try {
+ // It's up to the user to somehow abort the async iterable.
+ for await (const chunk of body) {
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ if (!writer.write(chunk)) {
+ await waitForDrain()
+ }
+ }
+
+ writer.end()
+ } catch (err) {
+ writer.destroy(err)
+ } finally {
+ socket
+ .off('close', onDrain)
+ .off('drain', onDrain)
+ }
+}
+
+class AsyncWriter {
+ /**
+ *
+ * @param {object} arg
+ * @param {AbortCallback} arg.abort
+ * @param {import('net').Socket} arg.socket
+ * @param {import('../core/request.js')} arg.request
+ * @param {number} arg.contentLength
+ * @param {import('./client.js')} arg.client
+ * @param {boolean} arg.expectsPayload
+ * @param {string} arg.header
+ */
+ constructor ({ abort, socket, request, contentLength, client, expectsPayload, header }) {
+ this.socket = socket
+ this.request = request
+ this.contentLength = contentLength
+ this.client = client
+ this.bytesWritten = 0
+ this.expectsPayload = expectsPayload
+ this.header = header
+ this.abort = abort
+
+ socket[kWriting] = true
+ }
+
+ /**
+ * @param {Buffer} chunk
+ * @returns
+ */
+ write (chunk) {
+ const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this
+
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ if (socket.destroyed) {
+ return false
+ }
+
+ const len = Buffer.byteLength(chunk)
+ if (!len) {
+ return true
+ }
+
+ // We should defer writing chunks.
+ if (contentLength !== null && bytesWritten + len > contentLength) {
+ if (client[kStrictContentLength]) {
+ throw new RequestContentLengthMismatchError()
+ }
+
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+
+ socket.cork()
+
+ if (bytesWritten === 0) {
+ if (!expectsPayload && request.reset !== false) {
+ socket[kReset] = true
+ }
+
+ if (contentLength === null) {
+ socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1')
+ } else {
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
+ }
+ }
+
+ if (contentLength === null) {
+ socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1')
+ }
+
+ this.bytesWritten += len
+
+ const ret = socket.write(chunk)
+
+ socket.uncork()
+
+ request.onBodySent(chunk)
+
+ if (!ret) {
+ if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
+ if (socket[kParser].timeout.refresh) {
+ socket[kParser].timeout.refresh()
+ }
+ }
+ }
+
+ return ret
+ }
+
+ /**
+ * @returns {void}
+ */
+ end () {
+ const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this
+ request.onRequestSent()
+
+ socket[kWriting] = false
+
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ if (socket.destroyed) {
+ return
+ }
+
+ if (bytesWritten === 0) {
+ if (expectsPayload) {
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
+ // A user agent SHOULD send a Content-Length in a request message when
+ // no Transfer-Encoding is sent and the request method defines a meaning
+ // for an enclosed payload body.
+
+ socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
+ } else {
+ socket.write(`${header}\r\n`, 'latin1')
+ }
+ } else if (contentLength === null) {
+ socket.write('\r\n0\r\n\r\n', 'latin1')
+ }
+
+ if (contentLength !== null && bytesWritten !== contentLength) {
+ if (client[kStrictContentLength]) {
+ throw new RequestContentLengthMismatchError()
+ } else {
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+ }
+
+ if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) {
+ if (socket[kParser].timeout.refresh) {
+ socket[kParser].timeout.refresh()
+ }
+ }
+
+ client[kResume]()
+ }
+
+ /**
+ * @param {Error} [err]
+ * @returns {void}
+ */
+ destroy (err) {
+ const { socket, client, abort } = this
+
+ socket[kWriting] = false
+
+ if (err) {
+ assert(client[kRunning] <= 1, 'pipeline should only contain this request')
+ abort(err)
+ }
+ }
+}
+
+module.exports = connectH1
diff --git a/vanilla/node_modules/undici/lib/dispatcher/client-h2.js b/vanilla/node_modules/undici/lib/dispatcher/client-h2.js
new file mode 100644
index 0000000..0969108
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/client-h2.js
@@ -0,0 +1,990 @@
+'use strict'
+
+const assert = require('node:assert')
+const { pipeline } = require('node:stream')
+const util = require('../core/util.js')
+const {
+ RequestContentLengthMismatchError,
+ RequestAbortedError,
+ SocketError,
+ InformationalError,
+ InvalidArgumentError
+} = require('../core/errors.js')
+const {
+ kUrl,
+ kReset,
+ kClient,
+ kRunning,
+ kPending,
+ kQueue,
+ kPendingIdx,
+ kRunningIdx,
+ kError,
+ kSocket,
+ kStrictContentLength,
+ kOnError,
+ kMaxConcurrentStreams,
+ kPingInterval,
+ kHTTP2Session,
+ kHTTP2InitialWindowSize,
+ kHTTP2ConnectionWindowSize,
+ kResume,
+ kSize,
+ kHTTPContext,
+ kClosed,
+ kBodyTimeout,
+ kEnableConnectProtocol,
+ kRemoteSettings,
+ kHTTP2Stream,
+ kHTTP2SessionState
+} = require('../core/symbols.js')
+const { channels } = require('../core/diagnostics.js')
+
+const kOpenStreams = Symbol('open streams')
+
+let extractBody
+
+/** @type {import('http2')} */
+let http2
+try {
+ http2 = require('node:http2')
+} catch {
+ // @ts-ignore
+ http2 = { constants: {} }
+}
+
+const {
+ constants: {
+ HTTP2_HEADER_AUTHORITY,
+ HTTP2_HEADER_METHOD,
+ HTTP2_HEADER_PATH,
+ HTTP2_HEADER_SCHEME,
+ HTTP2_HEADER_CONTENT_LENGTH,
+ HTTP2_HEADER_EXPECT,
+ HTTP2_HEADER_STATUS,
+ HTTP2_HEADER_PROTOCOL,
+ NGHTTP2_REFUSED_STREAM,
+ NGHTTP2_CANCEL
+ }
+} = http2
+
+function parseH2Headers (headers) {
+ const result = []
+
+ for (const [name, value] of Object.entries(headers)) {
+ // h2 may concat the header value by array
+ // e.g. Set-Cookie
+ if (Array.isArray(value)) {
+ for (const subvalue of value) {
+ // we need to provide each header value of header name
+ // because the headers handler expect name-value pair
+ result.push(Buffer.from(name), Buffer.from(subvalue))
+ }
+ } else {
+ result.push(Buffer.from(name), Buffer.from(value))
+ }
+ }
+
+ return result
+}
+
+function connectH2 (client, socket) {
+ client[kSocket] = socket
+
+ const http2InitialWindowSize = client[kHTTP2InitialWindowSize]
+ const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize]
+
+ const session = http2.connect(client[kUrl], {
+ createConnection: () => socket,
+ peerMaxConcurrentStreams: client[kMaxConcurrentStreams],
+ settings: {
+ // TODO(metcoder95): add support for PUSH
+ enablePush: false,
+ ...(http2InitialWindowSize != null ? { initialWindowSize: http2InitialWindowSize } : null)
+ }
+ })
+
+ client[kSocket] = socket
+ session[kOpenStreams] = 0
+ session[kClient] = client
+ session[kSocket] = socket
+ session[kHTTP2SessionState] = {
+ ping: {
+ interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
+ }
+ }
+ // We set it to true by default in a best-effort; however once connected to an H2 server
+ // we will check if extended CONNECT protocol is supported or not
+ // and set this value accordingly.
+ session[kEnableConnectProtocol] = false
+ // States whether or not we have received the remote settings from the server
+ session[kRemoteSettings] = false
+
+ // Apply connection-level flow control once connected (if supported).
+ if (http2ConnectionWindowSize) {
+ util.addListener(session, 'connect', applyConnectionWindowSize.bind(session, http2ConnectionWindowSize))
+ }
+
+ util.addListener(session, 'error', onHttp2SessionError)
+ util.addListener(session, 'frameError', onHttp2FrameError)
+ util.addListener(session, 'end', onHttp2SessionEnd)
+ util.addListener(session, 'goaway', onHttp2SessionGoAway)
+ util.addListener(session, 'close', onHttp2SessionClose)
+ util.addListener(session, 'remoteSettings', onHttp2RemoteSettings)
+ // TODO (@metcoder95): implement SETTINGS support
+ // util.addListener(session, 'localSettings', onHttp2RemoteSettings)
+
+ session.unref()
+
+ client[kHTTP2Session] = session
+ socket[kHTTP2Session] = session
+
+ util.addListener(socket, 'error', onHttp2SocketError)
+ util.addListener(socket, 'end', onHttp2SocketEnd)
+ util.addListener(socket, 'close', onHttp2SocketClose)
+
+ socket[kClosed] = false
+ socket.on('close', onSocketClose)
+
+ return {
+ version: 'h2',
+ defaultPipelining: Infinity,
+ /**
+ * @param {import('../core/request.js')} request
+ * @returns {boolean}
+ */
+ write (request) {
+ return writeH2(client, request)
+ },
+ /**
+ * @returns {void}
+ */
+ resume () {
+ resumeH2(client)
+ },
+ /**
+ * @param {Error | null} err
+ * @param {() => void} callback
+ */
+ destroy (err, callback) {
+ if (socket[kClosed]) {
+ queueMicrotask(callback)
+ } else {
+ socket.destroy(err).on('close', callback)
+ }
+ },
+ /**
+ * @type {boolean}
+ */
+ get destroyed () {
+ return socket.destroyed
+ },
+ /**
+ * @param {import('../core/request.js')} request
+ * @returns {boolean}
+ */
+ busy (request) {
+ if (request != null) {
+ if (client[kRunning] > 0) {
+ // We are already processing requests
+
+ // Non-idempotent request cannot be retried.
+ // Ensure that no other requests are inflight and
+ // could cause failure.
+ if (request.idempotent === false) return true
+ // Don't dispatch an upgrade until all preceding requests have completed.
+ // Possibly, we do not have remote settings confirmed yet.
+ if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
+ // Request with stream or iterator body can error while other requests
+ // are inflight and indirectly error those as well.
+ // Ensure this doesn't happen by waiting for inflight
+ // to complete before dispatching.
+
+ // Request with stream or iterator body cannot be retried.
+ // Ensure that no other requests are inflight and
+ // could cause failure.
+ if (util.bodyLength(request.body) !== 0 &&
+ (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true
+ } else {
+ return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false
+ }
+ }
+
+ return false
+ }
+ }
+}
+
+function resumeH2 (client) {
+ const socket = client[kSocket]
+
+ if (socket?.destroyed === false) {
+ if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
+ socket.unref()
+ client[kHTTP2Session].unref()
+ } else {
+ socket.ref()
+ client[kHTTP2Session].ref()
+ }
+ }
+}
+
+function applyConnectionWindowSize (connectionWindowSize) {
+ try {
+ if (typeof this.setLocalWindowSize === 'function') {
+ this.setLocalWindowSize(connectionWindowSize)
+ }
+ } catch {
+ // Best-effort only.
+ }
+}
+
+function onHttp2RemoteSettings (settings) {
+ // Fallbacks are a safe bet, remote setting will always override
+ this[kClient][kMaxConcurrentStreams] = settings.maxConcurrentStreams ?? this[kClient][kMaxConcurrentStreams]
+ /**
+ * From RFC-8441
+ * A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter
+ * with the value of 0 after previously sending a value of 1.
+ */
+ // Note: Cannot be tested in Node, it does not supports disabling the extended CONNECT protocol once enabled
+ if (this[kRemoteSettings] === true && this[kEnableConnectProtocol] === true && settings.enableConnectProtocol === false) {
+ const err = new InformationalError('HTTP/2: Server disabled extended CONNECT protocol against RFC-8441')
+ this[kSocket][kError] = err
+ this[kClient][kOnError](err)
+ return
+ }
+
+ this[kEnableConnectProtocol] = settings.enableConnectProtocol ?? this[kEnableConnectProtocol]
+ this[kRemoteSettings] = true
+ this[kClient][kResume]()
+}
+
+function onHttp2SendPing (session) {
+ const state = session[kHTTP2SessionState]
+ if ((session.closed || session.destroyed) && state.ping.interval != null) {
+ clearInterval(state.ping.interval)
+ state.ping.interval = null
+ return
+ }
+
+ // If no ping sent, do nothing
+ session.ping(onPing.bind(session))
+
+ function onPing (err, duration) {
+ const client = this[kClient]
+ const socket = this[kClient]
+
+ if (err != null) {
+ const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
+ socket[kError] = error
+ client[kOnError](error)
+ } else {
+ client.emit('ping', duration)
+ }
+ }
+}
+
+function onHttp2SessionError (err) {
+ assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
+
+ this[kSocket][kError] = err
+ this[kClient][kOnError](err)
+}
+
+function onHttp2FrameError (type, code, id) {
+ if (id === 0) {
+ const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
+ this[kSocket][kError] = err
+ this[kClient][kOnError](err)
+ }
+}
+
+function onHttp2SessionEnd () {
+ const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket]))
+ this.destroy(err)
+ util.destroy(this[kSocket], err)
+}
+
+/**
+ * This is the root cause of #3011
+ * We need to handle GOAWAY frames properly, and trigger the session close
+ * along with the socket right away
+ *
+ * @this {import('http2').ClientHttp2Session}
+ * @param {number} errorCode
+ */
+function onHttp2SessionGoAway (errorCode) {
+ // TODO(mcollina): Verify if GOAWAY implements the spec correctly:
+ // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
+ // Specifically, we do not verify the "valid" stream id.
+
+ const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket]))
+ const client = this[kClient]
+
+ client[kSocket] = null
+ client[kHTTPContext] = null
+
+ // this is an HTTP2 session
+ this.close()
+ this[kHTTP2Session] = null
+
+ util.destroy(this[kSocket], err)
+
+ // Fail head of pipeline.
+ if (client[kRunningIdx] < client[kQueue].length) {
+ const request = client[kQueue][client[kRunningIdx]]
+ client[kQueue][client[kRunningIdx]++] = null
+ util.errorRequest(client, request, err)
+ client[kPendingIdx] = client[kRunningIdx]
+ }
+
+ assert(client[kRunning] === 0)
+
+ client.emit('disconnect', client[kUrl], [client], err)
+ client.emit('connectionError', client[kUrl], [client], err)
+
+ client[kResume]()
+}
+
+function onHttp2SessionClose () {
+ const { [kClient]: client, [kHTTP2SessionState]: state } = this
+ const { [kSocket]: socket } = client
+
+ const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
+
+ client[kSocket] = null
+ client[kHTTPContext] = null
+
+ if (state.ping.interval != null) {
+ clearInterval(state.ping.interval)
+ state.ping.interval = null
+ }
+
+ if (client.destroyed) {
+ assert(client[kPending] === 0)
+
+ // Fail entire queue.
+ const requests = client[kQueue].splice(client[kRunningIdx])
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ util.errorRequest(client, request, err)
+ }
+ }
+}
+
+function onHttp2SocketClose () {
+ const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
+
+ const client = this[kHTTP2Session][kClient]
+
+ client[kSocket] = null
+ client[kHTTPContext] = null
+
+ if (this[kHTTP2Session] !== null) {
+ this[kHTTP2Session].destroy(err)
+ }
+
+ client[kPendingIdx] = client[kRunningIdx]
+
+ assert(client[kRunning] === 0)
+
+ client.emit('disconnect', client[kUrl], [client], err)
+
+ client[kResume]()
+}
+
+function onHttp2SocketError (err) {
+ assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
+
+ this[kError] = err
+
+ this[kClient][kOnError](err)
+}
+
+function onHttp2SocketEnd () {
+ util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
+}
+
+function onSocketClose () {
+ this[kClosed] = true
+}
+
+// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
+function shouldSendContentLength (method) {
+ return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
+}
+
+function writeH2 (client, request) {
+ const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
+ const session = client[kHTTP2Session]
+ const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
+ let { body } = request
+
+ if (upgrade != null && upgrade !== 'websocket') {
+ util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
+ return false
+ }
+
+ const headers = {}
+ for (let n = 0; n < reqHeaders.length; n += 2) {
+ const key = reqHeaders[n + 0]
+ const val = reqHeaders[n + 1]
+
+ if (key === 'cookie') {
+ if (headers[key] != null) {
+ headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val]
+ } else {
+ headers[key] = val
+ }
+
+ continue
+ }
+
+ if (Array.isArray(val)) {
+ for (let i = 0; i < val.length; i++) {
+ if (headers[key]) {
+ headers[key] += `, ${val[i]}`
+ } else {
+ headers[key] = val[i]
+ }
+ }
+ } else if (headers[key]) {
+ headers[key] += `, ${val}`
+ } else {
+ headers[key] = val
+ }
+ }
+
+ /** @type {import('node:http2').ClientHttp2Stream} */
+ let stream = null
+
+ const { hostname, port } = client[kUrl]
+
+ headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
+ headers[HTTP2_HEADER_METHOD] = method
+
+ const abort = (err) => {
+ if (request.aborted || request.completed) {
+ return
+ }
+
+ err = err || new RequestAbortedError()
+
+ util.errorRequest(client, request, err)
+
+ if (stream != null) {
+ // Some chunks might still come after abort,
+ // let's ignore them
+ stream.removeAllListeners('data')
+
+ // On Abort, we close the stream to send RST_STREAM frame
+ stream.close()
+
+ // We move the running index to the next request
+ client[kOnError](err)
+ client[kResume]()
+ }
+
+ // We do not destroy the socket as we can continue using the session
+ // the stream gets destroyed and the session remains to create new streams
+ util.destroy(body, err)
+ }
+
+ try {
+ // We are already connected, streams are pending.
+ // We can call on connect, and wait for abort
+ request.onConnect(abort)
+ } catch (err) {
+ util.errorRequest(client, request, err)
+ }
+
+ if (request.aborted) {
+ return false
+ }
+
+ if (upgrade || method === 'CONNECT') {
+ session.ref()
+
+ if (upgrade === 'websocket') {
+ // We cannot upgrade to websocket if extended CONNECT protocol is not supported
+ if (session[kEnableConnectProtocol] === false) {
+ util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
+ session.unref()
+ return false
+ }
+
+ // We force the method to CONNECT
+ // as per RFC-8441
+ // https://datatracker.ietf.org/doc/html/rfc8441#section-4
+ headers[HTTP2_HEADER_METHOD] = 'CONNECT'
+ headers[HTTP2_HEADER_PROTOCOL] = 'websocket'
+ // :path and :scheme headers must be omitted when sending CONNECT but set if extended-CONNECT
+ headers[HTTP2_HEADER_PATH] = path
+
+ if (protocol === 'ws:' || protocol === 'wss:') {
+ headers[HTTP2_HEADER_SCHEME] = protocol === 'ws:' ? 'http' : 'https'
+ } else {
+ headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
+ }
+
+ stream = session.request(headers, { endStream: false, signal })
+ stream[kHTTP2Stream] = true
+
+ stream.once('response', (headers, _flags) => {
+ const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
+
+ request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
+
+ ++session[kOpenStreams]
+ client[kQueue][client[kRunningIdx]++] = null
+ })
+
+ stream.on('error', () => {
+ if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) {
+ // NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8)
+ // We do not treat those as errors as the server might
+ // not support websockets and refuse the stream
+ abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
+ }
+ })
+
+ stream.once('close', () => {
+ session[kOpenStreams] -= 1
+ if (session[kOpenStreams] === 0) session.unref()
+ })
+
+ stream.setTimeout(requestTimeout)
+ return true
+ }
+
+ // TODO: consolidate once we support CONNECT properly
+ // NOTE: We are already connected, streams are pending, first request
+ // will create a new stream. We trigger a request to create the stream and wait until
+ // `ready` event is triggered
+ // We disabled endStream to allow the user to write to the stream
+ stream = session.request(headers, { endStream: false, signal })
+ stream[kHTTP2Stream] = true
+ stream.on('response', headers => {
+ const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
+
+ request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
+ ++session[kOpenStreams]
+ client[kQueue][client[kRunningIdx]++] = null
+ })
+ stream.once('close', () => {
+ session[kOpenStreams] -= 1
+ if (session[kOpenStreams] === 0) session.unref()
+ })
+ stream.setTimeout(requestTimeout)
+
+ return true
+ }
+
+ // https://tools.ietf.org/html/rfc7540#section-8.3
+ // :path and :scheme headers must be omitted when sending CONNECT
+ headers[HTTP2_HEADER_PATH] = path
+ headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
+
+ // https://tools.ietf.org/html/rfc7231#section-4.3.1
+ // https://tools.ietf.org/html/rfc7231#section-4.3.2
+ // https://tools.ietf.org/html/rfc7231#section-4.3.5
+
+ // Sending a payload body on a request that does not
+ // expect it can cause undefined behavior on some
+ // servers and corrupt connection state. Do not
+ // re-use the connection for further requests.
+
+ const expectsPayload = (
+ method === 'PUT' ||
+ method === 'POST' ||
+ method === 'PATCH'
+ )
+
+ if (body && typeof body.read === 'function') {
+ // Try to read EOF in order to get length.
+ body.read(0)
+ }
+
+ let contentLength = util.bodyLength(body)
+
+ if (util.isFormDataLike(body)) {
+ extractBody ??= require('../web/fetch/body.js').extractBody
+
+ const [bodyStream, contentType] = extractBody(body)
+ headers['content-type'] = contentType
+
+ body = bodyStream.stream
+ contentLength = bodyStream.length
+ }
+
+ if (contentLength == null) {
+ contentLength = request.contentLength
+ }
+
+ if (!expectsPayload) {
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
+ // A user agent SHOULD NOT send a Content-Length header field when
+ // the request message does not contain a payload body and the method
+ // semantics do not anticipate such a body.
+ // And for methods that don't expect a payload, omit Content-Length.
+ contentLength = null
+ }
+
+ // https://github.com/nodejs/undici/issues/2046
+ // A user agent may send a Content-Length header with 0 value, this should be allowed.
+ if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
+ if (client[kStrictContentLength]) {
+ util.errorRequest(client, request, new RequestContentLengthMismatchError())
+ return false
+ }
+
+ process.emitWarning(new RequestContentLengthMismatchError())
+ }
+
+ if (contentLength != null) {
+ assert(body || contentLength === 0, 'no body must not have content length')
+ headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
+ }
+
+ session.ref()
+
+ if (channels.sendHeaders.hasSubscribers) {
+ let header = ''
+ for (const key in headers) {
+ header += `${key}: ${headers[key]}\r\n`
+ }
+ channels.sendHeaders.publish({ request, headers: header, socket: session[kSocket] })
+ }
+
+ // TODO(metcoder95): add support for sending trailers
+ const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
+ if (expectContinue) {
+ headers[HTTP2_HEADER_EXPECT] = '100-continue'
+ stream = session.request(headers, { endStream: shouldEndStream, signal })
+ stream[kHTTP2Stream] = true
+
+ stream.once('continue', writeBodyH2)
+ } else {
+ stream = session.request(headers, {
+ endStream: shouldEndStream,
+ signal
+ })
+ stream[kHTTP2Stream] = true
+
+ writeBodyH2()
+ }
+
+ // Increment counter as we have new streams open
+ ++session[kOpenStreams]
+ stream.setTimeout(requestTimeout)
+
+ // Track whether we received a response (headers)
+ let responseReceived = false
+
+ stream.once('response', headers => {
+ const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
+ request.onResponseStarted()
+ responseReceived = true
+
+ // Due to the stream nature, it is possible we face a race condition
+ // where the stream has been assigned, but the request has been aborted
+ // the request remains in-flight and headers hasn't been received yet
+ // for those scenarios, best effort is to destroy the stream immediately
+ // as there's no value to keep it open.
+ if (request.aborted) {
+ stream.removeAllListeners('data')
+ return
+ }
+
+ if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
+ stream.pause()
+ }
+ })
+
+ stream.on('data', (chunk) => {
+ if (request.onData(chunk) === false) {
+ stream.pause()
+ }
+ })
+
+ stream.once('end', () => {
+ stream.removeAllListeners('data')
+ // If we received a response, this is a normal completion
+ if (responseReceived) {
+ if (!request.aborted && !request.completed) {
+ request.onComplete({})
+ }
+
+ client[kQueue][client[kRunningIdx]++] = null
+ client[kResume]()
+ } else {
+ // Stream ended without receiving a response - this is an error
+ // (e.g., server destroyed the stream before sending headers)
+ abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
+ client[kQueue][client[kRunningIdx]++] = null
+ client[kPendingIdx] = client[kRunningIdx]
+ client[kResume]()
+ }
+ })
+
+ stream.once('close', () => {
+ stream.removeAllListeners('data')
+ session[kOpenStreams] -= 1
+ if (session[kOpenStreams] === 0) {
+ session.unref()
+ }
+ })
+
+ stream.once('error', function (err) {
+ stream.removeAllListeners('data')
+ abort(err)
+ })
+
+ stream.once('frameError', (type, code) => {
+ stream.removeAllListeners('data')
+ abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
+ })
+
+ stream.on('aborted', () => {
+ stream.removeAllListeners('data')
+ })
+
+ stream.on('timeout', () => {
+ const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
+ stream.removeAllListeners('data')
+ session[kOpenStreams] -= 1
+
+ if (session[kOpenStreams] === 0) {
+ session.unref()
+ }
+
+ abort(err)
+ })
+
+ stream.once('trailers', trailers => {
+ if (request.aborted || request.completed) {
+ return
+ }
+
+ request.onComplete(trailers)
+ })
+
+ return true
+
+ function writeBodyH2 () {
+ if (!body || contentLength === 0) {
+ writeBuffer(
+ abort,
+ stream,
+ null,
+ client,
+ request,
+ client[kSocket],
+ contentLength,
+ expectsPayload
+ )
+ } else if (util.isBuffer(body)) {
+ writeBuffer(
+ abort,
+ stream,
+ body,
+ client,
+ request,
+ client[kSocket],
+ contentLength,
+ expectsPayload
+ )
+ } else if (util.isBlobLike(body)) {
+ if (typeof body.stream === 'function') {
+ writeIterable(
+ abort,
+ stream,
+ body.stream(),
+ client,
+ request,
+ client[kSocket],
+ contentLength,
+ expectsPayload
+ )
+ } else {
+ writeBlob(
+ abort,
+ stream,
+ body,
+ client,
+ request,
+ client[kSocket],
+ contentLength,
+ expectsPayload
+ )
+ }
+ } else if (util.isStream(body)) {
+ writeStream(
+ abort,
+ client[kSocket],
+ expectsPayload,
+ stream,
+ body,
+ client,
+ request,
+ contentLength
+ )
+ } else if (util.isIterable(body)) {
+ writeIterable(
+ abort,
+ stream,
+ body,
+ client,
+ request,
+ client[kSocket],
+ contentLength,
+ expectsPayload
+ )
+ } else {
+ assert(false)
+ }
+ }
+}
+
+function writeBuffer (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
+ try {
+ if (body != null && util.isBuffer(body)) {
+ assert(contentLength === body.byteLength, 'buffer body must have content length')
+ h2stream.cork()
+ h2stream.write(body)
+ h2stream.uncork()
+ h2stream.end()
+
+ request.onBodySent(body)
+ }
+
+ if (!expectsPayload) {
+ socket[kReset] = true
+ }
+
+ request.onRequestSent()
+ client[kResume]()
+ } catch (error) {
+ abort(error)
+ }
+}
+
+function writeStream (abort, socket, expectsPayload, h2stream, body, client, request, contentLength) {
+ assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
+
+ // For HTTP/2, is enough to pipe the stream
+ const pipe = pipeline(
+ body,
+ h2stream,
+ (err) => {
+ if (err) {
+ util.destroy(pipe, err)
+ abort(err)
+ } else {
+ util.removeAllListeners(pipe)
+ request.onRequestSent()
+
+ if (!expectsPayload) {
+ socket[kReset] = true
+ }
+
+ client[kResume]()
+ }
+ }
+ )
+
+ util.addListener(pipe, 'data', onPipeData)
+
+ function onPipeData (chunk) {
+ request.onBodySent(chunk)
+ }
+}
+
+async function writeBlob (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
+ assert(contentLength === body.size, 'blob body must have content length')
+
+ try {
+ if (contentLength != null && contentLength !== body.size) {
+ throw new RequestContentLengthMismatchError()
+ }
+
+ const buffer = Buffer.from(await body.arrayBuffer())
+
+ h2stream.cork()
+ h2stream.write(buffer)
+ h2stream.uncork()
+ h2stream.end()
+
+ request.onBodySent(buffer)
+ request.onRequestSent()
+
+ if (!expectsPayload) {
+ socket[kReset] = true
+ }
+
+ client[kResume]()
+ } catch (err) {
+ abort(err)
+ }
+}
+
+async function writeIterable (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
+ assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
+
+ let callback = null
+ function onDrain () {
+ if (callback) {
+ const cb = callback
+ callback = null
+ cb()
+ }
+ }
+
+ const waitForDrain = () => new Promise((resolve, reject) => {
+ assert(callback === null)
+
+ if (socket[kError]) {
+ reject(socket[kError])
+ } else {
+ callback = resolve
+ }
+ })
+
+ h2stream
+ .on('close', onDrain)
+ .on('drain', onDrain)
+
+ try {
+ // It's up to the user to somehow abort the async iterable.
+ for await (const chunk of body) {
+ if (socket[kError]) {
+ throw socket[kError]
+ }
+
+ const res = h2stream.write(chunk)
+ request.onBodySent(chunk)
+ if (!res) {
+ await waitForDrain()
+ }
+ }
+
+ h2stream.end()
+
+ request.onRequestSent()
+
+ if (!expectsPayload) {
+ socket[kReset] = true
+ }
+
+ client[kResume]()
+ } catch (err) {
+ abort(err)
+ } finally {
+ h2stream
+ .off('close', onDrain)
+ .off('drain', onDrain)
+ }
+}
+
+module.exports = connectH2
diff --git a/vanilla/node_modules/undici/lib/dispatcher/client.js b/vanilla/node_modules/undici/lib/dispatcher/client.js
new file mode 100644
index 0000000..101acb6
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/client.js
@@ -0,0 +1,647 @@
+'use strict'
+
+const assert = require('node:assert')
+const net = require('node:net')
+const http = require('node:http')
+const util = require('../core/util.js')
+const { ClientStats } = require('../util/stats.js')
+const { channels } = require('../core/diagnostics.js')
+const Request = require('../core/request.js')
+const DispatcherBase = require('./dispatcher-base')
+const {
+ InvalidArgumentError,
+ InformationalError,
+ ClientDestroyedError
+} = require('../core/errors.js')
+const buildConnector = require('../core/connect.js')
+const {
+ kUrl,
+ kServerName,
+ kClient,
+ kBusy,
+ kConnect,
+ kResuming,
+ kRunning,
+ kPending,
+ kSize,
+ kQueue,
+ kConnected,
+ kConnecting,
+ kNeedDrain,
+ kKeepAliveDefaultTimeout,
+ kHostHeader,
+ kPendingIdx,
+ kRunningIdx,
+ kError,
+ kPipelining,
+ kKeepAliveTimeoutValue,
+ kMaxHeadersSize,
+ kKeepAliveMaxTimeout,
+ kKeepAliveTimeoutThreshold,
+ kHeadersTimeout,
+ kBodyTimeout,
+ kStrictContentLength,
+ kConnector,
+ kMaxRequests,
+ kCounter,
+ kClose,
+ kDestroy,
+ kDispatch,
+ kLocalAddress,
+ kMaxResponseSize,
+ kOnError,
+ kHTTPContext,
+ kMaxConcurrentStreams,
+ kHTTP2InitialWindowSize,
+ kHTTP2ConnectionWindowSize,
+ kResume,
+ kPingInterval
+} = require('../core/symbols.js')
+const connectH1 = require('./client-h1.js')
+const connectH2 = require('./client-h2.js')
+
+const kClosedResolve = Symbol('kClosedResolve')
+
+const getDefaultNodeMaxHeaderSize = http &&
+ http.maxHeaderSize &&
+ Number.isInteger(http.maxHeaderSize) &&
+ http.maxHeaderSize > 0
+ ? () => http.maxHeaderSize
+ : () => { throw new InvalidArgumentError('http module not available or http.maxHeaderSize invalid') }
+
+const noop = () => {}
+
+function getPipelining (client) {
+ return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1
+}
+
+/**
+ * @type {import('../../types/client.js').default}
+ */
+class Client extends DispatcherBase {
+ /**
+ *
+ * @param {string|URL} url
+ * @param {import('../../types/client.js').Client.Options} options
+ */
+ constructor (url, {
+ maxHeaderSize,
+ headersTimeout,
+ socketTimeout,
+ requestTimeout,
+ connectTimeout,
+ bodyTimeout,
+ idleTimeout,
+ keepAlive,
+ keepAliveTimeout,
+ maxKeepAliveTimeout,
+ keepAliveMaxTimeout,
+ keepAliveTimeoutThreshold,
+ socketPath,
+ pipelining,
+ tls,
+ strictContentLength,
+ maxCachedSessions,
+ connect,
+ maxRequestsPerClient,
+ localAddress,
+ maxResponseSize,
+ autoSelectFamily,
+ autoSelectFamilyAttemptTimeout,
+ // h2
+ maxConcurrentStreams,
+ allowH2,
+ useH2c,
+ initialWindowSize,
+ connectionWindowSize,
+ pingInterval
+ } = {}) {
+ if (keepAlive !== undefined) {
+ throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
+ }
+
+ if (socketTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead')
+ }
+
+ if (requestTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead')
+ }
+
+ if (idleTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead')
+ }
+
+ if (maxKeepAliveTimeout !== undefined) {
+ throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
+ }
+
+ if (maxHeaderSize != null) {
+ if (!Number.isInteger(maxHeaderSize) || maxHeaderSize < 1) {
+ throw new InvalidArgumentError('invalid maxHeaderSize')
+ }
+ } else {
+ // If maxHeaderSize is not provided, use the default value from the http module
+ // or if that is not available, throw an error.
+ maxHeaderSize = getDefaultNodeMaxHeaderSize()
+ }
+
+ if (socketPath != null && typeof socketPath !== 'string') {
+ throw new InvalidArgumentError('invalid socketPath')
+ }
+
+ if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) {
+ throw new InvalidArgumentError('invalid connectTimeout')
+ }
+
+ if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) {
+ throw new InvalidArgumentError('invalid keepAliveTimeout')
+ }
+
+ if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) {
+ throw new InvalidArgumentError('invalid keepAliveMaxTimeout')
+ }
+
+ if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) {
+ throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold')
+ }
+
+ if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError('headersTimeout must be a positive integer or zero')
+ }
+
+ if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero')
+ }
+
+ if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
+ throw new InvalidArgumentError('connect must be a function or an object')
+ }
+
+ if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) {
+ throw new InvalidArgumentError('maxRequestsPerClient must be a positive number')
+ }
+
+ if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) {
+ throw new InvalidArgumentError('localAddress must be valid string IP address')
+ }
+
+ if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) {
+ throw new InvalidArgumentError('maxResponseSize must be a positive number')
+ }
+
+ if (
+ autoSelectFamilyAttemptTimeout != null &&
+ (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1)
+ ) {
+ throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number')
+ }
+
+ // h2
+ if (allowH2 != null && typeof allowH2 !== 'boolean') {
+ throw new InvalidArgumentError('allowH2 must be a valid boolean value')
+ }
+
+ if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) {
+ throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0')
+ }
+
+ if (useH2c != null && typeof useH2c !== 'boolean') {
+ throw new InvalidArgumentError('useH2c must be a valid boolean value')
+ }
+
+ if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) {
+ throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0')
+ }
+
+ if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) {
+ throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0')
+ }
+
+ if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) {
+ throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
+ }
+
+ super()
+
+ if (typeof connect !== 'function') {
+ connect = buildConnector({
+ ...tls,
+ maxCachedSessions,
+ allowH2,
+ useH2c,
+ socketPath,
+ timeout: connectTimeout,
+ ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
+ ...connect
+ })
+ }
+
+ this[kUrl] = util.parseOrigin(url)
+ this[kConnector] = connect
+ this[kPipelining] = pipelining != null ? pipelining : 1
+ this[kMaxHeadersSize] = maxHeaderSize
+ this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout
+ this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout
+ this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 2e3 : keepAliveTimeoutThreshold
+ this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout]
+ this[kServerName] = null
+ this[kLocalAddress] = localAddress != null ? localAddress : null
+ this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming
+ this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming
+ this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n`
+ this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3
+ this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3
+ this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength
+ this[kMaxRequests] = maxRequestsPerClient
+ this[kClosedResolve] = null
+ this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
+ this[kHTTPContext] = null
+ // h2
+ this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
+ // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance:
+ // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1)
+ // Allows more data to be sent before requiring acknowledgment, improving throughput
+ // especially on high-latency networks. This matches common production HTTP/2 servers.
+ // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set)
+ // Provides better flow control for the entire connection across multiple streams.
+ this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144
+ this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288
+ this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute
+
+ // kQueue is built up of 3 sections separated by
+ // the kRunningIdx and kPendingIdx indices.
+ // | complete | running | pending |
+ // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length
+ // kRunningIdx points to the first running element.
+ // kPendingIdx points to the first pending element.
+ // This implements a fast queue with an amortized
+ // time of O(1).
+
+ this[kQueue] = []
+ this[kRunningIdx] = 0
+ this[kPendingIdx] = 0
+
+ this[kResume] = (sync) => resume(this, sync)
+ this[kOnError] = (err) => onError(this, err)
+ }
+
+ get pipelining () {
+ return this[kPipelining]
+ }
+
+ set pipelining (value) {
+ this[kPipelining] = value
+ this[kResume](true)
+ }
+
+ get stats () {
+ return new ClientStats(this)
+ }
+
+ get [kPending] () {
+ return this[kQueue].length - this[kPendingIdx]
+ }
+
+ get [kRunning] () {
+ return this[kPendingIdx] - this[kRunningIdx]
+ }
+
+ get [kSize] () {
+ return this[kQueue].length - this[kRunningIdx]
+ }
+
+ get [kConnected] () {
+ return !!this[kHTTPContext] && !this[kConnecting] && !this[kHTTPContext].destroyed
+ }
+
+ get [kBusy] () {
+ return Boolean(
+ this[kHTTPContext]?.busy(null) ||
+ (this[kSize] >= (getPipelining(this) || 1)) ||
+ this[kPending] > 0
+ )
+ }
+
+ [kConnect] (cb) {
+ connect(this)
+ this.once('connect', cb)
+ }
+
+ [kDispatch] (opts, handler) {
+ const request = new Request(this[kUrl].origin, opts, handler)
+
+ this[kQueue].push(request)
+ if (this[kResuming]) {
+ // Do nothing.
+ } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) {
+ // Wait a tick in case stream/iterator is ended in the same tick.
+ this[kResuming] = 1
+ queueMicrotask(() => resume(this))
+ } else {
+ this[kResume](true)
+ }
+
+ if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) {
+ this[kNeedDrain] = 2
+ }
+
+ return this[kNeedDrain] < 2
+ }
+
+ [kClose] () {
+ // TODO: for H2 we need to gracefully flush the remaining enqueued
+ // request and close each stream.
+ return new Promise((resolve) => {
+ if (this[kSize]) {
+ this[kClosedResolve] = resolve
+ } else {
+ resolve(null)
+ }
+ })
+ }
+
+ [kDestroy] (err) {
+ return new Promise((resolve) => {
+ const requests = this[kQueue].splice(this[kPendingIdx])
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ util.errorRequest(this, request, err)
+ }
+
+ const callback = () => {
+ if (this[kClosedResolve]) {
+ // TODO (fix): Should we error here with ClientDestroyedError?
+ this[kClosedResolve]()
+ this[kClosedResolve] = null
+ }
+ resolve(null)
+ }
+
+ if (this[kHTTPContext]) {
+ this[kHTTPContext].destroy(err, callback)
+ this[kHTTPContext] = null
+ } else {
+ queueMicrotask(callback)
+ }
+
+ this[kResume]()
+ })
+ }
+}
+
+function onError (client, err) {
+ if (
+ client[kRunning] === 0 &&
+ err.code !== 'UND_ERR_INFO' &&
+ err.code !== 'UND_ERR_SOCKET'
+ ) {
+ // Error is not caused by running request and not a recoverable
+ // socket error.
+
+ assert(client[kPendingIdx] === client[kRunningIdx])
+
+ const requests = client[kQueue].splice(client[kRunningIdx])
+
+ for (let i = 0; i < requests.length; i++) {
+ const request = requests[i]
+ util.errorRequest(client, request, err)
+ }
+ assert(client[kSize] === 0)
+ }
+}
+
+/**
+ * @param {Client} client
+ * @returns {void}
+ */
+function connect (client) {
+ assert(!client[kConnecting])
+ assert(!client[kHTTPContext])
+
+ let { host, hostname, protocol, port } = client[kUrl]
+
+ // Resolve ipv6
+ if (hostname[0] === '[') {
+ const idx = hostname.indexOf(']')
+
+ assert(idx !== -1)
+ const ip = hostname.substring(1, idx)
+
+ assert(net.isIPv6(ip))
+ hostname = ip
+ }
+
+ client[kConnecting] = true
+
+ if (channels.beforeConnect.hasSubscribers) {
+ channels.beforeConnect.publish({
+ connectParams: {
+ host,
+ hostname,
+ protocol,
+ port,
+ version: client[kHTTPContext]?.version,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ },
+ connector: client[kConnector]
+ })
+ }
+
+ client[kConnector]({
+ host,
+ hostname,
+ protocol,
+ port,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ }, (err, socket) => {
+ if (err) {
+ handleConnectError(client, err, { host, hostname, protocol, port })
+ client[kResume]()
+ return
+ }
+
+ if (client.destroyed) {
+ util.destroy(socket.on('error', noop), new ClientDestroyedError())
+ client[kResume]()
+ return
+ }
+
+ assert(socket)
+
+ try {
+ client[kHTTPContext] = socket.alpnProtocol === 'h2'
+ ? connectH2(client, socket)
+ : connectH1(client, socket)
+ } catch (err) {
+ socket.destroy().on('error', noop)
+ handleConnectError(client, err, { host, hostname, protocol, port })
+ client[kResume]()
+ return
+ }
+
+ client[kConnecting] = false
+
+ socket[kCounter] = 0
+ socket[kMaxRequests] = client[kMaxRequests]
+ socket[kClient] = client
+ socket[kError] = null
+
+ if (channels.connected.hasSubscribers) {
+ channels.connected.publish({
+ connectParams: {
+ host,
+ hostname,
+ protocol,
+ port,
+ version: client[kHTTPContext]?.version,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ },
+ connector: client[kConnector],
+ socket
+ })
+ }
+
+ client.emit('connect', client[kUrl], [client])
+ client[kResume]()
+ })
+}
+
+function handleConnectError (client, err, { host, hostname, protocol, port }) {
+ if (client.destroyed) {
+ return
+ }
+
+ client[kConnecting] = false
+
+ if (channels.connectError.hasSubscribers) {
+ channels.connectError.publish({
+ connectParams: {
+ host,
+ hostname,
+ protocol,
+ port,
+ version: client[kHTTPContext]?.version,
+ servername: client[kServerName],
+ localAddress: client[kLocalAddress]
+ },
+ connector: client[kConnector],
+ error: err
+ })
+ }
+
+ if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
+ assert(client[kRunning] === 0)
+ while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
+ const request = client[kQueue][client[kPendingIdx]++]
+ util.errorRequest(client, request, err)
+ }
+ } else {
+ onError(client, err)
+ }
+
+ client.emit('connectionError', client[kUrl], [client], err)
+}
+
+function emitDrain (client) {
+ client[kNeedDrain] = 0
+ client.emit('drain', client[kUrl], [client])
+}
+
+function resume (client, sync) {
+ if (client[kResuming] === 2) {
+ return
+ }
+
+ client[kResuming] = 2
+
+ _resume(client, sync)
+ client[kResuming] = 0
+
+ if (client[kRunningIdx] > 256) {
+ client[kQueue].splice(0, client[kRunningIdx])
+ client[kPendingIdx] -= client[kRunningIdx]
+ client[kRunningIdx] = 0
+ }
+}
+
+function _resume (client, sync) {
+ while (true) {
+ if (client.destroyed) {
+ assert(client[kPending] === 0)
+ return
+ }
+
+ if (client[kClosedResolve] && !client[kSize]) {
+ client[kClosedResolve]()
+ client[kClosedResolve] = null
+ return
+ }
+
+ if (client[kHTTPContext]) {
+ client[kHTTPContext].resume()
+ }
+
+ if (client[kBusy]) {
+ client[kNeedDrain] = 2
+ } else if (client[kNeedDrain] === 2) {
+ if (sync) {
+ client[kNeedDrain] = 1
+ queueMicrotask(() => emitDrain(client))
+ } else {
+ emitDrain(client)
+ }
+ continue
+ }
+
+ if (client[kPending] === 0) {
+ return
+ }
+
+ if (client[kRunning] >= (getPipelining(client) || 1)) {
+ return
+ }
+
+ const request = client[kQueue][client[kPendingIdx]]
+
+ if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) {
+ if (client[kRunning] > 0) {
+ return
+ }
+
+ client[kServerName] = request.servername
+ client[kHTTPContext]?.destroy(new InformationalError('servername changed'), () => {
+ client[kHTTPContext] = null
+ resume(client)
+ })
+ }
+
+ if (client[kConnecting]) {
+ return
+ }
+
+ if (!client[kHTTPContext]) {
+ connect(client)
+ return
+ }
+
+ if (client[kHTTPContext].destroyed) {
+ return
+ }
+
+ if (client[kHTTPContext].busy(request)) {
+ return
+ }
+
+ if (!request.aborted && client[kHTTPContext].write(request)) {
+ client[kPendingIdx]++
+ } else {
+ client[kQueue].splice(client[kPendingIdx], 1)
+ }
+ }
+}
+
+module.exports = Client
diff --git a/vanilla/node_modules/undici/lib/dispatcher/dispatcher-base.js b/vanilla/node_modules/undici/lib/dispatcher/dispatcher-base.js
new file mode 100644
index 0000000..a6f4710
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/dispatcher-base.js
@@ -0,0 +1,165 @@
+'use strict'
+
+const Dispatcher = require('./dispatcher')
+const UnwrapHandler = require('../handler/unwrap-handler')
+const {
+ ClientDestroyedError,
+ ClientClosedError,
+ InvalidArgumentError
+} = require('../core/errors')
+const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/symbols')
+
+const kOnDestroyed = Symbol('onDestroyed')
+const kOnClosed = Symbol('onClosed')
+
+class DispatcherBase extends Dispatcher {
+ /** @type {boolean} */
+ [kDestroyed] = false;
+
+ /** @type {Array<Function|null} */
+ [kOnDestroyed] = null;
+
+ /** @type {boolean} */
+ [kClosed] = false;
+
+ /** @type {Array<Function>|null} */
+ [kOnClosed] = null
+
+ /** @returns {boolean} */
+ get destroyed () {
+ return this[kDestroyed]
+ }
+
+ /** @returns {boolean} */
+ get closed () {
+ return this[kClosed]
+ }
+
+ close (callback) {
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ this.close((err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (this[kDestroyed]) {
+ const err = new ClientDestroyedError()
+ queueMicrotask(() => callback(err, null))
+ return
+ }
+
+ if (this[kClosed]) {
+ if (this[kOnClosed]) {
+ this[kOnClosed].push(callback)
+ } else {
+ queueMicrotask(() => callback(null, null))
+ }
+ return
+ }
+
+ this[kClosed] = true
+ this[kOnClosed] ??= []
+ this[kOnClosed].push(callback)
+
+ const onClosed = () => {
+ const callbacks = this[kOnClosed]
+ this[kOnClosed] = null
+ for (let i = 0; i < callbacks.length; i++) {
+ callbacks[i](null, null)
+ }
+ }
+
+ // Should not error.
+ this[kClose]()
+ .then(() => this.destroy())
+ .then(() => queueMicrotask(onClosed))
+ }
+
+ destroy (err, callback) {
+ if (typeof err === 'function') {
+ callback = err
+ err = null
+ }
+
+ if (callback === undefined) {
+ return new Promise((resolve, reject) => {
+ this.destroy(err, (err, data) => {
+ return err ? reject(err) : resolve(data)
+ })
+ })
+ }
+
+ if (typeof callback !== 'function') {
+ throw new InvalidArgumentError('invalid callback')
+ }
+
+ if (this[kDestroyed]) {
+ if (this[kOnDestroyed]) {
+ this[kOnDestroyed].push(callback)
+ } else {
+ queueMicrotask(() => callback(null, null))
+ }
+ return
+ }
+
+ if (!err) {
+ err = new ClientDestroyedError()
+ }
+
+ this[kDestroyed] = true
+ this[kOnDestroyed] ??= []
+ this[kOnDestroyed].push(callback)
+
+ const onDestroyed = () => {
+ const callbacks = this[kOnDestroyed]
+ this[kOnDestroyed] = null
+ for (let i = 0; i < callbacks.length; i++) {
+ callbacks[i](null, null)
+ }
+ }
+
+ // Should not error.
+ this[kDestroy](err)
+ .then(() => queueMicrotask(onDestroyed))
+ }
+
+ dispatch (opts, handler) {
+ if (!handler || typeof handler !== 'object') {
+ throw new InvalidArgumentError('handler must be an object')
+ }
+
+ handler = UnwrapHandler.unwrap(handler)
+
+ try {
+ if (!opts || typeof opts !== 'object') {
+ throw new InvalidArgumentError('opts must be an object.')
+ }
+
+ if (this[kDestroyed] || this[kOnDestroyed]) {
+ throw new ClientDestroyedError()
+ }
+
+ if (this[kClosed]) {
+ throw new ClientClosedError()
+ }
+
+ return this[kDispatch](opts, handler)
+ } catch (err) {
+ if (typeof handler.onError !== 'function') {
+ throw err
+ }
+
+ handler.onError(err)
+
+ return false
+ }
+ }
+}
+
+module.exports = DispatcherBase
diff --git a/vanilla/node_modules/undici/lib/dispatcher/dispatcher.js b/vanilla/node_modules/undici/lib/dispatcher/dispatcher.js
new file mode 100644
index 0000000..824dfb6
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/dispatcher.js
@@ -0,0 +1,48 @@
+'use strict'
+const EventEmitter = require('node:events')
+const WrapHandler = require('../handler/wrap-handler')
+
+const wrapInterceptor = (dispatch) => (opts, handler) => dispatch(opts, WrapHandler.wrap(handler))
+
+class Dispatcher extends EventEmitter {
+ dispatch () {
+ throw new Error('not implemented')
+ }
+
+ close () {
+ throw new Error('not implemented')
+ }
+
+ destroy () {
+ throw new Error('not implemented')
+ }
+
+ compose (...args) {
+ // So we handle [interceptor1, interceptor2] or interceptor1, interceptor2, ...
+ const interceptors = Array.isArray(args[0]) ? args[0] : args
+ let dispatch = this.dispatch.bind(this)
+
+ for (const interceptor of interceptors) {
+ if (interceptor == null) {
+ continue
+ }
+
+ if (typeof interceptor !== 'function') {
+ throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`)
+ }
+
+ dispatch = interceptor(dispatch)
+ dispatch = wrapInterceptor(dispatch)
+
+ if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
+ throw new TypeError('invalid interceptor')
+ }
+ }
+
+ return new Proxy(this, {
+ get: (target, key) => key === 'dispatch' ? dispatch : target[key]
+ })
+ }
+}
+
+module.exports = Dispatcher
diff --git a/vanilla/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js b/vanilla/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js
new file mode 100644
index 0000000..f88437f
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js
@@ -0,0 +1,146 @@
+'use strict'
+
+const DispatcherBase = require('./dispatcher-base')
+const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require('../core/symbols')
+const ProxyAgent = require('./proxy-agent')
+const Agent = require('./agent')
+
+const DEFAULT_PORTS = {
+ 'http:': 80,
+ 'https:': 443
+}
+
+class EnvHttpProxyAgent extends DispatcherBase {
+ #noProxyValue = null
+ #noProxyEntries = null
+ #opts = null
+
+ constructor (opts = {}) {
+ super()
+ this.#opts = opts
+
+ const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts
+
+ this[kNoProxyAgent] = new Agent(agentOpts)
+
+ const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY
+ if (HTTP_PROXY) {
+ this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY })
+ } else {
+ this[kHttpProxyAgent] = this[kNoProxyAgent]
+ }
+
+ const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY
+ if (HTTPS_PROXY) {
+ this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY })
+ } else {
+ this[kHttpsProxyAgent] = this[kHttpProxyAgent]
+ }
+
+ this.#parseNoProxy()
+ }
+
+ [kDispatch] (opts, handler) {
+ const url = new URL(opts.origin)
+ const agent = this.#getProxyAgentForUrl(url)
+ return agent.dispatch(opts, handler)
+ }
+
+ [kClose] () {
+ return Promise.all([
+ this[kNoProxyAgent].close(),
+ !this[kHttpProxyAgent][kClosed] && this[kHttpProxyAgent].close(),
+ !this[kHttpsProxyAgent][kClosed] && this[kHttpsProxyAgent].close()
+ ])
+ }
+
+ [kDestroy] (err) {
+ return Promise.all([
+ this[kNoProxyAgent].destroy(err),
+ !this[kHttpProxyAgent][kDestroyed] && this[kHttpProxyAgent].destroy(err),
+ !this[kHttpsProxyAgent][kDestroyed] && this[kHttpsProxyAgent].destroy(err)
+ ])
+ }
+
+ #getProxyAgentForUrl (url) {
+ let { protocol, host: hostname, port } = url
+
+ // Stripping ports in this way instead of using parsedUrl.hostname to make
+ // sure that the brackets around IPv6 addresses are kept.
+ hostname = hostname.replace(/:\d*$/, '').toLowerCase()
+ port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0
+ if (!this.#shouldProxy(hostname, port)) {
+ return this[kNoProxyAgent]
+ }
+ if (protocol === 'https:') {
+ return this[kHttpsProxyAgent]
+ }
+ return this[kHttpProxyAgent]
+ }
+
+ #shouldProxy (hostname, port) {
+ if (this.#noProxyChanged) {
+ this.#parseNoProxy()
+ }
+
+ if (this.#noProxyEntries.length === 0) {
+ return true // Always proxy if NO_PROXY is not set or empty.
+ }
+ if (this.#noProxyValue === '*') {
+ return false // Never proxy if wildcard is set.
+ }
+
+ for (let i = 0; i < this.#noProxyEntries.length; i++) {
+ const entry = this.#noProxyEntries[i]
+ if (entry.port && entry.port !== port) {
+ continue // Skip if ports don't match.
+ }
+ // Don't proxy if the hostname is equal with the no_proxy host.
+ if (hostname === entry.hostname) {
+ return false
+ }
+ // Don't proxy if the hostname is the subdomain of the no_proxy host.
+ // Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485
+ if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ #parseNoProxy () {
+ const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv
+ const noProxySplit = noProxyValue.split(/[,\s]/)
+ const noProxyEntries = []
+
+ for (let i = 0; i < noProxySplit.length; i++) {
+ const entry = noProxySplit[i]
+ if (!entry) {
+ continue
+ }
+ const parsed = entry.match(/^(.+):(\d+)$/)
+ noProxyEntries.push({
+ // strip leading dot or asterisk with dot
+ hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(),
+ port: parsed ? Number.parseInt(parsed[2], 10) : 0
+ })
+ }
+
+ this.#noProxyValue = noProxyValue
+ this.#noProxyEntries = noProxyEntries
+ }
+
+ get #noProxyChanged () {
+ if (this.#opts.noProxy !== undefined) {
+ return false
+ }
+ return this.#noProxyValue !== this.#noProxyEnv
+ }
+
+ get #noProxyEnv () {
+ return process.env.no_proxy ?? process.env.NO_PROXY ?? ''
+ }
+}
+
+module.exports = EnvHttpProxyAgent
diff --git a/vanilla/node_modules/undici/lib/dispatcher/fixed-queue.js b/vanilla/node_modules/undici/lib/dispatcher/fixed-queue.js
new file mode 100644
index 0000000..f918849
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/fixed-queue.js
@@ -0,0 +1,135 @@
+'use strict'
+
+// Extracted from node/lib/internal/fixed_queue.js
+
+// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two.
+const kSize = 2048
+const kMask = kSize - 1
+
+// The FixedQueue is implemented as a singly-linked list of fixed-size
+// circular buffers. It looks something like this:
+//
+// head tail
+// | |
+// v v
+// +-----------+ <-----\ +-----------+ <------\ +-----------+
+// | [null] | \----- | next | \------- | next |
+// +-----------+ +-----------+ +-----------+
+// | item | <-- bottom | item | <-- bottom | undefined |
+// | item | | item | | undefined |
+// | item | | item | | undefined |
+// | item | | item | | undefined |
+// | item | | item | bottom --> | item |
+// | item | | item | | item |
+// | ... | | ... | | ... |
+// | item | | item | | item |
+// | item | | item | | item |
+// | undefined | <-- top | item | | item |
+// | undefined | | item | | item |
+// | undefined | | undefined | <-- top top --> | undefined |
+// +-----------+ +-----------+ +-----------+
+//
+// Or, if there is only one circular buffer, it looks something
+// like either of these:
+//
+// head tail head tail
+// | | | |
+// v v v v
+// +-----------+ +-----------+
+// | [null] | | [null] |
+// +-----------+ +-----------+
+// | undefined | | item |
+// | undefined | | item |
+// | item | <-- bottom top --> | undefined |
+// | item | | undefined |
+// | undefined | <-- top bottom --> | item |
+// | undefined | | item |
+// +-----------+ +-----------+
+//
+// Adding a value means moving `top` forward by one, removing means
+// moving `bottom` forward by one. After reaching the end, the queue
+// wraps around.
+//
+// When `top === bottom` the current queue is empty and when
+// `top + 1 === bottom` it's full. This wastes a single space of storage
+// but allows much quicker checks.
+
+/**
+ * @type {FixedCircularBuffer}
+ * @template T
+ */
+class FixedCircularBuffer {
+ /** @type {number} */
+ bottom = 0
+ /** @type {number} */
+ top = 0
+ /** @type {Array<T|undefined>} */
+ list = new Array(kSize).fill(undefined)
+ /** @type {T|null} */
+ next = null
+
+ /** @returns {boolean} */
+ isEmpty () {
+ return this.top === this.bottom
+ }
+
+ /** @returns {boolean} */
+ isFull () {
+ return ((this.top + 1) & kMask) === this.bottom
+ }
+
+ /**
+ * @param {T} data
+ * @returns {void}
+ */
+ push (data) {
+ this.list[this.top] = data
+ this.top = (this.top + 1) & kMask
+ }
+
+ /** @returns {T|null} */
+ shift () {
+ const nextItem = this.list[this.bottom]
+ if (nextItem === undefined) { return null }
+ this.list[this.bottom] = undefined
+ this.bottom = (this.bottom + 1) & kMask
+ return nextItem
+ }
+}
+
+/**
+ * @template T
+ */
+module.exports = class FixedQueue {
+ constructor () {
+ /** @type {FixedCircularBuffer<T>} */
+ this.head = this.tail = new FixedCircularBuffer()
+ }
+
+ /** @returns {boolean} */
+ isEmpty () {
+ return this.head.isEmpty()
+ }
+
+ /** @param {T} data */
+ push (data) {
+ if (this.head.isFull()) {
+ // Head is full: Creates a new queue, sets the old queue's `.next` to it,
+ // and sets it as the new main queue.
+ this.head = this.head.next = new FixedCircularBuffer()
+ }
+ this.head.push(data)
+ }
+
+ /** @returns {T|null} */
+ shift () {
+ const tail = this.tail
+ const next = tail.shift()
+ if (tail.isEmpty() && tail.next !== null) {
+ // If there is another queue, it forms the new tail.
+ this.tail = tail.next
+ tail.next = null
+ }
+ return next
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/dispatcher/h2c-client.js b/vanilla/node_modules/undici/lib/dispatcher/h2c-client.js
new file mode 100644
index 0000000..bd38522
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/h2c-client.js
@@ -0,0 +1,51 @@
+'use strict'
+
+const { InvalidArgumentError } = require('../core/errors')
+const Client = require('./client')
+
+class H2CClient extends Client {
+ constructor (origin, clientOpts) {
+ if (typeof origin === 'string') {
+ origin = new URL(origin)
+ }
+
+ if (origin.protocol !== 'http:') {
+ throw new InvalidArgumentError(
+ 'h2c-client: Only h2c protocol is supported'
+ )
+ }
+
+ const { connect, maxConcurrentStreams, pipelining, ...opts } =
+ clientOpts ?? {}
+ let defaultMaxConcurrentStreams = 100
+ let defaultPipelining = 100
+
+ if (
+ maxConcurrentStreams != null &&
+ Number.isInteger(maxConcurrentStreams) &&
+ maxConcurrentStreams > 0
+ ) {
+ defaultMaxConcurrentStreams = maxConcurrentStreams
+ }
+
+ if (pipelining != null && Number.isInteger(pipelining) && pipelining > 0) {
+ defaultPipelining = pipelining
+ }
+
+ if (defaultPipelining > defaultMaxConcurrentStreams) {
+ throw new InvalidArgumentError(
+ 'h2c-client: pipelining cannot be greater than maxConcurrentStreams'
+ )
+ }
+
+ super(origin, {
+ ...opts,
+ maxConcurrentStreams: defaultMaxConcurrentStreams,
+ pipelining: defaultPipelining,
+ allowH2: true,
+ useH2c: true
+ })
+ }
+}
+
+module.exports = H2CClient
diff --git a/vanilla/node_modules/undici/lib/dispatcher/pool-base.js b/vanilla/node_modules/undici/lib/dispatcher/pool-base.js
new file mode 100644
index 0000000..6c1f238
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/pool-base.js
@@ -0,0 +1,214 @@
+'use strict'
+
+const { PoolStats } = require('../util/stats.js')
+const DispatcherBase = require('./dispatcher-base')
+const FixedQueue = require('./fixed-queue')
+const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require('../core/symbols')
+
+const kClients = Symbol('clients')
+const kNeedDrain = Symbol('needDrain')
+const kQueue = Symbol('queue')
+const kClosedResolve = Symbol('closed resolve')
+const kOnDrain = Symbol('onDrain')
+const kOnConnect = Symbol('onConnect')
+const kOnDisconnect = Symbol('onDisconnect')
+const kOnConnectionError = Symbol('onConnectionError')
+const kGetDispatcher = Symbol('get dispatcher')
+const kAddClient = Symbol('add client')
+const kRemoveClient = Symbol('remove client')
+
+class PoolBase extends DispatcherBase {
+ [kQueue] = new FixedQueue();
+
+ [kQueued] = 0;
+
+ [kClients] = [];
+
+ [kNeedDrain] = false;
+
+ [kOnDrain] (client, origin, targets) {
+ const queue = this[kQueue]
+
+ let needDrain = false
+
+ while (!needDrain) {
+ const item = queue.shift()
+ if (!item) {
+ break
+ }
+ this[kQueued]--
+ needDrain = !client.dispatch(item.opts, item.handler)
+ }
+
+ client[kNeedDrain] = needDrain
+
+ if (!needDrain && this[kNeedDrain]) {
+ this[kNeedDrain] = false
+ this.emit('drain', origin, [this, ...targets])
+ }
+
+ if (this[kClosedResolve] && queue.isEmpty()) {
+ const closeAll = []
+ for (let i = 0; i < this[kClients].length; i++) {
+ const client = this[kClients][i]
+ if (!client.destroyed) {
+ closeAll.push(client.close())
+ }
+ }
+ return Promise.all(closeAll)
+ .then(this[kClosedResolve])
+ }
+ }
+
+ [kOnConnect] = (origin, targets) => {
+ this.emit('connect', origin, [this, ...targets])
+ };
+
+ [kOnDisconnect] = (origin, targets, err) => {
+ this.emit('disconnect', origin, [this, ...targets], err)
+ };
+
+ [kOnConnectionError] = (origin, targets, err) => {
+ this.emit('connectionError', origin, [this, ...targets], err)
+ }
+
+ get [kBusy] () {
+ return this[kNeedDrain]
+ }
+
+ get [kConnected] () {
+ let ret = 0
+ for (const { [kConnected]: connected } of this[kClients]) {
+ ret += connected
+ }
+ return ret
+ }
+
+ get [kFree] () {
+ let ret = 0
+ for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) {
+ ret += connected && !needDrain
+ }
+ return ret
+ }
+
+ get [kPending] () {
+ let ret = this[kQueued]
+ for (const { [kPending]: pending } of this[kClients]) {
+ ret += pending
+ }
+ return ret
+ }
+
+ get [kRunning] () {
+ let ret = 0
+ for (const { [kRunning]: running } of this[kClients]) {
+ ret += running
+ }
+ return ret
+ }
+
+ get [kSize] () {
+ let ret = this[kQueued]
+ for (const { [kSize]: size } of this[kClients]) {
+ ret += size
+ }
+ return ret
+ }
+
+ get stats () {
+ return new PoolStats(this)
+ }
+
+ [kClose] () {
+ if (this[kQueue].isEmpty()) {
+ const closeAll = []
+ for (let i = 0; i < this[kClients].length; i++) {
+ const client = this[kClients][i]
+ if (!client.destroyed) {
+ closeAll.push(client.close())
+ }
+ }
+ return Promise.all(closeAll)
+ } else {
+ return new Promise((resolve) => {
+ this[kClosedResolve] = resolve
+ })
+ }
+ }
+
+ [kDestroy] (err) {
+ while (true) {
+ const item = this[kQueue].shift()
+ if (!item) {
+ break
+ }
+ item.handler.onError(err)
+ }
+
+ const destroyAll = new Array(this[kClients].length)
+ for (let i = 0; i < this[kClients].length; i++) {
+ destroyAll[i] = this[kClients][i].destroy(err)
+ }
+ return Promise.all(destroyAll)
+ }
+
+ [kDispatch] (opts, handler) {
+ const dispatcher = this[kGetDispatcher]()
+
+ if (!dispatcher) {
+ this[kNeedDrain] = true
+ this[kQueue].push({ opts, handler })
+ this[kQueued]++
+ } else if (!dispatcher.dispatch(opts, handler)) {
+ dispatcher[kNeedDrain] = true
+ this[kNeedDrain] = !this[kGetDispatcher]()
+ }
+
+ return !this[kNeedDrain]
+ }
+
+ [kAddClient] (client) {
+ client
+ .on('drain', this[kOnDrain].bind(this, client))
+ .on('connect', this[kOnConnect])
+ .on('disconnect', this[kOnDisconnect])
+ .on('connectionError', this[kOnConnectionError])
+
+ this[kClients].push(client)
+
+ if (this[kNeedDrain]) {
+ queueMicrotask(() => {
+ if (this[kNeedDrain]) {
+ this[kOnDrain](client, client[kUrl], [client, this])
+ }
+ })
+ }
+
+ return this
+ }
+
+ [kRemoveClient] (client) {
+ client.close(() => {
+ const idx = this[kClients].indexOf(client)
+ if (idx !== -1) {
+ this[kClients].splice(idx, 1)
+ }
+ })
+
+ this[kNeedDrain] = this[kClients].some(dispatcher => (
+ !dispatcher[kNeedDrain] &&
+ dispatcher.closed !== true &&
+ dispatcher.destroyed !== true
+ ))
+ }
+}
+
+module.exports = {
+ PoolBase,
+ kClients,
+ kNeedDrain,
+ kAddClient,
+ kRemoveClient,
+ kGetDispatcher
+}
diff --git a/vanilla/node_modules/undici/lib/dispatcher/pool.js b/vanilla/node_modules/undici/lib/dispatcher/pool.js
new file mode 100644
index 0000000..77fd532
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/pool.js
@@ -0,0 +1,118 @@
+'use strict'
+
+const {
+ PoolBase,
+ kClients,
+ kNeedDrain,
+ kAddClient,
+ kGetDispatcher,
+ kRemoveClient
+} = require('./pool-base')
+const Client = require('./client')
+const {
+ InvalidArgumentError
+} = require('../core/errors')
+const util = require('../core/util')
+const { kUrl } = require('../core/symbols')
+const buildConnector = require('../core/connect')
+
+const kOptions = Symbol('options')
+const kConnections = Symbol('connections')
+const kFactory = Symbol('factory')
+
+function defaultFactory (origin, opts) {
+ return new Client(origin, opts)
+}
+
+class Pool extends PoolBase {
+ constructor (origin, {
+ connections,
+ factory = defaultFactory,
+ connect,
+ connectTimeout,
+ tls,
+ maxCachedSessions,
+ socketPath,
+ autoSelectFamily,
+ autoSelectFamilyAttemptTimeout,
+ allowH2,
+ clientTtl,
+ ...options
+ } = {}) {
+ if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
+ throw new InvalidArgumentError('invalid connections')
+ }
+
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('factory must be a function.')
+ }
+
+ if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
+ throw new InvalidArgumentError('connect must be a function or an object')
+ }
+
+ if (typeof connect !== 'function') {
+ connect = buildConnector({
+ ...tls,
+ maxCachedSessions,
+ allowH2,
+ socketPath,
+ timeout: connectTimeout,
+ ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
+ ...connect
+ })
+ }
+
+ super()
+
+ this[kConnections] = connections || null
+ this[kUrl] = util.parseOrigin(origin)
+ this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
+ this[kOptions].interceptors = options.interceptors
+ ? { ...options.interceptors }
+ : undefined
+ this[kFactory] = factory
+
+ this.on('connect', (origin, targets) => {
+ if (clientTtl != null && clientTtl > 0) {
+ for (const target of targets) {
+ Object.assign(target, { ttl: Date.now() })
+ }
+ }
+ })
+
+ this.on('connectionError', (origin, targets, error) => {
+ // If a connection error occurs, we remove the client from the pool,
+ // and emit a connectionError event. They will not be re-used.
+ // Fixes https://github.com/nodejs/undici/issues/3895
+ for (const target of targets) {
+ // Do not use kRemoveClient here, as it will close the client,
+ // but the client cannot be closed in this state.
+ const idx = this[kClients].indexOf(target)
+ if (idx !== -1) {
+ this[kClients].splice(idx, 1)
+ }
+ }
+ })
+ }
+
+ [kGetDispatcher] () {
+ const clientTtlOption = this[kOptions].clientTtl
+ for (const client of this[kClients]) {
+ // check ttl of client and if it's stale, remove it from the pool
+ if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
+ this[kRemoveClient](client)
+ } else if (!client[kNeedDrain]) {
+ return client
+ }
+ }
+
+ if (!this[kConnections] || this[kClients].length < this[kConnections]) {
+ const dispatcher = this[kFactory](this[kUrl], this[kOptions])
+ this[kAddClient](dispatcher)
+ return dispatcher
+ }
+ }
+}
+
+module.exports = Pool
diff --git a/vanilla/node_modules/undici/lib/dispatcher/proxy-agent.js b/vanilla/node_modules/undici/lib/dispatcher/proxy-agent.js
new file mode 100644
index 0000000..4403b8d
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/proxy-agent.js
@@ -0,0 +1,287 @@
+'use strict'
+
+const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
+const Agent = require('./agent')
+const Pool = require('./pool')
+const DispatcherBase = require('./dispatcher-base')
+const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
+const buildConnector = require('../core/connect')
+const Client = require('./client')
+const { channels } = require('../core/diagnostics')
+
+const kAgent = Symbol('proxy agent')
+const kClient = Symbol('proxy client')
+const kProxyHeaders = Symbol('proxy headers')
+const kRequestTls = Symbol('request tls settings')
+const kProxyTls = Symbol('proxy tls settings')
+const kConnectEndpoint = Symbol('connect endpoint function')
+const kTunnelProxy = Symbol('tunnel proxy')
+
+function defaultProtocolPort (protocol) {
+ return protocol === 'https:' ? 443 : 80
+}
+
+function defaultFactory (origin, opts) {
+ return new Pool(origin, opts)
+}
+
+const noop = () => {}
+
+function defaultAgentFactory (origin, opts) {
+ if (opts.connections === 1) {
+ return new Client(origin, opts)
+ }
+ return new Pool(origin, opts)
+}
+
+class Http1ProxyWrapper extends DispatcherBase {
+ #client
+
+ constructor (proxyUrl, { headers = {}, connect, factory }) {
+ if (!proxyUrl) {
+ throw new InvalidArgumentError('Proxy URL is mandatory')
+ }
+
+ super()
+
+ this[kProxyHeaders] = headers
+ if (factory) {
+ this.#client = factory(proxyUrl, { connect })
+ } else {
+ this.#client = new Client(proxyUrl, { connect })
+ }
+ }
+
+ [kDispatch] (opts, handler) {
+ const onHeaders = handler.onHeaders
+ handler.onHeaders = function (statusCode, data, resume) {
+ if (statusCode === 407) {
+ if (typeof handler.onError === 'function') {
+ handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
+ }
+ return
+ }
+ if (onHeaders) onHeaders.call(this, statusCode, data, resume)
+ }
+
+ // Rewrite request as an HTTP1 Proxy request, without tunneling.
+ const {
+ origin,
+ path = '/',
+ headers = {}
+ } = opts
+
+ opts.path = origin + path
+
+ if (!('host' in headers) && !('Host' in headers)) {
+ const { host } = new URL(origin)
+ headers.host = host
+ }
+ opts.headers = { ...this[kProxyHeaders], ...headers }
+
+ return this.#client[kDispatch](opts, handler)
+ }
+
+ [kClose] () {
+ return this.#client.close()
+ }
+
+ [kDestroy] (err) {
+ return this.#client.destroy(err)
+ }
+}
+
+class ProxyAgent extends DispatcherBase {
+ constructor (opts) {
+ if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
+ throw new InvalidArgumentError('Proxy uri is mandatory')
+ }
+
+ const { clientFactory = defaultFactory } = opts
+ if (typeof clientFactory !== 'function') {
+ throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
+ }
+
+ const { proxyTunnel = true } = opts
+
+ super()
+
+ const url = this.#getUrl(opts)
+ const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
+
+ this[kProxy] = { uri: href, protocol }
+ this[kRequestTls] = opts.requestTls
+ this[kProxyTls] = opts.proxyTls
+ this[kProxyHeaders] = opts.headers || {}
+ this[kTunnelProxy] = proxyTunnel
+
+ if (opts.auth && opts.token) {
+ throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
+ } else if (opts.auth) {
+ /* @deprecated in favour of opts.token */
+ this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
+ } else if (opts.token) {
+ this[kProxyHeaders]['proxy-authorization'] = opts.token
+ } else if (username && password) {
+ this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
+ }
+
+ const connect = buildConnector({ ...opts.proxyTls })
+ this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
+
+ const agentFactory = opts.factory || defaultAgentFactory
+ const factory = (origin, options) => {
+ const { protocol } = new URL(origin)
+ if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
+ return new Http1ProxyWrapper(this[kProxy].uri, {
+ headers: this[kProxyHeaders],
+ connect,
+ factory: agentFactory
+ })
+ }
+ return agentFactory(origin, options)
+ }
+ this[kClient] = clientFactory(url, { connect })
+ this[kAgent] = new Agent({
+ ...opts,
+ factory,
+ connect: async (opts, callback) => {
+ let requestedPath = opts.host
+ if (!opts.port) {
+ requestedPath += `:${defaultProtocolPort(opts.protocol)}`
+ }
+ try {
+ const connectParams = {
+ origin,
+ port,
+ path: requestedPath,
+ signal: opts.signal,
+ headers: {
+ ...this[kProxyHeaders],
+ host: opts.host,
+ ...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {})
+ },
+ servername: this[kProxyTls]?.servername || proxyHostname
+ }
+ const { socket, statusCode } = await this[kClient].connect(connectParams)
+ if (statusCode !== 200) {
+ socket.on('error', noop).destroy()
+ callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
+ return
+ }
+
+ if (channels.proxyConnected.hasSubscribers) {
+ channels.proxyConnected.publish({
+ socket,
+ connectParams
+ })
+ }
+
+ if (opts.protocol !== 'https:') {
+ callback(null, socket)
+ return
+ }
+ let servername
+ if (this[kRequestTls]) {
+ servername = this[kRequestTls].servername
+ } else {
+ servername = opts.servername
+ }
+ this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
+ } catch (err) {
+ if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
+ // Throw a custom error to avoid loop in client.js#connect
+ callback(new SecureProxyConnectionError(err))
+ } else {
+ callback(err)
+ }
+ }
+ }
+ })
+ }
+
+ dispatch (opts, handler) {
+ const headers = buildHeaders(opts.headers)
+ throwIfProxyAuthIsSent(headers)
+
+ if (headers && !('host' in headers) && !('Host' in headers)) {
+ const { host } = new URL(opts.origin)
+ headers.host = host
+ }
+
+ return this[kAgent].dispatch(
+ {
+ ...opts,
+ headers
+ },
+ handler
+ )
+ }
+
+ /**
+ * @param {import('../../types/proxy-agent').ProxyAgent.Options | string | URL} opts
+ * @returns {URL}
+ */
+ #getUrl (opts) {
+ if (typeof opts === 'string') {
+ return new URL(opts)
+ } else if (opts instanceof URL) {
+ return opts
+ } else {
+ return new URL(opts.uri)
+ }
+ }
+
+ [kClose] () {
+ return Promise.all([
+ this[kAgent].close(),
+ this[kClient].close()
+ ])
+ }
+
+ [kDestroy] () {
+ return Promise.all([
+ this[kAgent].destroy(),
+ this[kClient].destroy()
+ ])
+ }
+}
+
+/**
+ * @param {string[] | Record<string, string>} headers
+ * @returns {Record<string, string>}
+ */
+function buildHeaders (headers) {
+ // When using undici.fetch, the headers list is stored
+ // as an array.
+ if (Array.isArray(headers)) {
+ /** @type {Record<string, string>} */
+ const headersPair = {}
+
+ for (let i = 0; i < headers.length; i += 2) {
+ headersPair[headers[i]] = headers[i + 1]
+ }
+
+ return headersPair
+ }
+
+ return headers
+}
+
+/**
+ * @param {Record<string, string>} headers
+ *
+ * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
+ * Nevertheless, it was changed and to avoid a security vulnerability by end users
+ * this check was created.
+ * It should be removed in the next major version for performance reasons
+ */
+function throwIfProxyAuthIsSent (headers) {
+ const existProxyAuth = headers && Object.keys(headers)
+ .find((key) => key.toLowerCase() === 'proxy-authorization')
+ if (existProxyAuth) {
+ throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor')
+ }
+}
+
+module.exports = ProxyAgent
diff --git a/vanilla/node_modules/undici/lib/dispatcher/retry-agent.js b/vanilla/node_modules/undici/lib/dispatcher/retry-agent.js
new file mode 100644
index 0000000..0c2120d
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/retry-agent.js
@@ -0,0 +1,35 @@
+'use strict'
+
+const Dispatcher = require('./dispatcher')
+const RetryHandler = require('../handler/retry-handler')
+
+class RetryAgent extends Dispatcher {
+ #agent = null
+ #options = null
+ constructor (agent, options = {}) {
+ super(options)
+ this.#agent = agent
+ this.#options = options
+ }
+
+ dispatch (opts, handler) {
+ const retry = new RetryHandler({
+ ...opts,
+ retryOptions: this.#options
+ }, {
+ dispatch: this.#agent.dispatch.bind(this.#agent),
+ handler
+ })
+ return this.#agent.dispatch(opts, retry)
+ }
+
+ close () {
+ return this.#agent.close()
+ }
+
+ destroy () {
+ return this.#agent.destroy()
+ }
+}
+
+module.exports = RetryAgent
diff --git a/vanilla/node_modules/undici/lib/dispatcher/round-robin-pool.js b/vanilla/node_modules/undici/lib/dispatcher/round-robin-pool.js
new file mode 100644
index 0000000..b113aa9
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/dispatcher/round-robin-pool.js
@@ -0,0 +1,137 @@
+'use strict'
+
+const {
+ PoolBase,
+ kClients,
+ kNeedDrain,
+ kAddClient,
+ kGetDispatcher,
+ kRemoveClient
+} = require('./pool-base')
+const Client = require('./client')
+const {
+ InvalidArgumentError
+} = require('../core/errors')
+const util = require('../core/util')
+const { kUrl } = require('../core/symbols')
+const buildConnector = require('../core/connect')
+
+const kOptions = Symbol('options')
+const kConnections = Symbol('connections')
+const kFactory = Symbol('factory')
+const kIndex = Symbol('index')
+
+function defaultFactory (origin, opts) {
+ return new Client(origin, opts)
+}
+
+class RoundRobinPool extends PoolBase {
+ constructor (origin, {
+ connections,
+ factory = defaultFactory,
+ connect,
+ connectTimeout,
+ tls,
+ maxCachedSessions,
+ socketPath,
+ autoSelectFamily,
+ autoSelectFamilyAttemptTimeout,
+ allowH2,
+ clientTtl,
+ ...options
+ } = {}) {
+ if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
+ throw new InvalidArgumentError('invalid connections')
+ }
+
+ if (typeof factory !== 'function') {
+ throw new InvalidArgumentError('factory must be a function.')
+ }
+
+ if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
+ throw new InvalidArgumentError('connect must be a function or an object')
+ }
+
+ if (typeof connect !== 'function') {
+ connect = buildConnector({
+ ...tls,
+ maxCachedSessions,
+ allowH2,
+ socketPath,
+ timeout: connectTimeout,
+ ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
+ ...connect
+ })
+ }
+
+ super()
+
+ this[kConnections] = connections || null
+ this[kUrl] = util.parseOrigin(origin)
+ this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
+ this[kOptions].interceptors = options.interceptors
+ ? { ...options.interceptors }
+ : undefined
+ this[kFactory] = factory
+ this[kIndex] = -1
+
+ this.on('connect', (origin, targets) => {
+ if (clientTtl != null && clientTtl > 0) {
+ for (const target of targets) {
+ Object.assign(target, { ttl: Date.now() })
+ }
+ }
+ })
+
+ this.on('connectionError', (origin, targets, error) => {
+ for (const target of targets) {
+ const idx = this[kClients].indexOf(target)
+ if (idx !== -1) {
+ this[kClients].splice(idx, 1)
+ }
+ }
+ })
+ }
+
+ [kGetDispatcher] () {
+ const clientTtlOption = this[kOptions].clientTtl
+ const clientsLength = this[kClients].length
+
+ // If we have no clients yet, create one
+ if (clientsLength === 0) {
+ const dispatcher = this[kFactory](this[kUrl], this[kOptions])
+ this[kAddClient](dispatcher)
+ return dispatcher
+ }
+
+ // Round-robin through existing clients
+ let checked = 0
+ while (checked < clientsLength) {
+ this[kIndex] = (this[kIndex] + 1) % clientsLength
+ const client = this[kClients][this[kIndex]]
+
+ // Check if client is stale (TTL expired)
+ if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
+ this[kRemoveClient](client)
+ checked++
+ continue
+ }
+
+ // Return client if it's not draining
+ if (!client[kNeedDrain]) {
+ return client
+ }
+
+ checked++
+ }
+
+ // All clients are busy, create a new one if we haven't reached the limit
+ if (!this[kConnections] || clientsLength < this[kConnections]) {
+ const dispatcher = this[kFactory](this[kUrl], this[kOptions])
+ this[kAddClient](dispatcher)
+ return dispatcher
+ }
+ }
+}
+
+module.exports = RoundRobinPool
diff --git a/vanilla/node_modules/undici/lib/encoding/index.js b/vanilla/node_modules/undici/lib/encoding/index.js
new file mode 100644
index 0000000..41f7794
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/encoding/index.js
@@ -0,0 +1,33 @@
+'use strict'
+
+const textDecoder = new TextDecoder()
+
+/**
+ * @see https://encoding.spec.whatwg.org/#utf-8-decode
+ * @param {Uint8Array} buffer
+ */
+function utf8DecodeBytes (buffer) {
+ if (buffer.length === 0) {
+ return ''
+ }
+
+ // 1. Let buffer be the result of peeking three bytes from
+ // ioQueue, converted to a byte sequence.
+
+ // 2. If buffer is 0xEF 0xBB 0xBF, then read three
+ // bytes from ioQueue. (Do nothing with those bytes.)
+ if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
+ buffer = buffer.subarray(3)
+ }
+
+ // 3. Process a queue with an instance of UTF-8’s
+ // decoder, ioQueue, output, and "replacement".
+ const output = textDecoder.decode(buffer)
+
+ // 4. Return output.
+ return output
+}
+
+module.exports = {
+ utf8DecodeBytes
+}
diff --git a/vanilla/node_modules/undici/lib/global.js b/vanilla/node_modules/undici/lib/global.js
new file mode 100644
index 0000000..b61d779
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/global.js
@@ -0,0 +1,50 @@
+'use strict'
+
+// We include a version number for the Dispatcher API. In case of breaking changes,
+// this version number must be increased to avoid conflicts.
+const globalDispatcher = Symbol.for('undici.globalDispatcher.1')
+const { InvalidArgumentError } = require('./core/errors')
+const Agent = require('./dispatcher/agent')
+
+if (getGlobalDispatcher() === undefined) {
+ setGlobalDispatcher(new Agent())
+}
+
+function setGlobalDispatcher (agent) {
+ if (!agent || typeof agent.dispatch !== 'function') {
+ throw new InvalidArgumentError('Argument agent must implement Agent')
+ }
+ Object.defineProperty(globalThis, globalDispatcher, {
+ value: agent,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+}
+
+function getGlobalDispatcher () {
+ return globalThis[globalDispatcher]
+}
+
+// These are the globals that can be installed by undici.install().
+// Not exported by index.js to avoid use outside of this module.
+const installedExports = /** @type {const} */ (
+ [
+ 'fetch',
+ 'Headers',
+ 'Response',
+ 'Request',
+ 'FormData',
+ 'WebSocket',
+ 'CloseEvent',
+ 'ErrorEvent',
+ 'MessageEvent',
+ 'EventSource'
+ ]
+)
+
+module.exports = {
+ setGlobalDispatcher,
+ getGlobalDispatcher,
+ installedExports
+}
diff --git a/vanilla/node_modules/undici/lib/handler/cache-handler.js b/vanilla/node_modules/undici/lib/handler/cache-handler.js
new file mode 100644
index 0000000..93a70e8
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/cache-handler.js
@@ -0,0 +1,561 @@
+'use strict'
+
+const util = require('../core/util')
+const {
+ parseCacheControlHeader,
+ parseVaryHeader,
+ isEtagUsable
+} = require('../util/cache')
+const { parseHttpDate } = require('../util/date.js')
+
+function noop () {}
+
+// Status codes that we can use some heuristics on to cache
+const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
+ 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
+]
+
+// Status codes which semantic is not handled by the cache
+// https://datatracker.ietf.org/doc/html/rfc9111#section-3
+// This list should not grow beyond 206 unless the RFC is updated
+// by a newer one including more. Please introduce another list if
+// implementing caching of responses with the 'must-understand' directive.
+const NOT_UNDERSTOOD_STATUS_CODES = [
+ 206
+]
+
+const MAX_RESPONSE_AGE = 2147483647000
+
+/**
+ * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
+ *
+ * @implements {DispatchHandler}
+ */
+class CacheHandler {
+ /**
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
+ */
+ #cacheKey
+
+ /**
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions['type']}
+ */
+ #cacheType
+
+ /**
+ * @type {number | undefined}
+ */
+ #cacheByDefault
+
+ /**
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
+ */
+ #store
+
+ /**
+ * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
+ */
+ #handler
+
+ /**
+ * @type {import('node:stream').Writable | undefined}
+ */
+ #writeStream
+
+ /**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
+ */
+ constructor ({ store, type, cacheByDefault }, cacheKey, handler) {
+ this.#store = store
+ this.#cacheType = type
+ this.#cacheByDefault = cacheByDefault
+ this.#cacheKey = cacheKey
+ this.#handler = handler
+ }
+
+ onRequestStart (controller, context) {
+ this.#writeStream?.destroy()
+ this.#writeStream = undefined
+ this.#handler.onRequestStart?.(controller, context)
+ }
+
+ onRequestUpgrade (controller, statusCode, headers, socket) {
+ this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
+ }
+
+ /**
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
+ * @param {number} statusCode
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
+ * @param {string} statusMessage
+ */
+ onResponseStart (
+ controller,
+ statusCode,
+ resHeaders,
+ statusMessage
+ ) {
+ const downstreamOnHeaders = () =>
+ this.#handler.onResponseStart?.(
+ controller,
+ statusCode,
+ resHeaders,
+ statusMessage
+ )
+ const handler = this
+
+ if (
+ !util.safeHTTPMethods.includes(this.#cacheKey.method) &&
+ statusCode >= 200 &&
+ statusCode <= 399
+ ) {
+ // Successful response to an unsafe method, delete it from cache
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
+ try {
+ this.#store.delete(this.#cacheKey)?.catch?.(noop)
+ } catch {
+ // Fail silently
+ }
+ return downstreamOnHeaders()
+ }
+
+ const cacheControlHeader = resHeaders['cache-control']
+ const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)
+ if (
+ !cacheControlHeader &&
+ !resHeaders['expires'] &&
+ !heuristicallyCacheable &&
+ !this.#cacheByDefault
+ ) {
+ // Don't have anything to tell us this response is cachable and we're not
+ // caching by default
+ return downstreamOnHeaders()
+ }
+
+ const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
+ if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) {
+ return downstreamOnHeaders()
+ }
+
+ const now = Date.now()
+ const resAge = resHeaders.age ? getAge(resHeaders.age) : undefined
+ if (resAge && resAge >= MAX_RESPONSE_AGE) {
+ // Response considered stale
+ return downstreamOnHeaders()
+ }
+
+ const resDate = typeof resHeaders.date === 'string'
+ ? parseHttpDate(resHeaders.date)
+ : undefined
+
+ const staleAt =
+ determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ??
+ this.#cacheByDefault
+ if (staleAt === undefined || (resAge && resAge > staleAt)) {
+ return downstreamOnHeaders()
+ }
+
+ const baseTime = resDate ? resDate.getTime() : now
+ const absoluteStaleAt = staleAt + baseTime
+ if (now >= absoluteStaleAt) {
+ // Response is already stale
+ return downstreamOnHeaders()
+ }
+
+ let varyDirectives
+ if (this.#cacheKey.headers && resHeaders.vary) {
+ varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
+ if (!varyDirectives) {
+ // Parse error
+ return downstreamOnHeaders()
+ }
+ }
+
+ const deleteAt = determineDeleteAt(baseTime, cacheControlDirectives, absoluteStaleAt)
+ const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)
+
+ /**
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
+ */
+ const value = {
+ statusCode,
+ statusMessage,
+ headers: strippedHeaders,
+ vary: varyDirectives,
+ cacheControlDirectives,
+ cachedAt: resAge ? now - resAge : now,
+ staleAt: absoluteStaleAt,
+ deleteAt
+ }
+
+ // Not modified, re-use the cached value
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
+ if (statusCode === 304) {
+ const handle304 = (cachedValue) => {
+ if (!cachedValue) {
+ // Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
+ return downstreamOnHeaders()
+ }
+
+ // Re-use the cached value: statuscode, statusmessage, headers and body
+ value.statusCode = cachedValue.statusCode
+ value.statusMessage = cachedValue.statusMessage
+ value.etag = cachedValue.etag
+ value.headers = { ...cachedValue.headers, ...strippedHeaders }
+
+ downstreamOnHeaders()
+
+ this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
+
+ if (!this.#writeStream || !cachedValue?.body) {
+ return
+ }
+
+ if (typeof cachedValue.body.values === 'function') {
+ const bodyIterator = cachedValue.body.values()
+
+ const streamCachedBody = () => {
+ for (const chunk of bodyIterator) {
+ const full = this.#writeStream.write(chunk) === false
+ this.#handler.onResponseData?.(controller, chunk)
+ // when stream is full stop writing until we get a 'drain' event
+ if (full) {
+ break
+ }
+ }
+ }
+
+ this.#writeStream
+ .on('error', function () {
+ handler.#writeStream = undefined
+ handler.#store.delete(handler.#cacheKey)
+ })
+ .on('drain', () => {
+ streamCachedBody()
+ })
+ .on('close', function () {
+ if (handler.#writeStream === this) {
+ handler.#writeStream = undefined
+ }
+ })
+
+ streamCachedBody()
+ } else if (typeof cachedValue.body.on === 'function') {
+ // Readable stream body (e.g. from async/remote cache stores)
+ cachedValue.body
+ .on('data', (chunk) => {
+ this.#writeStream.write(chunk)
+ this.#handler.onResponseData?.(controller, chunk)
+ })
+ .on('end', () => {
+ this.#writeStream.end()
+ })
+ .on('error', () => {
+ this.#writeStream = undefined
+ this.#store.delete(this.#cacheKey)
+ })
+
+ this.#writeStream
+ .on('error', function () {
+ handler.#writeStream = undefined
+ handler.#store.delete(handler.#cacheKey)
+ })
+ .on('close', function () {
+ if (handler.#writeStream === this) {
+ handler.#writeStream = undefined
+ }
+ })
+ }
+ }
+
+ /**
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
+ */
+ const result = this.#store.get(this.#cacheKey)
+ if (result && typeof result.then === 'function') {
+ result.then(handle304)
+ } else {
+ handle304(result)
+ }
+ } else {
+ if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
+ value.etag = resHeaders.etag
+ }
+
+ this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
+
+ if (!this.#writeStream) {
+ return downstreamOnHeaders()
+ }
+
+ this.#writeStream
+ .on('drain', () => controller.resume())
+ .on('error', function () {
+ // TODO (fix): Make error somehow observable?
+ handler.#writeStream = undefined
+
+ // Delete the value in case the cache store is holding onto state from
+ // the call to createWriteStream
+ handler.#store.delete(handler.#cacheKey)
+ })
+ .on('close', function () {
+ if (handler.#writeStream === this) {
+ handler.#writeStream = undefined
+ }
+
+ // TODO (fix): Should we resume even if was paused downstream?
+ controller.resume()
+ })
+
+ downstreamOnHeaders()
+ }
+ }
+
+ onResponseData (controller, chunk) {
+ if (this.#writeStream?.write(chunk) === false) {
+ controller.pause()
+ }
+
+ this.#handler.onResponseData?.(controller, chunk)
+ }
+
+ onResponseEnd (controller, trailers) {
+ this.#writeStream?.end()
+ this.#handler.onResponseEnd?.(controller, trailers)
+ }
+
+ onResponseError (controller, err) {
+ this.#writeStream?.destroy(err)
+ this.#writeStream = undefined
+ this.#handler.onResponseError?.(controller, err)
+ }
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
+ *
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
+ * @param {number} statusCode
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
+ */
+function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
+ // Status code must be final and understood.
+ if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) {
+ return false
+ }
+ // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching
+ // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3
+ if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] &&
+ !cacheControlDirectives.public &&
+ cacheControlDirectives['max-age'] === undefined &&
+ // RFC 9111: a private response directive, if the cache is not shared
+ !(cacheControlDirectives.private && cacheType === 'private') &&
+ !(cacheControlDirectives['s-maxage'] !== undefined && cacheType === 'shared')
+ ) {
+ return false
+ }
+
+ if (cacheControlDirectives['no-store']) {
+ return false
+ }
+
+ if (cacheType === 'shared' && cacheControlDirectives.private === true) {
+ return false
+ }
+
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
+ if (resHeaders.vary?.includes('*')) {
+ return false
+ }
+
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
+ if (resHeaders.authorization) {
+ if (!cacheControlDirectives.public || typeof resHeaders.authorization !== 'string') {
+ return false
+ }
+
+ if (
+ Array.isArray(cacheControlDirectives['no-cache']) &&
+ cacheControlDirectives['no-cache'].includes('authorization')
+ ) {
+ return false
+ }
+
+ if (
+ Array.isArray(cacheControlDirectives['private']) &&
+ cacheControlDirectives['private'].includes('authorization')
+ ) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @param {string | string[]} ageHeader
+ * @returns {number | undefined}
+ */
+function getAge (ageHeader) {
+ const age = parseInt(Array.isArray(ageHeader) ? ageHeader[0] : ageHeader)
+
+ return isNaN(age) ? undefined : age * 1000
+}
+
+/**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
+ * @param {number} now
+ * @param {number | undefined} age
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
+ * @param {Date | undefined} responseDate
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
+ *
+ * @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached
+ */
+function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) {
+ if (cacheType === 'shared') {
+ // Prioritize s-maxage since we're a shared cache
+ // s-maxage > max-age > Expire
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3
+ const sMaxAge = cacheControlDirectives['s-maxage']
+ if (sMaxAge !== undefined) {
+ return sMaxAge > 0 ? sMaxAge * 1000 : undefined
+ }
+ }
+
+ const maxAge = cacheControlDirectives['max-age']
+ if (maxAge !== undefined) {
+ return maxAge > 0 ? maxAge * 1000 : undefined
+ }
+
+ if (typeof resHeaders.expires === 'string') {
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
+ const expiresDate = parseHttpDate(resHeaders.expires)
+ if (expiresDate) {
+ if (now >= expiresDate.getTime()) {
+ return undefined
+ }
+
+ if (responseDate) {
+ if (responseDate >= expiresDate) {
+ return undefined
+ }
+
+ if (age !== undefined && age > (expiresDate - responseDate)) {
+ return undefined
+ }
+ }
+
+ return expiresDate.getTime() - now
+ }
+ }
+
+ if (typeof resHeaders['last-modified'] === 'string') {
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh
+ const lastModified = new Date(resHeaders['last-modified'])
+ if (isValidDate(lastModified)) {
+ if (lastModified.getTime() >= now) {
+ return undefined
+ }
+
+ const responseAge = now - lastModified.getTime()
+
+ return responseAge * 0.1
+ }
+ }
+
+ if (cacheControlDirectives.immutable) {
+ // https://www.rfc-editor.org/rfc/rfc8246.html#section-2.2
+ return 31536000
+ }
+
+ return undefined
+}
+
+/**
+ * @param {number} now
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
+ * @param {number} staleAt
+ */
+function determineDeleteAt (now, cacheControlDirectives, staleAt) {
+ let staleWhileRevalidate = -Infinity
+ let staleIfError = -Infinity
+ let immutable = -Infinity
+
+ if (cacheControlDirectives['stale-while-revalidate']) {
+ staleWhileRevalidate = staleAt + (cacheControlDirectives['stale-while-revalidate'] * 1000)
+ }
+
+ if (cacheControlDirectives['stale-if-error']) {
+ staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
+ }
+
+ if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
+ immutable = now + 31536000000
+ }
+
+ return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
+}
+
+/**
+ * Strips headers required to be removed in cached responses
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
+ * @returns {Record<string, string | string []>}
+ */
+function stripNecessaryHeaders (resHeaders, cacheControlDirectives) {
+ const headersToRemove = [
+ 'connection',
+ 'proxy-authenticate',
+ 'proxy-authentication-info',
+ 'proxy-authorization',
+ 'proxy-connection',
+ 'te',
+ 'transfer-encoding',
+ 'upgrade',
+ // We'll add age back when serving it
+ 'age'
+ ]
+
+ if (resHeaders['connection']) {
+ if (Array.isArray(resHeaders['connection'])) {
+ // connection: a
+ // connection: b
+ headersToRemove.push(...resHeaders['connection'].map(header => header.trim()))
+ } else {
+ // connection: a, b
+ headersToRemove.push(...resHeaders['connection'].split(',').map(header => header.trim()))
+ }
+ }
+
+ if (Array.isArray(cacheControlDirectives['no-cache'])) {
+ headersToRemove.push(...cacheControlDirectives['no-cache'])
+ }
+
+ if (Array.isArray(cacheControlDirectives['private'])) {
+ headersToRemove.push(...cacheControlDirectives['private'])
+ }
+
+ let strippedHeaders
+ for (const headerName of headersToRemove) {
+ if (resHeaders[headerName]) {
+ strippedHeaders ??= { ...resHeaders }
+ delete strippedHeaders[headerName]
+ }
+ }
+
+ return strippedHeaders ?? resHeaders
+}
+
+/**
+ * @param {Date} date
+ * @returns {boolean}
+ */
+function isValidDate (date) {
+ return date instanceof Date && Number.isFinite(date.valueOf())
+}
+
+module.exports = CacheHandler
diff --git a/vanilla/node_modules/undici/lib/handler/cache-revalidation-handler.js b/vanilla/node_modules/undici/lib/handler/cache-revalidation-handler.js
new file mode 100644
index 0000000..393d16d
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/cache-revalidation-handler.js
@@ -0,0 +1,124 @@
+'use strict'
+
+const assert = require('node:assert')
+
+/**
+ * This takes care of revalidation requests we send to the origin. If we get
+ * a response indicating that what we have is cached (via a HTTP 304), we can
+ * continue using the cached value. Otherwise, we'll receive the new response
+ * here, which we then just pass on to the next handler (most likely a
+ * CacheHandler). Note that this assumes the proper headers were already
+ * included in the request to tell the origin that we want to revalidate the
+ * response (i.e. if-modified-since or if-none-match).
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
+ *
+ * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
+ */
+class CacheRevalidationHandler {
+ #successful = false
+
+ /**
+ * @type {((boolean, any) => void) | null}
+ */
+ #callback
+
+ /**
+ * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)}
+ */
+ #handler
+
+ #context
+
+ /**
+ * @type {boolean}
+ */
+ #allowErrorStatusCodes
+
+ /**
+ * @param {(boolean) => void} callback Function to call if the cached value is valid
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
+ * @param {boolean} allowErrorStatusCodes
+ */
+ constructor (callback, handler, allowErrorStatusCodes) {
+ if (typeof callback !== 'function') {
+ throw new TypeError('callback must be a function')
+ }
+
+ this.#callback = callback
+ this.#handler = handler
+ this.#allowErrorStatusCodes = allowErrorStatusCodes
+ }
+
+ onRequestStart (_, context) {
+ this.#successful = false
+ this.#context = context
+ }
+
+ onRequestUpgrade (controller, statusCode, headers, socket) {
+ this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
+ }
+
+ onResponseStart (
+ controller,
+ statusCode,
+ headers,
+ statusMessage
+ ) {
+ assert(this.#callback != null)
+
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
+ // https://datatracker.ietf.org/doc/html/rfc5861#section-4
+ this.#successful = statusCode === 304 ||
+ (this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504)
+ this.#callback(this.#successful, this.#context)
+ this.#callback = null
+
+ if (this.#successful) {
+ return true
+ }
+
+ this.#handler.onRequestStart?.(controller, this.#context)
+ this.#handler.onResponseStart?.(
+ controller,
+ statusCode,
+ headers,
+ statusMessage
+ )
+ }
+
+ onResponseData (controller, chunk) {
+ if (this.#successful) {
+ return
+ }
+
+ return this.#handler.onResponseData?.(controller, chunk)
+ }
+
+ onResponseEnd (controller, trailers) {
+ if (this.#successful) {
+ return
+ }
+
+ this.#handler.onResponseEnd?.(controller, trailers)
+ }
+
+ onResponseError (controller, err) {
+ if (this.#successful) {
+ return
+ }
+
+ if (this.#callback) {
+ this.#callback(false)
+ this.#callback = null
+ }
+
+ if (typeof this.#handler.onResponseError === 'function') {
+ this.#handler.onResponseError(controller, err)
+ } else {
+ throw err
+ }
+ }
+}
+
+module.exports = CacheRevalidationHandler
diff --git a/vanilla/node_modules/undici/lib/handler/decorator-handler.js b/vanilla/node_modules/undici/lib/handler/decorator-handler.js
new file mode 100644
index 0000000..50fbb0c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/decorator-handler.js
@@ -0,0 +1,67 @@
+'use strict'
+
+const assert = require('node:assert')
+const WrapHandler = require('./wrap-handler')
+
+/**
+ * @deprecated
+ */
+module.exports = class DecoratorHandler {
+ #handler
+ #onCompleteCalled = false
+ #onErrorCalled = false
+ #onResponseStartCalled = false
+
+ constructor (handler) {
+ if (typeof handler !== 'object' || handler === null) {
+ throw new TypeError('handler must be an object')
+ }
+ this.#handler = WrapHandler.wrap(handler)
+ }
+
+ onRequestStart (...args) {
+ this.#handler.onRequestStart?.(...args)
+ }
+
+ onRequestUpgrade (...args) {
+ assert(!this.#onCompleteCalled)
+ assert(!this.#onErrorCalled)
+
+ return this.#handler.onRequestUpgrade?.(...args)
+ }
+
+ onResponseStart (...args) {
+ assert(!this.#onCompleteCalled)
+ assert(!this.#onErrorCalled)
+ assert(!this.#onResponseStartCalled)
+
+ this.#onResponseStartCalled = true
+
+ return this.#handler.onResponseStart?.(...args)
+ }
+
+ onResponseData (...args) {
+ assert(!this.#onCompleteCalled)
+ assert(!this.#onErrorCalled)
+
+ return this.#handler.onResponseData?.(...args)
+ }
+
+ onResponseEnd (...args) {
+ assert(!this.#onCompleteCalled)
+ assert(!this.#onErrorCalled)
+
+ this.#onCompleteCalled = true
+ return this.#handler.onResponseEnd?.(...args)
+ }
+
+ onResponseError (...args) {
+ this.#onErrorCalled = true
+ return this.#handler.onResponseError?.(...args)
+ }
+
+ /**
+ * @deprecated
+ */
+ onBodySent () {}
+}
diff --git a/vanilla/node_modules/undici/lib/handler/deduplication-handler.js b/vanilla/node_modules/undici/lib/handler/deduplication-handler.js
new file mode 100644
index 0000000..33a2c4f
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/deduplication-handler.js
@@ -0,0 +1,216 @@
+'use strict'
+
+/**
+ * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
+ */
+
+/**
+ * Handler that buffers response data and notifies multiple waiting handlers.
+ * Used for request deduplication.
+ *
+ * @implements {DispatchHandler}
+ */
+class DeduplicationHandler {
+ /**
+ * @type {DispatchHandler}
+ */
+ #primaryHandler
+
+ /**
+ * @type {DispatchHandler[]}
+ */
+ #waitingHandlers = []
+
+ /**
+ * @type {Buffer[]}
+ */
+ #chunks = []
+
+ /**
+ * @type {number}
+ */
+ #statusCode = 0
+
+ /**
+ * @type {Record<string, string | string[]>}
+ */
+ #headers = {}
+
+ /**
+ * @type {string}
+ */
+ #statusMessage = ''
+
+ /**
+ * @type {boolean}
+ */
+ #aborted = false
+
+ /**
+ * @type {import('../../types/dispatcher.d.ts').default.DispatchController | null}
+ */
+ #controller = null
+
+ /**
+ * @type {(() => void) | null}
+ */
+ #onComplete = null
+
+ /**
+ * @param {DispatchHandler} primaryHandler The primary handler
+ * @param {() => void} onComplete Callback when request completes
+ */
+ constructor (primaryHandler, onComplete) {
+ this.#primaryHandler = primaryHandler
+ this.#onComplete = onComplete
+ }
+
+ /**
+ * Add a waiting handler that will receive the buffered response
+ * @param {DispatchHandler} handler
+ */
+ addWaitingHandler (handler) {
+ this.#waitingHandlers.push(handler)
+ }
+
+ /**
+ * @param {() => void} abort
+ * @param {any} context
+ */
+ onRequestStart (controller, context) {
+ this.#controller = controller
+ this.#primaryHandler.onRequestStart?.(controller, context)
+ }
+
+ /**
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
+ * @param {number} statusCode
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers
+ * @param {Socket} socket
+ */
+ onRequestUpgrade (controller, statusCode, headers, socket) {
+ this.#primaryHandler.onRequestUpgrade?.(controller, statusCode, headers, socket)
+ }
+
+ /**
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
+ * @param {number} statusCode
+ * @param {Record<string, string | string[]>} headers
+ * @param {string} statusMessage
+ */
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ this.#statusCode = statusCode
+ this.#headers = headers
+ this.#statusMessage = statusMessage
+ this.#primaryHandler.onResponseStart?.(controller, statusCode, headers, statusMessage)
+ }
+
+ /**
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
+ * @param {Buffer} chunk
+ */
+ onResponseData (controller, chunk) {
+ // Buffer the chunk for waiting handlers
+ this.#chunks.push(Buffer.from(chunk))
+ this.#primaryHandler.onResponseData?.(controller, chunk)
+ }
+
+ /**
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
+ * @param {object} trailers
+ */
+ onResponseEnd (controller, trailers) {
+ this.#primaryHandler.onResponseEnd?.(controller, trailers)
+ this.#notifyWaitingHandlers()
+ this.#onComplete?.()
+ }
+
+ /**
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchController} controller
+ * @param {Error} err
+ */
+ onResponseError (controller, err) {
+ this.#aborted = true
+ this.#primaryHandler.onResponseError?.(controller, err)
+ this.#notifyWaitingHandlersError(err)
+ this.#onComplete?.()
+ }
+
+ /**
+ * Notify all waiting handlers with the buffered response
+ */
+ #notifyWaitingHandlers () {
+ const body = Buffer.concat(this.#chunks)
+
+ for (const handler of this.#waitingHandlers) {
+ // Create a simple controller for each waiting handler
+ const waitingController = {
+ resume () {},
+ pause () {},
+ get paused () { return false },
+ get aborted () { return false },
+ get reason () { return null },
+ abort () {}
+ }
+
+ try {
+ handler.onRequestStart?.(waitingController, null)
+
+ if (waitingController.aborted) {
+ continue
+ }
+
+ handler.onResponseStart?.(
+ waitingController,
+ this.#statusCode,
+ this.#headers,
+ this.#statusMessage
+ )
+
+ if (waitingController.aborted) {
+ continue
+ }
+
+ if (body.length > 0) {
+ handler.onResponseData?.(waitingController, body)
+ }
+
+ handler.onResponseEnd?.(waitingController, {})
+ } catch {
+ // Ignore errors from waiting handlers
+ }
+ }
+
+ this.#waitingHandlers = []
+ this.#chunks = []
+ }
+
+ /**
+ * Notify all waiting handlers of an error
+ * @param {Error} err
+ */
+ #notifyWaitingHandlersError (err) {
+ for (const handler of this.#waitingHandlers) {
+ const waitingController = {
+ resume () {},
+ pause () {},
+ get paused () { return false },
+ get aborted () { return true },
+ get reason () { return err },
+ abort () {}
+ }
+
+ try {
+ handler.onRequestStart?.(waitingController, null)
+ handler.onResponseError?.(waitingController, err)
+ } catch {
+ // Ignore errors from waiting handlers
+ }
+ }
+
+ this.#waitingHandlers = []
+ this.#chunks = []
+ }
+}
+
+module.exports = DeduplicationHandler
diff --git a/vanilla/node_modules/undici/lib/handler/redirect-handler.js b/vanilla/node_modules/undici/lib/handler/redirect-handler.js
new file mode 100644
index 0000000..dd0f471
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/redirect-handler.js
@@ -0,0 +1,237 @@
+'use strict'
+
+const util = require('../core/util')
+const { kBodyUsed } = require('../core/symbols')
+const assert = require('node:assert')
+const { InvalidArgumentError } = require('../core/errors')
+const EE = require('node:events')
+
+const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
+
+const kBody = Symbol('body')
+
+const noop = () => {}
+
+class BodyAsyncIterable {
+ constructor (body) {
+ this[kBody] = body
+ this[kBodyUsed] = false
+ }
+
+ async * [Symbol.asyncIterator] () {
+ assert(!this[kBodyUsed], 'disturbed')
+ this[kBodyUsed] = true
+ yield * this[kBody]
+ }
+}
+
+class RedirectHandler {
+ static buildDispatch (dispatcher, maxRedirections) {
+ if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
+ throw new InvalidArgumentError('maxRedirections must be a positive number')
+ }
+
+ const dispatch = dispatcher.dispatch.bind(dispatcher)
+ return (opts, originalHandler) => dispatch(opts, new RedirectHandler(dispatch, maxRedirections, opts, originalHandler))
+ }
+
+ constructor (dispatch, maxRedirections, opts, handler) {
+ if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
+ throw new InvalidArgumentError('maxRedirections must be a positive number')
+ }
+
+ this.dispatch = dispatch
+ this.location = null
+ const { maxRedirections: _, ...cleanOpts } = opts
+ this.opts = cleanOpts // opts must be a copy, exclude maxRedirections
+ this.maxRedirections = maxRedirections
+ this.handler = handler
+ this.history = []
+
+ if (util.isStream(this.opts.body)) {
+ // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
+ // so that it can be dispatched again?
+ // TODO (fix): Do we need 100-expect support to provide a way to do this properly?
+ if (util.bodyLength(this.opts.body) === 0) {
+ this.opts.body
+ .on('data', function () {
+ assert(false)
+ })
+ }
+
+ if (typeof this.opts.body.readableDidRead !== 'boolean') {
+ this.opts.body[kBodyUsed] = false
+ EE.prototype.on.call(this.opts.body, 'data', function () {
+ this[kBodyUsed] = true
+ })
+ }
+ } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') {
+ // TODO (fix): We can't access ReadableStream internal state
+ // to determine whether or not it has been disturbed. This is just
+ // a workaround.
+ this.opts.body = new BodyAsyncIterable(this.opts.body)
+ } else if (
+ this.opts.body &&
+ typeof this.opts.body !== 'string' &&
+ !ArrayBuffer.isView(this.opts.body) &&
+ util.isIterable(this.opts.body) &&
+ !util.isFormDataLike(this.opts.body)
+ ) {
+ // TODO: Should we allow re-using iterable if !this.opts.idempotent
+ // or through some other flag?
+ this.opts.body = new BodyAsyncIterable(this.opts.body)
+ }
+ }
+
+ onRequestStart (controller, context) {
+ this.handler.onRequestStart?.(controller, { ...context, history: this.history })
+ }
+
+ onRequestUpgrade (controller, statusCode, headers, socket) {
+ this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
+ }
+
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
+ throw new Error('max redirects')
+ }
+
+ // https://tools.ietf.org/html/rfc7231#section-6.4.2
+ // https://fetch.spec.whatwg.org/#http-redirect-fetch
+ // In case of HTTP 301 or 302 with POST, change the method to GET
+ if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
+ this.opts.method = 'GET'
+ if (util.isStream(this.opts.body)) {
+ util.destroy(this.opts.body.on('error', noop))
+ }
+ this.opts.body = null
+ }
+
+ // https://tools.ietf.org/html/rfc7231#section-6.4.4
+ // In case of HTTP 303, always replace method to be either HEAD or GET
+ if (statusCode === 303 && this.opts.method !== 'HEAD') {
+ this.opts.method = 'GET'
+ if (util.isStream(this.opts.body)) {
+ util.destroy(this.opts.body.on('error', noop))
+ }
+ this.opts.body = null
+ }
+
+ this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) || redirectableStatusCodes.indexOf(statusCode) === -1
+ ? null
+ : headers.location
+
+ if (this.opts.origin) {
+ this.history.push(new URL(this.opts.path, this.opts.origin))
+ }
+
+ if (!this.location) {
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
+ return
+ }
+
+ const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
+ const path = search ? `${pathname}${search}` : pathname
+
+ // Check for redirect loops by seeing if we've already visited this URL in our history
+ // This catches the case where Client/Pool try to handle cross-origin redirects but fail
+ // and keep redirecting to the same URL in an infinite loop
+ const redirectUrlString = `${origin}${path}`
+ for (const historyUrl of this.history) {
+ if (historyUrl.toString() === redirectUrlString) {
+ throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`)
+ }
+ }
+
+ // Remove headers referring to the original URL.
+ // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
+ // https://tools.ietf.org/html/rfc7231#section-6.4
+ this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin)
+ this.opts.path = path
+ this.opts.origin = origin
+ this.opts.query = null
+ }
+
+ onResponseData (controller, chunk) {
+ if (this.location) {
+ /*
+ https://tools.ietf.org/html/rfc7231#section-6.4
+
+ TLDR: undici always ignores 3xx response bodies.
+
+ Redirection is used to serve the requested resource from another URL, so it assumes that
+ no body is generated (and thus can be ignored). Even though generating a body is not prohibited.
+
+ For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually
+ (which means it's optional and not mandated) contain just an hyperlink to the value of
+ the Location response header, so the body can be ignored safely.
+
+ For status 300, which is "Multiple Choices", the spec mentions both generating a Location
+ response header AND a response body with the other possible location to follow.
+ Since the spec explicitly chooses not to specify a format for such body and leave it to
+ servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
+ */
+ } else {
+ this.handler.onResponseData?.(controller, chunk)
+ }
+ }
+
+ onResponseEnd (controller, trailers) {
+ if (this.location) {
+ /*
+ https://tools.ietf.org/html/rfc7231#section-6.4
+
+ TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections
+ and neither are useful if present.
+
+ See comment on onData method above for more detailed information.
+ */
+ this.dispatch(this.opts, this)
+ } else {
+ this.handler.onResponseEnd(controller, trailers)
+ }
+ }
+
+ onResponseError (controller, error) {
+ this.handler.onResponseError?.(controller, error)
+ }
+}
+
+// https://tools.ietf.org/html/rfc7231#section-6.4.4
+function shouldRemoveHeader (header, removeContent, unknownOrigin) {
+ if (header.length === 4) {
+ return util.headerNameToString(header) === 'host'
+ }
+ if (removeContent && util.headerNameToString(header).startsWith('content-')) {
+ return true
+ }
+ if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
+ const name = util.headerNameToString(header)
+ return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'
+ }
+ return false
+}
+
+// https://tools.ietf.org/html/rfc7231#section-6.4
+function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
+ const ret = []
+ if (Array.isArray(headers)) {
+ for (let i = 0; i < headers.length; i += 2) {
+ if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) {
+ ret.push(headers[i], headers[i + 1])
+ }
+ }
+ } else if (headers && typeof headers === 'object') {
+ const entries = typeof headers[Symbol.iterator] === 'function' ? headers : Object.entries(headers)
+ for (const [key, value] of entries) {
+ if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
+ ret.push(key, value)
+ }
+ }
+ } else {
+ assert(headers == null, 'headers must be an object or an array')
+ }
+ return ret
+}
+
+module.exports = RedirectHandler
diff --git a/vanilla/node_modules/undici/lib/handler/retry-handler.js b/vanilla/node_modules/undici/lib/handler/retry-handler.js
new file mode 100644
index 0000000..1cbc789
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/retry-handler.js
@@ -0,0 +1,394 @@
+'use strict'
+const assert = require('node:assert')
+
+const { kRetryHandlerDefaultRetry } = require('../core/symbols')
+const { RequestRetryError } = require('../core/errors')
+const WrapHandler = require('./wrap-handler')
+const {
+ isDisturbed,
+ parseRangeHeader,
+ wrapRequestBody
+} = require('../core/util')
+
+function calculateRetryAfterHeader (retryAfter) {
+ const retryTime = new Date(retryAfter).getTime()
+ return isNaN(retryTime) ? 0 : retryTime - Date.now()
+}
+
+class RetryHandler {
+ constructor (opts, { dispatch, handler }) {
+ const { retryOptions, ...dispatchOpts } = opts
+ const {
+ // Retry scoped
+ retry: retryFn,
+ maxRetries,
+ maxTimeout,
+ minTimeout,
+ timeoutFactor,
+ // Response scoped
+ methods,
+ errorCodes,
+ retryAfter,
+ statusCodes,
+ throwOnError
+ } = retryOptions ?? {}
+
+ this.error = null
+ this.dispatch = dispatch
+ this.handler = WrapHandler.wrap(handler)
+ this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
+ this.retryOpts = {
+ throwOnError: throwOnError ?? true,
+ retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
+ retryAfter: retryAfter ?? true,
+ maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
+ minTimeout: minTimeout ?? 500, // .5s
+ timeoutFactor: timeoutFactor ?? 2,
+ maxRetries: maxRetries ?? 5,
+ // What errors we should retry
+ methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
+ // Indicates which errors to retry
+ statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
+ // List of errors to retry
+ errorCodes: errorCodes ?? [
+ 'ECONNRESET',
+ 'ECONNREFUSED',
+ 'ENOTFOUND',
+ 'ENETDOWN',
+ 'ENETUNREACH',
+ 'EHOSTDOWN',
+ 'EHOSTUNREACH',
+ 'EPIPE',
+ 'UND_ERR_SOCKET'
+ ]
+ }
+
+ this.retryCount = 0
+ this.retryCountCheckpoint = 0
+ this.headersSent = false
+ this.start = 0
+ this.end = null
+ this.etag = null
+ }
+
+ onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
+ if (this.retryOpts.throwOnError) {
+ // Preserve old behavior for status codes that are not eligible for retry
+ if (this.retryOpts.statusCodes.includes(statusCode) === false) {
+ this.headersSent = true
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
+ } else {
+ this.error = err
+ }
+
+ return
+ }
+
+ if (isDisturbed(this.opts.body)) {
+ this.headersSent = true
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
+ return
+ }
+
+ function shouldRetry (passedErr) {
+ if (passedErr) {
+ this.headersSent = true
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
+ controller.resume()
+ return
+ }
+
+ this.error = err
+ controller.resume()
+ }
+
+ controller.pause()
+ this.retryOpts.retry(
+ err,
+ {
+ state: { counter: this.retryCount },
+ opts: { retryOptions: this.retryOpts, ...this.opts }
+ },
+ shouldRetry.bind(this)
+ )
+ }
+
+ onRequestStart (controller, context) {
+ if (!this.headersSent) {
+ this.handler.onRequestStart?.(controller, context)
+ }
+ }
+
+ onRequestUpgrade (controller, statusCode, headers, socket) {
+ this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
+ }
+
+ static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
+ const { statusCode, code, headers } = err
+ const { method, retryOptions } = opts
+ const {
+ maxRetries,
+ minTimeout,
+ maxTimeout,
+ timeoutFactor,
+ statusCodes,
+ errorCodes,
+ methods
+ } = retryOptions
+ const { counter } = state
+
+ // Any code that is not a Undici's originated and allowed to retry
+ if (code && code !== 'UND_ERR_REQ_RETRY' && !errorCodes.includes(code)) {
+ cb(err)
+ return
+ }
+
+ // If a set of method are provided and the current method is not in the list
+ if (Array.isArray(methods) && !methods.includes(method)) {
+ cb(err)
+ return
+ }
+
+ // If a set of status code are provided and the current status code is not in the list
+ if (
+ statusCode != null &&
+ Array.isArray(statusCodes) &&
+ !statusCodes.includes(statusCode)
+ ) {
+ cb(err)
+ return
+ }
+
+ // If we reached the max number of retries
+ if (counter > maxRetries) {
+ cb(err)
+ return
+ }
+
+ let retryAfterHeader = headers?.['retry-after']
+ if (retryAfterHeader) {
+ retryAfterHeader = Number(retryAfterHeader)
+ retryAfterHeader = Number.isNaN(retryAfterHeader)
+ ? calculateRetryAfterHeader(headers['retry-after'])
+ : retryAfterHeader * 1e3 // Retry-After is in seconds
+ }
+
+ const retryTimeout =
+ retryAfterHeader > 0
+ ? Math.min(retryAfterHeader, maxTimeout)
+ : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
+
+ setTimeout(() => cb(null), retryTimeout)
+ }
+
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ this.error = null
+ this.retryCount += 1
+
+ if (statusCode >= 300) {
+ const err = new RequestRetryError('Request failed', statusCode, {
+ headers,
+ data: {
+ count: this.retryCount
+ }
+ })
+
+ this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
+ return
+ }
+
+ // Checkpoint for resume from where we left it
+ if (this.headersSent) {
+ // Only Partial Content 206 supposed to provide Content-Range,
+ // any other status code that partially consumed the payload
+ // should not be retried because it would result in downstream
+ // wrongly concatenate multiple responses.
+ if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
+ throw new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
+ headers,
+ data: { count: this.retryCount }
+ })
+ }
+
+ const contentRange = parseRangeHeader(headers['content-range'])
+ // If no content range
+ if (!contentRange) {
+ // We always throw here as we want to indicate that we entred unexpected path
+ throw new RequestRetryError('Content-Range mismatch', statusCode, {
+ headers,
+ data: { count: this.retryCount }
+ })
+ }
+
+ // Let's start with a weak etag check
+ if (this.etag != null && this.etag !== headers.etag) {
+ // We always throw here as we want to indicate that we entred unexpected path
+ throw new RequestRetryError('ETag mismatch', statusCode, {
+ headers,
+ data: { count: this.retryCount }
+ })
+ }
+
+ const { start, size, end = size ? size - 1 : null } = contentRange
+
+ assert(this.start === start, 'content-range mismatch')
+ assert(this.end == null || this.end === end, 'content-range mismatch')
+
+ return
+ }
+
+ if (this.end == null) {
+ if (statusCode === 206) {
+ // First time we receive 206
+ const range = parseRangeHeader(headers['content-range'])
+
+ if (range == null) {
+ this.headersSent = true
+ this.handler.onResponseStart?.(
+ controller,
+ statusCode,
+ headers,
+ statusMessage
+ )
+ return
+ }
+
+ const { start, size, end = size ? size - 1 : null } = range
+ assert(
+ start != null && Number.isFinite(start),
+ 'content-range mismatch'
+ )
+ assert(end != null && Number.isFinite(end), 'invalid content-length')
+
+ this.start = start
+ this.end = end
+ }
+
+ // We make our best to checkpoint the body for further range headers
+ if (this.end == null) {
+ const contentLength = headers['content-length']
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
+ }
+
+ assert(Number.isFinite(this.start))
+ assert(
+ this.end == null || Number.isFinite(this.end),
+ 'invalid content-length'
+ )
+
+ this.resume = true
+ this.etag = headers.etag != null ? headers.etag : null
+
+ // Weak etags are not useful for comparison nor cache
+ // for instance not safe to assume if the response is byte-per-byte
+ // equal
+ if (
+ this.etag != null &&
+ this.etag[0] === 'W' &&
+ this.etag[1] === '/'
+ ) {
+ this.etag = null
+ }
+
+ this.headersSent = true
+ this.handler.onResponseStart?.(
+ controller,
+ statusCode,
+ headers,
+ statusMessage
+ )
+ } else {
+ throw new RequestRetryError('Request failed', statusCode, {
+ headers,
+ data: { count: this.retryCount }
+ })
+ }
+ }
+
+ onResponseData (controller, chunk) {
+ if (this.error) {
+ return
+ }
+
+ this.start += chunk.length
+
+ this.handler.onResponseData?.(controller, chunk)
+ }
+
+ onResponseEnd (controller, trailers) {
+ if (this.error && this.retryOpts.throwOnError) {
+ throw this.error
+ }
+
+ if (!this.error) {
+ this.retryCount = 0
+ return this.handler.onResponseEnd?.(controller, trailers)
+ }
+
+ this.retry(controller)
+ }
+
+ retry (controller) {
+ if (this.start !== 0) {
+ const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
+
+ // Weak etag check - weak etags will make comparison algorithms never match
+ if (this.etag != null) {
+ headers['if-match'] = this.etag
+ }
+
+ this.opts = {
+ ...this.opts,
+ headers: {
+ ...this.opts.headers,
+ ...headers
+ }
+ }
+ }
+
+ try {
+ this.retryCountCheckpoint = this.retryCount
+ this.dispatch(this.opts, this)
+ } catch (err) {
+ this.handler.onResponseError?.(controller, err)
+ }
+ }
+
+ onResponseError (controller, err) {
+ if (controller?.aborted || isDisturbed(this.opts.body)) {
+ this.handler.onResponseError?.(controller, err)
+ return
+ }
+
+ function shouldRetry (returnedErr) {
+ if (!returnedErr) {
+ this.retry(controller)
+ return
+ }
+
+ this.handler?.onResponseError?.(controller, returnedErr)
+ }
+
+ // We reconcile in case of a mix between network errors
+ // and server error response
+ if (this.retryCount - this.retryCountCheckpoint > 0) {
+ // We count the difference between the last checkpoint and the current retry count
+ this.retryCount =
+ this.retryCountCheckpoint +
+ (this.retryCount - this.retryCountCheckpoint)
+ } else {
+ this.retryCount += 1
+ }
+
+ this.retryOpts.retry(
+ err,
+ {
+ state: { counter: this.retryCount },
+ opts: { retryOptions: this.retryOpts, ...this.opts }
+ },
+ shouldRetry.bind(this)
+ )
+ }
+}
+
+module.exports = RetryHandler
diff --git a/vanilla/node_modules/undici/lib/handler/unwrap-handler.js b/vanilla/node_modules/undici/lib/handler/unwrap-handler.js
new file mode 100644
index 0000000..865593a
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/unwrap-handler.js
@@ -0,0 +1,96 @@
+'use strict'
+
+const { parseHeaders } = require('../core/util')
+const { InvalidArgumentError } = require('../core/errors')
+
+const kResume = Symbol('resume')
+
+class UnwrapController {
+ #paused = false
+ #reason = null
+ #aborted = false
+ #abort
+
+ [kResume] = null
+
+ constructor (abort) {
+ this.#abort = abort
+ }
+
+ pause () {
+ this.#paused = true
+ }
+
+ resume () {
+ if (this.#paused) {
+ this.#paused = false
+ this[kResume]?.()
+ }
+ }
+
+ abort (reason) {
+ if (!this.#aborted) {
+ this.#aborted = true
+ this.#reason = reason
+ this.#abort(reason)
+ }
+ }
+
+ get aborted () {
+ return this.#aborted
+ }
+
+ get reason () {
+ return this.#reason
+ }
+
+ get paused () {
+ return this.#paused
+ }
+}
+
+module.exports = class UnwrapHandler {
+ #handler
+ #controller
+
+ constructor (handler) {
+ this.#handler = handler
+ }
+
+ static unwrap (handler) {
+ // TODO (fix): More checks...
+ return !handler.onRequestStart ? handler : new UnwrapHandler(handler)
+ }
+
+ onConnect (abort, context) {
+ this.#controller = new UnwrapController(abort)
+ this.#handler.onRequestStart?.(this.#controller, context)
+ }
+
+ onUpgrade (statusCode, rawHeaders, socket) {
+ this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket)
+ }
+
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
+ this.#controller[kResume] = resume
+ this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage)
+ return !this.#controller.paused
+ }
+
+ onData (data) {
+ this.#handler.onResponseData?.(this.#controller, data)
+ return !this.#controller.paused
+ }
+
+ onComplete (rawTrailers) {
+ this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers))
+ }
+
+ onError (err) {
+ if (!this.#handler.onResponseError) {
+ throw new InvalidArgumentError('invalid onError method')
+ }
+
+ this.#handler.onResponseError?.(this.#controller, err)
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/handler/wrap-handler.js b/vanilla/node_modules/undici/lib/handler/wrap-handler.js
new file mode 100644
index 0000000..47caa5f
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/handler/wrap-handler.js
@@ -0,0 +1,95 @@
+'use strict'
+
+const { InvalidArgumentError } = require('../core/errors')
+
+module.exports = class WrapHandler {
+ #handler
+
+ constructor (handler) {
+ this.#handler = handler
+ }
+
+ static wrap (handler) {
+ // TODO (fix): More checks...
+ return handler.onRequestStart ? handler : new WrapHandler(handler)
+ }
+
+ // Unwrap Interface
+
+ onConnect (abort, context) {
+ return this.#handler.onConnect?.(abort, context)
+ }
+
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
+ return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
+ }
+
+ onUpgrade (statusCode, rawHeaders, socket) {
+ return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
+ }
+
+ onData (data) {
+ return this.#handler.onData?.(data)
+ }
+
+ onComplete (trailers) {
+ return this.#handler.onComplete?.(trailers)
+ }
+
+ onError (err) {
+ if (!this.#handler.onError) {
+ throw err
+ }
+
+ return this.#handler.onError?.(err)
+ }
+
+ // Wrap Interface
+
+ onRequestStart (controller, context) {
+ this.#handler.onConnect?.((reason) => controller.abort(reason), context)
+ }
+
+ onRequestUpgrade (controller, statusCode, headers, socket) {
+ const rawHeaders = []
+ for (const [key, val] of Object.entries(headers)) {
+ rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
+ }
+
+ this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
+ }
+
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ const rawHeaders = []
+ for (const [key, val] of Object.entries(headers)) {
+ rawHeaders.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
+ }
+
+ if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) {
+ controller.pause()
+ }
+ }
+
+ onResponseData (controller, data) {
+ if (this.#handler.onData?.(data) === false) {
+ controller.pause()
+ }
+ }
+
+ onResponseEnd (controller, trailers) {
+ const rawTrailers = []
+ for (const [key, val] of Object.entries(trailers)) {
+ rawTrailers.push(Buffer.from(key), Array.isArray(val) ? val.map(v => Buffer.from(v)) : Buffer.from(val))
+ }
+
+ this.#handler.onComplete?.(rawTrailers)
+ }
+
+ onResponseError (controller, err) {
+ if (!this.#handler.onError) {
+ throw new InvalidArgumentError('invalid onError method')
+ }
+
+ this.#handler.onError?.(err)
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/interceptor/cache.js b/vanilla/node_modules/undici/lib/interceptor/cache.js
new file mode 100644
index 0000000..81d7cb1
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/cache.js
@@ -0,0 +1,495 @@
+'use strict'
+
+const assert = require('node:assert')
+const { Readable } = require('node:stream')
+const util = require('../core/util')
+const CacheHandler = require('../handler/cache-handler')
+const MemoryCacheStore = require('../cache/memory-cache-store')
+const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
+const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js')
+const { AbortError } = require('../core/errors.js')
+
+/**
+ * @param {(string | RegExp)[] | undefined} origins
+ * @param {string} name
+ */
+function assertCacheOrigins (origins, name) {
+ if (origins === undefined) return
+ if (!Array.isArray(origins)) {
+ throw new TypeError(`expected ${name} to be an array or undefined, got ${typeof origins}`)
+ }
+ for (let i = 0; i < origins.length; i++) {
+ const origin = origins[i]
+ if (typeof origin !== 'string' && !(origin instanceof RegExp)) {
+ throw new TypeError(`expected ${name}[${i}] to be a string or RegExp, got ${typeof origin}`)
+ }
+ }
+}
+
+const nop = () => {}
+
+/**
+ * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
+ */
+
+/**
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
+ * @returns {boolean}
+ */
+function needsRevalidation (result, cacheControlDirectives, { headers = {} }) {
+ // Always revalidate requests with the no-cache request directive.
+ if (cacheControlDirectives?.['no-cache']) {
+ return true
+ }
+
+ // Always revalidate requests with unqualified no-cache response directive.
+ if (result.cacheControlDirectives?.['no-cache'] && !Array.isArray(result.cacheControlDirectives['no-cache'])) {
+ return true
+ }
+
+ // Always revalidate requests with conditional headers.
+ if (headers['if-modified-since'] || headers['if-none-match']) {
+ return true
+ }
+
+ return false
+}
+
+/**
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
+ * @returns {boolean}
+ */
+function isStale (result, cacheControlDirectives) {
+ const now = Date.now()
+ if (now > result.staleAt) {
+ // Response is stale
+ if (cacheControlDirectives?.['max-stale']) {
+ // There's a threshold where we can serve stale responses, let's see if
+ // we're in it
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
+ const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000)
+ return now > gracePeriod
+ }
+
+ return true
+ }
+
+ if (cacheControlDirectives?.['min-fresh']) {
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
+
+ // At this point, staleAt is always > now
+ const timeLeftTillStale = result.staleAt - now
+ const threshold = cacheControlDirectives['min-fresh'] * 1000
+
+ return timeLeftTillStale <= threshold
+ }
+
+ return false
+}
+
+/**
+ * Check if we're within the stale-while-revalidate window for a stale response
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
+ * @returns {boolean}
+ */
+function withinStaleWhileRevalidateWindow (result) {
+ const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
+ if (!staleWhileRevalidate) {
+ return false
+ }
+
+ const now = Date.now()
+ const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000)
+ return now <= staleWhileRevalidateExpiry
+}
+
+/**
+ * @param {DispatchFn} dispatch
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
+ */
+function handleUncachedResponse (
+ dispatch,
+ globalOpts,
+ cacheKey,
+ handler,
+ opts,
+ reqCacheControl
+) {
+ if (reqCacheControl?.['only-if-cached']) {
+ let aborted = false
+ try {
+ if (typeof handler.onConnect === 'function') {
+ handler.onConnect(() => {
+ aborted = true
+ })
+
+ if (aborted) {
+ return
+ }
+ }
+
+ if (typeof handler.onHeaders === 'function') {
+ handler.onHeaders(504, [], nop, 'Gateway Timeout')
+ if (aborted) {
+ return
+ }
+ }
+
+ if (typeof handler.onComplete === 'function') {
+ handler.onComplete([])
+ }
+ } catch (err) {
+ if (typeof handler.onError === 'function') {
+ handler.onError(err)
+ }
+ }
+
+ return true
+ }
+
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
+}
+
+/**
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
+ * @param {number} age
+ * @param {any} context
+ * @param {boolean} isStale
+ */
+function sendCachedValue (handler, opts, result, age, context, isStale) {
+ // TODO (perf): Readable.from path can be optimized...
+ const stream = util.isStream(result.body)
+ ? result.body
+ : Readable.from(result.body ?? [])
+
+ assert(!stream.destroyed, 'stream should not be destroyed')
+ assert(!stream.readableDidRead, 'stream should not be readableDidRead')
+
+ const controller = {
+ resume () {
+ stream.resume()
+ },
+ pause () {
+ stream.pause()
+ },
+ get paused () {
+ return stream.isPaused()
+ },
+ get aborted () {
+ return stream.destroyed
+ },
+ get reason () {
+ return stream.errored
+ },
+ abort (reason) {
+ stream.destroy(reason ?? new AbortError())
+ }
+ }
+
+ stream
+ .on('error', function (err) {
+ if (!this.readableEnded) {
+ if (typeof handler.onResponseError === 'function') {
+ handler.onResponseError(controller, err)
+ } else {
+ throw err
+ }
+ }
+ })
+ .on('close', function () {
+ if (!this.errored) {
+ handler.onResponseEnd?.(controller, {})
+ }
+ })
+
+ handler.onRequestStart?.(controller, context)
+
+ if (stream.destroyed) {
+ return
+ }
+
+ // Add the age header
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
+ const headers = { ...result.headers, age: String(age) }
+
+ if (isStale) {
+ // Add warning header
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning
+ headers.warning = '110 - "response is stale"'
+ }
+
+ handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
+
+ if (opts.method === 'HEAD') {
+ stream.destroy()
+ } else {
+ stream.on('data', function (chunk) {
+ handler.onResponseData?.(controller, chunk)
+ })
+ }
+}
+
+/**
+ * @param {DispatchFn} dispatch
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result
+ */
+function handleResult (
+ dispatch,
+ globalOpts,
+ cacheKey,
+ handler,
+ opts,
+ reqCacheControl,
+ result
+) {
+ if (!result) {
+ return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
+ }
+
+ const now = Date.now()
+ if (now > result.deleteAt) {
+ // Response is expired, cache store shouldn't have given this to us
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
+ }
+
+ const age = Math.round((now - result.cachedAt) / 1000)
+ if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
+ // Response is considered expired for this specific request
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
+ return dispatch(opts, handler)
+ }
+
+ const stale = isStale(result, reqCacheControl)
+ const revalidate = needsRevalidation(result, reqCacheControl, opts)
+
+ // Check if the response is stale
+ if (stale || revalidate) {
+ if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
+ // If body is a stream we can't revalidate...
+ // TODO (fix): This could be less strict...
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
+ }
+
+ // RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
+ // and revalidate in background, unless immediate revalidation is necessary
+ if (!revalidate && withinStaleWhileRevalidateWindow(result)) {
+ // Serve stale response immediately
+ sendCachedValue(handler, opts, result, age, null, true)
+
+ // Start background revalidation (fire-and-forget)
+ queueMicrotask(() => {
+ const headers = {
+ ...opts.headers,
+ 'if-modified-since': new Date(result.cachedAt).toUTCString()
+ }
+
+ if (result.etag) {
+ headers['if-none-match'] = result.etag
+ }
+
+ if (result.vary) {
+ for (const key in result.vary) {
+ if (result.vary[key] != null) {
+ headers[key] = result.vary[key]
+ }
+ }
+ }
+
+ // Background revalidation - update cache if we get new data
+ dispatch(
+ {
+ ...opts,
+ headers
+ },
+ new CacheHandler(globalOpts, cacheKey, {
+ // Silent handler that just updates the cache
+ onRequestStart () {},
+ onRequestUpgrade () {},
+ onResponseStart () {},
+ onResponseData () {},
+ onResponseEnd () {},
+ onResponseError () {}
+ })
+ )
+ })
+
+ return true
+ }
+
+ let withinStaleIfErrorThreshold = false
+ const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
+ if (staleIfErrorExpiry) {
+ withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
+ }
+
+ const headers = {
+ ...opts.headers,
+ 'if-modified-since': new Date(result.cachedAt).toUTCString()
+ }
+
+ if (result.etag) {
+ headers['if-none-match'] = result.etag
+ }
+
+ if (result.vary) {
+ for (const key in result.vary) {
+ if (result.vary[key] != null) {
+ headers[key] = result.vary[key]
+ }
+ }
+ }
+
+ // We need to revalidate the response
+ return dispatch(
+ {
+ ...opts,
+ headers
+ },
+ new CacheRevalidationHandler(
+ (success, context) => {
+ if (success) {
+ // TODO: successful revalidation should be considered fresh (not give stale warning).
+ sendCachedValue(handler, opts, result, age, context, stale)
+ } else if (util.isStream(result.body)) {
+ result.body.on('error', nop).destroy()
+ }
+ },
+ new CacheHandler(globalOpts, cacheKey, handler),
+ withinStaleIfErrorThreshold
+ )
+ )
+ }
+
+ // Dump request body.
+ if (util.isStream(opts.body)) {
+ opts.body.on('error', nop).destroy()
+ }
+
+ sendCachedValue(handler, opts, result, age, null, false)
+}
+
+/**
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
+ * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
+ */
+module.exports = (opts = {}) => {
+ const {
+ store = new MemoryCacheStore(),
+ methods = ['GET'],
+ cacheByDefault = undefined,
+ type = 'shared',
+ origins = undefined
+ } = opts
+
+ if (typeof opts !== 'object' || opts === null) {
+ throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
+ }
+
+ assertCacheStore(store, 'opts.store')
+ assertCacheMethods(methods, 'opts.methods')
+ assertCacheOrigins(origins, 'opts.origins')
+
+ if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
+ throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
+ }
+
+ if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
+ throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`)
+ }
+
+ const globalOpts = {
+ store,
+ methods,
+ cacheByDefault,
+ type
+ }
+
+ const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
+
+ return dispatch => {
+ return (opts, handler) => {
+ if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
+ // Not a method we want to cache or we don't have the origin, skip
+ return dispatch(opts, handler)
+ }
+
+ // Check if origin is in whitelist
+ if (origins !== undefined) {
+ const requestOrigin = opts.origin.toString().toLowerCase()
+ let isAllowed = false
+
+ for (let i = 0; i < origins.length; i++) {
+ const allowed = origins[i]
+ if (typeof allowed === 'string') {
+ if (allowed.toLowerCase() === requestOrigin) {
+ isAllowed = true
+ break
+ }
+ } else if (allowed.test(requestOrigin)) {
+ isAllowed = true
+ break
+ }
+ }
+
+ if (!isAllowed) {
+ return dispatch(opts, handler)
+ }
+ }
+
+ opts = {
+ ...opts,
+ headers: normalizeHeaders(opts)
+ }
+
+ const reqCacheControl = opts.headers?.['cache-control']
+ ? parseCacheControlHeader(opts.headers['cache-control'])
+ : undefined
+
+ if (reqCacheControl?.['no-store']) {
+ return dispatch(opts, handler)
+ }
+
+ /**
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
+ */
+ const cacheKey = makeCacheKey(opts)
+ const result = store.get(cacheKey)
+
+ if (result && typeof result.then === 'function') {
+ return result
+ .then(result => handleResult(dispatch,
+ globalOpts,
+ cacheKey,
+ handler,
+ opts,
+ reqCacheControl,
+ result
+ ))
+ } else {
+ return handleResult(
+ dispatch,
+ globalOpts,
+ cacheKey,
+ handler,
+ opts,
+ reqCacheControl,
+ result
+ )
+ }
+ }
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/interceptor/decompress.js b/vanilla/node_modules/undici/lib/interceptor/decompress.js
new file mode 100644
index 0000000..ee4202a
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/decompress.js
@@ -0,0 +1,259 @@
+'use strict'
+
+const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib')
+const { pipeline } = require('node:stream')
+const DecoratorHandler = require('../handler/decorator-handler')
+const { runtimeFeatures } = require('../util/runtime-features')
+
+/** @typedef {import('node:stream').Transform} Transform */
+/** @typedef {import('node:stream').Transform} Controller */
+/** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
+
+/** @type {Record<string, () => DecompressorStream>} */
+const supportedEncodings = {
+ gzip: createGunzip,
+ 'x-gzip': createGunzip,
+ br: createBrotliDecompress,
+ deflate: createInflate,
+ compress: createInflate,
+ 'x-compress': createInflate,
+ ...(runtimeFeatures.has('zstd') ? { zstd: createZstdDecompress } : {})
+}
+
+const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
+
+let warningEmitted = /** @type {boolean} */ (false)
+
+/**
+ * @typedef {Object} DecompressHandlerOptions
+ * @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
+ * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
+ */
+
+class DecompressHandler extends DecoratorHandler {
+ /** @type {Transform[]} */
+ #decompressors = []
+ /** @type {Readonly<number[]>} */
+ #skipStatusCodes
+ /** @type {boolean} */
+ #skipErrorResponses
+
+ constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true } = {}) {
+ super(handler)
+ this.#skipStatusCodes = skipStatusCodes
+ this.#skipErrorResponses = skipErrorResponses
+ }
+
+ /**
+ * Determines if decompression should be skipped based on encoding and status code
+ * @param {string} contentEncoding - Content-Encoding header value
+ * @param {number} statusCode - HTTP status code of the response
+ * @returns {boolean} - True if decompression should be skipped
+ */
+ #shouldSkipDecompression (contentEncoding, statusCode) {
+ if (!contentEncoding || statusCode < 200) return true
+ if (this.#skipStatusCodes.includes(statusCode)) return true
+ if (this.#skipErrorResponses && statusCode >= 400) return true
+ return false
+ }
+
+ /**
+ * Creates a chain of decompressors for multiple content encodings
+ *
+ * @param {string} encodings - Comma-separated list of content encodings
+ * @returns {Array<DecompressorStream>} - Array of decompressor streams
+ * @throws {Error} - If the number of content-encodings exceeds the maximum allowed
+ */
+ #createDecompressionChain (encodings) {
+ const parts = encodings.split(',')
+
+ // Limit the number of content-encodings to prevent resource exhaustion.
+ // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
+ const maxContentEncodings = 5
+ if (parts.length > maxContentEncodings) {
+ throw new Error(`too many content-encodings in response: ${parts.length}, maximum allowed is ${maxContentEncodings}`)
+ }
+
+ /** @type {DecompressorStream[]} */
+ const decompressors = []
+
+ for (let i = parts.length - 1; i >= 0; i--) {
+ const encoding = parts[i].trim()
+ if (!encoding) continue
+
+ if (!supportedEncodings[encoding]) {
+ decompressors.length = 0 // Clear if unsupported encoding
+ return decompressors // Unsupported encoding
+ }
+
+ decompressors.push(supportedEncodings[encoding]())
+ }
+
+ return decompressors
+ }
+
+ /**
+ * Sets up event handlers for a decompressor stream using readable events
+ * @param {DecompressorStream} decompressor - The decompressor stream
+ * @param {Controller} controller - The controller to coordinate with
+ * @returns {void}
+ */
+ #setupDecompressorEvents (decompressor, controller) {
+ decompressor.on('readable', () => {
+ let chunk
+ while ((chunk = decompressor.read()) !== null) {
+ const result = super.onResponseData(controller, chunk)
+ if (result === false) {
+ break
+ }
+ }
+ })
+
+ decompressor.on('error', (error) => {
+ super.onResponseError(controller, error)
+ })
+ }
+
+ /**
+ * Sets up event handling for a single decompressor
+ * @param {Controller} controller - The controller to handle events
+ * @returns {void}
+ */
+ #setupSingleDecompressor (controller) {
+ const decompressor = this.#decompressors[0]
+ this.#setupDecompressorEvents(decompressor, controller)
+
+ decompressor.on('end', () => {
+ super.onResponseEnd(controller, {})
+ })
+ }
+
+ /**
+ * Sets up event handling for multiple chained decompressors using pipeline
+ * @param {Controller} controller - The controller to handle events
+ * @returns {void}
+ */
+ #setupMultipleDecompressors (controller) {
+ const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
+ this.#setupDecompressorEvents(lastDecompressor, controller)
+
+ pipeline(this.#decompressors, (err) => {
+ if (err) {
+ super.onResponseError(controller, err)
+ return
+ }
+ super.onResponseEnd(controller, {})
+ })
+ }
+
+ /**
+ * Cleans up decompressor references to prevent memory leaks
+ * @returns {void}
+ */
+ #cleanupDecompressors () {
+ this.#decompressors.length = 0
+ }
+
+ /**
+ * @param {Controller} controller
+ * @param {number} statusCode
+ * @param {Record<string, string | string[] | undefined>} headers
+ * @param {string} statusMessage
+ * @returns {void}
+ */
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ const contentEncoding = headers['content-encoding']
+
+ // If content encoding is not supported or status code is in skip list
+ if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
+ return super.onResponseStart(controller, statusCode, headers, statusMessage)
+ }
+
+ const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
+
+ if (decompressors.length === 0) {
+ this.#cleanupDecompressors()
+ return super.onResponseStart(controller, statusCode, headers, statusMessage)
+ }
+
+ this.#decompressors = decompressors
+
+ // Remove compression headers since we're decompressing
+ const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
+
+ if (this.#decompressors.length === 1) {
+ this.#setupSingleDecompressor(controller)
+ } else {
+ this.#setupMultipleDecompressors(controller)
+ }
+
+ return super.onResponseStart(controller, statusCode, newHeaders, statusMessage)
+ }
+
+ /**
+ * @param {Controller} controller
+ * @param {Buffer} chunk
+ * @returns {void}
+ */
+ onResponseData (controller, chunk) {
+ if (this.#decompressors.length > 0) {
+ this.#decompressors[0].write(chunk)
+ return
+ }
+ super.onResponseData(controller, chunk)
+ }
+
+ /**
+ * @param {Controller} controller
+ * @param {Record<string, string | string[]> | undefined} trailers
+ * @returns {void}
+ */
+ onResponseEnd (controller, trailers) {
+ if (this.#decompressors.length > 0) {
+ this.#decompressors[0].end()
+ this.#cleanupDecompressors()
+ return
+ }
+ super.onResponseEnd(controller, trailers)
+ }
+
+ /**
+ * @param {Controller} controller
+ * @param {Error} err
+ * @returns {void}
+ */
+ onResponseError (controller, err) {
+ if (this.#decompressors.length > 0) {
+ for (const decompressor of this.#decompressors) {
+ decompressor.destroy(err)
+ }
+ this.#cleanupDecompressors()
+ }
+ super.onResponseError(controller, err)
+ }
+}
+
+/**
+ * Creates a decompression interceptor for HTTP responses
+ * @param {DecompressHandlerOptions} [options] - Options for the interceptor
+ * @returns {Function} - Interceptor function
+ */
+function createDecompressInterceptor (options = {}) {
+ // Emit experimental warning only once
+ if (!warningEmitted) {
+ process.emitWarning(
+ 'DecompressInterceptor is experimental and subject to change',
+ 'ExperimentalWarning'
+ )
+ warningEmitted = true
+ }
+
+ return (dispatch) => {
+ return (opts, handler) => {
+ const decompressHandler = new DecompressHandler(handler, options)
+ return dispatch(opts, decompressHandler)
+ }
+ }
+}
+
+module.exports = createDecompressInterceptor
diff --git a/vanilla/node_modules/undici/lib/interceptor/deduplicate.js b/vanilla/node_modules/undici/lib/interceptor/deduplicate.js
new file mode 100644
index 0000000..11c4f37
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/deduplicate.js
@@ -0,0 +1,107 @@
+'use strict'
+
+const diagnosticsChannel = require('node:diagnostics_channel')
+const util = require('../core/util')
+const DeduplicationHandler = require('../handler/deduplication-handler')
+const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = require('../util/cache.js')
+
+const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests')
+
+/**
+ * @param {import('../../types/interceptors.d.ts').default.DeduplicateInterceptorOpts} [opts]
+ * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
+ */
+module.exports = (opts = {}) => {
+ const {
+ methods = ['GET'],
+ skipHeaderNames = [],
+ excludeHeaderNames = []
+ } = opts
+
+ if (typeof opts !== 'object' || opts === null) {
+ throw new TypeError(`expected type of opts to be an Object, got ${opts === null ? 'null' : typeof opts}`)
+ }
+
+ if (!Array.isArray(methods)) {
+ throw new TypeError(`expected opts.methods to be an array, got ${typeof methods}`)
+ }
+
+ for (const method of methods) {
+ if (!util.safeHTTPMethods.includes(method)) {
+ throw new TypeError(`expected opts.methods to only contain safe HTTP methods, got ${method}`)
+ }
+ }
+
+ if (!Array.isArray(skipHeaderNames)) {
+ throw new TypeError(`expected opts.skipHeaderNames to be an array, got ${typeof skipHeaderNames}`)
+ }
+
+ if (!Array.isArray(excludeHeaderNames)) {
+ throw new TypeError(`expected opts.excludeHeaderNames to be an array, got ${typeof excludeHeaderNames}`)
+ }
+
+ // Convert to lowercase Set for case-insensitive header matching
+ const skipHeaderNamesSet = new Set(skipHeaderNames.map(name => name.toLowerCase()))
+
+ // Convert to lowercase Set for case-insensitive header exclusion from deduplication key
+ const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase()))
+
+ /**
+ * Map of pending requests for deduplication
+ * @type {Map<string, DeduplicationHandler>}
+ */
+ const pendingRequests = new Map()
+
+ return dispatch => {
+ return (opts, handler) => {
+ if (!opts.origin || methods.includes(opts.method) === false) {
+ return dispatch(opts, handler)
+ }
+
+ opts = {
+ ...opts,
+ headers: normalizeHeaders(opts)
+ }
+
+ // Skip deduplication if request contains any of the specified headers
+ if (skipHeaderNamesSet.size > 0) {
+ for (const headerName of Object.keys(opts.headers)) {
+ if (skipHeaderNamesSet.has(headerName.toLowerCase())) {
+ return dispatch(opts, handler)
+ }
+ }
+ }
+
+ const cacheKey = makeCacheKey(opts)
+ const dedupeKey = makeDeduplicationKey(cacheKey, excludeHeaderNamesSet)
+
+ // Check if there's already a pending request for this key
+ const pendingHandler = pendingRequests.get(dedupeKey)
+ if (pendingHandler) {
+ // Add this handler to the waiting list
+ pendingHandler.addWaitingHandler(handler)
+ return true
+ }
+
+ // Create a new deduplication handler
+ const deduplicationHandler = new DeduplicationHandler(
+ handler,
+ () => {
+ // Clean up when request completes
+ pendingRequests.delete(dedupeKey)
+ if (pendingRequestsChannel.hasSubscribers) {
+ pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'removed' })
+ }
+ }
+ )
+
+ // Register the pending request
+ pendingRequests.set(dedupeKey, deduplicationHandler)
+ if (pendingRequestsChannel.hasSubscribers) {
+ pendingRequestsChannel.publish({ size: pendingRequests.size, key: dedupeKey, type: 'added' })
+ }
+
+ return dispatch(opts, deduplicationHandler)
+ }
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/interceptor/dns.js b/vanilla/node_modules/undici/lib/interceptor/dns.js
new file mode 100644
index 0000000..9dba957
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/dns.js
@@ -0,0 +1,474 @@
+'use strict'
+const { isIP } = require('node:net')
+const { lookup } = require('node:dns')
+const DecoratorHandler = require('../handler/decorator-handler')
+const { InvalidArgumentError, InformationalError } = require('../core/errors')
+const maxInt = Math.pow(2, 31) - 1
+
+class DNSStorage {
+ #maxItems = 0
+ #records = new Map()
+
+ constructor (opts) {
+ this.#maxItems = opts.maxItems
+ }
+
+ get size () {
+ return this.#records.size
+ }
+
+ get (hostname) {
+ return this.#records.get(hostname) ?? null
+ }
+
+ set (hostname, records) {
+ this.#records.set(hostname, records)
+ }
+
+ delete (hostname) {
+ this.#records.delete(hostname)
+ }
+
+ // Delegate to storage decide can we do more lookups or not
+ full () {
+ return this.size >= this.#maxItems
+ }
+}
+
+class DNSInstance {
+ #maxTTL = 0
+ #maxItems = 0
+ dualStack = true
+ affinity = null
+ lookup = null
+ pick = null
+ storage = null
+
+ constructor (opts) {
+ this.#maxTTL = opts.maxTTL
+ this.#maxItems = opts.maxItems
+ this.dualStack = opts.dualStack
+ this.affinity = opts.affinity
+ this.lookup = opts.lookup ?? this.#defaultLookup
+ this.pick = opts.pick ?? this.#defaultPick
+ this.storage = opts.storage ?? new DNSStorage(opts)
+ }
+
+ runLookup (origin, opts, cb) {
+ const ips = this.storage.get(origin.hostname)
+
+ // If full, we just return the origin
+ if (ips == null && this.storage.full()) {
+ cb(null, origin)
+ return
+ }
+
+ const newOpts = {
+ affinity: this.affinity,
+ dualStack: this.dualStack,
+ lookup: this.lookup,
+ pick: this.pick,
+ ...opts.dns,
+ maxTTL: this.#maxTTL,
+ maxItems: this.#maxItems
+ }
+
+ // If no IPs we lookup
+ if (ips == null) {
+ this.lookup(origin, newOpts, (err, addresses) => {
+ if (err || addresses == null || addresses.length === 0) {
+ cb(err ?? new InformationalError('No DNS entries found'))
+ return
+ }
+
+ this.setRecords(origin, addresses)
+ const records = this.storage.get(origin.hostname)
+
+ const ip = this.pick(
+ origin,
+ records,
+ newOpts.affinity
+ )
+
+ let port
+ if (typeof ip.port === 'number') {
+ port = `:${ip.port}`
+ } else if (origin.port !== '') {
+ port = `:${origin.port}`
+ } else {
+ port = ''
+ }
+
+ cb(
+ null,
+ new URL(`${origin.protocol}//${
+ ip.family === 6 ? `[${ip.address}]` : ip.address
+ }${port}`)
+ )
+ })
+ } else {
+ // If there's IPs we pick
+ const ip = this.pick(
+ origin,
+ ips,
+ newOpts.affinity
+ )
+
+ // If no IPs we lookup - deleting old records
+ if (ip == null) {
+ this.storage.delete(origin.hostname)
+ this.runLookup(origin, opts, cb)
+ return
+ }
+
+ let port
+ if (typeof ip.port === 'number') {
+ port = `:${ip.port}`
+ } else if (origin.port !== '') {
+ port = `:${origin.port}`
+ } else {
+ port = ''
+ }
+
+ cb(
+ null,
+ new URL(`${origin.protocol}//${
+ ip.family === 6 ? `[${ip.address}]` : ip.address
+ }${port}`)
+ )
+ }
+ }
+
+ #defaultLookup (origin, opts, cb) {
+ lookup(
+ origin.hostname,
+ {
+ all: true,
+ family: this.dualStack === false ? this.affinity : 0,
+ order: 'ipv4first'
+ },
+ (err, addresses) => {
+ if (err) {
+ return cb(err)
+ }
+
+ const results = new Map()
+
+ for (const addr of addresses) {
+ // On linux we found duplicates, we attempt to remove them with
+ // the latest record
+ results.set(`${addr.address}:${addr.family}`, addr)
+ }
+
+ cb(null, results.values())
+ }
+ )
+ }
+
+ #defaultPick (origin, hostnameRecords, affinity) {
+ let ip = null
+ const { records, offset } = hostnameRecords
+
+ let family
+ if (this.dualStack) {
+ if (affinity == null) {
+ // Balance between ip families
+ if (offset == null || offset === maxInt) {
+ hostnameRecords.offset = 0
+ affinity = 4
+ } else {
+ hostnameRecords.offset++
+ affinity = (hostnameRecords.offset & 1) === 1 ? 6 : 4
+ }
+ }
+
+ if (records[affinity] != null && records[affinity].ips.length > 0) {
+ family = records[affinity]
+ } else {
+ family = records[affinity === 4 ? 6 : 4]
+ }
+ } else {
+ family = records[affinity]
+ }
+
+ // If no IPs we return null
+ if (family == null || family.ips.length === 0) {
+ return ip
+ }
+
+ if (family.offset == null || family.offset === maxInt) {
+ family.offset = 0
+ } else {
+ family.offset++
+ }
+
+ const position = family.offset % family.ips.length
+ ip = family.ips[position] ?? null
+
+ if (ip == null) {
+ return ip
+ }
+
+ if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
+ // We delete expired records
+ // It is possible that they have different TTL, so we manage them individually
+ family.ips.splice(position, 1)
+ return this.pick(origin, hostnameRecords, affinity)
+ }
+
+ return ip
+ }
+
+ pickFamily (origin, ipFamily) {
+ const records = this.storage.get(origin.hostname)?.records
+ if (!records) {
+ return null
+ }
+
+ const family = records[ipFamily]
+ if (!family) {
+ return null
+ }
+
+ if (family.offset == null || family.offset === maxInt) {
+ family.offset = 0
+ } else {
+ family.offset++
+ }
+
+ const position = family.offset % family.ips.length
+ const ip = family.ips[position] ?? null
+ if (ip == null) {
+ return ip
+ }
+
+ if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
+ // We delete expired records
+ // It is possible that they have different TTL, so we manage them individually
+ family.ips.splice(position, 1)
+ }
+
+ return ip
+ }
+
+ setRecords (origin, addresses) {
+ const timestamp = Date.now()
+ const records = { records: { 4: null, 6: null } }
+ let minTTL = this.#maxTTL
+ for (const record of addresses) {
+ record.timestamp = timestamp
+ if (typeof record.ttl === 'number') {
+ // The record TTL is expected to be in ms
+ record.ttl = Math.min(record.ttl, this.#maxTTL)
+ minTTL = Math.min(minTTL, record.ttl)
+ } else {
+ record.ttl = this.#maxTTL
+ }
+
+ const familyRecords = records.records[record.family] ?? { ips: [] }
+
+ familyRecords.ips.push(record)
+ records.records[record.family] = familyRecords
+ }
+
+ // We provide a default TTL if external storage will be used without TTL per record-level support
+ this.storage.set(origin.hostname, records, { ttl: minTTL })
+ }
+
+ deleteRecords (origin) {
+ this.storage.delete(origin.hostname)
+ }
+
+ getHandler (meta, opts) {
+ return new DNSDispatchHandler(this, meta, opts)
+ }
+}
+
+class DNSDispatchHandler extends DecoratorHandler {
+ #state = null
+ #opts = null
+ #dispatch = null
+ #origin = null
+ #controller = null
+ #newOrigin = null
+ #firstTry = true
+
+ constructor (state, { origin, handler, dispatch, newOrigin }, opts) {
+ super(handler)
+ this.#origin = origin
+ this.#newOrigin = newOrigin
+ this.#opts = { ...opts }
+ this.#state = state
+ this.#dispatch = dispatch
+ }
+
+ onResponseError (controller, err) {
+ switch (err.code) {
+ case 'ETIMEDOUT':
+ case 'ECONNREFUSED': {
+ if (this.#state.dualStack) {
+ if (!this.#firstTry) {
+ super.onResponseError(controller, err)
+ return
+ }
+ this.#firstTry = false
+
+ // Pick an ip address from the other family
+ const otherFamily = this.#newOrigin.hostname[0] === '[' ? 4 : 6
+ const ip = this.#state.pickFamily(this.#origin, otherFamily)
+ if (ip == null) {
+ super.onResponseError(controller, err)
+ return
+ }
+
+ let port
+ if (typeof ip.port === 'number') {
+ port = `:${ip.port}`
+ } else if (this.#origin.port !== '') {
+ port = `:${this.#origin.port}`
+ } else {
+ port = ''
+ }
+
+ const dispatchOpts = {
+ ...this.#opts,
+ origin: `${this.#origin.protocol}//${
+ ip.family === 6 ? `[${ip.address}]` : ip.address
+ }${port}`
+ }
+ this.#dispatch(dispatchOpts, this)
+ return
+ }
+
+ // if dual-stack disabled, we error out
+ super.onResponseError(controller, err)
+ break
+ }
+ case 'ENOTFOUND':
+ this.#state.deleteRecords(this.#origin)
+ super.onResponseError(controller, err)
+ break
+ default:
+ super.onResponseError(controller, err)
+ break
+ }
+ }
+}
+
+module.exports = interceptorOpts => {
+ if (
+ interceptorOpts?.maxTTL != null &&
+ (typeof interceptorOpts?.maxTTL !== 'number' || interceptorOpts?.maxTTL < 0)
+ ) {
+ throw new InvalidArgumentError('Invalid maxTTL. Must be a positive number')
+ }
+
+ if (
+ interceptorOpts?.maxItems != null &&
+ (typeof interceptorOpts?.maxItems !== 'number' ||
+ interceptorOpts?.maxItems < 1)
+ ) {
+ throw new InvalidArgumentError(
+ 'Invalid maxItems. Must be a positive number and greater than zero'
+ )
+ }
+
+ if (
+ interceptorOpts?.affinity != null &&
+ interceptorOpts?.affinity !== 4 &&
+ interceptorOpts?.affinity !== 6
+ ) {
+ throw new InvalidArgumentError('Invalid affinity. Must be either 4 or 6')
+ }
+
+ if (
+ interceptorOpts?.dualStack != null &&
+ typeof interceptorOpts?.dualStack !== 'boolean'
+ ) {
+ throw new InvalidArgumentError('Invalid dualStack. Must be a boolean')
+ }
+
+ if (
+ interceptorOpts?.lookup != null &&
+ typeof interceptorOpts?.lookup !== 'function'
+ ) {
+ throw new InvalidArgumentError('Invalid lookup. Must be a function')
+ }
+
+ if (
+ interceptorOpts?.pick != null &&
+ typeof interceptorOpts?.pick !== 'function'
+ ) {
+ throw new InvalidArgumentError('Invalid pick. Must be a function')
+ }
+
+ if (
+ interceptorOpts?.storage != null &&
+ (typeof interceptorOpts?.storage?.get !== 'function' ||
+ typeof interceptorOpts?.storage?.set !== 'function' ||
+ typeof interceptorOpts?.storage?.full !== 'function' ||
+ typeof interceptorOpts?.storage?.delete !== 'function'
+ )
+ ) {
+ throw new InvalidArgumentError('Invalid storage. Must be a object with methods: { get, set, full, delete }')
+ }
+
+ const dualStack = interceptorOpts?.dualStack ?? true
+ let affinity
+ if (dualStack) {
+ affinity = interceptorOpts?.affinity ?? null
+ } else {
+ affinity = interceptorOpts?.affinity ?? 4
+ }
+
+ const opts = {
+ maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms
+ lookup: interceptorOpts?.lookup ?? null,
+ pick: interceptorOpts?.pick ?? null,
+ dualStack,
+ affinity,
+ maxItems: interceptorOpts?.maxItems ?? Infinity,
+ storage: interceptorOpts?.storage
+ }
+
+ const instance = new DNSInstance(opts)
+
+ return dispatch => {
+ return function dnsInterceptor (origDispatchOpts, handler) {
+ const origin =
+ origDispatchOpts.origin.constructor === URL
+ ? origDispatchOpts.origin
+ : new URL(origDispatchOpts.origin)
+
+ if (isIP(origin.hostname) !== 0) {
+ return dispatch(origDispatchOpts, handler)
+ }
+
+ instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => {
+ if (err) {
+ return handler.onResponseError(null, err)
+ }
+
+ const dispatchOpts = {
+ ...origDispatchOpts,
+ servername: origin.hostname, // For SNI on TLS
+ origin: newOrigin.origin,
+ headers: {
+ host: origin.host,
+ ...origDispatchOpts.headers
+ }
+ }
+
+ dispatch(
+ dispatchOpts,
+ instance.getHandler(
+ { origin, dispatch, handler, newOrigin },
+ origDispatchOpts
+ )
+ )
+ })
+
+ return true
+ }
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/interceptor/dump.js b/vanilla/node_modules/undici/lib/interceptor/dump.js
new file mode 100644
index 0000000..4810a09
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/dump.js
@@ -0,0 +1,112 @@
+'use strict'
+
+const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
+const DecoratorHandler = require('../handler/decorator-handler')
+
+class DumpHandler extends DecoratorHandler {
+ #maxSize = 1024 * 1024
+ #dumped = false
+ #size = 0
+ #controller = null
+ aborted = false
+ reason = false
+
+ constructor ({ maxSize, signal }, handler) {
+ if (maxSize != null && (!Number.isFinite(maxSize) || maxSize < 1)) {
+ throw new InvalidArgumentError('maxSize must be a number greater than 0')
+ }
+
+ super(handler)
+
+ this.#maxSize = maxSize ?? this.#maxSize
+ // this.#handler = handler
+ }
+
+ #abort (reason) {
+ this.aborted = true
+ this.reason = reason
+ }
+
+ onRequestStart (controller, context) {
+ controller.abort = this.#abort.bind(this)
+ this.#controller = controller
+
+ return super.onRequestStart(controller, context)
+ }
+
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ const contentLength = headers['content-length']
+
+ if (contentLength != null && contentLength > this.#maxSize) {
+ throw new RequestAbortedError(
+ `Response size (${contentLength}) larger than maxSize (${
+ this.#maxSize
+ })`
+ )
+ }
+
+ if (this.aborted === true) {
+ return true
+ }
+
+ return super.onResponseStart(controller, statusCode, headers, statusMessage)
+ }
+
+ onResponseError (controller, err) {
+ if (this.#dumped) {
+ return
+ }
+
+ // On network errors before connect, controller will be null
+ err = this.#controller?.reason ?? err
+
+ super.onResponseError(controller, err)
+ }
+
+ onResponseData (controller, chunk) {
+ this.#size = this.#size + chunk.length
+
+ if (this.#size >= this.#maxSize) {
+ this.#dumped = true
+
+ if (this.aborted === true) {
+ super.onResponseError(controller, this.reason)
+ } else {
+ super.onResponseEnd(controller, {})
+ }
+ }
+
+ return true
+ }
+
+ onResponseEnd (controller, trailers) {
+ if (this.#dumped) {
+ return
+ }
+
+ if (this.#controller.aborted === true) {
+ super.onResponseError(controller, this.reason)
+ return
+ }
+
+ super.onResponseEnd(controller, trailers)
+ }
+}
+
+function createDumpInterceptor (
+ { maxSize: defaultMaxSize } = {
+ maxSize: 1024 * 1024
+ }
+) {
+ return dispatch => {
+ return function Intercept (opts, handler) {
+ const { dumpMaxSize = defaultMaxSize } = opts
+
+ const dumpHandler = new DumpHandler({ maxSize: dumpMaxSize, signal: opts.signal }, handler)
+
+ return dispatch(opts, dumpHandler)
+ }
+ }
+}
+
+module.exports = createDumpInterceptor
diff --git a/vanilla/node_modules/undici/lib/interceptor/redirect.js b/vanilla/node_modules/undici/lib/interceptor/redirect.js
new file mode 100644
index 0000000..b7df180
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/redirect.js
@@ -0,0 +1,21 @@
+'use strict'
+
+const RedirectHandler = require('../handler/redirect-handler')
+
+function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections } = {}) {
+ return (dispatch) => {
+ return function Intercept (opts, handler) {
+ const { maxRedirections = defaultMaxRedirections, ...rest } = opts
+
+ if (maxRedirections == null || maxRedirections === 0) {
+ return dispatch(opts, handler)
+ }
+
+ const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting.
+ const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler)
+ return dispatch(dispatchOpts, redirectHandler)
+ }
+ }
+}
+
+module.exports = createRedirectInterceptor
diff --git a/vanilla/node_modules/undici/lib/interceptor/response-error.js b/vanilla/node_modules/undici/lib/interceptor/response-error.js
new file mode 100644
index 0000000..a8105aa
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/response-error.js
@@ -0,0 +1,95 @@
+'use strict'
+
+// const { parseHeaders } = require('../core/util')
+const DecoratorHandler = require('../handler/decorator-handler')
+const { ResponseError } = require('../core/errors')
+
+class ResponseErrorHandler extends DecoratorHandler {
+ #statusCode
+ #contentType
+ #decoder
+ #headers
+ #body
+
+ constructor (_opts, { handler }) {
+ super(handler)
+ }
+
+ #checkContentType (contentType) {
+ return (this.#contentType ?? '').indexOf(contentType) === 0
+ }
+
+ onRequestStart (controller, context) {
+ this.#statusCode = 0
+ this.#contentType = null
+ this.#decoder = null
+ this.#headers = null
+ this.#body = ''
+
+ return super.onRequestStart(controller, context)
+ }
+
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ this.#statusCode = statusCode
+ this.#headers = headers
+ this.#contentType = headers['content-type']
+
+ if (this.#statusCode < 400) {
+ return super.onResponseStart(controller, statusCode, headers, statusMessage)
+ }
+
+ if (this.#checkContentType('application/json') || this.#checkContentType('text/plain')) {
+ this.#decoder = new TextDecoder('utf-8')
+ }
+ }
+
+ onResponseData (controller, chunk) {
+ if (this.#statusCode < 400) {
+ return super.onResponseData(controller, chunk)
+ }
+
+ this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
+ }
+
+ onResponseEnd (controller, trailers) {
+ if (this.#statusCode >= 400) {
+ this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
+
+ if (this.#checkContentType('application/json')) {
+ try {
+ this.#body = JSON.parse(this.#body)
+ } catch {
+ // Do nothing...
+ }
+ }
+
+ let err
+ const stackTraceLimit = Error.stackTraceLimit
+ Error.stackTraceLimit = 0
+ try {
+ err = new ResponseError('Response Error', this.#statusCode, {
+ body: this.#body,
+ headers: this.#headers
+ })
+ } finally {
+ Error.stackTraceLimit = stackTraceLimit
+ }
+
+ super.onResponseError(controller, err)
+ } else {
+ super.onResponseEnd(controller, trailers)
+ }
+ }
+
+ onResponseError (controller, err) {
+ super.onResponseError(controller, err)
+ }
+}
+
+module.exports = () => {
+ return (dispatch) => {
+ return function Intercept (opts, handler) {
+ return dispatch(opts, new ResponseErrorHandler(opts, { handler }))
+ }
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/interceptor/retry.js b/vanilla/node_modules/undici/lib/interceptor/retry.js
new file mode 100644
index 0000000..1c16fd8
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/interceptor/retry.js
@@ -0,0 +1,19 @@
+'use strict'
+const RetryHandler = require('../handler/retry-handler')
+
+module.exports = globalOpts => {
+ return dispatch => {
+ return function retryInterceptor (opts, handler) {
+ return dispatch(
+ opts,
+ new RetryHandler(
+ { ...opts, retryOptions: { ...globalOpts, ...opts.retryOptions } },
+ {
+ handler,
+ dispatch
+ }
+ )
+ )
+ }
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/llhttp/.gitkeep b/vanilla/node_modules/undici/lib/llhttp/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/llhttp/.gitkeep
diff --git a/vanilla/node_modules/undici/lib/llhttp/constants.d.ts b/vanilla/node_modules/undici/lib/llhttp/constants.d.ts
new file mode 100644
index 0000000..def2436
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/llhttp/constants.d.ts
@@ -0,0 +1,195 @@
+export type IntDict = Record<string, number>;
+export declare const ERROR: IntDict;
+export declare const TYPE: IntDict;
+export declare const FLAGS: IntDict;
+export declare const LENIENT_FLAGS: IntDict;
+export declare const METHODS: IntDict;
+export declare const STATUSES: IntDict;
+export declare const FINISH: IntDict;
+export declare const HEADER_STATE: IntDict;
+export declare const METHODS_HTTP: number[];
+export declare const METHODS_ICE: number[];
+export declare const METHODS_RTSP: number[];
+export declare const METHOD_MAP: IntDict;
+export declare const H_METHOD_MAP: {
+ [k: string]: number;
+};
+export declare const STATUSES_HTTP: number[];
+export type CharList = (string | number)[];
+export declare const ALPHA: CharList;
+export declare const NUM_MAP: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+};
+export declare const HEX_MAP: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+ A: number;
+ B: number;
+ C: number;
+ D: number;
+ E: number;
+ F: number;
+ a: number;
+ b: number;
+ c: number;
+ d: number;
+ e: number;
+ f: number;
+};
+export declare const NUM: CharList;
+export declare const ALPHANUM: CharList;
+export declare const MARK: CharList;
+export declare const USERINFO_CHARS: CharList;
+export declare const URL_CHAR: CharList;
+export declare const HEX: CharList;
+export declare const TOKEN: CharList;
+export declare const HEADER_CHARS: CharList;
+export declare const CONNECTION_TOKEN_CHARS: CharList;
+export declare const QUOTED_STRING: CharList;
+export declare const HTAB_SP_VCHAR_OBS_TEXT: CharList;
+export declare const MAJOR: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+};
+export declare const MINOR: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+};
+export declare const SPECIAL_HEADERS: {
+ connection: number;
+ 'content-length': number;
+ 'proxy-connection': number;
+ 'transfer-encoding': number;
+ upgrade: number;
+};
+declare const _default: {
+ ERROR: IntDict;
+ TYPE: IntDict;
+ FLAGS: IntDict;
+ LENIENT_FLAGS: IntDict;
+ METHODS: IntDict;
+ STATUSES: IntDict;
+ FINISH: IntDict;
+ HEADER_STATE: IntDict;
+ ALPHA: CharList;
+ NUM_MAP: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+ };
+ HEX_MAP: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+ A: number;
+ B: number;
+ C: number;
+ D: number;
+ E: number;
+ F: number;
+ a: number;
+ b: number;
+ c: number;
+ d: number;
+ e: number;
+ f: number;
+ };
+ NUM: CharList;
+ ALPHANUM: CharList;
+ MARK: CharList;
+ USERINFO_CHARS: CharList;
+ URL_CHAR: CharList;
+ HEX: CharList;
+ TOKEN: CharList;
+ HEADER_CHARS: CharList;
+ CONNECTION_TOKEN_CHARS: CharList;
+ QUOTED_STRING: CharList;
+ HTAB_SP_VCHAR_OBS_TEXT: CharList;
+ MAJOR: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+ };
+ MINOR: {
+ 0: number;
+ 1: number;
+ 2: number;
+ 3: number;
+ 4: number;
+ 5: number;
+ 6: number;
+ 7: number;
+ 8: number;
+ 9: number;
+ };
+ SPECIAL_HEADERS: {
+ connection: number;
+ 'content-length': number;
+ 'proxy-connection': number;
+ 'transfer-encoding': number;
+ upgrade: number;
+ };
+ METHODS_HTTP: number[];
+ METHODS_ICE: number[];
+ METHODS_RTSP: number[];
+ METHOD_MAP: IntDict;
+ H_METHOD_MAP: {
+ [k: string]: number;
+ };
+ STATUSES_HTTP: number[];
+};
+export default _default;
diff --git a/vanilla/node_modules/undici/lib/llhttp/constants.js b/vanilla/node_modules/undici/lib/llhttp/constants.js
new file mode 100644
index 0000000..8b88dfd
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/llhttp/constants.js
@@ -0,0 +1,531 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.SPECIAL_HEADERS = exports.MINOR = exports.MAJOR = exports.HTAB_SP_VCHAR_OBS_TEXT = exports.QUOTED_STRING = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.HEX = exports.URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.STATUSES_HTTP = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.HEADER_STATE = exports.FINISH = exports.STATUSES = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0;
+const utils_1 = require("./utils");
+// Emums
+exports.ERROR = {
+ OK: 0,
+ INTERNAL: 1,
+ STRICT: 2,
+ CR_EXPECTED: 25,
+ LF_EXPECTED: 3,
+ UNEXPECTED_CONTENT_LENGTH: 4,
+ UNEXPECTED_SPACE: 30,
+ CLOSED_CONNECTION: 5,
+ INVALID_METHOD: 6,
+ INVALID_URL: 7,
+ INVALID_CONSTANT: 8,
+ INVALID_VERSION: 9,
+ INVALID_HEADER_TOKEN: 10,
+ INVALID_CONTENT_LENGTH: 11,
+ INVALID_CHUNK_SIZE: 12,
+ INVALID_STATUS: 13,
+ INVALID_EOF_STATE: 14,
+ INVALID_TRANSFER_ENCODING: 15,
+ CB_MESSAGE_BEGIN: 16,
+ CB_HEADERS_COMPLETE: 17,
+ CB_MESSAGE_COMPLETE: 18,
+ CB_CHUNK_HEADER: 19,
+ CB_CHUNK_COMPLETE: 20,
+ PAUSED: 21,
+ PAUSED_UPGRADE: 22,
+ PAUSED_H2_UPGRADE: 23,
+ USER: 24,
+ CB_URL_COMPLETE: 26,
+ CB_STATUS_COMPLETE: 27,
+ CB_METHOD_COMPLETE: 32,
+ CB_VERSION_COMPLETE: 33,
+ CB_HEADER_FIELD_COMPLETE: 28,
+ CB_HEADER_VALUE_COMPLETE: 29,
+ CB_CHUNK_EXTENSION_NAME_COMPLETE: 34,
+ CB_CHUNK_EXTENSION_VALUE_COMPLETE: 35,
+ CB_RESET: 31,
+ CB_PROTOCOL_COMPLETE: 38,
+};
+exports.TYPE = {
+ BOTH: 0, // default
+ REQUEST: 1,
+ RESPONSE: 2,
+};
+exports.FLAGS = {
+ CONNECTION_KEEP_ALIVE: 1 << 0,
+ CONNECTION_CLOSE: 1 << 1,
+ CONNECTION_UPGRADE: 1 << 2,
+ CHUNKED: 1 << 3,
+ UPGRADE: 1 << 4,
+ CONTENT_LENGTH: 1 << 5,
+ SKIPBODY: 1 << 6,
+ TRAILING: 1 << 7,
+ // 1 << 8 is unused
+ TRANSFER_ENCODING: 1 << 9,
+};
+exports.LENIENT_FLAGS = {
+ HEADERS: 1 << 0,
+ CHUNKED_LENGTH: 1 << 1,
+ KEEP_ALIVE: 1 << 2,
+ TRANSFER_ENCODING: 1 << 3,
+ VERSION: 1 << 4,
+ DATA_AFTER_CLOSE: 1 << 5,
+ OPTIONAL_LF_AFTER_CR: 1 << 6,
+ OPTIONAL_CRLF_AFTER_CHUNK: 1 << 7,
+ OPTIONAL_CR_BEFORE_LF: 1 << 8,
+ SPACES_AFTER_CHUNK_SIZE: 1 << 9,
+};
+exports.METHODS = {
+ 'DELETE': 0,
+ 'GET': 1,
+ 'HEAD': 2,
+ 'POST': 3,
+ 'PUT': 4,
+ /* pathological */
+ 'CONNECT': 5,
+ 'OPTIONS': 6,
+ 'TRACE': 7,
+ /* WebDAV */
+ 'COPY': 8,
+ 'LOCK': 9,
+ 'MKCOL': 10,
+ 'MOVE': 11,
+ 'PROPFIND': 12,
+ 'PROPPATCH': 13,
+ 'SEARCH': 14,
+ 'UNLOCK': 15,
+ 'BIND': 16,
+ 'REBIND': 17,
+ 'UNBIND': 18,
+ 'ACL': 19,
+ /* subversion */
+ 'REPORT': 20,
+ 'MKACTIVITY': 21,
+ 'CHECKOUT': 22,
+ 'MERGE': 23,
+ /* upnp */
+ 'M-SEARCH': 24,
+ 'NOTIFY': 25,
+ 'SUBSCRIBE': 26,
+ 'UNSUBSCRIBE': 27,
+ /* RFC-5789 */
+ 'PATCH': 28,
+ 'PURGE': 29,
+ /* CalDAV */
+ 'MKCALENDAR': 30,
+ /* RFC-2068, section 19.6.1.2 */
+ 'LINK': 31,
+ 'UNLINK': 32,
+ /* icecast */
+ 'SOURCE': 33,
+ /* RFC-7540, section 11.6 */
+ 'PRI': 34,
+ /* RFC-2326 RTSP */
+ 'DESCRIBE': 35,
+ 'ANNOUNCE': 36,
+ 'SETUP': 37,
+ 'PLAY': 38,
+ 'PAUSE': 39,
+ 'TEARDOWN': 40,
+ 'GET_PARAMETER': 41,
+ 'SET_PARAMETER': 42,
+ 'REDIRECT': 43,
+ 'RECORD': 44,
+ /* RAOP */
+ 'FLUSH': 45,
+ /* DRAFT https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html */
+ 'QUERY': 46,
+};
+exports.STATUSES = {
+ CONTINUE: 100,
+ SWITCHING_PROTOCOLS: 101,
+ PROCESSING: 102,
+ EARLY_HINTS: 103,
+ RESPONSE_IS_STALE: 110, // Unofficial
+ REVALIDATION_FAILED: 111, // Unofficial
+ DISCONNECTED_OPERATION: 112, // Unofficial
+ HEURISTIC_EXPIRATION: 113, // Unofficial
+ MISCELLANEOUS_WARNING: 199, // Unofficial
+ OK: 200,
+ CREATED: 201,
+ ACCEPTED: 202,
+ NON_AUTHORITATIVE_INFORMATION: 203,
+ NO_CONTENT: 204,
+ RESET_CONTENT: 205,
+ PARTIAL_CONTENT: 206,
+ MULTI_STATUS: 207,
+ ALREADY_REPORTED: 208,
+ TRANSFORMATION_APPLIED: 214, // Unofficial
+ IM_USED: 226,
+ MISCELLANEOUS_PERSISTENT_WARNING: 299, // Unofficial
+ MULTIPLE_CHOICES: 300,
+ MOVED_PERMANENTLY: 301,
+ FOUND: 302,
+ SEE_OTHER: 303,
+ NOT_MODIFIED: 304,
+ USE_PROXY: 305,
+ SWITCH_PROXY: 306, // No longer used
+ TEMPORARY_REDIRECT: 307,
+ PERMANENT_REDIRECT: 308,
+ BAD_REQUEST: 400,
+ UNAUTHORIZED: 401,
+ PAYMENT_REQUIRED: 402,
+ FORBIDDEN: 403,
+ NOT_FOUND: 404,
+ METHOD_NOT_ALLOWED: 405,
+ NOT_ACCEPTABLE: 406,
+ PROXY_AUTHENTICATION_REQUIRED: 407,
+ REQUEST_TIMEOUT: 408,
+ CONFLICT: 409,
+ GONE: 410,
+ LENGTH_REQUIRED: 411,
+ PRECONDITION_FAILED: 412,
+ PAYLOAD_TOO_LARGE: 413,
+ URI_TOO_LONG: 414,
+ UNSUPPORTED_MEDIA_TYPE: 415,
+ RANGE_NOT_SATISFIABLE: 416,
+ EXPECTATION_FAILED: 417,
+ IM_A_TEAPOT: 418,
+ PAGE_EXPIRED: 419, // Unofficial
+ ENHANCE_YOUR_CALM: 420, // Unofficial
+ MISDIRECTED_REQUEST: 421,
+ UNPROCESSABLE_ENTITY: 422,
+ LOCKED: 423,
+ FAILED_DEPENDENCY: 424,
+ TOO_EARLY: 425,
+ UPGRADE_REQUIRED: 426,
+ PRECONDITION_REQUIRED: 428,
+ TOO_MANY_REQUESTS: 429,
+ REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL: 430, // Unofficial
+ REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
+ LOGIN_TIMEOUT: 440, // Unofficial
+ NO_RESPONSE: 444, // Unofficial
+ RETRY_WITH: 449, // Unofficial
+ BLOCKED_BY_PARENTAL_CONTROL: 450, // Unofficial
+ UNAVAILABLE_FOR_LEGAL_REASONS: 451,
+ CLIENT_CLOSED_LOAD_BALANCED_REQUEST: 460, // Unofficial
+ INVALID_X_FORWARDED_FOR: 463, // Unofficial
+ REQUEST_HEADER_TOO_LARGE: 494, // Unofficial
+ SSL_CERTIFICATE_ERROR: 495, // Unofficial
+ SSL_CERTIFICATE_REQUIRED: 496, // Unofficial
+ HTTP_REQUEST_SENT_TO_HTTPS_PORT: 497, // Unofficial
+ INVALID_TOKEN: 498, // Unofficial
+ CLIENT_CLOSED_REQUEST: 499, // Unofficial
+ INTERNAL_SERVER_ERROR: 500,
+ NOT_IMPLEMENTED: 501,
+ BAD_GATEWAY: 502,
+ SERVICE_UNAVAILABLE: 503,
+ GATEWAY_TIMEOUT: 504,
+ HTTP_VERSION_NOT_SUPPORTED: 505,
+ VARIANT_ALSO_NEGOTIATES: 506,
+ INSUFFICIENT_STORAGE: 507,
+ LOOP_DETECTED: 508,
+ BANDWIDTH_LIMIT_EXCEEDED: 509,
+ NOT_EXTENDED: 510,
+ NETWORK_AUTHENTICATION_REQUIRED: 511,
+ WEB_SERVER_UNKNOWN_ERROR: 520, // Unofficial
+ WEB_SERVER_IS_DOWN: 521, // Unofficial
+ CONNECTION_TIMEOUT: 522, // Unofficial
+ ORIGIN_IS_UNREACHABLE: 523, // Unofficial
+ TIMEOUT_OCCURED: 524, // Unofficial
+ SSL_HANDSHAKE_FAILED: 525, // Unofficial
+ INVALID_SSL_CERTIFICATE: 526, // Unofficial
+ RAILGUN_ERROR: 527, // Unofficial
+ SITE_IS_OVERLOADED: 529, // Unofficial
+ SITE_IS_FROZEN: 530, // Unofficial
+ IDENTITY_PROVIDER_AUTHENTICATION_ERROR: 561, // Unofficial
+ NETWORK_READ_TIMEOUT: 598, // Unofficial
+ NETWORK_CONNECT_TIMEOUT: 599, // Unofficial
+};
+exports.FINISH = {
+ SAFE: 0,
+ SAFE_WITH_CB: 1,
+ UNSAFE: 2,
+};
+exports.HEADER_STATE = {
+ GENERAL: 0,
+ CONNECTION: 1,
+ CONTENT_LENGTH: 2,
+ TRANSFER_ENCODING: 3,
+ UPGRADE: 4,
+ CONNECTION_KEEP_ALIVE: 5,
+ CONNECTION_CLOSE: 6,
+ CONNECTION_UPGRADE: 7,
+ TRANSFER_ENCODING_CHUNKED: 8,
+};
+// C headers
+exports.METHODS_HTTP = [
+ exports.METHODS.DELETE,
+ exports.METHODS.GET,
+ exports.METHODS.HEAD,
+ exports.METHODS.POST,
+ exports.METHODS.PUT,
+ exports.METHODS.CONNECT,
+ exports.METHODS.OPTIONS,
+ exports.METHODS.TRACE,
+ exports.METHODS.COPY,
+ exports.METHODS.LOCK,
+ exports.METHODS.MKCOL,
+ exports.METHODS.MOVE,
+ exports.METHODS.PROPFIND,
+ exports.METHODS.PROPPATCH,
+ exports.METHODS.SEARCH,
+ exports.METHODS.UNLOCK,
+ exports.METHODS.BIND,
+ exports.METHODS.REBIND,
+ exports.METHODS.UNBIND,
+ exports.METHODS.ACL,
+ exports.METHODS.REPORT,
+ exports.METHODS.MKACTIVITY,
+ exports.METHODS.CHECKOUT,
+ exports.METHODS.MERGE,
+ exports.METHODS['M-SEARCH'],
+ exports.METHODS.NOTIFY,
+ exports.METHODS.SUBSCRIBE,
+ exports.METHODS.UNSUBSCRIBE,
+ exports.METHODS.PATCH,
+ exports.METHODS.PURGE,
+ exports.METHODS.MKCALENDAR,
+ exports.METHODS.LINK,
+ exports.METHODS.UNLINK,
+ exports.METHODS.PRI,
+ // TODO(indutny): should we allow it with HTTP?
+ exports.METHODS.SOURCE,
+ exports.METHODS.QUERY,
+];
+exports.METHODS_ICE = [
+ exports.METHODS.SOURCE,
+];
+exports.METHODS_RTSP = [
+ exports.METHODS.OPTIONS,
+ exports.METHODS.DESCRIBE,
+ exports.METHODS.ANNOUNCE,
+ exports.METHODS.SETUP,
+ exports.METHODS.PLAY,
+ exports.METHODS.PAUSE,
+ exports.METHODS.TEARDOWN,
+ exports.METHODS.GET_PARAMETER,
+ exports.METHODS.SET_PARAMETER,
+ exports.METHODS.REDIRECT,
+ exports.METHODS.RECORD,
+ exports.METHODS.FLUSH,
+ // For AirPlay
+ exports.METHODS.GET,
+ exports.METHODS.POST,
+];
+exports.METHOD_MAP = (0, utils_1.enumToMap)(exports.METHODS);
+exports.H_METHOD_MAP = Object.fromEntries(Object.entries(exports.METHODS).filter(([k]) => k.startsWith('H')));
+exports.STATUSES_HTTP = [
+ exports.STATUSES.CONTINUE,
+ exports.STATUSES.SWITCHING_PROTOCOLS,
+ exports.STATUSES.PROCESSING,
+ exports.STATUSES.EARLY_HINTS,
+ exports.STATUSES.RESPONSE_IS_STALE,
+ exports.STATUSES.REVALIDATION_FAILED,
+ exports.STATUSES.DISCONNECTED_OPERATION,
+ exports.STATUSES.HEURISTIC_EXPIRATION,
+ exports.STATUSES.MISCELLANEOUS_WARNING,
+ exports.STATUSES.OK,
+ exports.STATUSES.CREATED,
+ exports.STATUSES.ACCEPTED,
+ exports.STATUSES.NON_AUTHORITATIVE_INFORMATION,
+ exports.STATUSES.NO_CONTENT,
+ exports.STATUSES.RESET_CONTENT,
+ exports.STATUSES.PARTIAL_CONTENT,
+ exports.STATUSES.MULTI_STATUS,
+ exports.STATUSES.ALREADY_REPORTED,
+ exports.STATUSES.TRANSFORMATION_APPLIED,
+ exports.STATUSES.IM_USED,
+ exports.STATUSES.MISCELLANEOUS_PERSISTENT_WARNING,
+ exports.STATUSES.MULTIPLE_CHOICES,
+ exports.STATUSES.MOVED_PERMANENTLY,
+ exports.STATUSES.FOUND,
+ exports.STATUSES.SEE_OTHER,
+ exports.STATUSES.NOT_MODIFIED,
+ exports.STATUSES.USE_PROXY,
+ exports.STATUSES.SWITCH_PROXY,
+ exports.STATUSES.TEMPORARY_REDIRECT,
+ exports.STATUSES.PERMANENT_REDIRECT,
+ exports.STATUSES.BAD_REQUEST,
+ exports.STATUSES.UNAUTHORIZED,
+ exports.STATUSES.PAYMENT_REQUIRED,
+ exports.STATUSES.FORBIDDEN,
+ exports.STATUSES.NOT_FOUND,
+ exports.STATUSES.METHOD_NOT_ALLOWED,
+ exports.STATUSES.NOT_ACCEPTABLE,
+ exports.STATUSES.PROXY_AUTHENTICATION_REQUIRED,
+ exports.STATUSES.REQUEST_TIMEOUT,
+ exports.STATUSES.CONFLICT,
+ exports.STATUSES.GONE,
+ exports.STATUSES.LENGTH_REQUIRED,
+ exports.STATUSES.PRECONDITION_FAILED,
+ exports.STATUSES.PAYLOAD_TOO_LARGE,
+ exports.STATUSES.URI_TOO_LONG,
+ exports.STATUSES.UNSUPPORTED_MEDIA_TYPE,
+ exports.STATUSES.RANGE_NOT_SATISFIABLE,
+ exports.STATUSES.EXPECTATION_FAILED,
+ exports.STATUSES.IM_A_TEAPOT,
+ exports.STATUSES.PAGE_EXPIRED,
+ exports.STATUSES.ENHANCE_YOUR_CALM,
+ exports.STATUSES.MISDIRECTED_REQUEST,
+ exports.STATUSES.UNPROCESSABLE_ENTITY,
+ exports.STATUSES.LOCKED,
+ exports.STATUSES.FAILED_DEPENDENCY,
+ exports.STATUSES.TOO_EARLY,
+ exports.STATUSES.UPGRADE_REQUIRED,
+ exports.STATUSES.PRECONDITION_REQUIRED,
+ exports.STATUSES.TOO_MANY_REQUESTS,
+ exports.STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE_UNOFFICIAL,
+ exports.STATUSES.REQUEST_HEADER_FIELDS_TOO_LARGE,
+ exports.STATUSES.LOGIN_TIMEOUT,
+ exports.STATUSES.NO_RESPONSE,
+ exports.STATUSES.RETRY_WITH,
+ exports.STATUSES.BLOCKED_BY_PARENTAL_CONTROL,
+ exports.STATUSES.UNAVAILABLE_FOR_LEGAL_REASONS,
+ exports.STATUSES.CLIENT_CLOSED_LOAD_BALANCED_REQUEST,
+ exports.STATUSES.INVALID_X_FORWARDED_FOR,
+ exports.STATUSES.REQUEST_HEADER_TOO_LARGE,
+ exports.STATUSES.SSL_CERTIFICATE_ERROR,
+ exports.STATUSES.SSL_CERTIFICATE_REQUIRED,
+ exports.STATUSES.HTTP_REQUEST_SENT_TO_HTTPS_PORT,
+ exports.STATUSES.INVALID_TOKEN,
+ exports.STATUSES.CLIENT_CLOSED_REQUEST,
+ exports.STATUSES.INTERNAL_SERVER_ERROR,
+ exports.STATUSES.NOT_IMPLEMENTED,
+ exports.STATUSES.BAD_GATEWAY,
+ exports.STATUSES.SERVICE_UNAVAILABLE,
+ exports.STATUSES.GATEWAY_TIMEOUT,
+ exports.STATUSES.HTTP_VERSION_NOT_SUPPORTED,
+ exports.STATUSES.VARIANT_ALSO_NEGOTIATES,
+ exports.STATUSES.INSUFFICIENT_STORAGE,
+ exports.STATUSES.LOOP_DETECTED,
+ exports.STATUSES.BANDWIDTH_LIMIT_EXCEEDED,
+ exports.STATUSES.NOT_EXTENDED,
+ exports.STATUSES.NETWORK_AUTHENTICATION_REQUIRED,
+ exports.STATUSES.WEB_SERVER_UNKNOWN_ERROR,
+ exports.STATUSES.WEB_SERVER_IS_DOWN,
+ exports.STATUSES.CONNECTION_TIMEOUT,
+ exports.STATUSES.ORIGIN_IS_UNREACHABLE,
+ exports.STATUSES.TIMEOUT_OCCURED,
+ exports.STATUSES.SSL_HANDSHAKE_FAILED,
+ exports.STATUSES.INVALID_SSL_CERTIFICATE,
+ exports.STATUSES.RAILGUN_ERROR,
+ exports.STATUSES.SITE_IS_OVERLOADED,
+ exports.STATUSES.SITE_IS_FROZEN,
+ exports.STATUSES.IDENTITY_PROVIDER_AUTHENTICATION_ERROR,
+ exports.STATUSES.NETWORK_READ_TIMEOUT,
+ exports.STATUSES.NETWORK_CONNECT_TIMEOUT,
+];
+exports.ALPHA = [];
+for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) {
+ // Upper case
+ exports.ALPHA.push(String.fromCharCode(i));
+ // Lower case
+ exports.ALPHA.push(String.fromCharCode(i + 0x20));
+}
+exports.NUM_MAP = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
+ 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
+};
+exports.HEX_MAP = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4,
+ 5: 5, 6: 6, 7: 7, 8: 8, 9: 9,
+ A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF,
+ a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf,
+};
+exports.NUM = [
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+];
+exports.ALPHANUM = exports.ALPHA.concat(exports.NUM);
+exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')'];
+exports.USERINFO_CHARS = exports.ALPHANUM
+ .concat(exports.MARK)
+ .concat(['%', ';', ':', '&', '=', '+', '$', ',']);
+// TODO(indutny): use RFC
+exports.URL_CHAR = [
+ '!', '"', '$', '%', '&', '\'',
+ '(', ')', '*', '+', ',', '-', '.', '/',
+ ':', ';', '<', '=', '>',
+ '@', '[', '\\', ']', '^', '_',
+ '`',
+ '{', '|', '}', '~',
+].concat(exports.ALPHANUM);
+exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']);
+/* Tokens as defined by rfc 2616. Also lowercases them.
+ * token = 1*<any CHAR except CTLs or separators>
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ */
+exports.TOKEN = [
+ '!', '#', '$', '%', '&', '\'',
+ '*', '+', '-', '.',
+ '^', '_', '`',
+ '|', '~',
+].concat(exports.ALPHANUM);
+/*
+ * Verify that a char is a valid visible (printable) US-ASCII
+ * character or %x80-FF
+ */
+exports.HEADER_CHARS = ['\t'];
+for (let i = 32; i <= 255; i++) {
+ if (i !== 127) {
+ exports.HEADER_CHARS.push(i);
+ }
+}
+// ',' = \x44
+exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44);
+exports.QUOTED_STRING = ['\t', ' '];
+for (let i = 0x21; i <= 0xff; i++) {
+ if (i !== 0x22 && i !== 0x5c) { // All characters in ASCII except \ and "
+ exports.QUOTED_STRING.push(i);
+ }
+}
+exports.HTAB_SP_VCHAR_OBS_TEXT = ['\t', ' '];
+// VCHAR: https://tools.ietf.org/html/rfc5234#appendix-B.1
+for (let i = 0x21; i <= 0x7E; i++) {
+ exports.HTAB_SP_VCHAR_OBS_TEXT.push(i);
+}
+// OBS_TEXT: https://datatracker.ietf.org/doc/html/rfc9110#name-collected-abnf
+for (let i = 0x80; i <= 0xff; i++) {
+ exports.HTAB_SP_VCHAR_OBS_TEXT.push(i);
+}
+exports.MAJOR = exports.NUM_MAP;
+exports.MINOR = exports.MAJOR;
+exports.SPECIAL_HEADERS = {
+ 'connection': exports.HEADER_STATE.CONNECTION,
+ 'content-length': exports.HEADER_STATE.CONTENT_LENGTH,
+ 'proxy-connection': exports.HEADER_STATE.CONNECTION,
+ 'transfer-encoding': exports.HEADER_STATE.TRANSFER_ENCODING,
+ 'upgrade': exports.HEADER_STATE.UPGRADE,
+};
+exports.default = {
+ ERROR: exports.ERROR,
+ TYPE: exports.TYPE,
+ FLAGS: exports.FLAGS,
+ LENIENT_FLAGS: exports.LENIENT_FLAGS,
+ METHODS: exports.METHODS,
+ STATUSES: exports.STATUSES,
+ FINISH: exports.FINISH,
+ HEADER_STATE: exports.HEADER_STATE,
+ ALPHA: exports.ALPHA,
+ NUM_MAP: exports.NUM_MAP,
+ HEX_MAP: exports.HEX_MAP,
+ NUM: exports.NUM,
+ ALPHANUM: exports.ALPHANUM,
+ MARK: exports.MARK,
+ USERINFO_CHARS: exports.USERINFO_CHARS,
+ URL_CHAR: exports.URL_CHAR,
+ HEX: exports.HEX,
+ TOKEN: exports.TOKEN,
+ HEADER_CHARS: exports.HEADER_CHARS,
+ CONNECTION_TOKEN_CHARS: exports.CONNECTION_TOKEN_CHARS,
+ QUOTED_STRING: exports.QUOTED_STRING,
+ HTAB_SP_VCHAR_OBS_TEXT: exports.HTAB_SP_VCHAR_OBS_TEXT,
+ MAJOR: exports.MAJOR,
+ MINOR: exports.MINOR,
+ SPECIAL_HEADERS: exports.SPECIAL_HEADERS,
+ METHODS_HTTP: exports.METHODS_HTTP,
+ METHODS_ICE: exports.METHODS_ICE,
+ METHODS_RTSP: exports.METHODS_RTSP,
+ METHOD_MAP: exports.METHOD_MAP,
+ H_METHOD_MAP: exports.H_METHOD_MAP,
+ STATUSES_HTTP: exports.STATUSES_HTTP,
+};
diff --git a/vanilla/node_modules/undici/lib/llhttp/llhttp-wasm.js b/vanilla/node_modules/undici/lib/llhttp/llhttp-wasm.js
new file mode 100644
index 0000000..8e89806
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/llhttp/llhttp-wasm.js
@@ -0,0 +1,15 @@
+'use strict'
+
+const { Buffer } = require('node:buffer')
+
+const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRw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AXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgL5YUCAgd/A34gASACaiEEAkAgACIDKAIMIgANACADKAIEBEAgAyABNgIECyMAQRBrIgkkAAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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C0HjAQwHC0HXAQwGC0HYAQwFC0HZAQwEC0HaAQwDC0HbAQwCC0HdAQwBC0HcAQshAgNAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJ/AkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAg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ogKiAqICogKiAqICogKiAqICogKiAqICogIAogILIAQgAUEBaiIBRw0AC0HJACECDOwCCyABQQFqIQEgA0Evai0AAEEBcQ2lAiADQQA2AhwgAyABNgIUIANBlxA2AhAgA0EKNgIMQQAhAgzrAgsgASAERwRAA0AgAS0AAEEgRw0VIAQgAUEBaiIBRw0AC0H4ACECDOsCC0H4ACECDOoCCyADQQI6ACgMOAtBACECIANBADYCHCADQb8LNgIQIANBAjYCDCADIAFBAWo2AhQM6AILQQAhAgzOAgtBDSECDM0CC0ETIQIMzAILQRUhAgzLAgtBFiECDMoCC0EYIQIMyQILQRkhAgzIAgtBGiECDMcCC0EbIQIMxgILQRwhAgzFAgtBHSECDMQCC0EeIQIMwwILQR8hAgzCAgtBICECDMECC0EiIQIMwAILQSMhAgy/AgtBJSECDL4CC0HlACECDL0CCyADQT02AhwgAyABNgIUIAMgADYCDEEAIQIM1QILIANBGzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDNQCCyADQSA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzTAgsgA0ETNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0gILIANBCzYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNECCyADQRA2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzQAgsgA0EgNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzwILIANBCzYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM4CCyADQQw2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzNAgtBACECIANBADYCHCADIAE2AhQgA0HdDjYCECADQRI2AgwMzAILAkADQAJAIAEtAABBCmsOBAACAgACCyAEIAFBAWoiAUcNAAtB/QEhAgzMAgsCQAJAIAMtADZBAUcNAEEAIQACQCADKAI4IgJFDQAgAigCYCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUcNASADQfwBNgIcIAMgATYCFCADQdwZNgIQIANBFTYCDEEAIQIMzQILQdwBIQIMswILIANBADYCHCADIAE2AhQgA0H5CzYCECADQR82AgxBACECDMsCCwJAAkAgAy0AKEEBaw4CBAEAC0HbASECDLICC0HUASECDLECCyADQQI6ADFBACEAAkAgAygCOCICRQ0AIAIoAgAiAkUNACADIAIRAAAhAAsgAEUEQEHdASECDLECCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQbQMNgIQIANBEDYCDEEAIQIMygILIANB+wE2AhwgAyABNgIUIANBgRo2AhAgA0EVNgIMQQAhAgzJAgsgASAERgRAQfoBIQIMyQILIAEtAABByABGDQEgA0EBOgAoC0HAASECDK4CC0HaASECDK0CCyABIARHBEAgA0EMNgIIIAMgATYCBEHZASECDK0CC0H5ASECDMUCCyABIARGBEBB+AEhAgzFAgsgAS0AAEHIAEcNBCABQQFqIQFB2AEhAgyrAgsgASAERgRAQfcBIQIMxAILAkACQCABLQAAQcUAaw4QAAUFBQUFBQUFBQUFBQUFAQULIAFBAWohAUHWASECDKsCCyABQQFqIQFB1wEhAgyqAgtB9gEhAiABIARGDcICIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbrVAGotAABHDQMgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMMCCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIARQRAQeMBIQIMqgILIANB9QE2AhwgAyABNgIUIAMgADYCDEEAIQIMwgILQfQBIQIgASAERg3BAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEG41QBqLQAARw0CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzCAgsgA0GBBDsBKCADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQLiIADQMMAgsgA0EANgIAC0EAIQIgA0EANgIcIAMgATYCFCADQeUfNgIQIANBCDYCDAy/AgtB1QEhAgylAgsgA0HzATYCHCADIAE2AhQgAyAANgIMQQAhAgy9AgtBACEAAkAgAygCOCICRQ0AIAIoAkAiAkUNACADIAIRAAAhAAsgAEUNbiAAQRVHBEAgA0EANgIcIAMgATYCFCADQYIPNgIQIANBIDYCDEEAIQIMvQILIANBjwE2AhwgAyABNgIUIANB7Bs2AhAgA0EVNgIMQQAhAgy8AgsgASAERwRAIANBDTYCCCADIAE2AgRB0wEhAgyjAgtB8gEhAgy7AgsgASAERgRAQfEBIQIMuwILAkACQAJAIAEtAABByABrDgsAAQgICAgICAgIAggLIAFBAWohAUHQASECDKMCCyABQQFqIQFB0QEhAgyiAgsgAUEBaiEBQdIBIQIMoQILQfABIQIgASAERg25AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBtdUAai0AAEcNBCAAQQJGDQMgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuQILQe8BIQIgASAERg24AiADKAIAIgAgBCABa2ohBiABIABrQQFqIQUDQCABLQAAIABBs9UAai0AAEcNAyAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMuAILQe4BIQIgASAERg23AiADKAIAIgAgBCABa2ohBiABIABrQQJqIQUDQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAY2AgAMtwILIAMoAgQhACADQgA3AwAgAyAAIAVBAWoiARArIgBFDQIgA0HsATYCHCADIAE2AhQgAyAANgIMQQAhAgy2AgsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNnAIgA0HtATYCHCADIAE2AhQgAyAANgIMQQAhAgy0AgtBzwEhAgyaAgtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDLQCC0HOASECDJoCCyADQesBNgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMsgILIAEgBEYEQEHrASECDLICCyABLQAAQS9GBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GyODYCECADQQg2AgxBACECDLECC0HNASECDJcCCyABIARHBEAgA0EONgIIIAMgATYCBEHMASECDJcCC0HqASECDK8CCyABIARGBEBB6QEhAgyvAgsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFBywEhAgyWAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZcCIANB6AE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAEgBEYEQEHnASECDK4CCwJAIAEtAABBLkYEQCABQQFqIQEMAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5gE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILQcoBIQIMlAILIAEgBEYEQEHlASECDK0CC0EAIQBBASEFQQEhB0EAIQICQAJAAkACQAJAAn8CQAJAAkACQAJAAkACQCABLQAAQTBrDgoKCQABAgMEBQYICwtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshAkEAIQVBACEHDAILQQkhAkEBIQBBACEFQQAhBwwBC0EAIQVBASECCyADIAI6ACsgAUEBaiEBAkACQCADLQAuQRBxDQACQAJAAkAgAy0AKg4DAQACBAsgB0UNAwwCCyAADQEMAgsgBUUNAQsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDQIgA0HiATYCHCADIAE2AhQgAyAANgIMQQAhAgyvAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZoCIANB4wE2AhwgAyABNgIUIAMgADYCDEEAIQIMrgILIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ2YAiADQeQBNgIcIAMgATYCFCADIAA2AgwMrQILQckBIQIMkwILQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANBpA02AhAgA0EhNgIMQQAhAgytAgtByAEhAgyTAgsgA0HhATYCHCADIAE2AhQgA0HQGjYCECADQRU2AgxBACECDKsCCyABIARGBEBB4QEhAgyrAgsCQCABLQAAQSBGBEAgA0EAOwE0IAFBAWohAQwBCyADQQA2AhwgAyABNgIUIANBmRE2AhAgA0EJNgIMQQAhAgyrAgtBxwEhAgyRAgsgASAERgRAQeABIQIMqgILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyrAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqgILQcYBIQIMkAILIAEgBEYEQEHfASECDKkCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqgILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKkCC0HFASECDI8CCyABIARGBEBB3gEhAgyoAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKkCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyoAgtBxAEhAgyOAgsgASAERgRAQd0BIQIMpwILAkACQAJAAkAgAS0AAEEKaw4XAgMDAAMDAwMDAwMDAwMDAwMDAwMDAwEDCyABQQFqDAULIAFBAWohAUHDASECDI8CCyABQQFqIQEgA0Evai0AAEEBcQ0IIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKcCCyADQQA2AhwgAyABNgIUIANBjQs2AhAgA0ENNgIMQQAhAgymAgsgASAERwRAIANBDzYCCCADIAE2AgRBASECDI0CC0HcASECDKUCCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtB2wEhAgymAgsgAygCBCEAIANBADYCBCADIAAgARAtIgBFBEAgAUEBaiEBDAQLIANB2gE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMpQILIAMoAgQhACADQQA2AgQgAyAAIAEQLSIADQEgAUEBagshAUHBASECDIoCCyADQdkBNgIcIAMgADYCDCADIAFBAWo2AhRBACECDKICC0HCASECDIgCCyADQS9qLQAAQQFxDQEgA0EANgIcIAMgATYCFCADQeQcNgIQIANBGTYCDEEAIQIMoAILIAEgBEYEQEHZASECDKACCwJAAkACQCABLQAAQQprDgQBAgIAAgsgAUEBaiEBDAILIAFBAWohAQwBCyADLQAuQcAAcUUNAQtBACEAAkAgAygCOCICRQ0AIAIoAjwiAkUNACADIAIRAAAhAAsgAEUNoAEgAEEVRgRAIANB2QA2AhwgAyABNgIUIANBtxo2AhAgA0EVNgIMQQAhAgyfAgsgA0EANgIcIAMgATYCFCADQYANNgIQIANBGzYCDEEAIQIMngILIANBADYCHCADIAE2AhQgA0HcKDYCECADQQI2AgxBACECDJ0CCyABIARHBEAgA0EMNgIIIAMgATYCBEG/ASECDIQCC0HYASECDJwCCyABIARGBEBB1wEhAgycAgsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAEtAABBwQBrDhUAAQIDWgQFBlpaWgcICQoLDA0ODxBaCyABQQFqIQFB+wAhAgySAgsgAUEBaiEBQfwAIQIMkQILIAFBAWohAUGBASECDJACCyABQQFqIQFBhQEhAgyPAgsgAUEBaiEBQYYBIQIMjgILIAFBAWohAUGJASECDI0CCyABQQFqIQFBigEhAgyMAgsgAUEBaiEBQY0BIQIMiwILIAFBAWohAUGWASECDIoCCyABQQFqIQFBlwEhAgyJAgsgAUEBaiEBQZgBIQIMiAILIAFBAWohAUGlASECDIcCCyABQQFqIQFBpgEhAgyGAgsgAUEBaiEBQawBIQIMhQILIAFBAWohAUG0ASECDIQCCyABQQFqIQFBtwEhAgyDAgsgAUEBaiEBQb4BIQIMggILIAEgBEYEQEHWASECDJsCCyABLQAAQc4ARw1IIAFBAWohAUG9ASECDIECCyABIARGBEBB1QEhAgyaAgsCQAJAAkAgAS0AAEHCAGsOEgBKSkpKSkpKSkoBSkpKSkpKAkoLIAFBAWohAUG4ASECDIICCyABQQFqIQFBuwEhAgyBAgsgAUEBaiEBQbwBIQIMgAILQdQBIQIgASAERg2YAiADKAIAIgAgBCABa2ohBSABIABrQQdqIQYCQANAIAEtAAAgAEGo1QBqLQAARw1FIABBB0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyZAgsgA0EANgIAIAZBAWohAUEbDEULIAEgBEYEQEHTASECDJgCCwJAAkAgAS0AAEHJAGsOBwBHR0dHRwFHCyABQQFqIQFBuQEhAgz/AQsgAUEBaiEBQboBIQIM/gELQdIBIQIgASAERg2WAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm1QBqLQAARw1DIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyXAgsgA0EANgIAIAZBAWohAUEPDEMLQdEBIQIgASAERg2VAiADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk1QBqLQAARw1CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyWAgsgA0EANgIAIAZBAWohAUEgDEILQdABIQIgASAERg2UAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGh1QBqLQAARw1BIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyVAgsgA0EANgIAIAZBAWohAUESDEELIAEgBEYEQEHPASECDJQCCwJAAkAgAS0AAEHFAGsODgBDQ0NDQ0NDQ0NDQ0MBQwsgAUEBaiEBQbUBIQIM+wELIAFBAWohAUG2ASECDPoBC0HOASECIAEgBEYNkgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBntUAai0AAEcNPyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMkwILIANBADYCACAGQQFqIQFBBww/C0HNASECIAEgBEYNkQIgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBmNUAai0AAEcNPiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMkgILIANBADYCACAGQQFqIQFBKAw+CyABIARGBEBBzAEhAgyRAgsCQAJAAkAgAS0AAEHFAGsOEQBBQUFBQUFBQUEBQUFBQUECQQsgAUEBaiEBQbEBIQIM+QELIAFBAWohAUGyASECDPgBCyABQQFqIQFBswEhAgz3AQtBywEhAiABIARGDY8CIAMoAgAiACAEIAFraiEFIAEgAGtBBmohBgJAA0AgAS0AACAAQZHVAGotAABHDTwgAEEGRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJACCyADQQA2AgAgBkEBaiEBQRoMPAtBygEhAiABIARGDY4CIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQY3VAGotAABHDTsgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADI8CCyADQQA2AgAgBkEBaiEBQSEMOwsgASAERgRAQckBIQIMjgILAkACQCABLQAAQcEAaw4UAD09PT09PT09PT09PT09PT09PQE9CyABQQFqIQFBrQEhAgz1AQsgAUEBaiEBQbABIQIM9AELIAEgBEYEQEHIASECDI0CCwJAAkAgAS0AAEHVAGsOCwA8PDw8PDw8PDwBPAsgAUEBaiEBQa4BIQIM9AELIAFBAWohAUGvASECDPMBC0HHASECIAEgBEYNiwIgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBhNUAai0AAEcNOCAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMjAILIANBADYCACAGQQFqIQFBKgw4CyABIARGBEBBxgEhAgyLAgsgAS0AAEHQAEcNOCABQQFqIQFBJQw3C0HFASECIAEgBEYNiQIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBgdUAai0AAEcNNiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMigILIANBADYCACAGQQFqIQFBDgw2CyABIARGBEBBxAEhAgyJAgsgAS0AAEHFAEcNNiABQQFqIQFBqwEhAgzvAQsgASAERgRAQcMBIQIMiAILAkACQAJAAkAgAS0AAEHCAGsODwABAjk5OTk5OTk5OTk5AzkLIAFBAWohAUGnASECDPEBCyABQQFqIQFBqAEhAgzwAQsgAUEBaiEBQakBIQIM7wELIAFBAWohAUGqASECDO4BC0HCASECIAEgBEYNhgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB/tQAai0AAEcNMyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhwILIANBADYCACAGQQFqIQFBFAwzC0HBASECIAEgBEYNhQIgAygCACIAIAQgAWtqIQUgASAAa0EEaiEGAkADQCABLQAAIABB+dQAai0AAEcNMiAAQQRGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhgILIANBADYCACAGQQFqIQFBKwwyC0HAASECIAEgBEYNhAIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB9tQAai0AAEcNMSAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhQILIANBADYCACAGQQFqIQFBLAwxC0G/ASECIAEgBEYNgwIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBodUAai0AAEcNMCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMhAILIANBADYCACAGQQFqIQFBEQwwC0G+ASECIAEgBEYNggIgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABB8tQAai0AAEcNLyAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMgwILIANBADYCACAGQQFqIQFBLgwvCyABIARGBEBBvQEhAgyCAgsCQAJAAkACQAJAIAEtAABBwQBrDhUANDQ0NDQ0NDQ0NAE0NAI0NAM0NAQ0CyABQQFqIQFBmwEhAgzsAQsgAUEBaiEBQZwBIQIM6wELIAFBAWohAUGdASECDOoBCyABQQFqIQFBogEhAgzpAQsgAUEBaiEBQaQBIQIM6AELIAEgBEYEQEG8ASECDIECCwJAAkAgAS0AAEHSAGsOAwAwATALIAFBAWohAUGjASECDOgBCyABQQFqIQFBBAwtC0G7ASECIAEgBEYN/wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB8NQAai0AAEcNLCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMgAILIANBADYCACAGQQFqIQFBHQwsCyABIARGBEBBugEhAgz/AQsCQAJAIAEtAABByQBrDgcBLi4uLi4ALgsgAUEBaiEBQaEBIQIM5gELIAFBAWohAUEiDCsLIAEgBEYEQEG5ASECDP4BCyABLQAAQdAARw0rIAFBAWohAUGgASECDOQBCyABIARGBEBBuAEhAgz9AQsCQAJAIAEtAABBxgBrDgsALCwsLCwsLCwsASwLIAFBAWohAUGeASECDOQBCyABQQFqIQFBnwEhAgzjAQtBtwEhAiABIARGDfsBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQezUAGotAABHDSggAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPwBCyADQQA2AgAgBkEBaiEBQQ0MKAtBtgEhAiABIARGDfoBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDScgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPsBCyADQQA2AgAgBkEBaiEBQQwMJwtBtQEhAiABIARGDfkBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQerUAGotAABHDSYgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPoBCyADQQA2AgAgBkEBaiEBQQMMJgtBtAEhAiABIARGDfgBIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQejUAGotAABHDSUgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADPkBCyADQQA2AgAgBkEBaiEBQSYMJQsgASAERgRAQbMBIQIM+AELAkACQCABLQAAQdQAaw4CAAEnCyABQQFqIQFBmQEhAgzfAQsgAUEBaiEBQZoBIQIM3gELQbIBIQIgASAERg32ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHm1ABqLQAARw0jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz3AQsgA0EANgIAIAZBAWohAUEnDCMLQbEBIQIgASAERg31ASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHk1ABqLQAARw0iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz2AQsgA0EANgIAIAZBAWohAUEcDCILQbABIQIgASAERg30ASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHe1ABqLQAARw0hIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz1AQsgA0EANgIAIAZBAWohAUEGDCELQa8BIQIgASAERg3zASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHZ1ABqLQAARw0gIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAz0AQsgA0EANgIAIAZBAWohAUEZDCALIAEgBEYEQEGuASECDPMBCwJAAkACQAJAIAEtAABBLWsOIwAkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJAEkJCQkJAIkJCQDJAsgAUEBaiEBQY4BIQIM3AELIAFBAWohAUGPASECDNsBCyABQQFqIQFBlAEhAgzaAQsgAUEBaiEBQZUBIQIM2QELQa0BIQIgASAERg3xASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHX1ABqLQAARw0eIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzyAQsgA0EANgIAIAZBAWohAUELDB4LIAEgBEYEQEGsASECDPEBCwJAAkAgAS0AAEHBAGsOAwAgASALIAFBAWohAUGQASECDNgBCyABQQFqIQFBkwEhAgzXAQsgASAERgRAQasBIQIM8AELAkACQCABLQAAQcEAaw4PAB8fHx8fHx8fHx8fHx8BHwsgAUEBaiEBQZEBIQIM1wELIAFBAWohAUGSASECDNYBCyABIARGBEBBqgEhAgzvAQsgAS0AAEHMAEcNHCABQQFqIQFBCgwbC0GpASECIAEgBEYN7QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABB0dQAai0AAEcNGiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7gELIANBADYCACAGQQFqIQFBHgwaC0GoASECIAEgBEYN7AEgAygCACIAIAQgAWtqIQUgASAAa0EGaiEGAkADQCABLQAAIABBytQAai0AAEcNGSAAQQZGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7QELIANBADYCACAGQQFqIQFBFQwZC0GnASECIAEgBEYN6wEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBx9QAai0AAEcNGCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM7AELIANBADYCACAGQQFqIQFBFwwYC0GmASECIAEgBEYN6gEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBwdQAai0AAEcNFyAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6wELIANBADYCACAGQQFqIQFBGAwXCyABIARGBEBBpQEhAgzqAQsCQAJAIAEtAABByQBrDgcAGRkZGRkBGQsgAUEBaiEBQYsBIQIM0QELIAFBAWohAUGMASECDNABC0GkASECIAEgBEYN6AEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBptUAai0AAEcNFSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6QELIANBADYCACAGQQFqIQFBCQwVC0GjASECIAEgBEYN5wEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBpNUAai0AAEcNFCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM6AELIANBADYCACAGQQFqIQFBHwwUC0GiASECIAEgBEYN5gEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBvtQAai0AAEcNEyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5wELIANBADYCACAGQQFqIQFBAgwTC0GhASECIAEgBEYN5QEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGA0AgAS0AACAAQbzUAGotAABHDREgAEEBRg0CIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADOUBCyABIARGBEBBoAEhAgzlAQtBASABLQAAQd8ARw0RGiABQQFqIQFBhwEhAgzLAQsgA0EANgIAIAZBAWohAUGIASECDMoBC0GfASECIAEgBEYN4gEgAygCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABBhNUAai0AAEcNDyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4wELIANBADYCACAGQQFqIQFBKQwPC0GeASECIAEgBEYN4QEgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBuNQAai0AAEcNDiAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM4gELIANBADYCACAGQQFqIQFBLQwOCyABIARGBEBBnQEhAgzhAQsgAS0AAEHFAEcNDiABQQFqIQFBhAEhAgzHAQsgASAERgRAQZwBIQIM4AELAkACQCABLQAAQcwAaw4IAA8PDw8PDwEPCyABQQFqIQFBggEhAgzHAQsgAUEBaiEBQYMBIQIMxgELQZsBIQIgASAERg3eASADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEGz1ABqLQAARw0LIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzfAQsgA0EANgIAIAZBAWohAUEjDAsLQZoBIQIgASAERg3dASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGw1ABqLQAARw0KIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzeAQsgA0EANgIAIAZBAWohAUEADAoLIAEgBEYEQEGZASECDN0BCwJAAkAgAS0AAEHIAGsOCAAMDAwMDAwBDAsgAUEBaiEBQf0AIQIMxAELIAFBAWohAUGAASECDMMBCyABIARGBEBBmAEhAgzcAQsCQAJAIAEtAABBzgBrDgMACwELCyABQQFqIQFB/gAhAgzDAQsgAUEBaiEBQf8AIQIMwgELIAEgBEYEQEGXASECDNsBCyABLQAAQdkARw0IIAFBAWohAUEIDAcLQZYBIQIgASAERg3ZASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEGs1ABqLQAARw0GIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzaAQsgA0EANgIAIAZBAWohAUEFDAYLQZUBIQIgASAERg3YASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGm1ABqLQAARw0FIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzZAQsgA0EANgIAIAZBAWohAUEWDAULQZQBIQIgASAERg3XASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGh1QBqLQAARw0EIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzYAQsgA0EANgIAIAZBAWohAUEQDAQLIAEgBEYEQEGTASECDNcBCwJAAkAgAS0AAEHDAGsODAAGBgYGBgYGBgYGAQYLIAFBAWohAUH5ACECDL4BCyABQQFqIQFB+gAhAgy9AQtBkgEhAiABIARGDdUBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQaDUAGotAABHDQIgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNYBCyADQQA2AgAgBkEBaiEBQSQMAgsgA0EANgIADAILIAEgBEYEQEGRASECDNQBCyABLQAAQcwARw0BIAFBAWohAUETCzoAKSADKAIEIQAgA0EANgIEIAMgACABEC4iAA0CDAELQQAhAiADQQA2AhwgAyABNgIUIANB/h82AhAgA0EGNgIMDNEBC0H4ACECDLcBCyADQZABNgIcIAMgATYCFCADIAA2AgxBACECDM8BC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ0AIABBFUYNASADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgzOAQtB9wAhAgy0AQsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDMwBCyABIARGBEBBjwEhAgzMAQsCQCABLQAAQSBGBEAgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GbHzYCECADQQY2AgxBACECDMwBC0ECIQIMsgELA0AgAS0AAEEgRw0CIAQgAUEBaiIBRw0AC0GOASECDMoBCyABIARGBEBBjQEhAgzKAQsCQCABLQAAQQlrDgRKAABKAAtB9QAhAgywAQsgAy0AKUEFRgRAQfYAIQIMsAELQfQAIQIMrwELIAEgBEYEQEGMASECDMgBCyADQRA2AgggAyABNgIEDAoLIAEgBEYEQEGLASECDMcBCwJAIAEtAABBCWsOBEcAAEcAC0HzACECDK0BCyABIARHBEAgA0EQNgIIIAMgATYCBEHxACECDK0BC0GKASECDMUBCwJAIAEgBEcEQANAIAEtAABBoNAAai0AACIAQQNHBEACQCAAQQFrDgJJAAQLQfAAIQIMrwELIAQgAUEBaiIBRw0AC0GIASEC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BCABQQFqIgFHDQALQfsAIQIMpwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMpgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMpQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0DIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMpAELIANBADYCHCADIAE2AhQgA0G8CjYCECADQQc2AgxBACECDKMBC0HPACECDIkBC0HRACECDIgBC0HnACECDIcBCyABIARGBEBB+gAhAgygAQsCQCABLQAAQQlrDgQgAAAgAAsgAUEBaiEBQeYAIQIMhgELIAEgBEYEQEH5ACECDJ8BCwJAIAEtAABBCWsOBB8AAB8AC0EAIQACQCADKAI4IgJFDQAgAigCOCICRQ0AIAMgAhEAACEACyAARQRAQeIBIQIMhgELIABBFUcEQCADQQA2AhwgAyABNgIUIANByQ02AhAgA0EaNgIMQQAhAgyfAQsgA0H4ADYCHCADIAE2AhQgA0HqGjYCECADQRU2AgxBACECDJ4BCyABIARHBEAgA0ENNgIIIAMgATYCBEHkACECDIUBC0H3ACECDJ0BCyABIARGBEBB9gAhAgydAQsCQAJAAkAgAS0AAEHIAGsOCwABCwsLCwsLCwsCCwsgAUEBaiEBQd0AIQIMhQELIAFBAWohAUHgACECDIQBCyABQQFqIQFB4wAhAgyDAQtB9QAhAiABIARGDZsBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbXVAGotAABHDQggAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJwBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIABEAgA0H0ADYCHCADIAE2AhQgAyAANgIMQQAhAgycAQtB4gAhAgyCAQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJwBC0HhACECDIIBCyADQfMANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMmgELIAMtACkiAEEja0ELSQ0JAkAgAEEGSw0AQQEgAHRBygBxRQ0ADAoLQQAhAiADQQA2AhwgAyABNgIUIANB7Qk2AhAgA0EINgIMDJkBC0HyACECIAEgBEYNmAEgAygCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBs9UAai0AAEcNBSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMmQELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfEANgIcIAMgATYCFCADIAA2AgxBACECDJkBC0HfACECDH8LQQAhAAJAIAMoAjgiAkUNACACKAI0IgJFDQAgAyACEQAAIQALAkAgAARAIABBFUYNASADQQA2AhwgAyABNgIUIANB6g02AhAgA0EmNgIMQQAhAgyZAQtB3gAhAgx/CyADQfAANgIcIAMgATYCFCADQYAbNgIQIANBFTYCDEEAIQIMlwELIAMtAClBIUYNBiADQQA2AhwgAyABNgIUIANBkQo2AhAgA0EINgIMQQAhAgyWAQtB7wAhAiABIARGDZUBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDVAGotAABHDQIgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADJYBCyADKAIEIQAgA0IANwMAIAMgACAGQQFqIgEQKyIARQ0CIANB7QA2AhwgAyABNgIUIAMgADYCDEEAIQIMlQELIANBADYCAAsgAygCBCEAIANBADYCBCADIAAgARArIgBFDYABIANB7gA2AhwgAyABNgIUIAMgADYCDEEAIQIMkwELQdwAIQIMeQtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJMBC0HbACECDHkLIANB7AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyRAQsgAy0AKSIAQSNJDQAgAEEuRg0AIANBADYCHCADIAE2AhQgA0HJCTYCECADQQg2AgxBACECDJABC0HaACECDHYLIAEgBEYEQEHrACECDI8BCwJAIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMjwELQdkAIQIMdQsgASAERwRAIANBDjYCCCADIAE2AgRB2AAhAgx1C0HqACECDI0BCyABIARGBEBB6QAhAgyNAQsgAS0AAEEwayIAQf8BcUEKSQRAIAMgADoAKiABQQFqIQFB1wAhAgx0CyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeiADQegANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyABIARGBEBB5wAhAgyMAQsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAMoAgQhACADQQA2AgQgAyAAIAEQLyIARQ17IANB5gA2AhwgAyABNgIUIAMgADYCDEEAIQIMjAELQdYAIQIMcgsgASAERgRAQeUAIQIMiwELQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIANgIcIAMgATYCFCADIAA2AgxBACECDI0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNfSADQeMANgIcIAMgATYCFCADIAA2AgxBACECDIwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNeyADQeQANgIcIAMgATYCFCADIAA2AgwMiwELQdQAIQIMcQsgAy0AKUEiRg2GAUHTACECDHALQQAhAAJAIAMoAjgiAkUNACACKAJEIgJFDQAgAyACEQAAIQALIABFBEBB1QAhAgxwCyAAQRVHBEAgA0EANgIcIAMgATYCFCADQaQNNgIQIANBITYCDEEAIQIMiQELIANB4QA2AhwgAyABNgIUIANB0Bo2AhAgA0EVNgIMQQAhAgyIAQsgASAERgRAQeAAIQIMiAELAkACQAJAAkACQCABLQAAQQprDgQBBAQABAsgAUEBaiEBDAELIAFBAWohASADQS9qLQAAQQFxRQ0BC0HSACECDHALIANBADYCHCADIAE2AhQgA0G2ETYCECADQQk2AgxBACECDIgBCyADQQA2AhwgAyABNgIUIANBthE2AhAgA0EJNgIMQQAhAgyHAQsgASAERgRAQd8AIQIMhwELIAEtAABBCkYEQCABQQFqIQEMCQsgAy0ALkHAAHENCCADQQA2AhwgAyABNgIUIANBthE2AhAgA0ECNgIMQQAhAgyGAQsgASAERgRAQd0AIQIMhgELIAEtAAAiAkENRgRAIAFBAWohAUHQACECDG0LIAEhACACQQlrDgQFAQEFAQsgBCABIgBGBEBB3AAhAgyFAQsgAC0AAEEKRw0AIABBAWoMAgtBACECIANBADYCHCADIAA2AhQgA0HKLTYCECADQQc2AgwMgwELIAEgBEYEQEHbACECDIMBCwJAIAEtAABBCWsOBAMAAAMACyABQQFqCyEBQc4AIQIMaAsgASAERgRAQdoAIQIMgQELIAEtAABBCWsOBAABAQABC0EAIQIgA0EANgIcIANBmhI2AhAgA0EHNgIMIAMgAUEBajYCFAx/CyADQYASOwEqQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB2QA2AhwgAyABNgIUIANB6ho2AhAgA0EVNgIMQQAhAgx+C0HNACECDGQLIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDHwLIAEgBEYEQEHZACECDHwLIAEtAABBIEcNPSABQQFqIQEgAy0ALkEBcQ09IANBADYCHCADIAE2AhQgA0HCHDYCECADQR42AgxBACECDHsLIAEgBEYEQEHYACECDHsLAkACQAJAAkACQCABLQAAIgBBCmsOBAIDAwABCyABQQFqIQFBLCECDGULIABBOkcNASADQQA2AhwgAyABNgIUIANB5xE2AhAgA0EKNgIMQQAhAgx9CyABQQFqIQEgA0Evai0AAEEBcUUNcyADLQAyQYABcUUEQCADQTJqIQIgAxA1QQAhAAJAIAMoAjgiBkUNACAGKAIoIgZFDQAgAyAGEQAAIQALAkACQCAADhZNTEsBAQEBAQEBAQEBAQEBAQEBAQEAAQsgA0EpNgIcIAMgATYCFCADQawZNgIQIANBFTYCDEEAIQIMfgsg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c3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw=='
+
+let wasmBuffer
+
+Object.defineProperty(module, 'exports', {
+ get: () => {
+ return wasmBuffer
+ ? wasmBuffer
+ : (wasmBuffer = Buffer.from(wasmBase64, 'base64'))
+ }
+})
diff --git a/vanilla/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js b/vanilla/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js
new file mode 100644
index 0000000..4508cb1
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js
@@ -0,0 +1,15 @@
+'use strict'
+
+const { Buffer } = require('node:buffer')
+
+const wasmBase64 = 'AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAn9/AGABfwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAzU0BQYAAAMAAAAAAAADAQMAAwMDAAACAAAAAAICAgICAgICAgIBAQEBAQEBAQEBAwAAAwAAAAQFAXABExMFAwEAAgYIAX8BQcDZBAsHxQcoBm1lbW9yeQIAC19pbml0aWFsaXplAAgZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAC2xsaHR0cF9pbml0AAkYbGxodHRwX3Nob3VsZF9rZWVwX2FsaXZlADcMbGxodHRwX2FsbG9jAAsGbWFsbG9jADkLbGxodHRwX2ZyZWUADARmcmVlAAwPbGxodHRwX2dldF90eXBlAA0VbGxodHRwX2dldF9odHRwX21ham9yAA4VbGxodHRwX2dldF9odHRwX21pbm9yAA8RbGxodHRwX2dldF9tZXRob2QAEBZsbGh0dHBfZ2V0X3N0YXR1c19jb2RlABESbGxodHRwX2dldF91cGdyYWRlABIMbGxodHRwX3Jlc2V0ABMObGxodHRwX2V4ZWN1dGUAFBRsbGh0dHBfc2V0dGluZ3NfaW5pdAAVDWxsaHR0cF9maW5pc2gAFgxsbGh0dHBfcGF1c2UAFw1sbGh0dHBfcmVzdW1lABgbbGxodHRwX3Jlc3VtZV9hZnRlcl91cGdyYWRlABkQbGxodHRwX2dldF9lcnJubwAaF2xsaHR0cF9nZXRfZXJyb3JfcmVhc29uABsXbGxodHRwX3NldF9lcnJvcl9yZWFzb24AHBRsbGh0dHBfZ2V0X2Vycm9yX3BvcwAdEWxsaHR0cF9lcnJub19uYW1lAB4SbGxodHRwX21ldGhvZF9uYW1lAB8SbGxodHRwX3N0YXR1c19uYW1lACAabGxodHRw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AXRrQT5qIQILIAEgAjYCHCABQgA3AhAgAkECdEH01wBqIQACQEHI1QAoAgAiA0EBIAJ0IgdxRQRAIAAgATYCAEHI1QAgAyAHcjYCACABIAA2AhggASABNgIIIAEgATYCDAwBCyAEQRkgAkEBdmtBACACQR9HG3QhAiAAKAIAIQACQANAIAAiAygCBEF4cSAERg0BIAJBHXYhACACQQF0IQIgAyAAQQRxakEQaiIHKAIAIgANAAsgByABNgIAIAEgAzYCGCABIAE2AgwgASABNgIIDAELIAMoAggiACABNgIMIAMgATYCCCABQQA2AhggASADNgIMIAEgADYCCAtB5NUAQeTVACgCAEEBayIAQX8gABs2AgALCwcAIAAtACgLBwAgAC0AKgsHACAALQArCwcAIAAtACkLBwAgAC8BNAsHACAALQAwC0ABBH8gACgCGCEBIAAvAS4hAiAALQAoIQMgACgCOCEEIAAQOCAAIAQ2AjggACADOgAoIAAgAjsBLiAAIAE2AhgLhocCAwd/A34BeyABIAJqIQQCQCAAIgMoAgwiAA0AIAMoAgQEQCADIAE2AgQLIwBBEGsiCSQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADKAIcIgJBAmsO/AEB+QECAwQFBgcICQoLDA0ODxAREvgBE/cBFBX2ARYX9QEYGRobHB0eHyD9AfsBIfQBIiMkJSYnKCkqK/MBLC0uLzAxMvIB8QEzNPAB7wE1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk/6AVBRUlPuAe0BVOwBVesBVldYWVrqAVtcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcBuAG5AboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBxwHIAckBygHLAcwBzQHOAekB6AHPAecB0AHmAdEB0gHTAdQB5QHVAdYB1wHYAdkB2gHbAdwB3QHeAd8B4AHhAeIB4wEA/AELQQAM4wELQQ4M4gELQQ0M4QELQQ8M4AELQRAM3wELQRMM3gELQRQM3QELQRUM3AELQRYM2wELQRcM2gELQRgM2QELQRkM2AELQRoM1wELQRsM1gELQRwM1QELQR0M1AELQR4M0wELQR8M0gELQSAM0QELQSEM0AELQQgMzwELQSIMzgELQSQMzQELQSMMzAELQQcMywELQSUMygELQSYMyQELQScMyAELQSgMxwELQRIMxgELQREMxQELQSkMxAELQSoMwwELQSsMwgELQSwMwQELQd4BDMABC0EuDL8BC0EvDL4BC0EwDL0BC0ExDLwBC0EyDLsBC0EzDLoBC0E0DLkBC0HfAQy4AQtBNQy3AQtBOQy2AQtBDAy1AQtBNgy0AQtBNwyzAQtBOAyyAQtBPgyxAQtBOgywAQtB4AEMrwELQQsMrgELQT8MrQELQTsMrAELQQoMqwELQTwMqgELQT0MqQELQeEBDKgBC0HBAAynAQtBwAAMpgELQcIADKUBC0EJDKQBC0EtDKMBC0HDAAyiAQtBxAAMoQELQcUADKABC0HGAAyfAQtBxwAMngELQcgADJ0BC0HJAAycAQtBygAMmwELQcsADJoBC0HMAAyZAQtBzQAMmAELQc4ADJcBC0HPAAyWAQtB0AAMlQELQdEADJQBC0HSAAyTAQtB0wAMkgELQdUADJEBC0HUAAyQAQtB1gAMjwELQdcADI4BC0HYAAyNAQtB2QAMjAELQdoADIsBC0HbAAyKAQtB3AAMiQELQd0ADIgBC0HeAAyHAQtB3wAMhgELQeAADIUBC0HhAAyEAQtB4gAMgwELQeMADIIBC0HkAAyBAQtB5QAMgAELQeIBDH8LQeYADH4LQecADH0LQQYMfAtB6AAMewtBBQx6C0HpAAx5C0EEDHgLQeoADHcLQesADHYLQewADHULQe0ADHQLQQMMcwtB7gAMcgtB7wAMcQtB8AAMcAtB8gAMbwtB8QAMbgtB8wAMbQtB9AAMbAtB9QAMawtB9gAMagtBAgxpC0H3AAxoC0H4AAxnC0H5AAxmC0H6AAxlC0H7AAxkC0H8AAxjC0H9AAxiC0H+AAxhC0H/AAxgC0GAAQxfC0GBAQxeC0GCAQxdC0GDAQxcC0GEAQxbC0GFAQxaC0GGAQxZC0GHAQxYC0GIAQxXC0GJAQxWC0GKAQxVC0GLAQxUC0GMAQxTC0GNAQxSC0GOAQxRC0GPAQxQC0GQAQxPC0GRAQxOC0GSAQxNC0GTAQxMC0GUAQxLC0GVAQxKC0GWAQxJC0GXAQxIC0GYAQxHC0GZAQxGC0GaAQxFC0GbAQxEC0GcAQxDC0GdAQxCC0GeAQxBC0GfAQxAC0GgAQw/C0GhAQw+C0GiAQw9C0GjAQw8C0GkAQw7C0GlAQw6C0GmAQw5C0GnAQw4C0GoAQw3C0GpAQw2C0GqAQw1C0GrAQw0C0GsAQwzC0GtAQwyC0GuAQwxC0GvAQwwC0GwAQwvC0GxAQwuC0GyAQwtC0GzAQwsC0G0AQwrC0G1AQwqC0G2AQwpC0G3AQwoC0G4AQwnC0G5AQwmC0G6AQwlC0G7AQwkC0G8AQwjC0G9AQwiC0G+AQwhC0G/AQwgC0HAAQwfC0HBAQweC0HCAQwdC0EBDBwLQcMBDBsLQcQBDBoLQcUBDBkLQcYBDBgLQccBDBcLQcgBDBYLQckBDBULQcoBDBQLQcsBDBMLQcwBDBILQc0BDBELQc4BDBALQc8BDA8LQdABDA4LQdEBDA0LQdIBDAwLQdMBDAsLQdQBDAoLQdUBDAkLQdYBDAgLQeMBDAcLQdcBDAYLQdgBDAULQdkBDAQLQdoBDAMLQdsBDAILQd0BDAELQdwBCyECA0ACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAMCfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAn8CQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAwJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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A0EXNgIcIAMgADYCDCADIAFBAWo2AhQM+AILIABBFUYNI0EAIQIgA0EANgIcIAMgATYCFCADQdYMNgIQIANBIzYCDAz3AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQwdCyADQRk2AhwgAyAANgIMIAMgAUEBajYCFAz2AgsgAygCBCEAQQAhAiADQQA2AgQgAyAAIAEQNCIARQRAIAFBAWohAQzvAgsgA0EaNgIcIAMgADYCDCADIAFBAWo2AhQM9QILIABBFUYNH0EAIQIgA0EANgIcIAMgATYCFCADQdAPNgIQIANBIjYCDAz0AgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDBsLIANBHDYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgzzAgsgAygCBCEAIANBADYCBCADIAAgARAzIgBFBEAgAUEBaiEBDOsCCyADQR02AhwgAyAANgIMIAMgAUEBajYCFEEAIQIM8gILIABBO0cNASABQQFqIQELQSYhAgzXAgtBACECIANBADYCHCADIAE2AhQgA0GfFTYCECADQQw2AgwM7wILIAEgBEcEQANAIAEtAABBIEcNhAIgBCABQQFqIgFHDQALQSwhAgzvAgtBLCECDO4CCyABIARGBEBBNCECDO4CCwJAAkADQAJAIAEtAABBCmsOBAIAAAMACyAEIAFBAWoiAUcNAAtBNCECDO8CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUNnwIgA0EyNgIcIAMgATYCFCADIAA2AgxBACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABEDEiAEUEQCABQQFqIQEMnwILIANBMjYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgztAgsgASAERwRAAkADQCABLQAAQTBrIgBB/wFxQQpPBEBBOiECDNcCCyADKQMgIgtCmbPmzJmz5swZVg0BIAMgC0IKfiIKNwMgIAogAK1C/wGDIgtCf4VWDQEgAyAKIAt8NwMgIAQgAUEBaiIBRw0AC0HAACECDO4CCyADKAIEIQAgA0EANgIEIAMgACABQQFqIgEQMSIADRcM4gILQcAAIQIM7AILIAEgBEYEQEHJACECDOwCCwJAA0ACQCABLQAAQQlrDhgAAqICogKpAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAqICogKiAgCiAgsgBCABQQFqIgFHDQALQckAIQIM7AILIAFBAWohASADQS9qLQAAQQFxDaUCIANBADYCHCADIAE2AhQgA0GXEDYCECADQQo2AgxBACECDOsCCyABIARHBEADQCABLQAAQSBHDRUgBCABQQFqIgFHDQALQfgAIQIM6wILQfgAIQIM6gILIANBAjoAKAw4C0EAIQIgA0EANgIcIANBvws2AhAgA0ECNgIMIAMgAUEBajYCFAzoAgtBACECDM4CC0ENIQIMzQILQRMhAgzMAgtBFSECDMsCC0EWIQIMygILQRghAgzJAgtBGSECDMgCC0EaIQIMxwILQRshAgzGAgtBHCECDMUCC0EdIQIMxAILQR4hAgzDAgtBHyECDMICC0EgIQIMwQILQSIhAgzAAgtBIyECDL8CC0ElIQIMvgILQeUAIQIMvQILIANBPTYCHCADIAE2AhQgAyAANgIMQQAhAgzVAgsgA0EbNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIM1AILIANBIDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNMCCyADQRM2AhwgAyABNgIUIANBmBo2AhAgA0EVNgIMQQAhAgzSAgsgA0ELNgIcIAMgATYCFCADQZgaNgIQIANBFTYCDEEAIQIM0QILIANBEDYCHCADIAE2AhQgA0GYGjYCECADQRU2AgxBACECDNACCyADQSA2AhwgAyABNgIUIANBpBw2AhAgA0EVNgIMQQAhAgzPAgsgA0ELNgIcIAMgATYCFCADQaQcNgIQIANBFTYCDEEAIQIMzgILIANBDDYCHCADIAE2AhQgA0GkHDYCECADQRU2AgxBACECDM0CC0EAIQIgA0EANgIcIAMgATYCFCADQd0ONgIQIANBEjYCDAzMAgsCQANAAkAgAS0AAEEKaw4EAAICAAILIAQgAUEBaiIBRw0AC0H9ASECDMwCCwJAAkAgAy0ANkEBRw0AQQAhAAJAIAMoAjgiAkUNACACKAJgIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRw0BIANB/AE2AhwgAyABNgIUIANB3Bk2AhAgA0EVNgIMQQAhAgzNAgtB3AEhAgyzAgsgA0EANgIcIAMgATYCFCADQfkLNgIQIANBHzYCDEEAIQIMywILAkACQCADLQAoQQFrDgIEAQALQdsBIQIMsgILQdQBIQIMsQILIANBAjoAMUEAIQACQCADKAI4IgJFDQAgAigCACICRQ0AIAMgAhEAACEACyAARQRAQd0BIQIMsQILIABBFUcEQCADQQA2AhwgAyABNgIUIANBtAw2AhAgA0EQNgIMQQAhAgzKAgsgA0H7ATYCHCADIAE2AhQgA0GBGjYCECADQRU2AgxBACECDMkCCyABIARGBEBB+gEhAgzJAgsgAS0AAEHIAEYNASADQQE6ACgLQcABIQIMrgILQdoBIQIMrQILIAEgBEcEQCADQQw2AgggAyABNgIEQdkBIQIMrQILQfkBIQIMxQILIAEgBEYEQEH4ASECDMUCCyABLQAAQcgARw0EIAFBAWohAUHYASECDKsCCyABIARGBEBB9wEhAgzEAgsCQAJAIAEtAABBxQBrDhAABQUFBQUFBQUFBQUFBQUBBQsgAUEBaiEBQdYBIQIMqwILIAFBAWohAUHXASECDKoCC0H2ASECIAEgBEYNwgIgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABButUAai0AAEcNAyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMwwILIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgBFBEBB4wEhAgyqAgsgA0H1ATYCHCADIAE2AhQgAyAANgIMQQAhAgzCAgtB9AEhAiABIARGDcECIAMoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbjVAGotAABHDQIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADMICCyADQYEEOwEoIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARAuIgANAwwCCyADQQA2AgALQQAhAiADQQA2AhwgAyABNgIUIANB5R82AhAgA0EINgIMDL8CC0HVASECDKUCCyADQfMBNgIcIAMgATYCFCADIAA2AgxBACECDL0CC0EAIQACQCADKAI4IgJFDQAgAigCQCICRQ0AIAMgAhEAACEACyAARQ1uIABBFUcEQCADQQA2AhwgAyABNgIUIANBgg82AhAgA0EgNgIMQQAhAgy9AgsgA0GPATYCHCADIAE2AhQgA0HsGzYCECADQRU2AgxBACECDLwCCyABIARHBEAgA0ENNgIIIAMgATYCBEHTASECDKMCC0HyASECDLsCCyABIARGBEBB8QEhAgy7AgsCQAJAAkAgAS0AAEHIAGsOCwABCAgICAgICAgCCAsgAUEBaiEBQdABIQIMowILIAFBAWohAUHRASECDKICCyABQQFqIQFB0gEhAgyhAgtB8AEhAiABIARGDbkCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEG11QBqLQAARw0EIABBAkYNAyAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy5AgtB7wEhAiABIARGDbgCIAMoAgAiACAEIAFraiEGIAEgAGtBAWohBQNAIAEtAAAgAEGz1QBqLQAARw0DIABBAUYNAiAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy4AgtB7gEhAiABIARGDbcCIAMoAgAiACAEIAFraiEGIAEgAGtBAmohBQNAIAEtAAAgAEGw1QBqLQAARw0CIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBjYCAAy3AgsgAygCBCEAIANCADcDACADIAAgBUEBaiIBECsiAEUNAiADQewBNgIcIAMgATYCFCADIAA2AgxBACECDLYCCyADQQA2AgALIAMoAgQhACADQQA2AgQgAyAAIAEQKyIARQ2cAiADQe0BNgIcIAMgATYCFCADIAA2AgxBACECDLQCC0HPASECDJoCC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMtAILQc4BIQIMmgILIANB6wE2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyyAgsgASAERgRAQesBIQIMsgILIAEtAABBL0YEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQbI4NgIQIANBCDYCDEEAIQIMsQILQc0BIQIMlwILIAEgBEcEQCADQQ42AgggAyABNgIEQcwBIQIMlwILQeoBIQIMrwILIAEgBEYEQEHpASECDK8CCyABLQAAQTBrIgBB/wFxQQpJBEAgAyAAOgAqIAFBAWohAUHLASECDJYCCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNlwIgA0HoATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgASAERgRAQecBIQIMrgILAkAgAS0AAEEuRgRAIAFBAWohAQwBCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmAIgA0HmATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgtBygEhAgyUAgsgASAERgRAQeUBIQIMrQILQQAhAEEBIQVBASEHQQAhAgJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAEtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyECQQAhBUEAIQcMAgtBCSECQQEhAEEAIQVBACEHDAELQQAhBUEBIQILIAMgAjoAKyABQQFqIQECQAJAIAMtAC5BEHENAAJAAkACQCADLQAqDgMBAAIECyAHRQ0DDAILIAANAQwCCyAFRQ0BCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNAiADQeIBNgIcIAMgATYCFCADIAA2AgxBACECDK8CCyADKAIEIQAgA0EANgIEIAMgACABEC8iAEUNmgIgA0HjATYCHCADIAE2AhQgAyAANgIMQQAhAgyuAgsgAygCBCEAIANBADYCBCADIAAgARAvIgBFDZgCIANB5AE2AhwgAyABNgIUIAMgADYCDAytAgtByQEhAgyTAgtBACEAAkAgAygCOCICRQ0AIAIoAkQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GkDTYCECADQSE2AgxBACECDK0CC0HIASECDJMCCyADQeEBNgIcIAMgATYCFCADQdAaNgIQIANBFTYCDEEAIQIMqwILIAEgBEYEQEHhASECDKsCCwJAIAEtAABBIEYEQCADQQA7ATQgAUEBaiEBDAELIANBADYCHCADIAE2AhQgA0GZETYCECADQQk2AgxBACECDKsCC0HHASECDJECCyABIARGBEBB4AEhAgyqAgsCQCABLQAAQTBrQf8BcSICQQpJBEAgAUEBaiEBAkAgAy8BNCIAQZkzSw0AIAMgAEEKbCIAOwE0IABB/v8DcSACQf//A3NLDQAgAyAAIAJqOwE0DAILQQAhAiADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMDKsCCyADQQA2AhwgAyABNgIUIANBlR42AhAgA0ENNgIMQQAhAgyqAgtBxgEhAgyQAgsgASAERgRAQd8BIQIMqQILAkAgAS0AAEEwa0H/AXEiAkEKSQRAIAFBAWohAQJAIAMvATQiAEGZM0sNACADIABBCmwiADsBNCAAQf7/A3EgAkH//wNzSw0AIAMgACACajsBNAwCC0EAIQIgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDAyqAgsgA0EANgIcIAMgATYCFCADQZUeNgIQIANBDTYCDEEAIQIMqQILQcUBIQIMjwILIAEgBEYEQEHeASECDKgCCwJAIAEtAABBMGtB/wFxIgJBCkkEQCABQQFqIQECQCADLwE0IgBBmTNLDQAgAyAAQQpsIgA7ATQgAEH+/wNxIAJB//8Dc0sNACADIAAgAmo7ATQMAgtBACECIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgwMqQILIANBADYCHCADIAE2AhQgA0GVHjYCECADQQ02AgxBACECDKgCC0HEASECDI4CCyABIARGBEBB3QEhAgynAgsCQAJAAkACQCABLQAAQQprDhcCAwMAAwMDAwMDAwMDAwMDAwMDAwMDAQMLIAFBAWoMBQsgAUEBaiEBQcMBIQIMjwILIAFBAWohASADQS9qLQAAQQFxDQggA0EANgIcIAMgATYCFCADQY0LNgIQIANBDTYCDEEAIQIMpwILIANBADYCHCADIAE2AhQgA0GNCzYCECADQQ02AgxBACECDKYCCyABIARHBEAgA0EPNgIIIAMgATYCBEEBIQIMjQILQdwBIQIMpQILAkACQANAAkAgAS0AAEEKaw4EAgAAAwALIAQgAUEBaiIBRw0AC0HbASECDKYCCyADKAIEIQAgA0EANgIEIAMgACABEC0iAEUEQCABQQFqIQEMBAsgA0HaATYCHCADIAA2AgwgAyABQQFqNgIUQQAhAgylAgsgAygCBCEAIANBADYCBCADIAAgARAtIgANASABQQFqCyEBQcEBIQIMigILIANB2QE2AhwgAyAANgIMIAMgAUEBajYCFEEAIQIMogILQcIBIQIMiAILIANBL2otAABBAXENASADQQA2AhwgAyABNgIUIANB5Bw2AhAgA0EZNgIMQQAhAgygAgsgASAERgRAQdkB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CyADIAU2AgAMkAILIANBADYCACAGQQFqIQFBGgw8C0HKASECIAEgBEYNjgIgAygCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBjdUAai0AAEcNOyAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMjwILIANBADYCACAGQQFqIQFBIQw7CyABIARGBEBByQEhAgyOAgsCQAJAIAEtAABBwQBrDhQAPT09PT09PT09PT09PT09PT09AT0LIAFBAWohAUGtASECDPUBCyABQQFqIQFBsAEhAgz0AQsgASAERgRAQcgBIQIMjQILAkACQCABLQAAQdUAaw4LADw8PDw8PDw8PAE8CyABQQFqIQFBrgEhAgz0AQsgAUEBaiEBQa8BIQIM8wELQccBIQIgASAERg2LAiADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw04IABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyMAgsgA0EANgIAIAZBAWohAUEqDDgLIAEgBEYEQEHGASECDIsCCyABLQAAQdAARw04IAFBAWohAUElDDcLQcUBIQIgASAERg2JAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGB1QBqLQAARw02IABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyKAgsgA0EANgIAIAZBAWohAUEODDYLIAEgBEYEQEHEASECDIkCCyABLQAAQcUARw02IAFBAWohAUGrASECDO8BCyABIARGBEBBwwEhAgyIAgsCQAJAAkACQCABLQAAQcIAaw4PAAECOTk5OTk5OTk5OTkDOQsgAUEBaiEBQacBIQIM8QELIAFBAWohAUGoASECDPABCyABQQFqIQFBqQEhAgzvAQsgAUEBaiEBQaoBIQIM7gELQcIBIQIgASAERg2GAiADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEH+1ABqLQAARw0zIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyHAgsgA0EANgIAIAZBAWohAUEUDDMLQcEBIQIgASAERg2FAiADKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANA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BTYCAAzsAQsgA0EANgIAIAZBAWohAUEXDBgLQaYBIQIgASAERg3qASADKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHB1ABqLQAARw0XIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzrAQsgA0EANgIAIAZBAWohAUEYDBcLIAEgBEYEQEGlASECDOoBCwJAAkAgAS0AAEHJAGsOBwAZGRkZGQEZCyABQQFqIQFBiwEhAgzRAQsgAUEBaiEBQYwBIQIM0AELQaQBIQIgASAERg3oASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGm1QBqLQAARw0VIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzpAQsgA0EANgIAIAZBAWohAUEJDBULQaMBIQIgASAERg3nASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGk1QBqLQAARw0UIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzoAQsgA0EANgIAIAZBAWohAUEfDBQLQaIBIQIgASAERg3mASADKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEG+1ABqLQAARw0TIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAznAQsgA0EANgIAIAZBAWohAUECDBMLQaEBIQIgASAERg3lASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYDQCABLQAAIABBvNQAai0AAEcNESAAQQFGDQIgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM5QELIAEgBEYEQEGgASECDOUBC0EBIAEtAABB3wBHDREaIAFBAWohAUGHASECDMsBCyADQQA2AgAgBkEBaiEBQYgBIQIMygELQZ8BIQIgASAERg3iASADKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEGE1QBqLQAARw0PIABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAzjAQsgA0EANgIAIAZBAWohAUEpDA8LQZ4BIQIgASAERg3hASADKAIAIgAgBCABa2ohBSABIABrQQNqIQYCQANAIAEtAAAgAEG41ABqLQAARw0OIABBA0YNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAziAQsgA0EANgIAIAZBAWohAUEtDA4LIAEgBEYEQEGdASECDOEBCyABLQAAQcUARw0OIAFBAWohAUGEASECDMcBCyABIARGBEBBnAEhAgzgAQsCQAJAIAEtAABBzABrDggADw8PDw8PAQ8LIAFBAWohAUGCASECDMcBCyABQQFqIQFBgwEhAgzGAQtBmwEhAiABIARGDd4BIAMoAgAiACAEIAFraiEFIAEgAGtBBGohBgJAA0AgAS0AACAAQbPUAGotAABHDQsgAEEERg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN8BCyADQQA2AgAgBkEBaiEBQSMMCwtBmgEhAiABIARGDd0BIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQbDUAGotAABHDQogAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADN4BCyADQQA2AgAgBkEBaiEBQQAMCgsgASAERgRAQZkBIQIM3QELAkACQCABLQAAQcgAaw4IAAwMDAwMDAEMCyABQQFqIQFB/QAhAgzEAQsgAUEBaiEBQYABIQIMwwELIAEgBEYEQEGYASECDNwBCwJAAkAgAS0AAEHOAGsOAwALAQsLIAFBAWohAUH+ACECDMMBCyABQQFqIQFB/wAhAgzCAQsgASAERgRAQZcBIQIM2wELIAEtAABB2QBHDQggAUEBaiEBQQgMBwtBlgEhAiABIARGDdkBIAMoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQazUAGotAABHDQYgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNoBCyADQQA2AgAgBkEBaiEBQQUMBgtBlQEhAiABIARGDdgBIAMoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQabUAGotAABHDQUgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNkBCyADQQA2AgAgBkEBaiEBQRYMBQtBlAEhAiABIARGDdcBIAMoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQaHVAGotAABHDQQgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAyAFNgIADNgBCyADQQA2AgAgBkEBaiEBQRAMBAsgASAERgRAQZMBIQIM1wELAkACQCABLQAAQcMAaw4MAAYGBgYGBgYGBgYBBgsgAUEBaiEBQfkAIQIMvgELIAFBAWohAUH6ACECDL0BC0GSASECIAEgBEYN1QEgAygCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBoNQAai0AAEcNAiAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAM1gELIANBADYCACAGQQFqIQFBJAwCCyADQQA2AgAMAgsgASAERgRAQZEBIQIM1AELIAEtAABBzABHDQEgAUEBaiEBQRMLOgApIAMoAgQhACADQQA2AgQgAyAAIAEQLiIADQIMAQtBACECIANBADYCHCADIAE2AhQgA0H+HzYCECADQQY2AgwM0QELQfgAIQIMtwELIANBkAE2AhwgAyABNgIUIAMgADYCDEEAIQIMzwELQQAhAAJAIAMoAjgiAkUNACACKAJAIgJFDQAgAyACEQAAIQALIABFDQAgAEEVRg0BIANBADYCHCADIAE2AhQgA0GCDzYCECADQSA2AgxBACECDM4BC0H3ACECDLQBCyADQY8BNgIcIAMgATYCFCADQewbNgIQIANBFTYCDEEAIQIMzAELIAEgBEYEQEGPASECDMwBCwJAIAEtAABBIEYEQCABQQFqIQEMAQsgA0EANgIcIAMgATYCFCADQZsfNgIQIANBBjYCDEEAIQIMzAELQQIhAgyyAQsDQCABLQAAQSBHDQIgBCABQQFqIgFHDQALQY4BIQIMygELIAEgBEYEQEGNASECDMoBCwJAIAEtAABBCWsOBEoAAEoAC0H1ACECDLABCyADLQApQQVGBEBB9gAhAgywAQtB9AAhAgyvAQsgASAERgRAQYwBIQIMyAELIANBEDYCCCADIAE2AgQMCgsgASAERgRAQYsBIQIMxwELAkAgAS0AAEEJaw4ERwAARwALQfMAIQIMrQELIAEgBEcEQCADQRA2AgggAyABNgIEQfEAIQIMrQELQYoBIQIMxQELAkAgASAERwRAA0AgAS0AAEGg0ABqLQAAIgBBA0cEQAJAIABBAWsOAkkABAtB8AAhAgyvAQsgBCABQQFqIgFHDQALQYgBIQIMxgELQYgBIQIMxQELIANBADYCHCADIAE2AhQgA0HbIDYCECADQQc2AgxBACECDMQBCyABIARGBEBBiQEhAgzEAQsCQAJAAkAgAS0AAEGg0gBqLQAAQQFrDgNGAgABC0HyACECDKwBCyADQQA2AhwgAyABNgIUIANBtBI2AhAgA0EHNgIMQQAhAgzEAQtB6gAhAgyqAQsgASAERwRAIAFBAWohAUHvACECDKoBC0GHASECDMIBCyAEIAEiAEYEQEGGASECDMIBCyAALQAAIgFBL0YEQCAAQQFqIQFB7gAhAgypAQsgAUEJayICQRdLDQEgACEBQQEgAnRBm4CABHENQQwBCyAEIAEiAEYEQEGFASECDMEBCyAALQAAQS9HDQAgAEEBaiEBDAMLQQAhAiADQQA2AhwgAyAANgIUIANB2yA2AhAgA0EHNgIMDL8BCwJAAkACQAJAAkADQCABLQAAQaDOAGotAAAiAEEFRwRAAkACQCAAQQFrDghHBQYHCAAEAQgLQesAIQIMrQELIAFBAWohAUHtACECDKwBCyAEIAFBAWoiAUcNAAtBhAEhAgzDAQsgAUEBagwUCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDMEBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDMABCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNHiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDL8BCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy+AQsgASAERgRAQYMBIQIMvgELAkAgAS0AAEGgzgBqLQAAQQFrDgg+BAUGAAgCAwcLIAFBAWohAQtBAyECDKMBCyABQQFqDA0LQQAhAiADQQA2AhwgA0HREjYCECADQQc2AgwgAyABQQFqNgIUDLoBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQdsANgIcIAMgATYCFCADIAA2AgxBACECDLkBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQd0ANgIcIAMgATYCFCADIAA2AgxBACECDLgBCyADKAIEIQAgA0EANgIEIAMgACABECwiAEUNFiADQfoANgIcIAMgATYCFCADIAA2AgxBACECDLcBCyADQQA2AhwgAyABNgIUIANB+Q82AhAgA0EHNgIMQQAhAgy2AQtB7AAhAgycAQsgASAERgRAQYIBIQIMtQELIAFBAWoMAgsgASAERgRAQYEBIQIMtAELIAFBAWoMAQsgASAERg0BIAFBAWoLIQFBBCECDJgBC0GAASECDLABCwNAIAEtAABBoMwAai0AACIAQQJHBEAgAEEBRwRAQekAIQIMmQELDDELIAQgAUEBaiIBRw0AC0H/ACECDK8BCyABIARGBEBB/gAhAgyvAQsCQCABLQAAQQlrDjcvAwYvBAYGBgYGBgYGBgYGBgYGBgYGBgUGBgIGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYABgsgAUEBagshAUEFIQIMlAELIAFBAWoMBgsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgyrAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgyqAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQggA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgypAQsgA0EANgIcIAMgATYCFCADQY0UNgIQIANBBzYCDEEAIQIMqAELAkACQAJAAkADQCABLQAAQaDKAGotAAAiAEEFRwRAAkAgAEEBaw4GLgMEBQYABgtB6AAhAgyUAQsgBCABQQFqIgFHDQALQf0AIQIMqwELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB2wA2AhwgAyABNgIUIAMgADYCDEEAIQIMqgELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB3QA2AhwgAyABNgIUIAMgADYCDEEAIQIMqQELIAMoAgQhACADQQA2AgQgAyAAIAEQLCIARQ0HIANB+gA2AhwgAyABNgIUIAMgADYCDEEAIQIMqAELIANBADYCHCADIAE2AhQgA0HkCDYCECADQQc2AgxBACECDKcBCyABIARGDQEgAUEBagshAUEGIQIMjAELQfwAIQIMpAELAkACQAJAAkADQCABLQAAQaDIAGotAAAiAEEFRwRAIABBAWsOBCkCAwQFCyAEIAFBAWoiAUcNAAtB+wAhAgynAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HbADYCHCADIAE2AhQgAyAANgIMQQAhAgymAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0HdADYCHCADIAE2AhQgAyAANgIMQQAhAgylAQsgAygCBCEAIANBADYCBCADIAAgARAsIgBFDQMgA0H6ADYCHCADIAE2AhQgAyAANgIMQQAhAgykAQsgA0EANgIcIAMgATYCFCADQbwKNgIQIANBBzYCDEEAIQIMowELQc8AIQIMiQELQdEAIQIMiAELQecAIQIMhwELIAEgBEYEQEH6ACECDKABCwJAIAEtAABBCWsOBCAAACAACyABQQFqIQFB5gAhAgyGAQsgASAERgRAQfkAIQIMnwELAkAgAS0AAEEJaw4EHwAAHwALQQAhAAJAIAMoAjgiAkUNACACKAI4IgJFDQAgAyACEQAAIQALIABFBEBB4gEhAgyGAQsgAEEVRwRAIANBADYCHCADIAE2AhQgA0HJDTYCECADQRo2AgxBACECDJ8BCyADQfgANgIcIAMgATYCFCADQeoaNgIQIANBFTYCDEEAIQIMngELIAEgBEcEQCADQQ02AgggAyABNgIEQeQAIQIMhQELQfcAIQIMnQELIAEgBEYEQEH2ACECDJ0BCwJAAkACQCABLQAAQcgAaw4LAAELCwsLCwsLCwILCyABQQFqIQFB3QAhAgyFAQsgAUEBaiEBQeAAIQIMhAELIAFBAWohAUHjACECDIMBC0H1ACECIAEgBEYNmwEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBtdUAai0AAEcNCCAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMnAELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgAEQCADQfQANgIcIAMgATYCFCADIAA2AgxBACECDJwBC0HiACECDIIBC0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMnAELQeEAIQIMggELIANB8wA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyaAQsgAy0AKSIAQSNrQQtJDQkCQCAAQQZLDQBBASAAdEHKAHFFDQAMCgtBACECIANBADYCHCADIAE2AhQgA0HtCTYCECADQQg2AgwMmQELQfIAIQIgASAERg2YASADKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGz1QBqLQAARw0FIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAMgBTYCAAyZAQsgAygCBCEAIANCADcDACADIAAgBkEBaiIBECsiAARAIANB8QA2AhwgAyABNgIUIAMgADYCDEEAIQIMmQELQd8AIQIMfwtBACEAAkAgAygCOCICRQ0AIAIoAjQiAkUNACADIAIRAAAhAAsCQCAABEAgAEEVRg0BIANBADYCHCADIAE2AhQgA0HqDTYCECADQSY2AgxBACECDJkBC0HeACECDH8LIANB8AA2AhwgAyABNgIUIANBgBs2AhAgA0EVNgIMQQAhAgyXAQsgAy0AKUEhRg0GIANBADYCHCADIAE2AhQgA0GRCjYCECADQQg2AgxBACECDJYBC0HvACECIAEgBEYNlQEgAygCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABBsNUAai0AAEcNAiAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyADIAU2AgAMlgELIAMoAgQhACADQgA3AwAgAyAAIAZBAWoiARArIgBFDQIgA0HtADYCHCADIAE2AhQgAyAANgIMQQAhAgyVAQsgA0EANgIACyADKAIEIQAgA0EANgIEIAMgACABECsiAEUNgAEgA0HuADYCHCADIAE2AhQgAyAANgIMQQAhAgyTAQtB3AAhAgx5C0EAIQACQCADKAI4IgJFDQAgAigCNCICRQ0AIAMgAhEAACEACwJAIAAEQCAAQRVGDQEgA0EANgIcIAMgATYCFCADQeoNNgIQIANBJjYCDEEAIQIMkwELQdsAIQIMeQsgA0HsADYCHCADIAE2AhQgA0GAGzYCECADQRU2AgxBACECDJEBCyADLQApIgBBI0kNACAAQS5GDQAgA0EANgIcIAMgATYCFCADQckJNgIQIANBCDYCDEEAIQIMkAELQdoAIQIMdgsgASAERgRAQesAIQIMjwELAkAgAS0AAEEvRgRAIAFBAWohAQwBCyAD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ciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBFeHBlY3RlZCBMRiBhZnRlciBoZWFkZXJzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3Byb3RvY29sX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fcHJvdG9jb2wARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgAVHJhbnNmZXItRW5jb2RpbmcgY2FuJ3QgYmUgcHJlc2VudCB3aXRoIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgc2l6ZQBFeHBlY3RlZCBMRiBhZnRlciBjaHVuayBzaXplAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBVbmV4cGVjdGVkIHdoaXRlc3BhY2UgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgaGVhZGVyIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciBjaHVuayBleHRlbnNpb24gdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIHF1b3RlZC1wYWlyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fcHJvdG9jb2xfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUATWlzc2luZyBleHBlY3RlZCBDUiBhZnRlciByZXNwb25zZSBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAE1pc3NpbmcgZXhwZWN0ZWQgQ1IgYWZ0ZXIgY2h1bmsgZXh0ZW5zaW9uIG5hbWUASW52YWxpZCBzdGF0dXMgY29kZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABNaXNzaW5nIGV4cGVjdGVkIENSIGFmdGVyIGNodW5rIGRhdGEARXhwZWN0ZWQgTEYgYWZ0ZXIgY2h1bmsgZGF0YQBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AARGF0YSBhZnRlciBgQ29ubmVjdGlvbjogY2xvc2VgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBRVUVSWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAEV4cGVjdGVkIExGIGFmdGVyIENSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX1BST1RPQ09MX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8sIFJUU1AvIG9yIElDRS8A5xUAAK8VAACkEgAAkhoAACYWAACeFAAA2xkAAHkVAAB+EgAA/hQAADYVAAALFgAA2BYAAPMSAABCGAAArBYAABIVAAAUFwAA7xcAAEgUAABxFwAAshoAAGsZAAB+GQAANRQAAIIaAABEFwAA/RYAAB4YAACHFwAAqhkAAJMSAAAHGAAALBcAAMoXAACkFwAA5xUAAOcVAABYFwAAOxgAAKASAAAtHAAAwxEAAEgRAADeEgAAQhMAAKQZAAD9EAAA9xUAAKUVAADvFgAA+BkAAEoWAABWFgAA9RUAAAoaAAAIGgAAARoAAKsVAABCEgAA1xAAAEwRAAAFGQAAVBYAAB4RAADKGQAAyBkAAE4WAAD/GAAAcRQAAPAVAADuFQAAlBkAAPwVAAC/GQAAmxkAAHwUAABDEQAAcBgAAJUUAAAnFAAAGRQAANUSAADUGQAARBYAAPcQAEG5OwsBAQBB0DsL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBuj0LBAEAAAIAQdE9C14DBAMDAwMDAAADAwADAwADAwMDAwMDAwMDAAUAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAwADAEG6PwsEAQAAAgBB0T8LXgMAAwMDAwMAAAMDAAMDAAMDAwMDAwMDAwMABAAFAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwADAAMAQbDBAAsNbG9zZWVlcC1hbGl2ZQBBycEACwEBAEHgwQAL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBycMACwEBAEHgwwAL5wEBAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAQfHFAAteAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBB0McACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQYDIAAsgcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQpTTQ0KDQoAQanIAAsFAQIAAQMAQcDIAAtfBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanKAAsFAQIAAQMAQcDKAAtfBAUFBgUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAQanMAAsEAQAAAQBBwcwAC14CAgACAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAEGpzgALBQECAAEDAEHAzgALXwQFAAAFBQUFBQUFBQUFBQYFBQUFBQUFBQUFBQUABQAHCAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQAFAAUABQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUAAAAFAEGp0AALBQEBAAEBAEHA0AALAQEAQdrQAAtBAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQanSAAsFAQEAAQEAQcDSAAsBAQBBytIACwYCAAAAAAIAQeHSAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBBoNQAC50BTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRVVFUllPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFVFRQQ0VUU1BBRFRQLw=='
+
+let wasmBuffer
+
+Object.defineProperty(module, 'exports', {
+ get: () => {
+ return wasmBuffer
+ ? wasmBuffer
+ : (wasmBuffer = Buffer.from(wasmBase64, 'base64'))
+ }
+})
diff --git a/vanilla/node_modules/undici/lib/llhttp/utils.d.ts b/vanilla/node_modules/undici/lib/llhttp/utils.d.ts
new file mode 100644
index 0000000..681b22b
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/llhttp/utils.d.ts
@@ -0,0 +1,2 @@
+import type { IntDict } from './constants';
+export declare function enumToMap(obj: IntDict, filter?: readonly number[], exceptions?: readonly number[]): IntDict;
diff --git a/vanilla/node_modules/undici/lib/llhttp/utils.js b/vanilla/node_modules/undici/lib/llhttp/utils.js
new file mode 100644
index 0000000..95081ea
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/llhttp/utils.js
@@ -0,0 +1,12 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.enumToMap = enumToMap;
+function enumToMap(obj, filter = [], exceptions = []) {
+ const emptyFilter = (filter?.length ?? 0) === 0;
+ const emptyExceptions = (exceptions?.length ?? 0) === 0;
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => {
+ return (typeof value === 'number' &&
+ (emptyFilter || filter.includes(value)) &&
+ (emptyExceptions || !exceptions.includes(value)));
+ }));
+}
diff --git a/vanilla/node_modules/undici/lib/mock/mock-agent.js b/vanilla/node_modules/undici/lib/mock/mock-agent.js
new file mode 100644
index 0000000..61449e0
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-agent.js
@@ -0,0 +1,232 @@
+'use strict'
+
+const { kClients } = require('../core/symbols')
+const Agent = require('../dispatcher/agent')
+const {
+ kAgent,
+ kMockAgentSet,
+ kMockAgentGet,
+ kDispatches,
+ kIsMockActive,
+ kNetConnect,
+ kGetNetConnect,
+ kOptions,
+ kFactory,
+ kMockAgentRegisterCallHistory,
+ kMockAgentIsCallHistoryEnabled,
+ kMockAgentAddCallHistoryLog,
+ kMockAgentMockCallHistoryInstance,
+ kMockAgentAcceptsNonStandardSearchParameters,
+ kMockCallHistoryAddLog,
+ kIgnoreTrailingSlash
+} = require('./mock-symbols')
+const MockClient = require('./mock-client')
+const MockPool = require('./mock-pool')
+const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = require('./mock-utils')
+const { InvalidArgumentError, UndiciError } = require('../core/errors')
+const Dispatcher = require('../dispatcher/dispatcher')
+const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
+const { MockCallHistory } = require('./mock-call-history')
+
+class MockAgent extends Dispatcher {
+ constructor (opts = {}) {
+ super(opts)
+
+ const mockOptions = buildAndValidateMockOptions(opts)
+
+ this[kNetConnect] = true
+ this[kIsMockActive] = true
+ this[kMockAgentIsCallHistoryEnabled] = mockOptions.enableCallHistory ?? false
+ this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions.acceptNonStandardSearchParameters ?? false
+ this[kIgnoreTrailingSlash] = mockOptions.ignoreTrailingSlash ?? false
+
+ // Instantiate Agent and encapsulate
+ if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
+ throw new InvalidArgumentError('Argument opts.agent must implement Agent')
+ }
+ const agent = opts?.agent ? opts.agent : new Agent(opts)
+ this[kAgent] = agent
+
+ this[kClients] = agent[kClients]
+ this[kOptions] = mockOptions
+
+ if (this[kMockAgentIsCallHistoryEnabled]) {
+ this[kMockAgentRegisterCallHistory]()
+ }
+ }
+
+ get (origin) {
+ // Normalize origin to handle URL objects and case-insensitive hostnames
+ const normalizedOrigin = normalizeOrigin(origin)
+ const originKey = this[kIgnoreTrailingSlash] ? normalizedOrigin.replace(/\/$/, '') : normalizedOrigin
+
+ let dispatcher = this[kMockAgentGet](originKey)
+
+ if (!dispatcher) {
+ dispatcher = this[kFactory](originKey)
+ this[kMockAgentSet](originKey, dispatcher)
+ }
+ return dispatcher
+ }
+
+ dispatch (opts, handler) {
+ opts.origin = normalizeOrigin(opts.origin)
+
+ // Call MockAgent.get to perform additional setup before dispatching as normal
+ this.get(opts.origin)
+
+ this[kMockAgentAddCallHistoryLog](opts)
+
+ const acceptNonStandardSearchParameters = this[kMockAgentAcceptsNonStandardSearchParameters]
+
+ const dispatchOpts = { ...opts }
+
+ if (acceptNonStandardSearchParameters && dispatchOpts.path) {
+ const [path, searchParams] = dispatchOpts.path.split('?')
+ const normalizedSearchParams = normalizeSearchParams(searchParams, acceptNonStandardSearchParameters)
+ dispatchOpts.path = `${path}?${normalizedSearchParams}`
+ }
+
+ return this[kAgent].dispatch(dispatchOpts, handler)
+ }
+
+ async close () {
+ this.clearCallHistory()
+ await this[kAgent].close()
+ this[kClients].clear()
+ }
+
+ deactivate () {
+ this[kIsMockActive] = false
+ }
+
+ activate () {
+ this[kIsMockActive] = true
+ }
+
+ enableNetConnect (matcher) {
+ if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) {
+ if (Array.isArray(this[kNetConnect])) {
+ this[kNetConnect].push(matcher)
+ } else {
+ this[kNetConnect] = [matcher]
+ }
+ } else if (typeof matcher === 'undefined') {
+ this[kNetConnect] = true
+ } else {
+ throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.')
+ }
+ }
+
+ disableNetConnect () {
+ this[kNetConnect] = false
+ }
+
+ enableCallHistory () {
+ this[kMockAgentIsCallHistoryEnabled] = true
+
+ return this
+ }
+
+ disableCallHistory () {
+ this[kMockAgentIsCallHistoryEnabled] = false
+
+ return this
+ }
+
+ getCallHistory () {
+ return this[kMockAgentMockCallHistoryInstance]
+ }
+
+ clearCallHistory () {
+ if (this[kMockAgentMockCallHistoryInstance] !== undefined) {
+ this[kMockAgentMockCallHistoryInstance].clear()
+ }
+ }
+
+ // This is required to bypass issues caused by using global symbols - see:
+ // https://github.com/nodejs/undici/issues/1447
+ get isMockActive () {
+ return this[kIsMockActive]
+ }
+
+ [kMockAgentRegisterCallHistory] () {
+ if (this[kMockAgentMockCallHistoryInstance] === undefined) {
+ this[kMockAgentMockCallHistoryInstance] = new MockCallHistory()
+ }
+ }
+
+ [kMockAgentAddCallHistoryLog] (opts) {
+ if (this[kMockAgentIsCallHistoryEnabled]) {
+ // additional setup when enableCallHistory class method is used after mockAgent instantiation
+ this[kMockAgentRegisterCallHistory]()
+
+ // add call history log on every call (intercepted or not)
+ this[kMockAgentMockCallHistoryInstance][kMockCallHistoryAddLog](opts)
+ }
+ }
+
+ [kMockAgentSet] (origin, dispatcher) {
+ this[kClients].set(origin, { count: 0, dispatcher })
+ }
+
+ [kFactory] (origin) {
+ const mockOptions = Object.assign({ agent: this }, this[kOptions])
+ return this[kOptions] && this[kOptions].connections === 1
+ ? new MockClient(origin, mockOptions)
+ : new MockPool(origin, mockOptions)
+ }
+
+ [kMockAgentGet] (origin) {
+ // First check if we can immediately find it
+ const result = this[kClients].get(origin)
+ if (result?.dispatcher) {
+ return result.dispatcher
+ }
+
+ // If the origin is not a string create a dummy parent pool and return to user
+ if (typeof origin !== 'string') {
+ const dispatcher = this[kFactory]('http://localhost:9999')
+ this[kMockAgentSet](origin, dispatcher)
+ return dispatcher
+ }
+
+ // If we match, create a pool and assign the same dispatches
+ for (const [keyMatcher, result] of Array.from(this[kClients])) {
+ if (result && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) {
+ const dispatcher = this[kFactory](origin)
+ this[kMockAgentSet](origin, dispatcher)
+ dispatcher[kDispatches] = result.dispatcher[kDispatches]
+ return dispatcher
+ }
+ }
+ }
+
+ [kGetNetConnect] () {
+ return this[kNetConnect]
+ }
+
+ pendingInterceptors () {
+ const mockAgentClients = this[kClients]
+
+ return Array.from(mockAgentClients.entries())
+ .flatMap(([origin, result]) => result.dispatcher[kDispatches].map(dispatch => ({ ...dispatch, origin })))
+ .filter(({ pending }) => pending)
+ }
+
+ assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
+ const pending = this.pendingInterceptors()
+
+ if (pending.length === 0) {
+ return
+ }
+
+ throw new UndiciError(
+ pending.length === 1
+ ? `1 interceptor is pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim()
+ : `${pending.length} interceptors are pending:\n\n${pendingInterceptorsFormatter.format(pending)}`.trim()
+ )
+ }
+}
+
+module.exports = MockAgent
diff --git a/vanilla/node_modules/undici/lib/mock/mock-call-history.js b/vanilla/node_modules/undici/lib/mock/mock-call-history.js
new file mode 100644
index 0000000..d4a92b2
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-call-history.js
@@ -0,0 +1,248 @@
+'use strict'
+
+const { kMockCallHistoryAddLog } = require('./mock-symbols')
+const { InvalidArgumentError } = require('../core/errors')
+
+function handleFilterCallsWithOptions (criteria, options, handler, store) {
+ switch (options.operator) {
+ case 'OR':
+ store.push(...handler(criteria))
+
+ return store
+ case 'AND':
+ return handler.call({ logs: store }, criteria)
+ default:
+ // guard -- should never happens because buildAndValidateFilterCallsOptions is called before
+ throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
+ }
+}
+
+function buildAndValidateFilterCallsOptions (options = {}) {
+ const finalOptions = {}
+
+ if ('operator' in options) {
+ if (typeof options.operator !== 'string' || (options.operator.toUpperCase() !== 'OR' && options.operator.toUpperCase() !== 'AND')) {
+ throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
+ }
+
+ return {
+ ...finalOptions,
+ operator: options.operator.toUpperCase()
+ }
+ }
+
+ return finalOptions
+}
+
+function makeFilterCalls (parameterName) {
+ return (parameterValue) => {
+ if (typeof parameterValue === 'string' || parameterValue == null) {
+ return this.logs.filter((log) => {
+ return log[parameterName] === parameterValue
+ })
+ }
+ if (parameterValue instanceof RegExp) {
+ return this.logs.filter((log) => {
+ return parameterValue.test(log[parameterName])
+ })
+ }
+
+ throw new InvalidArgumentError(`${parameterName} parameter should be one of string, regexp, undefined or null`)
+ }
+}
+function computeUrlWithMaybeSearchParameters (requestInit) {
+ // path can contains query url parameters
+ // or query can contains query url parameters
+ try {
+ const url = new URL(requestInit.path, requestInit.origin)
+
+ // requestInit.path contains query url parameters
+ // requestInit.query is then undefined
+ if (url.search.length !== 0) {
+ return url
+ }
+
+ // requestInit.query can be populated here
+ url.search = new URLSearchParams(requestInit.query).toString()
+
+ return url
+ } catch (error) {
+ throw new InvalidArgumentError('An error occurred when computing MockCallHistoryLog.url', { cause: error })
+ }
+}
+
+class MockCallHistoryLog {
+ constructor (requestInit = {}) {
+ this.body = requestInit.body
+ this.headers = requestInit.headers
+ this.method = requestInit.method
+
+ const url = computeUrlWithMaybeSearchParameters(requestInit)
+
+ this.fullUrl = url.toString()
+ this.origin = url.origin
+ this.path = url.pathname
+ this.searchParams = Object.fromEntries(url.searchParams)
+ this.protocol = url.protocol
+ this.host = url.host
+ this.port = url.port
+ this.hash = url.hash
+ }
+
+ toMap () {
+ return new Map([
+ ['protocol', this.protocol],
+ ['host', this.host],
+ ['port', this.port],
+ ['origin', this.origin],
+ ['path', this.path],
+ ['hash', this.hash],
+ ['searchParams', this.searchParams],
+ ['fullUrl', this.fullUrl],
+ ['method', this.method],
+ ['body', this.body],
+ ['headers', this.headers]]
+ )
+ }
+
+ toString () {
+ const options = { betweenKeyValueSeparator: '->', betweenPairSeparator: '|' }
+ let result = ''
+
+ this.toMap().forEach((value, key) => {
+ if (typeof value === 'string' || value === undefined || value === null) {
+ result = `${result}${key}${options.betweenKeyValueSeparator}${value}${options.betweenPairSeparator}`
+ }
+ if ((typeof value === 'object' && value !== null) || Array.isArray(value)) {
+ result = `${result}${key}${options.betweenKeyValueSeparator}${JSON.stringify(value)}${options.betweenPairSeparator}`
+ }
+ // maybe miss something for non Record / Array headers and searchParams here
+ })
+
+ // delete last betweenPairSeparator
+ return result.slice(0, -1)
+ }
+}
+
+class MockCallHistory {
+ logs = []
+
+ calls () {
+ return this.logs
+ }
+
+ firstCall () {
+ return this.logs.at(0)
+ }
+
+ lastCall () {
+ return this.logs.at(-1)
+ }
+
+ nthCall (number) {
+ if (typeof number !== 'number') {
+ throw new InvalidArgumentError('nthCall must be called with a number')
+ }
+ if (!Number.isInteger(number)) {
+ throw new InvalidArgumentError('nthCall must be called with an integer')
+ }
+ if (Math.sign(number) !== 1) {
+ throw new InvalidArgumentError('nthCall must be called with a positive value. use firstCall or lastCall instead')
+ }
+
+ // non zero based index. this is more human readable
+ return this.logs.at(number - 1)
+ }
+
+ filterCalls (criteria, options) {
+ // perf
+ if (this.logs.length === 0) {
+ return this.logs
+ }
+ if (typeof criteria === 'function') {
+ return this.logs.filter(criteria)
+ }
+ if (criteria instanceof RegExp) {
+ return this.logs.filter((log) => {
+ return criteria.test(log.toString())
+ })
+ }
+ if (typeof criteria === 'object' && criteria !== null) {
+ // no criteria - returning all logs
+ if (Object.keys(criteria).length === 0) {
+ return this.logs
+ }
+
+ const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }
+
+ let maybeDuplicatedLogsFiltered = []
+ if ('protocol' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
+ }
+ if ('host' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
+ }
+ if ('port' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
+ }
+ if ('origin' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
+ }
+ if ('path' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
+ }
+ if ('hash' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
+ }
+ if ('fullUrl' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
+ }
+ if ('method' in criteria) {
+ maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
+ }
+
+ const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
+
+ return uniqLogsFiltered
+ }
+
+ throw new InvalidArgumentError('criteria parameter should be one of function, regexp, or object')
+ }
+
+ filterCallsByProtocol = makeFilterCalls.call(this, 'protocol')
+
+ filterCallsByHost = makeFilterCalls.call(this, 'host')
+
+ filterCallsByPort = makeFilterCalls.call(this, 'port')
+
+ filterCallsByOrigin = makeFilterCalls.call(this, 'origin')
+
+ filterCallsByPath = makeFilterCalls.call(this, 'path')
+
+ filterCallsByHash = makeFilterCalls.call(this, 'hash')
+
+ filterCallsByFullUrl = makeFilterCalls.call(this, 'fullUrl')
+
+ filterCallsByMethod = makeFilterCalls.call(this, 'method')
+
+ clear () {
+ this.logs = []
+ }
+
+ [kMockCallHistoryAddLog] (requestInit) {
+ const log = new MockCallHistoryLog(requestInit)
+
+ this.logs.push(log)
+
+ return log
+ }
+
+ * [Symbol.iterator] () {
+ for (const log of this.calls()) {
+ yield log
+ }
+ }
+}
+
+module.exports.MockCallHistory = MockCallHistory
+module.exports.MockCallHistoryLog = MockCallHistoryLog
diff --git a/vanilla/node_modules/undici/lib/mock/mock-client.js b/vanilla/node_modules/undici/lib/mock/mock-client.js
new file mode 100644
index 0000000..b3be7ab
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-client.js
@@ -0,0 +1,68 @@
+'use strict'
+
+const { promisify } = require('node:util')
+const Client = require('../dispatcher/client')
+const { buildMockDispatch } = require('./mock-utils')
+const {
+ kDispatches,
+ kMockAgent,
+ kClose,
+ kOriginalClose,
+ kOrigin,
+ kOriginalDispatch,
+ kConnected,
+ kIgnoreTrailingSlash
+} = require('./mock-symbols')
+const { MockInterceptor } = require('./mock-interceptor')
+const Symbols = require('../core/symbols')
+const { InvalidArgumentError } = require('../core/errors')
+
+/**
+ * MockClient provides an API that extends the Client to influence the mockDispatches.
+ */
+class MockClient extends Client {
+ constructor (origin, opts) {
+ if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
+ throw new InvalidArgumentError('Argument opts.agent must implement Agent')
+ }
+
+ super(origin, opts)
+
+ this[kMockAgent] = opts.agent
+ this[kOrigin] = origin
+ this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false
+ this[kDispatches] = []
+ this[kConnected] = 1
+ this[kOriginalDispatch] = this.dispatch
+ this[kOriginalClose] = this.close.bind(this)
+
+ this.dispatch = buildMockDispatch.call(this)
+ this.close = this[kClose]
+ }
+
+ get [Symbols.kConnected] () {
+ return this[kConnected]
+ }
+
+ /**
+ * Sets up the base interceptor for mocking replies from undici.
+ */
+ intercept (opts) {
+ return new MockInterceptor(
+ opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts },
+ this[kDispatches]
+ )
+ }
+
+ cleanMocks () {
+ this[kDispatches] = []
+ }
+
+ async [kClose] () {
+ await promisify(this[kOriginalClose])()
+ this[kConnected] = 0
+ this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
+ }
+}
+
+module.exports = MockClient
diff --git a/vanilla/node_modules/undici/lib/mock/mock-errors.js b/vanilla/node_modules/undici/lib/mock/mock-errors.js
new file mode 100644
index 0000000..69e4f9c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-errors.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const { UndiciError } = require('../core/errors')
+
+const kMockNotMatchedError = Symbol.for('undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED')
+
+/**
+ * The request does not match any registered mock dispatches.
+ */
+class MockNotMatchedError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'MockNotMatchedError'
+ this.message = message || 'The request does not match any registered mock dispatches'
+ this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kMockNotMatchedError] === true
+ }
+
+ get [kMockNotMatchedError] () {
+ return true
+ }
+}
+
+module.exports = {
+ MockNotMatchedError
+}
diff --git a/vanilla/node_modules/undici/lib/mock/mock-interceptor.js b/vanilla/node_modules/undici/lib/mock/mock-interceptor.js
new file mode 100644
index 0000000..1ea7aac
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-interceptor.js
@@ -0,0 +1,209 @@
+'use strict'
+
+const { getResponseData, buildKey, addMockDispatch } = require('./mock-utils')
+const {
+ kDispatches,
+ kDispatchKey,
+ kDefaultHeaders,
+ kDefaultTrailers,
+ kContentLength,
+ kMockDispatch,
+ kIgnoreTrailingSlash
+} = require('./mock-symbols')
+const { InvalidArgumentError } = require('../core/errors')
+const { serializePathWithQuery } = require('../core/util')
+
+/**
+ * Defines the scope API for an interceptor reply
+ */
+class MockScope {
+ constructor (mockDispatch) {
+ this[kMockDispatch] = mockDispatch
+ }
+
+ /**
+ * Delay a reply by a set amount in ms.
+ */
+ delay (waitInMs) {
+ if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) {
+ throw new InvalidArgumentError('waitInMs must be a valid integer > 0')
+ }
+
+ this[kMockDispatch].delay = waitInMs
+ return this
+ }
+
+ /**
+ * For a defined reply, never mark as consumed.
+ */
+ persist () {
+ this[kMockDispatch].persist = true
+ return this
+ }
+
+ /**
+ * Allow one to define a reply for a set amount of matching requests.
+ */
+ times (repeatTimes) {
+ if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) {
+ throw new InvalidArgumentError('repeatTimes must be a valid integer > 0')
+ }
+
+ this[kMockDispatch].times = repeatTimes
+ return this
+ }
+}
+
+/**
+ * Defines an interceptor for a Mock
+ */
+class MockInterceptor {
+ constructor (opts, mockDispatches) {
+ if (typeof opts !== 'object') {
+ throw new InvalidArgumentError('opts must be an object')
+ }
+ if (typeof opts.path === 'undefined') {
+ throw new InvalidArgumentError('opts.path must be defined')
+ }
+ if (typeof opts.method === 'undefined') {
+ opts.method = 'GET'
+ }
+ // See https://github.com/nodejs/undici/issues/1245
+ // As per RFC 3986, clients are not supposed to send URI
+ // fragments to servers when they retrieve a document,
+ if (typeof opts.path === 'string') {
+ if (opts.query) {
+ opts.path = serializePathWithQuery(opts.path, opts.query)
+ } else {
+ // Matches https://github.com/nodejs/undici/blob/main/lib/web/fetch/index.js#L1811
+ const parsedURL = new URL(opts.path, 'data://')
+ opts.path = parsedURL.pathname + parsedURL.search
+ }
+ }
+ if (typeof opts.method === 'string') {
+ opts.method = opts.method.toUpperCase()
+ }
+
+ this[kDispatchKey] = buildKey(opts)
+ this[kDispatches] = mockDispatches
+ this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false
+ this[kDefaultHeaders] = {}
+ this[kDefaultTrailers] = {}
+ this[kContentLength] = false
+ }
+
+ createMockScopeDispatchData ({ statusCode, data, responseOptions }) {
+ const responseData = getResponseData(data)
+ const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
+ const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
+ const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers }
+
+ return { statusCode, data, headers, trailers }
+ }
+
+ validateReplyParameters (replyParameters) {
+ if (typeof replyParameters.statusCode === 'undefined') {
+ throw new InvalidArgumentError('statusCode must be defined')
+ }
+ if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) {
+ throw new InvalidArgumentError('responseOptions must be an object')
+ }
+ }
+
+ /**
+ * Mock an undici request with a defined reply.
+ */
+ reply (replyOptionsCallbackOrStatusCode) {
+ // Values of reply aren't available right now as they
+ // can only be available when the reply callback is invoked.
+ if (typeof replyOptionsCallbackOrStatusCode === 'function') {
+ // We'll first wrap the provided callback in another function,
+ // this function will properly resolve the data from the callback
+ // when invoked.
+ const wrappedDefaultsCallback = (opts) => {
+ // Our reply options callback contains the parameter for statusCode, data and options.
+ const resolvedData = replyOptionsCallbackOrStatusCode(opts)
+
+ // Check if it is in the right format
+ if (typeof resolvedData !== 'object' || resolvedData === null) {
+ throw new InvalidArgumentError('reply options callback must return an object')
+ }
+
+ const replyParameters = { data: '', responseOptions: {}, ...resolvedData }
+ this.validateReplyParameters(replyParameters)
+ // Since the values can be obtained immediately we return them
+ // from this higher order function that will be resolved later.
+ return {
+ ...this.createMockScopeDispatchData(replyParameters)
+ }
+ }
+
+ // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data.
+ const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
+ return new MockScope(newMockDispatch)
+ }
+
+ // We can have either one or three parameters, if we get here,
+ // we should have 1-3 parameters. So we spread the arguments of
+ // this function to obtain the parameters, since replyData will always
+ // just be the statusCode.
+ const replyParameters = {
+ statusCode: replyOptionsCallbackOrStatusCode,
+ data: arguments[1] === undefined ? '' : arguments[1],
+ responseOptions: arguments[2] === undefined ? {} : arguments[2]
+ }
+ this.validateReplyParameters(replyParameters)
+
+ // Send in-already provided data like usual
+ const dispatchData = this.createMockScopeDispatchData(replyParameters)
+ const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
+ return new MockScope(newMockDispatch)
+ }
+
+ /**
+ * Mock an undici request with a defined error.
+ */
+ replyWithError (error) {
+ if (typeof error === 'undefined') {
+ throw new InvalidArgumentError('error must be defined')
+ }
+
+ const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
+ return new MockScope(newMockDispatch)
+ }
+
+ /**
+ * Set default reply headers on the interceptor for subsequent replies
+ */
+ defaultReplyHeaders (headers) {
+ if (typeof headers === 'undefined') {
+ throw new InvalidArgumentError('headers must be defined')
+ }
+
+ this[kDefaultHeaders] = headers
+ return this
+ }
+
+ /**
+ * Set default reply trailers on the interceptor for subsequent replies
+ */
+ defaultReplyTrailers (trailers) {
+ if (typeof trailers === 'undefined') {
+ throw new InvalidArgumentError('trailers must be defined')
+ }
+
+ this[kDefaultTrailers] = trailers
+ return this
+ }
+
+ /**
+ * Set reply content length header for replies on the interceptor
+ */
+ replyContentLength () {
+ this[kContentLength] = true
+ return this
+ }
+}
+
+module.exports.MockInterceptor = MockInterceptor
+module.exports.MockScope = MockScope
diff --git a/vanilla/node_modules/undici/lib/mock/mock-pool.js b/vanilla/node_modules/undici/lib/mock/mock-pool.js
new file mode 100644
index 0000000..2121e3c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-pool.js
@@ -0,0 +1,68 @@
+'use strict'
+
+const { promisify } = require('node:util')
+const Pool = require('../dispatcher/pool')
+const { buildMockDispatch } = require('./mock-utils')
+const {
+ kDispatches,
+ kMockAgent,
+ kClose,
+ kOriginalClose,
+ kOrigin,
+ kOriginalDispatch,
+ kConnected,
+ kIgnoreTrailingSlash
+} = require('./mock-symbols')
+const { MockInterceptor } = require('./mock-interceptor')
+const Symbols = require('../core/symbols')
+const { InvalidArgumentError } = require('../core/errors')
+
+/**
+ * MockPool provides an API that extends the Pool to influence the mockDispatches.
+ */
+class MockPool extends Pool {
+ constructor (origin, opts) {
+ if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') {
+ throw new InvalidArgumentError('Argument opts.agent must implement Agent')
+ }
+
+ super(origin, opts)
+
+ this[kMockAgent] = opts.agent
+ this[kOrigin] = origin
+ this[kIgnoreTrailingSlash] = opts.ignoreTrailingSlash ?? false
+ this[kDispatches] = []
+ this[kConnected] = 1
+ this[kOriginalDispatch] = this.dispatch
+ this[kOriginalClose] = this.close.bind(this)
+
+ this.dispatch = buildMockDispatch.call(this)
+ this.close = this[kClose]
+ }
+
+ get [Symbols.kConnected] () {
+ return this[kConnected]
+ }
+
+ /**
+ * Sets up the base interceptor for mocking replies from undici.
+ */
+ intercept (opts) {
+ return new MockInterceptor(
+ opts && { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts },
+ this[kDispatches]
+ )
+ }
+
+ cleanMocks () {
+ this[kDispatches] = []
+ }
+
+ async [kClose] () {
+ await promisify(this[kOriginalClose])()
+ this[kConnected] = 0
+ this[kMockAgent][Symbols.kClients].delete(this[kOrigin])
+ }
+}
+
+module.exports = MockPool
diff --git a/vanilla/node_modules/undici/lib/mock/mock-symbols.js b/vanilla/node_modules/undici/lib/mock/mock-symbols.js
new file mode 100644
index 0000000..940dbe6
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-symbols.js
@@ -0,0 +1,31 @@
+'use strict'
+
+module.exports = {
+ kAgent: Symbol('agent'),
+ kOptions: Symbol('options'),
+ kFactory: Symbol('factory'),
+ kDispatches: Symbol('dispatches'),
+ kDispatchKey: Symbol('dispatch key'),
+ kDefaultHeaders: Symbol('default headers'),
+ kDefaultTrailers: Symbol('default trailers'),
+ kContentLength: Symbol('content length'),
+ kMockAgent: Symbol('mock agent'),
+ kMockAgentSet: Symbol('mock agent set'),
+ kMockAgentGet: Symbol('mock agent get'),
+ kMockDispatch: Symbol('mock dispatch'),
+ kClose: Symbol('close'),
+ kOriginalClose: Symbol('original agent close'),
+ kOriginalDispatch: Symbol('original dispatch'),
+ kOrigin: Symbol('origin'),
+ kIsMockActive: Symbol('is mock active'),
+ kNetConnect: Symbol('net connect'),
+ kGetNetConnect: Symbol('get net connect'),
+ kConnected: Symbol('connected'),
+ kIgnoreTrailingSlash: Symbol('ignore trailing slash'),
+ kMockAgentMockCallHistoryInstance: Symbol('mock agent mock call history name'),
+ kMockAgentRegisterCallHistory: Symbol('mock agent register mock call history'),
+ kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'),
+ kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'),
+ kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'),
+ kMockCallHistoryAddLog: Symbol('mock call history add log')
+}
diff --git a/vanilla/node_modules/undici/lib/mock/mock-utils.js b/vanilla/node_modules/undici/lib/mock/mock-utils.js
new file mode 100644
index 0000000..291a857
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/mock-utils.js
@@ -0,0 +1,480 @@
+'use strict'
+
+const { MockNotMatchedError } = require('./mock-errors')
+const {
+ kDispatches,
+ kMockAgent,
+ kOriginalDispatch,
+ kOrigin,
+ kGetNetConnect
+} = require('./mock-symbols')
+const { serializePathWithQuery } = require('../core/util')
+const { STATUS_CODES } = require('node:http')
+const {
+ types: {
+ isPromise
+ }
+} = require('node:util')
+const { InvalidArgumentError } = require('../core/errors')
+
+function matchValue (match, value) {
+ if (typeof match === 'string') {
+ return match === value
+ }
+ if (match instanceof RegExp) {
+ return match.test(value)
+ }
+ if (typeof match === 'function') {
+ return match(value) === true
+ }
+ return false
+}
+
+function lowerCaseEntries (headers) {
+ return Object.fromEntries(
+ Object.entries(headers).map(([headerName, headerValue]) => {
+ return [headerName.toLocaleLowerCase(), headerValue]
+ })
+ )
+}
+
+/**
+ * @param {import('../../index').Headers|string[]|Record<string, string>} headers
+ * @param {string} key
+ */
+function getHeaderByName (headers, key) {
+ if (Array.isArray(headers)) {
+ for (let i = 0; i < headers.length; i += 2) {
+ if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) {
+ return headers[i + 1]
+ }
+ }
+
+ return undefined
+ } else if (typeof headers.get === 'function') {
+ return headers.get(key)
+ } else {
+ return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
+ }
+}
+
+/** @param {string[]} headers */
+function buildHeadersFromArray (headers) { // fetch HeadersList
+ const clone = headers.slice()
+ const entries = []
+ for (let index = 0; index < clone.length; index += 2) {
+ entries.push([clone[index], clone[index + 1]])
+ }
+ return Object.fromEntries(entries)
+}
+
+function matchHeaders (mockDispatch, headers) {
+ if (typeof mockDispatch.headers === 'function') {
+ if (Array.isArray(headers)) { // fetch HeadersList
+ headers = buildHeadersFromArray(headers)
+ }
+ return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
+ }
+ if (typeof mockDispatch.headers === 'undefined') {
+ return true
+ }
+ if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') {
+ return false
+ }
+
+ for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
+ const headerValue = getHeaderByName(headers, matchHeaderName)
+
+ if (!matchValue(matchHeaderValue, headerValue)) {
+ return false
+ }
+ }
+ return true
+}
+
+function normalizeSearchParams (query) {
+ if (typeof query !== 'string') {
+ return query
+ }
+
+ const originalQp = new URLSearchParams(query)
+ const normalizedQp = new URLSearchParams()
+
+ for (let [key, value] of originalQp.entries()) {
+ key = key.replace('[]', '')
+
+ const valueRepresentsString = /^(['"]).*\1$/.test(value)
+ if (valueRepresentsString) {
+ normalizedQp.append(key, value)
+ continue
+ }
+
+ if (value.includes(',')) {
+ const values = value.split(',')
+ for (const v of values) {
+ normalizedQp.append(key, v)
+ }
+ continue
+ }
+
+ normalizedQp.append(key, value)
+ }
+
+ return normalizedQp
+}
+
+function safeUrl (path) {
+ if (typeof path !== 'string') {
+ return path
+ }
+ const pathSegments = path.split('?', 3)
+ if (pathSegments.length !== 2) {
+ return path
+ }
+
+ const qp = new URLSearchParams(pathSegments.pop())
+ qp.sort()
+ return [...pathSegments, qp.toString()].join('?')
+}
+
+function matchKey (mockDispatch, { path, method, body, headers }) {
+ const pathMatch = matchValue(mockDispatch.path, path)
+ const methodMatch = matchValue(mockDispatch.method, method)
+ const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true
+ const headersMatch = matchHeaders(mockDispatch, headers)
+ return pathMatch && methodMatch && bodyMatch && headersMatch
+}
+
+function getResponseData (data) {
+ if (Buffer.isBuffer(data)) {
+ return data
+ } else if (data instanceof Uint8Array) {
+ return data
+ } else if (data instanceof ArrayBuffer) {
+ return data
+ } else if (typeof data === 'object') {
+ return JSON.stringify(data)
+ } else if (data) {
+ return data.toString()
+ } else {
+ return ''
+ }
+}
+
+function getMockDispatch (mockDispatches, key) {
+ const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path
+ const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath
+
+ const resolvedPathWithoutTrailingSlash = removeTrailingSlash(resolvedPath)
+
+ // Match path
+ let matchedMockDispatches = mockDispatches
+ .filter(({ consumed }) => !consumed)
+ .filter(({ path, ignoreTrailingSlash }) => {
+ return ignoreTrailingSlash
+ ? matchValue(removeTrailingSlash(safeUrl(path)), resolvedPathWithoutTrailingSlash)
+ : matchValue(safeUrl(path), resolvedPath)
+ })
+ if (matchedMockDispatches.length === 0) {
+ throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
+ }
+
+ // Match method
+ matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method))
+ if (matchedMockDispatches.length === 0) {
+ throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}' on path '${resolvedPath}'`)
+ }
+
+ // Match body
+ matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true)
+ if (matchedMockDispatches.length === 0) {
+ throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}' on path '${resolvedPath}'`)
+ }
+
+ // Match headers
+ matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers))
+ if (matchedMockDispatches.length === 0) {
+ const headers = typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers
+ throw new MockNotMatchedError(`Mock dispatch not matched for headers '${headers}' on path '${resolvedPath}'`)
+ }
+
+ return matchedMockDispatches[0]
+}
+
+function addMockDispatch (mockDispatches, key, data, opts) {
+ const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false, ...opts }
+ const replyData = typeof data === 'function' ? { callback: data } : { ...data }
+ const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
+ mockDispatches.push(newMockDispatch)
+ return newMockDispatch
+}
+
+function deleteMockDispatch (mockDispatches, key) {
+ const index = mockDispatches.findIndex(dispatch => {
+ if (!dispatch.consumed) {
+ return false
+ }
+ return matchKey(dispatch, key)
+ })
+ if (index !== -1) {
+ mockDispatches.splice(index, 1)
+ }
+}
+
+/**
+ * @param {string} path Path to remove trailing slash from
+ */
+function removeTrailingSlash (path) {
+ while (path.endsWith('/')) {
+ path = path.slice(0, -1)
+ }
+
+ if (path.length === 0) {
+ path = '/'
+ }
+
+ return path
+}
+
+function buildKey (opts) {
+ const { path, method, body, headers, query } = opts
+
+ return {
+ path,
+ method,
+ body,
+ headers,
+ query
+ }
+}
+
+function generateKeyValues (data) {
+ const keys = Object.keys(data)
+ const result = []
+ for (let i = 0; i < keys.length; ++i) {
+ const key = keys[i]
+ const value = data[key]
+ const name = Buffer.from(`${key}`)
+ if (Array.isArray(value)) {
+ for (let j = 0; j < value.length; ++j) {
+ result.push(name, Buffer.from(`${value[j]}`))
+ }
+ } else {
+ result.push(name, Buffer.from(`${value}`))
+ }
+ }
+ return result
+}
+
+/**
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
+ * @param {number} statusCode
+ */
+function getStatusText (statusCode) {
+ return STATUS_CODES[statusCode] || 'unknown'
+}
+
+async function getResponse (body) {
+ const buffers = []
+ for await (const data of body) {
+ buffers.push(data)
+ }
+ return Buffer.concat(buffers).toString('utf8')
+}
+
+/**
+ * Mock dispatch function used to simulate undici dispatches
+ */
+function mockDispatch (opts, handler) {
+ // Get mock dispatch from built key
+ const key = buildKey(opts)
+ const mockDispatch = getMockDispatch(this[kDispatches], key)
+
+ mockDispatch.timesInvoked++
+
+ // Here's where we resolve a callback if a callback is present for the dispatch data.
+ if (mockDispatch.data.callback) {
+ mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) }
+ }
+
+ // Parse mockDispatch data
+ const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch
+ const { timesInvoked, times } = mockDispatch
+
+ // If it's used up and not persistent, mark as consumed
+ mockDispatch.consumed = !persist && timesInvoked >= times
+ mockDispatch.pending = timesInvoked < times
+
+ // If specified, trigger dispatch error
+ if (error !== null) {
+ deleteMockDispatch(this[kDispatches], key)
+ handler.onError(error)
+ return true
+ }
+
+ // Track whether the request has been aborted
+ let aborted = false
+ let timer = null
+
+ function abort (err) {
+ if (aborted) {
+ return
+ }
+ aborted = true
+
+ // Clear the pending delayed response if any
+ if (timer !== null) {
+ clearTimeout(timer)
+ timer = null
+ }
+
+ // Notify the handler of the abort
+ handler.onError(err)
+ }
+
+ // Call onConnect to allow the handler to register the abort callback
+ handler.onConnect?.(abort, null)
+
+ // Handle the request with a delay if necessary
+ if (typeof delay === 'number' && delay > 0) {
+ timer = setTimeout(() => {
+ timer = null
+ handleReply(this[kDispatches])
+ }, delay)
+ } else {
+ handleReply(this[kDispatches])
+ }
+
+ function handleReply (mockDispatches, _data = data) {
+ // Don't send response if the request was aborted
+ if (aborted) {
+ return
+ }
+
+ // fetch's HeadersList is a 1D string array
+ const optsHeaders = Array.isArray(opts.headers)
+ ? buildHeadersFromArray(opts.headers)
+ : opts.headers
+ const body = typeof _data === 'function'
+ ? _data({ ...opts, headers: optsHeaders })
+ : _data
+
+ // util.types.isPromise is likely needed for jest.
+ if (isPromise(body)) {
+ // If handleReply is asynchronous, throwing an error
+ // in the callback will reject the promise, rather than
+ // synchronously throw the error, which breaks some tests.
+ // Rather, we wait for the callback to resolve if it is a
+ // promise, and then re-run handleReply with the new body.
+ return body.then((newData) => handleReply(mockDispatches, newData))
+ }
+
+ // Check again if aborted after async body resolution
+ if (aborted) {
+ return
+ }
+
+ const responseData = getResponseData(body)
+ const responseHeaders = generateKeyValues(headers)
+ const responseTrailers = generateKeyValues(trailers)
+
+ handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
+ handler.onData?.(Buffer.from(responseData))
+ handler.onComplete?.(responseTrailers)
+ deleteMockDispatch(mockDispatches, key)
+ }
+
+ function resume () {}
+
+ return true
+}
+
+function buildMockDispatch () {
+ const agent = this[kMockAgent]
+ const origin = this[kOrigin]
+ const originalDispatch = this[kOriginalDispatch]
+
+ return function dispatch (opts, handler) {
+ if (agent.isMockActive) {
+ try {
+ mockDispatch.call(this, opts, handler)
+ } catch (error) {
+ if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
+ const netConnect = agent[kGetNetConnect]()
+ if (netConnect === false) {
+ throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
+ }
+ if (checkNetConnect(netConnect, origin)) {
+ originalDispatch.call(this, opts, handler)
+ } else {
+ throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`)
+ }
+ } else {
+ throw error
+ }
+ }
+ } else {
+ originalDispatch.call(this, opts, handler)
+ }
+ }
+}
+
+function checkNetConnect (netConnect, origin) {
+ const url = new URL(origin)
+ if (netConnect === true) {
+ return true
+ } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) {
+ return true
+ }
+ return false
+}
+
+function normalizeOrigin (origin) {
+ if (typeof origin !== 'string' && !(origin instanceof URL)) {
+ return origin
+ }
+
+ if (origin instanceof URL) {
+ return origin.origin
+ }
+
+ return origin.toLowerCase()
+}
+
+function buildAndValidateMockOptions (opts) {
+ const { agent, ...mockOptions } = opts
+
+ if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') {
+ throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
+ }
+
+ if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') {
+ throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean')
+ }
+
+ if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') {
+ throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean')
+ }
+
+ return mockOptions
+}
+
+module.exports = {
+ getResponseData,
+ getMockDispatch,
+ addMockDispatch,
+ deleteMockDispatch,
+ buildKey,
+ generateKeyValues,
+ matchValue,
+ getResponse,
+ getStatusText,
+ mockDispatch,
+ buildMockDispatch,
+ checkNetConnect,
+ buildAndValidateMockOptions,
+ getHeaderByName,
+ buildHeadersFromArray,
+ normalizeSearchParams,
+ normalizeOrigin
+}
diff --git a/vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js b/vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js
new file mode 100644
index 0000000..ccca951
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/pending-interceptors-formatter.js
@@ -0,0 +1,43 @@
+'use strict'
+
+const { Transform } = require('node:stream')
+const { Console } = require('node:console')
+
+const PERSISTENT = process.versions.icu ? '✅' : 'Y '
+const NOT_PERSISTENT = process.versions.icu ? '❌' : 'N '
+
+/**
+ * Gets the output of `console.table(…)` as a string.
+ */
+module.exports = class PendingInterceptorsFormatter {
+ constructor ({ disableColors } = {}) {
+ this.transform = new Transform({
+ transform (chunk, _enc, cb) {
+ cb(null, chunk)
+ }
+ })
+
+ this.logger = new Console({
+ stdout: this.transform,
+ inspectOptions: {
+ colors: !disableColors && !process.env.CI
+ }
+ })
+ }
+
+ format (pendingInterceptors) {
+ const withPrettyHeaders = pendingInterceptors.map(
+ ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({
+ Method: method,
+ Origin: origin,
+ Path: path,
+ 'Status code': statusCode,
+ Persistent: persist ? PERSISTENT : NOT_PERSISTENT,
+ Invocations: timesInvoked,
+ Remaining: persist ? Infinity : times - timesInvoked
+ }))
+
+ this.logger.table(withPrettyHeaders)
+ return this.transform.read().toString()
+ }
+}
diff --git a/vanilla/node_modules/undici/lib/mock/snapshot-agent.js b/vanilla/node_modules/undici/lib/mock/snapshot-agent.js
new file mode 100644
index 0000000..8028011
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/snapshot-agent.js
@@ -0,0 +1,353 @@
+'use strict'
+
+const Agent = require('../dispatcher/agent')
+const MockAgent = require('./mock-agent')
+const { SnapshotRecorder } = require('./snapshot-recorder')
+const WrapHandler = require('../handler/wrap-handler')
+const { InvalidArgumentError, UndiciError } = require('../core/errors')
+const { validateSnapshotMode } = require('./snapshot-utils')
+
+const kSnapshotRecorder = Symbol('kSnapshotRecorder')
+const kSnapshotMode = Symbol('kSnapshotMode')
+const kSnapshotPath = Symbol('kSnapshotPath')
+const kSnapshotLoaded = Symbol('kSnapshotLoaded')
+const kRealAgent = Symbol('kRealAgent')
+
+// Static flag to ensure warning is only emitted once per process
+let warningEmitted = false
+
+class SnapshotAgent extends MockAgent {
+ constructor (opts = {}) {
+ // Emit experimental warning only once
+ if (!warningEmitted) {
+ process.emitWarning(
+ 'SnapshotAgent is experimental and subject to change',
+ 'ExperimentalWarning'
+ )
+ warningEmitted = true
+ }
+
+ const {
+ mode = 'record',
+ snapshotPath = null,
+ ...mockAgentOpts
+ } = opts
+
+ super(mockAgentOpts)
+
+ validateSnapshotMode(mode)
+
+ // Validate snapshotPath is provided when required
+ if ((mode === 'playback' || mode === 'update') && !snapshotPath) {
+ throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`)
+ }
+
+ this[kSnapshotMode] = mode
+ this[kSnapshotPath] = snapshotPath
+
+ this[kSnapshotRecorder] = new SnapshotRecorder({
+ snapshotPath: this[kSnapshotPath],
+ mode: this[kSnapshotMode],
+ maxSnapshots: opts.maxSnapshots,
+ autoFlush: opts.autoFlush,
+ flushInterval: opts.flushInterval,
+ matchHeaders: opts.matchHeaders,
+ ignoreHeaders: opts.ignoreHeaders,
+ excludeHeaders: opts.excludeHeaders,
+ matchBody: opts.matchBody,
+ matchQuery: opts.matchQuery,
+ caseSensitive: opts.caseSensitive,
+ shouldRecord: opts.shouldRecord,
+ shouldPlayback: opts.shouldPlayback,
+ excludeUrls: opts.excludeUrls
+ })
+ this[kSnapshotLoaded] = false
+
+ // For recording/update mode, we need a real agent to make actual requests
+ // For playback mode, we need a real agent if there are excluded URLs
+ if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' ||
+ (this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) {
+ this[kRealAgent] = new Agent(opts)
+ }
+
+ // Auto-load snapshots in playback/update mode
+ if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) {
+ this.loadSnapshots().catch(() => {
+ // Ignore load errors - file might not exist yet
+ })
+ }
+ }
+
+ dispatch (opts, handler) {
+ handler = WrapHandler.wrap(handler)
+ const mode = this[kSnapshotMode]
+
+ // Check if URL should be excluded (pass through without mocking/recording)
+ if (this[kSnapshotRecorder].isUrlExcluded(opts)) {
+ // Real agent is guaranteed by constructor when excludeUrls is configured
+ return this[kRealAgent].dispatch(opts, handler)
+ }
+
+ if (mode === 'playback' || mode === 'update') {
+ // Ensure snapshots are loaded
+ if (!this[kSnapshotLoaded]) {
+ // Need to load asynchronously, delegate to async version
+ return this.#asyncDispatch(opts, handler)
+ }
+
+ // Try to find existing snapshot (synchronous)
+ const snapshot = this[kSnapshotRecorder].findSnapshot(opts)
+
+ if (snapshot) {
+ // Use recorded response (synchronous)
+ return this.#replaySnapshot(snapshot, handler)
+ } else if (mode === 'update') {
+ // Make real request and record it (async required)
+ return this.#recordAndReplay(opts, handler)
+ } else {
+ // Playback mode but no snapshot found
+ const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`)
+ if (handler.onError) {
+ handler.onError(error)
+ return
+ }
+ throw error
+ }
+ } else if (mode === 'record') {
+ // Record mode - make real request and save response (async required)
+ return this.#recordAndReplay(opts, handler)
+ }
+ }
+
+ /**
+ * Async version of dispatch for when we need to load snapshots first
+ */
+ async #asyncDispatch (opts, handler) {
+ await this.loadSnapshots()
+ return this.dispatch(opts, handler)
+ }
+
+ /**
+ * Records a real request and replays the response
+ */
+ #recordAndReplay (opts, handler) {
+ const responseData = {
+ statusCode: null,
+ headers: {},
+ trailers: {},
+ body: []
+ }
+
+ const self = this // Capture 'this' context for use within nested handler callbacks
+
+ const recordingHandler = {
+ onRequestStart (controller, context) {
+ return handler.onRequestStart(controller, { ...context, history: this.history })
+ },
+
+ onRequestUpgrade (controller, statusCode, headers, socket) {
+ return handler.onRequestUpgrade(controller, statusCode, headers, socket)
+ },
+
+ onResponseStart (controller, statusCode, headers, statusMessage) {
+ responseData.statusCode = statusCode
+ responseData.headers = headers
+ return handler.onResponseStart(controller, statusCode, headers, statusMessage)
+ },
+
+ onResponseData (controller, chunk) {
+ responseData.body.push(chunk)
+ return handler.onResponseData(controller, chunk)
+ },
+
+ onResponseEnd (controller, trailers) {
+ responseData.trailers = trailers
+
+ // Record the interaction using captured 'self' context (fire and forget)
+ const responseBody = Buffer.concat(responseData.body)
+ self[kSnapshotRecorder].record(opts, {
+ statusCode: responseData.statusCode,
+ headers: responseData.headers,
+ body: responseBody,
+ trailers: responseData.trailers
+ })
+ .then(() => handler.onResponseEnd(controller, trailers))
+ .catch((error) => handler.onResponseError(controller, error))
+ }
+ }
+
+ // Use composed agent if available (includes interceptors), otherwise use real agent
+ const agent = this[kRealAgent]
+ return agent.dispatch(opts, recordingHandler)
+ }
+
+ /**
+ * Replays a recorded response
+ *
+ * @param {Object} snapshot - The recorded snapshot to replay.
+ * @param {Object} handler - The handler to call with the response data.
+ * @returns {void}
+ */
+ #replaySnapshot (snapshot, handler) {
+ try {
+ const { response } = snapshot
+
+ const controller = {
+ pause () { },
+ resume () { },
+ abort (reason) {
+ this.aborted = true
+ this.reason = reason
+ },
+
+ aborted: false,
+ paused: false
+ }
+
+ handler.onRequestStart(controller)
+
+ handler.onResponseStart(controller, response.statusCode, response.headers)
+
+ // Body is always stored as base64 string
+ const body = Buffer.from(response.body, 'base64')
+ handler.onResponseData(controller, body)
+
+ handler.onResponseEnd(controller, response.trailers)
+ } catch (error) {
+ handler.onError?.(error)
+ }
+ }
+
+ /**
+ * Loads snapshots from file
+ *
+ * @param {string} [filePath] - Optional file path to load snapshots from.
+ * @returns {Promise<void>} - Resolves when snapshots are loaded.
+ */
+ async loadSnapshots (filePath) {
+ await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath])
+ this[kSnapshotLoaded] = true
+
+ // In playback mode, set up MockAgent interceptors for all snapshots
+ if (this[kSnapshotMode] === 'playback') {
+ this.#setupMockInterceptors()
+ }
+ }
+
+ /**
+ * Saves snapshots to file
+ *
+ * @param {string} [filePath] - Optional file path to save snapshots to.
+ * @returns {Promise<void>} - Resolves when snapshots are saved.
+ */
+ async saveSnapshots (filePath) {
+ return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath])
+ }
+
+ /**
+ * Sets up MockAgent interceptors based on recorded snapshots.
+ *
+ * This method creates MockAgent interceptors for each recorded snapshot,
+ * allowing the SnapshotAgent to fall back to MockAgent's standard intercept
+ * mechanism in playback mode. Each interceptor is configured to persist
+ * (remain active for multiple requests) and responds with the recorded
+ * response data.
+ *
+ * Called automatically when loading snapshots in playback mode.
+ *
+ * @returns {void}
+ */
+ #setupMockInterceptors () {
+ for (const snapshot of this[kSnapshotRecorder].getSnapshots()) {
+ const { request, responses, response } = snapshot
+ const url = new URL(request.url)
+
+ const mockPool = this.get(url.origin)
+
+ // Handle both new format (responses array) and legacy format (response object)
+ const responseData = responses ? responses[0] : response
+ if (!responseData) continue
+
+ mockPool.intercept({
+ path: url.pathname + url.search,
+ method: request.method,
+ headers: request.headers,
+ body: request.body
+ }).reply(responseData.statusCode, responseData.body, {
+ headers: responseData.headers,
+ trailers: responseData.trailers
+ }).persist()
+ }
+ }
+
+ /**
+ * Gets the snapshot recorder
+ * @return {SnapshotRecorder} - The snapshot recorder instance
+ */
+ getRecorder () {
+ return this[kSnapshotRecorder]
+ }
+
+ /**
+ * Gets the current mode
+ * @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode
+ */
+ getMode () {
+ return this[kSnapshotMode]
+ }
+
+ /**
+ * Clears all snapshots
+ * @returns {void}
+ */
+ clearSnapshots () {
+ this[kSnapshotRecorder].clear()
+ }
+
+ /**
+ * Resets call counts for all snapshots (useful for test cleanup)
+ * @returns {void}
+ */
+ resetCallCounts () {
+ this[kSnapshotRecorder].resetCallCounts()
+ }
+
+ /**
+ * Deletes a specific snapshot by request options
+ * @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot
+ * @return {Promise<boolean>} - Returns true if the snapshot was deleted, false if not found
+ */
+ deleteSnapshot (requestOpts) {
+ return this[kSnapshotRecorder].deleteSnapshot(requestOpts)
+ }
+
+ /**
+ * Gets information about a specific snapshot
+ * @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found
+ */
+ getSnapshotInfo (requestOpts) {
+ return this[kSnapshotRecorder].getSnapshotInfo(requestOpts)
+ }
+
+ /**
+ * Replaces all snapshots with new data (full replacement)
+ * @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record<string, import('./snapshot-recorder').SnapshotEntry>} snapshotData - New snapshot data to replace existing snapshots
+ * @returns {void}
+ */
+ replaceSnapshots (snapshotData) {
+ this[kSnapshotRecorder].replaceSnapshots(snapshotData)
+ }
+
+ /**
+ * Closes the agent, saving snapshots and cleaning up resources.
+ *
+ * @returns {Promise<void>}
+ */
+ async close () {
+ await this[kSnapshotRecorder].close()
+ await this[kRealAgent]?.close()
+ await super.close()
+ }
+}
+
+module.exports = SnapshotAgent
diff --git a/vanilla/node_modules/undici/lib/mock/snapshot-recorder.js b/vanilla/node_modules/undici/lib/mock/snapshot-recorder.js
new file mode 100644
index 0000000..b5d07fa
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/snapshot-recorder.js
@@ -0,0 +1,588 @@
+'use strict'
+
+const { writeFile, readFile, mkdir } = require('node:fs/promises')
+const { dirname, resolve } = require('node:path')
+const { setTimeout, clearTimeout } = require('node:timers')
+const { InvalidArgumentError, UndiciError } = require('../core/errors')
+const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require('./snapshot-utils')
+
+/**
+ * @typedef {Object} SnapshotRequestOptions
+ * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
+ * @property {string} path - Request path
+ * @property {string} origin - Request origin (base URL)
+ * @property {import('./snapshot-utils').Headers|import('./snapshot-utils').UndiciHeaders} headers - Request headers
+ * @property {import('./snapshot-utils').NormalizedHeaders} _normalizedHeaders - Request headers as a lowercase object
+ * @property {string|Buffer} [body] - Request body (optional)
+ */
+
+/**
+ * @typedef {Object} SnapshotEntryRequest
+ * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
+ * @property {string} url - Full URL of the request
+ * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object
+ * @property {string|Buffer} [body] - Request body (optional)
+ */
+
+/**
+ * @typedef {Object} SnapshotEntryResponse
+ * @property {number} statusCode - HTTP status code of the response
+ * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized response headers as a lowercase object
+ * @property {string} body - Response body as a base64url encoded string
+ * @property {Object} [trailers] - Optional response trailers
+ */
+
+/**
+ * @typedef {Object} SnapshotEntry
+ * @property {SnapshotEntryRequest} request - The request object
+ * @property {Array<SnapshotEntryResponse>} responses - Array of response objects
+ * @property {number} callCount - Number of times this snapshot has been called
+ * @property {string} timestamp - ISO timestamp of when the snapshot was created
+ */
+
+/**
+ * @typedef {Object} SnapshotRecorderMatchOptions
+ * @property {Array<string>} [matchHeaders=[]] - Headers to match (empty array means match all headers)
+ * @property {Array<string>} [ignoreHeaders=[]] - Headers to ignore for matching
+ * @property {Array<string>} [excludeHeaders=[]] - Headers to exclude from matching
+ * @property {boolean} [matchBody=true] - Whether to match request body
+ * @property {boolean} [matchQuery=true] - Whether to match query properties
+ * @property {boolean} [caseSensitive=false] - Whether header matching is case-sensitive
+ */
+
+/**
+ * @typedef {Object} SnapshotRecorderOptions
+ * @property {string} [snapshotPath] - Path to save/load snapshots
+ * @property {import('./snapshot-utils').SnapshotMode} [mode='record'] - Mode: 'record' or 'playback'
+ * @property {number} [maxSnapshots=Infinity] - Maximum number of snapshots to keep
+ * @property {boolean} [autoFlush=false] - Whether to automatically flush snapshots to disk
+ * @property {number} [flushInterval=30000] - Auto-flush interval in milliseconds (default: 30 seconds)
+ * @property {Array<string|RegExp>} [excludeUrls=[]] - URLs to exclude from recording
+ * @property {function} [shouldRecord=null] - Function to filter requests for recording
+ * @property {function} [shouldPlayback=null] - Function to filter requests
+ */
+
+/**
+ * @typedef {Object} SnapshotFormattedRequest
+ * @property {string} method - HTTP method (e.g. 'GET', 'POST', etc.)
+ * @property {string} url - Full URL of the request (with query parameters if matchQuery is true)
+ * @property {import('./snapshot-utils').NormalizedHeaders} headers - Normalized headers as a lowercase object
+ * @property {string} body - Request body (optional, only if matchBody is true)
+ */
+
+/**
+ * @typedef {Object} SnapshotInfo
+ * @property {string} hash - Hash key for the snapshot
+ * @property {SnapshotEntryRequest} request - The request object
+ * @property {number} responseCount - Number of responses recorded for this request
+ * @property {number} callCount - Number of times this snapshot has been called
+ * @property {string} timestamp - ISO timestamp of when the snapshot was created
+ */
+
+/**
+ * Formats a request for consistent snapshot storage
+ * Caches normalized headers to avoid repeated processing
+ *
+ * @param {SnapshotRequestOptions} opts - Request options
+ * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached header sets for performance
+ * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers and body
+ * @returns {SnapshotFormattedRequest} - Formatted request object
+ */
+function formatRequestKey (opts, headerFilters, matchOptions = {}) {
+ const url = new URL(opts.path, opts.origin)
+
+ // Cache normalized headers if not already done
+ const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers)
+ if (!opts._normalizedHeaders) {
+ opts._normalizedHeaders = normalized
+ }
+
+ return {
+ method: opts.method || 'GET',
+ url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`,
+ headers: filterHeadersForMatching(normalized, headerFilters, matchOptions),
+ body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : ''
+ }
+}
+
+/**
+ * Filters headers based on matching configuration
+ *
+ * @param {import('./snapshot-utils').Headers} headers - Headers to filter
+ * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers
+ * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers
+ */
+function filterHeadersForMatching (headers, headerFilters, matchOptions = {}) {
+ if (!headers || typeof headers !== 'object') return {}
+
+ const {
+ caseSensitive = false
+ } = matchOptions
+
+ const filtered = {}
+ const { ignore, exclude, match } = headerFilters
+
+ for (const [key, value] of Object.entries(headers)) {
+ const headerKey = caseSensitive ? key : key.toLowerCase()
+
+ // Skip if in exclude list (for security)
+ if (exclude.has(headerKey)) continue
+
+ // Skip if in ignore list (for matching)
+ if (ignore.has(headerKey)) continue
+
+ // If matchHeaders is specified, only include those headers
+ if (match.size !== 0) {
+ if (!match.has(headerKey)) continue
+ }
+
+ filtered[headerKey] = value
+ }
+
+ return filtered
+}
+
+/**
+ * Filters headers for storage (only excludes sensitive headers)
+ *
+ * @param {import('./snapshot-utils').Headers} headers - Headers to filter
+ * @param {import('./snapshot-utils').HeaderFilters} headerFilters - Cached sets for ignore, exclude, and match headers
+ * @param {SnapshotRecorderMatchOptions} [matchOptions] - Matching options for headers
+ */
+function filterHeadersForStorage (headers, headerFilters, matchOptions = {}) {
+ if (!headers || typeof headers !== 'object') return {}
+
+ const {
+ caseSensitive = false
+ } = matchOptions
+
+ const filtered = {}
+ const { exclude: excludeSet } = headerFilters
+
+ for (const [key, value] of Object.entries(headers)) {
+ const headerKey = caseSensitive ? key : key.toLowerCase()
+
+ // Skip if in exclude list (for security)
+ if (excludeSet.has(headerKey)) continue
+
+ filtered[headerKey] = value
+ }
+
+ return filtered
+}
+
+/**
+ * Creates a hash key for request matching
+ * Properly orders headers to avoid conflicts and uses crypto hashing when available
+ *
+ * @param {SnapshotFormattedRequest} formattedRequest - Request object
+ * @returns {string} - Base64url encoded hash of the request
+ */
+function createRequestHash (formattedRequest) {
+ const parts = [
+ formattedRequest.method,
+ formattedRequest.url
+ ]
+
+ // Process headers in a deterministic way to avoid conflicts
+ if (formattedRequest.headers && typeof formattedRequest.headers === 'object') {
+ const headerKeys = Object.keys(formattedRequest.headers).sort()
+ for (const key of headerKeys) {
+ const values = Array.isArray(formattedRequest.headers[key])
+ ? formattedRequest.headers[key]
+ : [formattedRequest.headers[key]]
+
+ // Add header name
+ parts.push(key)
+
+ // Add all values for this header, sorted for consistency
+ for (const value of values.sort()) {
+ parts.push(String(value))
+ }
+ }
+ }
+
+ // Add body
+ parts.push(formattedRequest.body)
+
+ const content = parts.join('|')
+
+ return hashId(content)
+}
+
+class SnapshotRecorder {
+ /** @type {NodeJS.Timeout | null} */
+ #flushTimeout
+
+ /** @type {import('./snapshot-utils').IsUrlExcluded} */
+ #isUrlExcluded
+
+ /** @type {Map<string, SnapshotEntry>} */
+ #snapshots = new Map()
+
+ /** @type {string|undefined} */
+ #snapshotPath
+
+ /** @type {number} */
+ #maxSnapshots = Infinity
+
+ /** @type {boolean} */
+ #autoFlush = false
+
+ /** @type {import('./snapshot-utils').HeaderFilters} */
+ #headerFilters
+
+ /**
+ * Creates a new SnapshotRecorder instance
+ * @param {SnapshotRecorderOptions&SnapshotRecorderMatchOptions} [options={}] - Configuration options for the recorder
+ */
+ constructor (options = {}) {
+ this.#snapshotPath = options.snapshotPath
+ this.#maxSnapshots = options.maxSnapshots || Infinity
+ this.#autoFlush = options.autoFlush || false
+ this.flushInterval = options.flushInterval || 30000 // 30 seconds default
+ this._flushTimer = null
+
+ // Matching configuration
+ /** @type {Required<SnapshotRecorderMatchOptions>} */
+ this.matchOptions = {
+ matchHeaders: options.matchHeaders || [], // empty means match all headers
+ ignoreHeaders: options.ignoreHeaders || [],
+ excludeHeaders: options.excludeHeaders || [],
+ matchBody: options.matchBody !== false, // default: true
+ matchQuery: options.matchQuery !== false, // default: true
+ caseSensitive: options.caseSensitive || false
+ }
+
+ // Cache processed header sets to avoid recreating them on every request
+ this.#headerFilters = createHeaderFilters(this.matchOptions)
+
+ // Request filtering callbacks
+ this.shouldRecord = options.shouldRecord || (() => true) // function(requestOpts) -> boolean
+ this.shouldPlayback = options.shouldPlayback || (() => true) // function(requestOpts) -> boolean
+
+ // URL pattern filtering
+ this.#isUrlExcluded = isUrlExcludedFactory(options.excludeUrls) // Array of regex patterns or strings
+
+ // Start auto-flush timer if enabled
+ if (this.#autoFlush && this.#snapshotPath) {
+ this.#startAutoFlush()
+ }
+ }
+
+ /**
+ * Records a request-response interaction
+ * @param {SnapshotRequestOptions} requestOpts - Request options
+ * @param {SnapshotEntryResponse} response - Response data to record
+ * @return {Promise<void>} - Resolves when the recording is complete
+ */
+ async record (requestOpts, response) {
+ // Check if recording should be filtered out
+ if (!this.shouldRecord(requestOpts)) {
+ return // Skip recording
+ }
+
+ // Check URL exclusion patterns
+ if (this.isUrlExcluded(requestOpts)) {
+ return // Skip recording
+ }
+
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
+ const hash = createRequestHash(request)
+
+ // Extract response data - always store body as base64
+ const normalizedHeaders = normalizeHeaders(response.headers)
+
+ /** @type {SnapshotEntryResponse} */
+ const responseData = {
+ statusCode: response.statusCode,
+ headers: filterHeadersForStorage(normalizedHeaders, this.#headerFilters, this.matchOptions),
+ body: Buffer.isBuffer(response.body)
+ ? response.body.toString('base64')
+ : Buffer.from(String(response.body || '')).toString('base64'),
+ trailers: response.trailers
+ }
+
+ // Remove oldest snapshot if we exceed maxSnapshots limit
+ if (this.#snapshots.size >= this.#maxSnapshots && !this.#snapshots.has(hash)) {
+ const oldestKey = this.#snapshots.keys().next().value
+ this.#snapshots.delete(oldestKey)
+ }
+
+ // Support sequential responses - if snapshot exists, add to responses array
+ const existingSnapshot = this.#snapshots.get(hash)
+ if (existingSnapshot && existingSnapshot.responses) {
+ existingSnapshot.responses.push(responseData)
+ existingSnapshot.timestamp = new Date().toISOString()
+ } else {
+ this.#snapshots.set(hash, {
+ request,
+ responses: [responseData], // Always store as array for consistency
+ callCount: 0,
+ timestamp: new Date().toISOString()
+ })
+ }
+
+ // Auto-flush if enabled
+ if (this.#autoFlush && this.#snapshotPath) {
+ this.#scheduleFlush()
+ }
+ }
+
+ /**
+ * Checks if a URL should be excluded from recording/playback
+ * @param {SnapshotRequestOptions} requestOpts - Request options to check
+ * @returns {boolean} - True if URL is excluded
+ */
+ isUrlExcluded (requestOpts) {
+ const url = new URL(requestOpts.path, requestOpts.origin).toString()
+ return this.#isUrlExcluded(url)
+ }
+
+ /**
+ * Finds a matching snapshot for the given request
+ * Returns the appropriate response based on call count for sequential responses
+ *
+ * @param {SnapshotRequestOptions} requestOpts - Request options to match
+ * @returns {SnapshotEntry&Record<'response', SnapshotEntryResponse>|undefined} - Matching snapshot response or undefined if not found
+ */
+ findSnapshot (requestOpts) {
+ // Check if playback should be filtered out
+ if (!this.shouldPlayback(requestOpts)) {
+ return undefined // Skip playback
+ }
+
+ // Check URL exclusion patterns
+ if (this.isUrlExcluded(requestOpts)) {
+ return undefined // Skip playback
+ }
+
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
+ const hash = createRequestHash(request)
+ const snapshot = this.#snapshots.get(hash)
+
+ if (!snapshot) return undefined
+
+ // Handle sequential responses
+ const currentCallCount = snapshot.callCount || 0
+ const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
+ snapshot.callCount = currentCallCount + 1
+
+ return {
+ ...snapshot,
+ response: snapshot.responses[responseIndex]
+ }
+ }
+
+ /**
+ * Loads snapshots from file
+ * @param {string} [filePath] - Optional file path to load snapshots from
+ * @return {Promise<void>} - Resolves when snapshots are loaded
+ */
+ async loadSnapshots (filePath) {
+ const path = filePath || this.#snapshotPath
+ if (!path) {
+ throw new InvalidArgumentError('Snapshot path is required')
+ }
+
+ try {
+ const data = await readFile(resolve(path), 'utf8')
+ const parsed = JSON.parse(data)
+
+ // Convert array format back to Map
+ if (Array.isArray(parsed)) {
+ this.#snapshots.clear()
+ for (const { hash, snapshot } of parsed) {
+ this.#snapshots.set(hash, snapshot)
+ }
+ } else {
+ // Legacy object format
+ this.#snapshots = new Map(Object.entries(parsed))
+ }
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ // File doesn't exist yet - that's ok for recording mode
+ this.#snapshots.clear()
+ } else {
+ throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error })
+ }
+ }
+ }
+
+ /**
+ * Saves snapshots to file
+ *
+ * @param {string} [filePath] - Optional file path to save snapshots
+ * @returns {Promise<void>} - Resolves when snapshots are saved
+ */
+ async saveSnapshots (filePath) {
+ const path = filePath || this.#snapshotPath
+ if (!path) {
+ throw new InvalidArgumentError('Snapshot path is required')
+ }
+
+ const resolvedPath = resolve(path)
+
+ // Ensure directory exists
+ await mkdir(dirname(resolvedPath), { recursive: true })
+
+ // Convert Map to serializable format
+ const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
+ hash,
+ snapshot
+ }))
+
+ await writeFile(resolvedPath, JSON.stringify(data, null, 2), { flush: true })
+ }
+
+ /**
+ * Clears all recorded snapshots
+ * @returns {void}
+ */
+ clear () {
+ this.#snapshots.clear()
+ }
+
+ /**
+ * Gets all recorded snapshots
+ * @return {Array<SnapshotEntry>} - Array of all recorded snapshots
+ */
+ getSnapshots () {
+ return Array.from(this.#snapshots.values())
+ }
+
+ /**
+ * Gets snapshot count
+ * @return {number} - Number of recorded snapshots
+ */
+ size () {
+ return this.#snapshots.size
+ }
+
+ /**
+ * Resets call counts for all snapshots (useful for test cleanup)
+ * @returns {void}
+ */
+ resetCallCounts () {
+ for (const snapshot of this.#snapshots.values()) {
+ snapshot.callCount = 0
+ }
+ }
+
+ /**
+ * Deletes a specific snapshot by request options
+ * @param {SnapshotRequestOptions} requestOpts - Request options to match
+ * @returns {boolean} - True if snapshot was deleted, false if not found
+ */
+ deleteSnapshot (requestOpts) {
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
+ const hash = createRequestHash(request)
+ return this.#snapshots.delete(hash)
+ }
+
+ /**
+ * Gets information about a specific snapshot
+ * @param {SnapshotRequestOptions} requestOpts - Request options to match
+ * @returns {SnapshotInfo|null} - Snapshot information or null if not found
+ */
+ getSnapshotInfo (requestOpts) {
+ const request = formatRequestKey(requestOpts, this.#headerFilters, this.matchOptions)
+ const hash = createRequestHash(request)
+ const snapshot = this.#snapshots.get(hash)
+
+ if (!snapshot) return null
+
+ return {
+ hash,
+ request: snapshot.request,
+ responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0), // .response for legacy snapshots
+ callCount: snapshot.callCount || 0,
+ timestamp: snapshot.timestamp
+ }
+ }
+
+ /**
+ * Replaces all snapshots with new data (full replacement)
+ * @param {Array<{hash: string; snapshot: SnapshotEntry}>|Record<string, SnapshotEntry>} snapshotData - New snapshot data to replace existing ones
+ * @returns {void}
+ */
+ replaceSnapshots (snapshotData) {
+ this.#snapshots.clear()
+
+ if (Array.isArray(snapshotData)) {
+ for (const { hash, snapshot } of snapshotData) {
+ this.#snapshots.set(hash, snapshot)
+ }
+ } else if (snapshotData && typeof snapshotData === 'object') {
+ // Legacy object format
+ this.#snapshots = new Map(Object.entries(snapshotData))
+ }
+ }
+
+ /**
+ * Starts the auto-flush timer
+ * @returns {void}
+ */
+ #startAutoFlush () {
+ return this.#scheduleFlush()
+ }
+
+ /**
+ * Stops the auto-flush timer
+ * @returns {void}
+ */
+ #stopAutoFlush () {
+ if (this.#flushTimeout) {
+ clearTimeout(this.#flushTimeout)
+ // Ensure any pending flush is completed
+ this.saveSnapshots().catch(() => {
+ // Ignore flush errors
+ })
+ this.#flushTimeout = null
+ }
+ }
+
+ /**
+ * Schedules a flush (debounced to avoid excessive writes)
+ */
+ #scheduleFlush () {
+ this.#flushTimeout = setTimeout(() => {
+ this.saveSnapshots().catch(() => {
+ // Ignore flush errors
+ })
+ if (this.#autoFlush) {
+ this.#flushTimeout?.refresh()
+ } else {
+ this.#flushTimeout = null
+ }
+ }, 1000) // 1 second debounce
+ }
+
+ /**
+ * Cleanup method to stop timers
+ * @returns {void}
+ */
+ destroy () {
+ this.#stopAutoFlush()
+ if (this.#flushTimeout) {
+ clearTimeout(this.#flushTimeout)
+ this.#flushTimeout = null
+ }
+ }
+
+ /**
+ * Async close method that saves all recordings and performs cleanup
+ * @returns {Promise<void>}
+ */
+ async close () {
+ // Save any pending recordings if we have a snapshot path
+ if (this.#snapshotPath && this.#snapshots.size !== 0) {
+ await this.saveSnapshots()
+ }
+
+ // Perform cleanup
+ this.destroy()
+ }
+}
+
+module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, createHeaderFilters }
diff --git a/vanilla/node_modules/undici/lib/mock/snapshot-utils.js b/vanilla/node_modules/undici/lib/mock/snapshot-utils.js
new file mode 100644
index 0000000..a14b69c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/mock/snapshot-utils.js
@@ -0,0 +1,158 @@
+'use strict'
+
+const { InvalidArgumentError } = require('../core/errors')
+const { runtimeFeatures } = require('../util/runtime-features.js')
+
+/**
+ * @typedef {Object} HeaderFilters
+ * @property {Set<string>} ignore - Set of headers to ignore for matching
+ * @property {Set<string>} exclude - Set of headers to exclude from matching
+ * @property {Set<string>} match - Set of headers to match (empty means match
+ */
+
+/**
+ * Creates cached header sets for performance
+ *
+ * @param {import('./snapshot-recorder').SnapshotRecorderMatchOptions} matchOptions - Matching options for headers
+ * @returns {HeaderFilters} - Cached sets for ignore, exclude, and match headers
+ */
+function createHeaderFilters (matchOptions = {}) {
+ const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = [], caseSensitive = false } = matchOptions
+
+ return {
+ ignore: new Set(ignoreHeaders.map(header => caseSensitive ? header : header.toLowerCase())),
+ exclude: new Set(excludeHeaders.map(header => caseSensitive ? header : header.toLowerCase())),
+ match: new Set(matchHeaders.map(header => caseSensitive ? header : header.toLowerCase()))
+ }
+}
+
+const crypto = runtimeFeatures.has('crypto')
+ ? require('node:crypto')
+ : null
+
+/**
+ * @callback HashIdFunction
+ * @param {string} value - The value to hash
+ * @returns {string} - The base64url encoded hash of the value
+ */
+
+/**
+ * Generates a hash for a given value
+ * @type {HashIdFunction}
+ */
+const hashId = crypto?.hash
+ ? (value) => crypto.hash('sha256', value, 'base64url')
+ : (value) => Buffer.from(value).toString('base64url')
+
+/**
+ * @typedef {(url: string) => boolean} IsUrlExcluded Checks if a URL matches any of the exclude patterns
+ */
+
+/** @typedef {{[key: Lowercase<string>]: string}} NormalizedHeaders */
+/** @typedef {Array<string>} UndiciHeaders */
+/** @typedef {Record<string, string|string[]>} Headers */
+
+/**
+ * @param {*} headers
+ * @returns {headers is UndiciHeaders}
+ */
+function isUndiciHeaders (headers) {
+ return Array.isArray(headers) && (headers.length & 1) === 0
+}
+
+/**
+ * Factory function to create a URL exclusion checker
+ * @param {Array<string| RegExp>} [excludePatterns=[]] - Array of patterns to exclude
+ * @returns {IsUrlExcluded} - A function that checks if a URL matches any of the exclude patterns
+ */
+function isUrlExcludedFactory (excludePatterns = []) {
+ if (excludePatterns.length === 0) {
+ return () => false
+ }
+
+ return function isUrlExcluded (url) {
+ let urlLowerCased
+
+ for (const pattern of excludePatterns) {
+ if (typeof pattern === 'string') {
+ if (!urlLowerCased) {
+ // Convert URL to lowercase only once
+ urlLowerCased = url.toLowerCase()
+ }
+ // Simple string match (case-insensitive)
+ if (urlLowerCased.includes(pattern.toLowerCase())) {
+ return true
+ }
+ } else if (pattern instanceof RegExp) {
+ // Regex pattern match
+ if (pattern.test(url)) {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+}
+
+/**
+ * Normalizes headers for consistent comparison
+ *
+ * @param {Object|UndiciHeaders} headers - Headers to normalize
+ * @returns {NormalizedHeaders} - Normalized headers as a lowercase object
+ */
+function normalizeHeaders (headers) {
+ /** @type {NormalizedHeaders} */
+ const normalizedHeaders = {}
+
+ if (!headers) return normalizedHeaders
+
+ // Handle array format (undici internal format: [name, value, name, value, ...])
+ if (isUndiciHeaders(headers)) {
+ for (let i = 0; i < headers.length; i += 2) {
+ const key = headers[i]
+ const value = headers[i + 1]
+ if (key && value !== undefined) {
+ // Convert Buffers to strings if needed
+ const keyStr = Buffer.isBuffer(key) ? key.toString() : key
+ const valueStr = Buffer.isBuffer(value) ? value.toString() : value
+ normalizedHeaders[keyStr.toLowerCase()] = valueStr
+ }
+ }
+ return normalizedHeaders
+ }
+
+ // Handle object format
+ if (headers && typeof headers === 'object') {
+ for (const [key, value] of Object.entries(headers)) {
+ if (key && typeof key === 'string') {
+ normalizedHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
+ }
+ }
+ }
+
+ return normalizedHeaders
+}
+
+const validSnapshotModes = /** @type {const} */ (['record', 'playback', 'update'])
+
+/** @typedef {typeof validSnapshotModes[number]} SnapshotMode */
+
+/**
+ * @param {*} mode - The snapshot mode to validate
+ * @returns {asserts mode is SnapshotMode}
+ */
+function validateSnapshotMode (mode) {
+ if (!validSnapshotModes.includes(mode)) {
+ throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validSnapshotModes.join(', ')}`)
+ }
+}
+
+module.exports = {
+ createHeaderFilters,
+ hashId,
+ isUndiciHeaders,
+ normalizeHeaders,
+ isUrlExcludedFactory,
+ validateSnapshotMode
+}
diff --git a/vanilla/node_modules/undici/lib/util/cache.js b/vanilla/node_modules/undici/lib/util/cache.js
new file mode 100644
index 0000000..b7627d0
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/util/cache.js
@@ -0,0 +1,405 @@
+'use strict'
+
+const {
+ safeHTTPMethods,
+ pathHasQueryOrFragment
+} = require('../core/util')
+
+const { serializePathWithQuery } = require('../core/util')
+
+/**
+ * @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(opts.path)) {
+ fullPath = serializePathWithQuery(fullPath, opts.query)
+ }
+
+ return {
+ origin: opts.origin.toString(),
+ method: opts.method,
+ path: fullPath,
+ headers: opts.headers
+ }
+}
+
+/**
+ * @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[Symbol.iterator] === 'function') {
+ 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')
+ }
+ headers[key.toLowerCase()] = val
+ }
+ } else if (typeof opts.headers === 'object') {
+ headers = {}
+
+ for (const key of Object.keys(opts.headers)) {
+ headers[key.toLowerCase()] = 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 = {}
+
+ let directives
+ if (Array.isArray(header)) {
+ directives = []
+
+ for (const directive of header) {
+ directives.push(...directive.split(','))
+ }
+ } else {
+ directives = header.split(',')
+ }
+
+ for (let i = 0; i < directives.length; i++) {
+ const directive = directives[i].toLowerCase()
+ const keyValueDelimiter = directive.indexOf('=')
+
+ let key
+ let value
+ if (keyValueDelimiter !== -1) {
+ key = directive.substring(0, keyValueDelimiter).trimStart()
+ value = directive.substring(keyValueDelimiter + 1)
+ } else {
+ key = directive.trim()
+ }
+
+ switch (key) {
+ case 'min-fresh':
+ case 'max-stale':
+ case 'max-age':
+ case 's-maxage':
+ case 'stale-while-revalidate':
+ case 'stale-if-error': {
+ if (value === undefined || value[0] === ' ') {
+ continue
+ }
+
+ if (
+ value.length >= 2 &&
+ value[0] === '"' &&
+ value[value.length - 1] === '"'
+ ) {
+ value = value.substring(1, value.length - 1)
+ }
+
+ const parsedValue = parseInt(value, 10)
+ // eslint-disable-next-line no-self-compare
+ if (parsedValue !== parsedValue) {
+ continue
+ }
+
+ if (key === 'max-age' && key in output && output[key] >= parsedValue) {
+ continue
+ }
+
+ output[key] = parsedValue
+
+ break
+ }
+ case 'private':
+ case 'no-cache': {
+ 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 ends in a
+ // quote. We can then just splice all of the parts in between the
+ // starting quote and the ending quote out of the directives array
+ // 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"`.
+
+ // Add the first header on and cut off the leading quote
+ const headers = [value.substring(1)]
+
+ let foundEndingQuote = value[value.length - 1] === '"'
+ if (!foundEndingQuote) {
+ // Something like `no-cache="some-header, another-header"`
+ // This can still be something invalid, e.g. `no-cache="some-header, ...`
+ for (let j = i + 1; j < directives.length; j++) {
+ const nextPart = directives[j]
+ const nextPartLength = nextPart.length
+
+ headers.push(nextPart.trim())
+
+ if (nextPartLength !== 0 && nextPart[nextPartLength - 1] === '"') {
+ foundEndingQuote = true
+ break
+ }
+ }
+ }
+
+ if (foundEndingQuote) {
+ let lastHeader = headers[headers.length - 1]
+ if (lastHeader[lastHeader.length - 1] === '"') {
+ lastHeader = lastHeader.substring(0, lastHeader.length - 1)
+ headers[headers.length - 1] = lastHeader
+ }
+
+ if (key in output) {
+ output[key] = output[key].concat(headers)
+ } else {
+ output[key] = headers
+ }
+ }
+ } else {
+ // Something like `no-cache="some-header"`
+ if (key in output) {
+ output[key] = output[key].concat(value)
+ } else {
+ output[key] = [value]
+ }
+ }
+
+ break
+ }
+ }
+ // eslint-disable-next-line no-fallthrough
+ case 'public':
+ case 'no-store':
+ case 'must-revalidate':
+ case 'proxy-revalidate':
+ case 'immutable':
+ case 'no-transform':
+ case 'must-understand':
+ case 'only-if-cached':
+ if (value) {
+ // These are qualified (something like `public=...`) when they aren't
+ // allowed to be, skip
+ continue
+ }
+
+ 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
+ * @param {Record<string, string | string[]>} headers Request headers
+ * @returns {Record<string, string | string[]>}
+ */
+function parseVaryHeader (varyHeader, headers) {
+ if (typeof varyHeader === 'string' && varyHeader.includes('*')) {
+ return headers
+ }
+
+ const output = /** @type {Record<string, string | string[] | null>} */ ({})
+
+ const varyingHeaders = typeof varyHeader === 'string'
+ ? varyHeader.split(',')
+ : varyHeader
+
+ for (const header of varyingHeaders) {
+ const trimmedHeader = header.trim().toLowerCase()
+
+ output[trimmedHeader] = headers[trimmedHeader] ?? null
+ }
+
+ return output
+}
+
+/**
+ * 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 (!safeHTTPMethods.includes(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) {
+ // Create a deterministic string key from the cache key
+ // Include origin, method, path, and sorted headers
+ let key = `${cacheKey.origin}:${cacheKey.method}:${cacheKey.path}`
+
+ if (cacheKey.headers) {
+ const sortedHeaders = Object.keys(cacheKey.headers).sort()
+ for (const header of sortedHeaders) {
+ // Skip excluded headers
+ if (excludeHeaders?.has(header.toLowerCase())) {
+ continue
+ }
+ const value = cacheKey.headers[header]
+ key += `:${header}=${Array.isArray(value) ? value.join(',') : value}`
+ }
+ }
+
+ return key
+}
+
+module.exports = {
+ makeCacheKey,
+ normalizeHeaders,
+ assertCacheKey,
+ assertCacheValue,
+ parseCacheControlHeader,
+ parseVaryHeader,
+ isEtagUsable,
+ assertCacheMethods,
+ assertCacheStore,
+ makeDeduplicationKey
+}
diff --git a/vanilla/node_modules/undici/lib/util/date.js b/vanilla/node_modules/undici/lib/util/date.js
new file mode 100644
index 0000000..99dd150
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/util/date.js
@@ -0,0 +1,653 @@
+'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)
+ }
+}
+
+/**
+ * @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
+ }
+
+ const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
+ return result.getUTCDay() === weekday ? result : undefined
+}
+
+/**
+ * @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)
+
+ const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
+ return result.getUTCDay() === weekday ? result : undefined
+}
+
+/**
+ * @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
+ }
+
+ const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
+ return result.getUTCDay() === weekday ? result : undefined
+}
+
+module.exports = {
+ parseHttpDate
+}
diff --git a/vanilla/node_modules/undici/lib/util/promise.js b/vanilla/node_modules/undici/lib/util/promise.js
new file mode 100644
index 0000000..048f86e
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/util/promise.js
@@ -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
+}
diff --git a/vanilla/node_modules/undici/lib/util/runtime-features.js b/vanilla/node_modules/undici/lib/util/runtime-features.js
new file mode 100644
index 0000000..3e62dc0
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/util/runtime-features.js
@@ -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
diff --git a/vanilla/node_modules/undici/lib/util/stats.js b/vanilla/node_modules/undici/lib/util/stats.js
new file mode 100644
index 0000000..a13132e
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/util/stats.js
@@ -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 }
diff --git a/vanilla/node_modules/undici/lib/util/timers.js b/vanilla/node_modules/undici/lib/util/timers.js
new file mode 100644
index 0000000..14984d4
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/util/timers.js
@@ -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
+}
diff --git a/vanilla/node_modules/undici/lib/web/cache/cache.js b/vanilla/node_modules/undici/lib/web/cache/cache.js
new file mode 100644
index 0000000..10decbe
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/cache/cache.js
@@ -0,0 +1,864 @@
+'use strict'
+
+const assert = require('node:assert')
+
+const { kConstruct } = require('../../core/symbols')
+const { urlEquals, getFieldValues } = require('./util')
+const { kEnumerableProperty, isDisturbed } = require('../../core/util')
+const { webidl } = require('../webidl')
+const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response')
+const { Request, fromInnerRequest, getRequestState } = require('../fetch/request')
+const { fetching } = require('../fetch/index')
+const { urlIsHttpHttpsScheme, readAllBytes } = require('../fetch/util')
+const { createDeferredPromise } = require('../../util/promise')
+
+/**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation
+ * @typedef {Object} CacheBatchOperation
+ * @property {'delete' | 'put'} type
+ * @property {any} request
+ * @property {any} response
+ * @property {import('../../../types/cache').CacheQueryOptions} options
+ */
+
+/**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list
+ * @typedef {[any, any][]} requestResponseList
+ */
+
+class Cache {
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list
+ * @type {requestResponseList}
+ */
+ #relevantRequestResponseList
+
+ constructor () {
+ if (arguments[0] !== kConstruct) {
+ webidl.illegalConstructor()
+ }
+
+ webidl.util.markAsUncloneable(this)
+ this.#relevantRequestResponseList = arguments[1]
+ }
+
+ async match (request, options = {}) {
+ webidl.brandCheck(this, Cache)
+
+ const prefix = 'Cache.match'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
+
+ const p = this.#internalMatchAll(request, options, 1)
+
+ if (p.length === 0) {
+ return
+ }
+
+ return p[0]
+ }
+
+ async matchAll (request = undefined, options = {}) {
+ webidl.brandCheck(this, Cache)
+
+ const prefix = 'Cache.matchAll'
+ if (request !== undefined) request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
+
+ return this.#internalMatchAll(request, options)
+ }
+
+ async add (request) {
+ webidl.brandCheck(this, Cache)
+
+ const prefix = 'Cache.add'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ request = webidl.converters.RequestInfo(request)
+
+ // 1.
+ const requests = [request]
+
+ // 2.
+ const responseArrayPromise = this.addAll(requests)
+
+ // 3.
+ return await responseArrayPromise
+ }
+
+ async addAll (requests) {
+ webidl.brandCheck(this, Cache)
+
+ const prefix = 'Cache.addAll'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ // 1.
+ const responsePromises = []
+
+ // 2.
+ const requestList = []
+
+ // 3.
+ for (let request of requests) {
+ if (request === undefined) {
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: 'Argument 1',
+ types: ['undefined is not allowed']
+ })
+ }
+
+ request = webidl.converters.RequestInfo(request)
+
+ if (typeof request === 'string') {
+ continue
+ }
+
+ // 3.1
+ const r = getRequestState(request)
+
+ // 3.2
+ if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: 'Expected http/s scheme when method is not GET.'
+ })
+ }
+ }
+
+ // 4.
+ /** @type {ReturnType<typeof fetching>[]} */
+ const fetchControllers = []
+
+ // 5.
+ for (const request of requests) {
+ // 5.1
+ const r = getRequestState(new Request(request))
+
+ // 5.2
+ if (!urlIsHttpHttpsScheme(r.url)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: 'Expected http/s scheme.'
+ })
+ }
+
+ // 5.4
+ r.initiator = 'fetch'
+ r.destination = 'subresource'
+
+ // 5.5
+ requestList.push(r)
+
+ // 5.6
+ const responsePromise = createDeferredPromise()
+
+ // 5.7
+ fetchControllers.push(fetching({
+ request: r,
+ processResponse (response) {
+ // 1.
+ if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) {
+ responsePromise.reject(webidl.errors.exception({
+ header: 'Cache.addAll',
+ message: 'Received an invalid status code or the request failed.'
+ }))
+ } else if (response.headersList.contains('vary')) { // 2.
+ // 2.1
+ const fieldValues = getFieldValues(response.headersList.get('vary'))
+
+ // 2.2
+ for (const fieldValue of fieldValues) {
+ // 2.2.1
+ if (fieldValue === '*') {
+ responsePromise.reject(webidl.errors.exception({
+ header: 'Cache.addAll',
+ message: 'invalid vary field value'
+ }))
+
+ for (const controller of fetchControllers) {
+ controller.abort()
+ }
+
+ return
+ }
+ }
+ }
+ },
+ processResponseEndOfBody (response) {
+ // 1.
+ if (response.aborted) {
+ responsePromise.reject(new DOMException('aborted', 'AbortError'))
+ return
+ }
+
+ // 2.
+ responsePromise.resolve(response)
+ }
+ }))
+
+ // 5.8
+ responsePromises.push(responsePromise.promise)
+ }
+
+ // 6.
+ const p = Promise.all(responsePromises)
+
+ // 7.
+ const responses = await p
+
+ // 7.1
+ const operations = []
+
+ // 7.2
+ let index = 0
+
+ // 7.3
+ for (const response of responses) {
+ // 7.3.1
+ /** @type {CacheBatchOperation} */
+ const operation = {
+ type: 'put', // 7.3.2
+ request: requestList[index], // 7.3.3
+ response // 7.3.4
+ }
+
+ operations.push(operation) // 7.3.5
+
+ index++ // 7.3.6
+ }
+
+ // 7.5
+ const cacheJobPromise = createDeferredPromise()
+
+ // 7.6.1
+ let errorData = null
+
+ // 7.6.2
+ try {
+ this.#batchCacheOperations(operations)
+ } catch (e) {
+ errorData = e
+ }
+
+ // 7.6.3
+ queueMicrotask(() => {
+ // 7.6.3.1
+ if (errorData === null) {
+ cacheJobPromise.resolve(undefined)
+ } else {
+ // 7.6.3.2
+ cacheJobPromise.reject(errorData)
+ }
+ })
+
+ // 7.7
+ return cacheJobPromise.promise
+ }
+
+ async put (request, response) {
+ webidl.brandCheck(this, Cache)
+
+ const prefix = 'Cache.put'
+ webidl.argumentLengthCheck(arguments, 2, prefix)
+
+ request = webidl.converters.RequestInfo(request)
+ response = webidl.converters.Response(response, prefix, 'response')
+
+ // 1.
+ let innerRequest = null
+
+ // 2.
+ if (webidl.is.Request(request)) {
+ innerRequest = getRequestState(request)
+ } else { // 3.
+ innerRequest = getRequestState(new Request(request))
+ }
+
+ // 4.
+ if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: 'Expected an http/s scheme when method is not GET'
+ })
+ }
+
+ // 5.
+ const innerResponse = getResponseState(response)
+
+ // 6.
+ if (innerResponse.status === 206) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: 'Got 206 status'
+ })
+ }
+
+ // 7.
+ if (innerResponse.headersList.contains('vary')) {
+ // 7.1.
+ const fieldValues = getFieldValues(innerResponse.headersList.get('vary'))
+
+ // 7.2.
+ for (const fieldValue of fieldValues) {
+ // 7.2.1
+ if (fieldValue === '*') {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: 'Got * vary field value'
+ })
+ }
+ }
+ }
+
+ // 8.
+ if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: 'Response body is locked or disturbed'
+ })
+ }
+
+ // 9.
+ const clonedResponse = cloneResponse(innerResponse)
+
+ // 10.
+ const bodyReadPromise = createDeferredPromise()
+
+ // 11.
+ if (innerResponse.body != null) {
+ // 11.1
+ const stream = innerResponse.body.stream
+
+ // 11.2
+ const reader = stream.getReader()
+
+ // 11.3
+ readAllBytes(reader, bodyReadPromise.resolve, bodyReadPromise.reject)
+ } else {
+ bodyReadPromise.resolve(undefined)
+ }
+
+ // 12.
+ /** @type {CacheBatchOperation[]} */
+ const operations = []
+
+ // 13.
+ /** @type {CacheBatchOperation} */
+ const operation = {
+ type: 'put', // 14.
+ request: innerRequest, // 15.
+ response: clonedResponse // 16.
+ }
+
+ // 17.
+ operations.push(operation)
+
+ // 19.
+ const bytes = await bodyReadPromise.promise
+
+ if (clonedResponse.body != null) {
+ clonedResponse.body.source = bytes
+ }
+
+ // 19.1
+ const cacheJobPromise = createDeferredPromise()
+
+ // 19.2.1
+ let errorData = null
+
+ // 19.2.2
+ try {
+ this.#batchCacheOperations(operations)
+ } catch (e) {
+ errorData = e
+ }
+
+ // 19.2.3
+ queueMicrotask(() => {
+ // 19.2.3.1
+ if (errorData === null) {
+ cacheJobPromise.resolve()
+ } else { // 19.2.3.2
+ cacheJobPromise.reject(errorData)
+ }
+ })
+
+ return cacheJobPromise.promise
+ }
+
+ async delete (request, options = {}) {
+ webidl.brandCheck(this, Cache)
+
+ const prefix = 'Cache.delete'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
+
+ /**
+ * @type {Request}
+ */
+ let r = null
+
+ if (webidl.is.Request(request)) {
+ r = getRequestState(request)
+
+ if (r.method !== 'GET' && !options.ignoreMethod) {
+ return false
+ }
+ } else {
+ assert(typeof request === 'string')
+
+ r = getRequestState(new Request(request))
+ }
+
+ /** @type {CacheBatchOperation[]} */
+ const operations = []
+
+ /** @type {CacheBatchOperation} */
+ const operation = {
+ type: 'delete',
+ request: r,
+ options
+ }
+
+ operations.push(operation)
+
+ const cacheJobPromise = createDeferredPromise()
+
+ let errorData = null
+ let requestResponses
+
+ try {
+ requestResponses = this.#batchCacheOperations(operations)
+ } catch (e) {
+ errorData = e
+ }
+
+ queueMicrotask(() => {
+ if (errorData === null) {
+ cacheJobPromise.resolve(!!requestResponses?.length)
+ } else {
+ cacheJobPromise.reject(errorData)
+ }
+ })
+
+ return cacheJobPromise.promise
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys
+ * @param {any} request
+ * @param {import('../../../types/cache').CacheQueryOptions} options
+ * @returns {Promise<readonly Request[]>}
+ */
+ async keys (request = undefined, options = {}) {
+ webidl.brandCheck(this, Cache)
+
+ const prefix = 'Cache.keys'
+
+ if (request !== undefined) request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.CacheQueryOptions(options, prefix, 'options')
+
+ // 1.
+ let r = null
+
+ // 2.
+ if (request !== undefined) {
+ // 2.1
+ if (webidl.is.Request(request)) {
+ // 2.1.1
+ r = getRequestState(request)
+
+ // 2.1.2
+ if (r.method !== 'GET' && !options.ignoreMethod) {
+ return []
+ }
+ } else if (typeof request === 'string') { // 2.2
+ r = getRequestState(new Request(request))
+ }
+ }
+
+ // 4.
+ const promise = createDeferredPromise()
+
+ // 5.
+ // 5.1
+ const requests = []
+
+ // 5.2
+ if (request === undefined) {
+ // 5.2.1
+ for (const requestResponse of this.#relevantRequestResponseList) {
+ // 5.2.1.1
+ requests.push(requestResponse[0])
+ }
+ } else { // 5.3
+ // 5.3.1
+ const requestResponses = this.#queryCache(r, options)
+
+ // 5.3.2
+ for (const requestResponse of requestResponses) {
+ // 5.3.2.1
+ requests.push(requestResponse[0])
+ }
+ }
+
+ // 5.4
+ queueMicrotask(() => {
+ // 5.4.1
+ const requestList = []
+
+ // 5.4.2
+ for (const request of requests) {
+ const requestObject = fromInnerRequest(
+ request,
+ undefined,
+ new AbortController().signal,
+ 'immutable'
+ )
+ // 5.4.2.1
+ requestList.push(requestObject)
+ }
+
+ // 5.4.3
+ promise.resolve(Object.freeze(requestList))
+ })
+
+ return promise.promise
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm
+ * @param {CacheBatchOperation[]} operations
+ * @returns {requestResponseList}
+ */
+ #batchCacheOperations (operations) {
+ // 1.
+ const cache = this.#relevantRequestResponseList
+
+ // 2.
+ const backupCache = [...cache]
+
+ // 3.
+ const addedItems = []
+
+ // 4.1
+ const resultList = []
+
+ try {
+ // 4.2
+ for (const operation of operations) {
+ // 4.2.1
+ if (operation.type !== 'delete' && operation.type !== 'put') {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'operation type does not match "delete" or "put"'
+ })
+ }
+
+ // 4.2.2
+ if (operation.type === 'delete' && operation.response != null) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'delete operation should not have an associated response'
+ })
+ }
+
+ // 4.2.3
+ if (this.#queryCache(operation.request, operation.options, addedItems).length) {
+ throw new DOMException('???', 'InvalidStateError')
+ }
+
+ // 4.2.4
+ let requestResponses
+
+ // 4.2.5
+ if (operation.type === 'delete') {
+ // 4.2.5.1
+ requestResponses = this.#queryCache(operation.request, operation.options)
+
+ // TODO: the spec is wrong, this is needed to pass WPTs
+ if (requestResponses.length === 0) {
+ return []
+ }
+
+ // 4.2.5.2
+ for (const requestResponse of requestResponses) {
+ const idx = cache.indexOf(requestResponse)
+ assert(idx !== -1)
+
+ // 4.2.5.2.1
+ cache.splice(idx, 1)
+ }
+ } else if (operation.type === 'put') { // 4.2.6
+ // 4.2.6.1
+ if (operation.response == null) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'put operation should have an associated response'
+ })
+ }
+
+ // 4.2.6.2
+ const r = operation.request
+
+ // 4.2.6.3
+ if (!urlIsHttpHttpsScheme(r.url)) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'expected http or https scheme'
+ })
+ }
+
+ // 4.2.6.4
+ if (r.method !== 'GET') {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'not get method'
+ })
+ }
+
+ // 4.2.6.5
+ if (operation.options != null) {
+ throw webidl.errors.exception({
+ header: 'Cache.#batchCacheOperations',
+ message: 'options must not be defined'
+ })
+ }
+
+ // 4.2.6.6
+ requestResponses = this.#queryCache(operation.request)
+
+ // 4.2.6.7
+ for (const requestResponse of requestResponses) {
+ const idx = cache.indexOf(requestResponse)
+ assert(idx !== -1)
+
+ // 4.2.6.7.1
+ cache.splice(idx, 1)
+ }
+
+ // 4.2.6.8
+ cache.push([operation.request, operation.response])
+
+ // 4.2.6.10
+ addedItems.push([operation.request, operation.response])
+ }
+
+ // 4.2.7
+ resultList.push([operation.request, operation.response])
+ }
+
+ // 4.3
+ return resultList
+ } catch (e) { // 5.
+ // 5.1
+ this.#relevantRequestResponseList.length = 0
+
+ // 5.2
+ this.#relevantRequestResponseList = backupCache
+
+ // 5.3
+ throw e
+ }
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#query-cache
+ * @param {any} requestQuery
+ * @param {import('../../../types/cache').CacheQueryOptions} options
+ * @param {requestResponseList} targetStorage
+ * @returns {requestResponseList}
+ */
+ #queryCache (requestQuery, options, targetStorage) {
+ /** @type {requestResponseList} */
+ const resultList = []
+
+ const storage = targetStorage ?? this.#relevantRequestResponseList
+
+ for (const requestResponse of storage) {
+ const [cachedRequest, cachedResponse] = requestResponse
+ if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) {
+ resultList.push(requestResponse)
+ }
+ }
+
+ return resultList
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm
+ * @param {any} requestQuery
+ * @param {any} request
+ * @param {any | null} response
+ * @param {import('../../../types/cache').CacheQueryOptions | undefined} options
+ * @returns {boolean}
+ */
+ #requestMatchesCachedItem (requestQuery, request, response = null, options) {
+ // if (options?.ignoreMethod === false && request.method === 'GET') {
+ // return false
+ // }
+
+ const queryURL = new URL(requestQuery.url)
+
+ const cachedURL = new URL(request.url)
+
+ if (options?.ignoreSearch) {
+ cachedURL.search = ''
+
+ queryURL.search = ''
+ }
+
+ if (!urlEquals(queryURL, cachedURL, true)) {
+ return false
+ }
+
+ if (
+ response == null ||
+ options?.ignoreVary ||
+ !response.headersList.contains('vary')
+ ) {
+ return true
+ }
+
+ const fieldValues = getFieldValues(response.headersList.get('vary'))
+
+ for (const fieldValue of fieldValues) {
+ if (fieldValue === '*') {
+ return false
+ }
+
+ const requestValue = request.headersList.get(fieldValue)
+ const queryValue = requestQuery.headersList.get(fieldValue)
+
+ // If one has the header and the other doesn't, or one has
+ // a different value than the other, return false
+ if (requestValue !== queryValue) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ #internalMatchAll (request, options, maxResponses = Infinity) {
+ // 1.
+ let r = null
+
+ // 2.
+ if (request !== undefined) {
+ if (webidl.is.Request(request)) {
+ // 2.1.1
+ r = getRequestState(request)
+
+ // 2.1.2
+ if (r.method !== 'GET' && !options.ignoreMethod) {
+ return []
+ }
+ } else if (typeof request === 'string') {
+ // 2.2.1
+ r = getRequestState(new Request(request))
+ }
+ }
+
+ // 5.
+ // 5.1
+ const responses = []
+
+ // 5.2
+ if (request === undefined) {
+ // 5.2.1
+ for (const requestResponse of this.#relevantRequestResponseList) {
+ responses.push(requestResponse[1])
+ }
+ } else { // 5.3
+ // 5.3.1
+ const requestResponses = this.#queryCache(r, options)
+
+ // 5.3.2
+ for (const requestResponse of requestResponses) {
+ responses.push(requestResponse[1])
+ }
+ }
+
+ // 5.4
+ // We don't implement CORs so we don't need to loop over the responses, yay!
+
+ // 5.5.1
+ const responseList = []
+
+ // 5.5.2
+ for (const response of responses) {
+ // 5.5.2.1
+ const responseObject = fromInnerResponse(cloneResponse(response), 'immutable')
+
+ responseList.push(responseObject)
+
+ if (responseList.length >= maxResponses) {
+ break
+ }
+ }
+
+ // 6.
+ return Object.freeze(responseList)
+ }
+}
+
+Object.defineProperties(Cache.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'Cache',
+ configurable: true
+ },
+ match: kEnumerableProperty,
+ matchAll: kEnumerableProperty,
+ add: kEnumerableProperty,
+ addAll: kEnumerableProperty,
+ put: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ keys: kEnumerableProperty
+})
+
+const cacheQueryOptionConverters = [
+ {
+ key: 'ignoreSearch',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'ignoreMethod',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'ignoreVary',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ }
+]
+
+webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters)
+
+webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([
+ ...cacheQueryOptionConverters,
+ {
+ key: 'cacheName',
+ converter: webidl.converters.DOMString
+ }
+])
+
+webidl.converters.Response = webidl.interfaceConverter(
+ webidl.is.Response,
+ 'Response'
+)
+
+webidl.converters['sequence<RequestInfo>'] = webidl.sequenceConverter(
+ webidl.converters.RequestInfo
+)
+
+module.exports = {
+ Cache
+}
diff --git a/vanilla/node_modules/undici/lib/web/cache/cachestorage.js b/vanilla/node_modules/undici/lib/web/cache/cachestorage.js
new file mode 100644
index 0000000..c49b1e8
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/cache/cachestorage.js
@@ -0,0 +1,152 @@
+'use strict'
+
+const { Cache } = require('./cache')
+const { webidl } = require('../webidl')
+const { kEnumerableProperty } = require('../../core/util')
+const { kConstruct } = require('../../core/symbols')
+
+class CacheStorage {
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map
+ * @type {Map<string, import('./cache').requestResponseList}
+ */
+ #caches = new Map()
+
+ constructor () {
+ if (arguments[0] !== kConstruct) {
+ webidl.illegalConstructor()
+ }
+
+ webidl.util.markAsUncloneable(this)
+ }
+
+ async match (request, options = {}) {
+ webidl.brandCheck(this, CacheStorage)
+ webidl.argumentLengthCheck(arguments, 1, 'CacheStorage.match')
+
+ request = webidl.converters.RequestInfo(request)
+ options = webidl.converters.MultiCacheQueryOptions(options)
+
+ // 1.
+ if (options.cacheName != null) {
+ // 1.1.1.1
+ if (this.#caches.has(options.cacheName)) {
+ // 1.1.1.1.1
+ const cacheList = this.#caches.get(options.cacheName)
+ const cache = new Cache(kConstruct, cacheList)
+
+ return await cache.match(request, options)
+ }
+ } else { // 2.
+ // 2.2
+ for (const cacheList of this.#caches.values()) {
+ const cache = new Cache(kConstruct, cacheList)
+
+ // 2.2.1.2
+ const response = await cache.match(request, options)
+
+ if (response !== undefined) {
+ return response
+ }
+ }
+ }
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#cache-storage-has
+ * @param {string} cacheName
+ * @returns {Promise<boolean>}
+ */
+ async has (cacheName) {
+ webidl.brandCheck(this, CacheStorage)
+
+ const prefix = 'CacheStorage.has'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
+
+ // 2.1.1
+ // 2.2
+ return this.#caches.has(cacheName)
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open
+ * @param {string} cacheName
+ * @returns {Promise<Cache>}
+ */
+ async open (cacheName) {
+ webidl.brandCheck(this, CacheStorage)
+
+ const prefix = 'CacheStorage.open'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
+
+ // 2.1
+ if (this.#caches.has(cacheName)) {
+ // await caches.open('v1') !== await caches.open('v1')
+
+ // 2.1.1
+ const cache = this.#caches.get(cacheName)
+
+ // 2.1.1.1
+ return new Cache(kConstruct, cache)
+ }
+
+ // 2.2
+ const cache = []
+
+ // 2.3
+ this.#caches.set(cacheName, cache)
+
+ // 2.4
+ return new Cache(kConstruct, cache)
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete
+ * @param {string} cacheName
+ * @returns {Promise<boolean>}
+ */
+ async delete (cacheName) {
+ webidl.brandCheck(this, CacheStorage)
+
+ const prefix = 'CacheStorage.delete'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ cacheName = webidl.converters.DOMString(cacheName, prefix, 'cacheName')
+
+ return this.#caches.delete(cacheName)
+ }
+
+ /**
+ * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys
+ * @returns {Promise<string[]>}
+ */
+ async keys () {
+ webidl.brandCheck(this, CacheStorage)
+
+ // 2.1
+ const keys = this.#caches.keys()
+
+ // 2.2
+ return [...keys]
+ }
+}
+
+Object.defineProperties(CacheStorage.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'CacheStorage',
+ configurable: true
+ },
+ match: kEnumerableProperty,
+ has: kEnumerableProperty,
+ open: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ keys: kEnumerableProperty
+})
+
+module.exports = {
+ CacheStorage
+}
diff --git a/vanilla/node_modules/undici/lib/web/cache/util.js b/vanilla/node_modules/undici/lib/web/cache/util.js
new file mode 100644
index 0000000..5ac9d84
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/cache/util.js
@@ -0,0 +1,45 @@
+'use strict'
+
+const assert = require('node:assert')
+const { URLSerializer } = require('../fetch/data-url')
+const { isValidHeaderName } = require('../fetch/util')
+
+/**
+ * @see https://url.spec.whatwg.org/#concept-url-equals
+ * @param {URL} A
+ * @param {URL} B
+ * @param {boolean | undefined} excludeFragment
+ * @returns {boolean}
+ */
+function urlEquals (A, B, excludeFragment = false) {
+ const serializedA = URLSerializer(A, excludeFragment)
+
+ const serializedB = URLSerializer(B, excludeFragment)
+
+ return serializedA === serializedB
+}
+
+/**
+ * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262
+ * @param {string} header
+ */
+function getFieldValues (header) {
+ assert(header !== null)
+
+ const values = []
+
+ for (let value of header.split(',')) {
+ value = value.trim()
+
+ if (isValidHeaderName(value)) {
+ values.push(value)
+ }
+ }
+
+ return values
+}
+
+module.exports = {
+ urlEquals,
+ getFieldValues
+}
diff --git a/vanilla/node_modules/undici/lib/web/cookies/constants.js b/vanilla/node_modules/undici/lib/web/cookies/constants.js
new file mode 100644
index 0000000..85f1fec
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/cookies/constants.js
@@ -0,0 +1,12 @@
+'use strict'
+
+// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size
+const maxAttributeValueSize = 1024
+
+// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size
+const maxNameValuePairSize = 4096
+
+module.exports = {
+ maxAttributeValueSize,
+ maxNameValuePairSize
+}
diff --git a/vanilla/node_modules/undici/lib/web/cookies/index.js b/vanilla/node_modules/undici/lib/web/cookies/index.js
new file mode 100644
index 0000000..be99f6f
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/cookies/index.js
@@ -0,0 +1,199 @@
+'use strict'
+
+const { parseSetCookie } = require('./parse')
+const { stringify } = require('./util')
+const { webidl } = require('../webidl')
+const { Headers } = require('../fetch/headers')
+
+const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filter(Boolean))
+
+/**
+ * @typedef {Object} Cookie
+ * @property {string} name
+ * @property {string} value
+ * @property {Date|number} [expires]
+ * @property {number} [maxAge]
+ * @property {string} [domain]
+ * @property {string} [path]
+ * @property {boolean} [secure]
+ * @property {boolean} [httpOnly]
+ * @property {'Strict'|'Lax'|'None'} [sameSite]
+ * @property {string[]} [unparsed]
+ */
+
+/**
+ * @param {Headers} headers
+ * @returns {Record<string, string>}
+ */
+function getCookies (headers) {
+ webidl.argumentLengthCheck(arguments, 1, 'getCookies')
+
+ brandChecks(headers)
+
+ const cookie = headers.get('cookie')
+
+ /** @type {Record<string, string>} */
+ const out = {}
+
+ if (!cookie) {
+ return out
+ }
+
+ for (const piece of cookie.split(';')) {
+ const [name, ...value] = piece.split('=')
+
+ out[name.trim()] = value.join('=')
+ }
+
+ return out
+}
+
+/**
+ * @param {Headers} headers
+ * @param {string} name
+ * @param {{ path?: string, domain?: string }|undefined} attributes
+ * @returns {void}
+ */
+function deleteCookie (headers, name, attributes) {
+ brandChecks(headers)
+
+ const prefix = 'deleteCookie'
+ webidl.argumentLengthCheck(arguments, 2, prefix)
+
+ name = webidl.converters.DOMString(name, prefix, 'name')
+ attributes = webidl.converters.DeleteCookieAttributes(attributes)
+
+ // Matches behavior of
+ // https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278
+ setCookie(headers, {
+ name,
+ value: '',
+ expires: new Date(0),
+ ...attributes
+ })
+}
+
+/**
+ * @param {Headers} headers
+ * @returns {Cookie[]}
+ */
+function getSetCookies (headers) {
+ webidl.argumentLengthCheck(arguments, 1, 'getSetCookies')
+
+ brandChecks(headers)
+
+ const cookies = headers.getSetCookie()
+
+ if (!cookies) {
+ return []
+ }
+
+ return cookies.map((pair) => parseSetCookie(pair))
+}
+
+/**
+ * Parses a cookie string
+ * @param {string} cookie
+ */
+function parseCookie (cookie) {
+ cookie = webidl.converters.DOMString(cookie)
+
+ return parseSetCookie(cookie)
+}
+
+/**
+ * @param {Headers} headers
+ * @param {Cookie} cookie
+ * @returns {void}
+ */
+function setCookie (headers, cookie) {
+ webidl.argumentLengthCheck(arguments, 2, 'setCookie')
+
+ brandChecks(headers)
+
+ cookie = webidl.converters.Cookie(cookie)
+
+ const str = stringify(cookie)
+
+ if (str) {
+ headers.append('set-cookie', str, true)
+ }
+}
+
+webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'path',
+ defaultValue: () => null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'domain',
+ defaultValue: () => null
+ }
+])
+
+webidl.converters.Cookie = webidl.dictionaryConverter([
+ {
+ converter: webidl.converters.DOMString,
+ key: 'name'
+ },
+ {
+ converter: webidl.converters.DOMString,
+ key: 'value'
+ },
+ {
+ converter: webidl.nullableConverter((value) => {
+ if (typeof value === 'number') {
+ return webidl.converters['unsigned long long'](value)
+ }
+
+ return new Date(value)
+ }),
+ key: 'expires',
+ defaultValue: () => null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters['long long']),
+ key: 'maxAge',
+ defaultValue: () => null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'domain',
+ defaultValue: () => null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.DOMString),
+ key: 'path',
+ defaultValue: () => null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.boolean),
+ key: 'secure',
+ defaultValue: () => null
+ },
+ {
+ converter: webidl.nullableConverter(webidl.converters.boolean),
+ key: 'httpOnly',
+ defaultValue: () => null
+ },
+ {
+ converter: webidl.converters.USVString,
+ key: 'sameSite',
+ allowedValues: ['Strict', 'Lax', 'None']
+ },
+ {
+ converter: webidl.sequenceConverter(webidl.converters.DOMString),
+ key: 'unparsed',
+ defaultValue: () => []
+ }
+])
+
+module.exports = {
+ getCookies,
+ deleteCookie,
+ getSetCookies,
+ setCookie,
+ parseCookie
+}
diff --git a/vanilla/node_modules/undici/lib/web/cookies/parse.js b/vanilla/node_modules/undici/lib/web/cookies/parse.js
new file mode 100644
index 0000000..74edf7e
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/cookies/parse.js
@@ -0,0 +1,322 @@
+'use strict'
+
+const { collectASequenceOfCodePointsFast } = require('../infra')
+const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
+const { isCTLExcludingHtab } = require('./util')
+const assert = require('node:assert')
+const { unescape: qsUnescape } = require('node:querystring')
+
+/**
+ * @description Parses the field-value attributes of a set-cookie header string.
+ * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
+ * @param {string} header
+ * @returns {import('./index').Cookie|null} if the header is invalid, null will be returned
+ */
+function parseSetCookie (header) {
+ // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F
+ // character (CTL characters excluding HTAB): Abort these steps and
+ // ignore the set-cookie-string entirely.
+ if (isCTLExcludingHtab(header)) {
+ return null
+ }
+
+ let nameValuePair = ''
+ let unparsedAttributes = ''
+ let name = ''
+ let value = ''
+
+ // 2. If the set-cookie-string contains a %x3B (";") character:
+ if (header.includes(';')) {
+ // 1. The name-value-pair string consists of the characters up to,
+ // but not including, the first %x3B (";"), and the unparsed-
+ // attributes consist of the remainder of the set-cookie-string
+ // (including the %x3B (";") in question).
+ const position = { position: 0 }
+
+ nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
+ unparsedAttributes = header.slice(position.position)
+ } else {
+ // Otherwise:
+
+ // 1. The name-value-pair string consists of all the characters
+ // contained in the set-cookie-string, and the unparsed-
+ // attributes is the empty string.
+ nameValuePair = header
+ }
+
+ // 3. If the name-value-pair string lacks a %x3D ("=") character, then
+ // the name string is empty, and the value string is the value of
+ // name-value-pair.
+ if (!nameValuePair.includes('=')) {
+ value = nameValuePair
+ } else {
+ // Otherwise, the name string consists of the characters up to, but
+ // not including, the first %x3D ("=") character, and the (possibly
+ // empty) value string consists of the characters after the first
+ // %x3D ("=") character.
+ const position = { position: 0 }
+ name = collectASequenceOfCodePointsFast(
+ '=',
+ nameValuePair,
+ position
+ )
+ value = nameValuePair.slice(position.position + 1)
+ }
+
+ // 4. Remove any leading or trailing WSP characters from the name
+ // string and the value string.
+ name = name.trim()
+ value = value.trim()
+
+ // 5. If the sum of the lengths of the name string and the value string
+ // is more than 4096 octets, abort these steps and ignore the set-
+ // cookie-string entirely.
+ if (name.length + value.length > maxNameValuePairSize) {
+ return null
+ }
+
+ // 6. The cookie-name is the name string, and the cookie-value is the
+ // value string.
+ // https://datatracker.ietf.org/doc/html/rfc6265
+ // To maximize compatibility with user agents, servers that wish to
+ // store arbitrary data in a cookie-value SHOULD encode that data, for
+ // example, using Base64 [RFC4648].
+ return {
+ name, value: qsUnescape(value), ...parseUnparsedAttributes(unparsedAttributes)
+ }
+}
+
+/**
+ * Parses the remaining attributes of a set-cookie header
+ * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4
+ * @param {string} unparsedAttributes
+ * @param {Object.<string, unknown>} [cookieAttributeList={}]
+ */
+function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) {
+ // 1. If the unparsed-attributes string is empty, skip the rest of
+ // these steps.
+ if (unparsedAttributes.length === 0) {
+ return cookieAttributeList
+ }
+
+ // 2. Discard the first character of the unparsed-attributes (which
+ // will be a %x3B (";") character).
+ assert(unparsedAttributes[0] === ';')
+ unparsedAttributes = unparsedAttributes.slice(1)
+
+ let cookieAv = ''
+
+ // 3. If the remaining unparsed-attributes contains a %x3B (";")
+ // character:
+ if (unparsedAttributes.includes(';')) {
+ // 1. Consume the characters of the unparsed-attributes up to, but
+ // not including, the first %x3B (";") character.
+ cookieAv = collectASequenceOfCodePointsFast(
+ ';',
+ unparsedAttributes,
+ { position: 0 }
+ )
+ unparsedAttributes = unparsedAttributes.slice(cookieAv.length)
+ } else {
+ // Otherwise:
+
+ // 1. Consume the remainder of the unparsed-attributes.
+ cookieAv = unparsedAttributes
+ unparsedAttributes = ''
+ }
+
+ // Let the cookie-av string be the characters consumed in this step.
+
+ let attributeName = ''
+ let attributeValue = ''
+
+ // 4. If the cookie-av string contains a %x3D ("=") character:
+ if (cookieAv.includes('=')) {
+ // 1. The (possibly empty) attribute-name string consists of the
+ // characters up to, but not including, the first %x3D ("=")
+ // character, and the (possibly empty) attribute-value string
+ // consists of the characters after the first %x3D ("=")
+ // character.
+ const position = { position: 0 }
+
+ attributeName = collectASequenceOfCodePointsFast(
+ '=',
+ cookieAv,
+ position
+ )
+ attributeValue = cookieAv.slice(position.position + 1)
+ } else {
+ // Otherwise:
+
+ // 1. The attribute-name string consists of the entire cookie-av
+ // string, and the attribute-value string is empty.
+ attributeName = cookieAv
+ }
+
+ // 5. Remove any leading or trailing WSP characters from the attribute-
+ // name string and the attribute-value string.
+ attributeName = attributeName.trim()
+ attributeValue = attributeValue.trim()
+
+ // 6. If the attribute-value is longer than 1024 octets, ignore the
+ // cookie-av string and return to Step 1 of this algorithm.
+ if (attributeValue.length > maxAttributeValueSize) {
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+ }
+
+ // 7. Process the attribute-name and attribute-value according to the
+ // requirements in the following subsections. (Notice that
+ // attributes with unrecognized attribute-names are ignored.)
+ const attributeNameLowercase = attributeName.toLowerCase()
+
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1
+ // If the attribute-name case-insensitively matches the string
+ // "Expires", the user agent MUST process the cookie-av as follows.
+ if (attributeNameLowercase === 'expires') {
+ // 1. Let the expiry-time be the result of parsing the attribute-value
+ // as cookie-date (see Section 5.1.1).
+ const expiryTime = new Date(attributeValue)
+
+ // 2. If the attribute-value failed to parse as a cookie date, ignore
+ // the cookie-av.
+
+ cookieAttributeList.expires = expiryTime
+ } else if (attributeNameLowercase === 'max-age') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
+ // If the attribute-name case-insensitively matches the string "Max-
+ // Age", the user agent MUST process the cookie-av as follows.
+
+ // 1. If the first character of the attribute-value is not a DIGIT or a
+ // "-" character, ignore the cookie-av.
+ const charCode = attributeValue.charCodeAt(0)
+
+ if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') {
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+ }
+
+ // 2. If the remainder of attribute-value contains a non-DIGIT
+ // character, ignore the cookie-av.
+ if (!/^\d+$/.test(attributeValue)) {
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+ }
+
+ // 3. Let delta-seconds be the attribute-value converted to an integer.
+ const deltaSeconds = Number(attributeValue)
+
+ // 4. Let cookie-age-limit be the maximum age of the cookie (which
+ // SHOULD be 400 days or less, see Section 4.1.2.2).
+
+ // 5. Set delta-seconds to the smaller of its present value and cookie-
+ // age-limit.
+ // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs)
+
+ // 6. If delta-seconds is less than or equal to zero (0), let expiry-
+ // time be the earliest representable date and time. Otherwise, let
+ // the expiry-time be the current date and time plus delta-seconds
+ // seconds.
+ // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds
+
+ // 7. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of Max-Age and an attribute-value of expiry-time.
+ cookieAttributeList.maxAge = deltaSeconds
+ } else if (attributeNameLowercase === 'domain') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3
+ // If the attribute-name case-insensitively matches the string "Domain",
+ // the user agent MUST process the cookie-av as follows.
+
+ // 1. Let cookie-domain be the attribute-value.
+ let cookieDomain = attributeValue
+
+ // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be
+ // cookie-domain without its leading %x2E (".").
+ if (cookieDomain[0] === '.') {
+ cookieDomain = cookieDomain.slice(1)
+ }
+
+ // 3. Convert the cookie-domain to lower case.
+ cookieDomain = cookieDomain.toLowerCase()
+
+ // 4. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of Domain and an attribute-value of cookie-domain.
+ cookieAttributeList.domain = cookieDomain
+ } else if (attributeNameLowercase === 'path') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4
+ // If the attribute-name case-insensitively matches the string "Path",
+ // the user agent MUST process the cookie-av as follows.
+
+ // 1. If the attribute-value is empty or if the first character of the
+ // attribute-value is not %x2F ("/"):
+ let cookiePath = ''
+ if (attributeValue.length === 0 || attributeValue[0] !== '/') {
+ // 1. Let cookie-path be the default-path.
+ cookiePath = '/'
+ } else {
+ // Otherwise:
+
+ // 1. Let cookie-path be the attribute-value.
+ cookiePath = attributeValue
+ }
+
+ // 2. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of Path and an attribute-value of cookie-path.
+ cookieAttributeList.path = cookiePath
+ } else if (attributeNameLowercase === 'secure') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5
+ // If the attribute-name case-insensitively matches the string "Secure",
+ // the user agent MUST append an attribute to the cookie-attribute-list
+ // with an attribute-name of Secure and an empty attribute-value.
+
+ cookieAttributeList.secure = true
+ } else if (attributeNameLowercase === 'httponly') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6
+ // If the attribute-name case-insensitively matches the string
+ // "HttpOnly", the user agent MUST append an attribute to the cookie-
+ // attribute-list with an attribute-name of HttpOnly and an empty
+ // attribute-value.
+
+ cookieAttributeList.httpOnly = true
+ } else if (attributeNameLowercase === 'samesite') {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7
+ // If the attribute-name case-insensitively matches the string
+ // "SameSite", the user agent MUST process the cookie-av as follows:
+
+ // 1. Let enforcement be "Default".
+ let enforcement = 'Default'
+
+ const attributeValueLowercase = attributeValue.toLowerCase()
+ // 2. If cookie-av's attribute-value is a case-insensitive match for
+ // "None", set enforcement to "None".
+ if (attributeValueLowercase.includes('none')) {
+ enforcement = 'None'
+ }
+
+ // 3. If cookie-av's attribute-value is a case-insensitive match for
+ // "Strict", set enforcement to "Strict".
+ if (attributeValueLowercase.includes('strict')) {
+ enforcement = 'Strict'
+ }
+
+ // 4. If cookie-av's attribute-value is a case-insensitive match for
+ // "Lax", set enforcement to "Lax".
+ if (attributeValueLowercase.includes('lax')) {
+ enforcement = 'Lax'
+ }
+
+ // 5. Append an attribute to the cookie-attribute-list with an
+ // attribute-name of "SameSite" and an attribute-value of
+ // enforcement.
+ cookieAttributeList.sameSite = enforcement
+ } else {
+ cookieAttributeList.unparsed ??= []
+
+ cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`)
+ }
+
+ // 8. Return to Step 1 of this algorithm.
+ return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList)
+}
+
+module.exports = {
+ parseSetCookie,
+ parseUnparsedAttributes
+}
diff --git a/vanilla/node_modules/undici/lib/web/cookies/util.js b/vanilla/node_modules/undici/lib/web/cookies/util.js
new file mode 100644
index 0000000..254f541
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/cookies/util.js
@@ -0,0 +1,282 @@
+'use strict'
+
+/**
+ * @param {string} value
+ * @returns {boolean}
+ */
+function isCTLExcludingHtab (value) {
+ for (let i = 0; i < value.length; ++i) {
+ const code = value.charCodeAt(i)
+
+ if (
+ (code >= 0x00 && code <= 0x08) ||
+ (code >= 0x0A && code <= 0x1F) ||
+ code === 0x7F
+ ) {
+ return true
+ }
+ }
+ return false
+}
+
+/**
+ CHAR = <any US-ASCII character (octets 0 - 127)>
+ token = 1*<any CHAR except CTLs or separators>
+ separators = "(" | ")" | "<" | ">" | "@"
+ | "," | ";" | ":" | "\" | <">
+ | "/" | "[" | "]" | "?" | "="
+ | "{" | "}" | SP | HT
+ * @param {string} name
+ */
+function validateCookieName (name) {
+ for (let i = 0; i < name.length; ++i) {
+ const code = name.charCodeAt(i)
+
+ if (
+ code < 0x21 || // exclude CTLs (0-31), SP and HT
+ code > 0x7E || // exclude non-ascii and DEL
+ code === 0x22 || // "
+ code === 0x28 || // (
+ code === 0x29 || // )
+ code === 0x3C || // <
+ code === 0x3E || // >
+ code === 0x40 || // @
+ code === 0x2C || // ,
+ code === 0x3B || // ;
+ code === 0x3A || // :
+ code === 0x5C || // \
+ code === 0x2F || // /
+ code === 0x5B || // [
+ code === 0x5D || // ]
+ code === 0x3F || // ?
+ code === 0x3D || // =
+ code === 0x7B || // {
+ code === 0x7D // }
+ ) {
+ throw new Error('Invalid cookie name')
+ }
+ }
+}
+
+/**
+ cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+ cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+ ; US-ASCII characters excluding CTLs,
+ ; whitespace DQUOTE, comma, semicolon,
+ ; and backslash
+ * @param {string} value
+ */
+function validateCookieValue (value) {
+ let len = value.length
+ let i = 0
+
+ // if the value is wrapped in DQUOTE
+ if (value[0] === '"') {
+ if (len === 1 || value[len - 1] !== '"') {
+ throw new Error('Invalid cookie value')
+ }
+ --len
+ ++i
+ }
+
+ while (i < len) {
+ const code = value.charCodeAt(i++)
+
+ if (
+ code < 0x21 || // exclude CTLs (0-31)
+ code > 0x7E || // non-ascii and DEL (127)
+ code === 0x22 || // "
+ code === 0x2C || // ,
+ code === 0x3B || // ;
+ code === 0x5C // \
+ ) {
+ throw new Error('Invalid cookie value')
+ }
+ }
+}
+
+/**
+ * path-value = <any CHAR except CTLs or ";">
+ * @param {string} path
+ */
+function validateCookiePath (path) {
+ for (let i = 0; i < path.length; ++i) {
+ const code = path.charCodeAt(i)
+
+ if (
+ code < 0x20 || // exclude CTLs (0-31)
+ code === 0x7F || // DEL
+ code === 0x3B // ;
+ ) {
+ throw new Error('Invalid cookie path')
+ }
+ }
+}
+
+/**
+ * I have no idea why these values aren't allowed to be honest,
+ * but Deno tests these. - Khafra
+ * @param {string} domain
+ */
+function validateCookieDomain (domain) {
+ if (
+ domain.startsWith('-') ||
+ domain.endsWith('.') ||
+ domain.endsWith('-')
+ ) {
+ throw new Error('Invalid cookie domain')
+ }
+}
+
+const IMFDays = [
+ 'Sun', 'Mon', 'Tue', 'Wed',
+ 'Thu', 'Fri', 'Sat'
+]
+
+const IMFMonths = [
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
+]
+
+const IMFPaddedNumbers = Array(61).fill(0).map((_, i) => i.toString().padStart(2, '0'))
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
+ * @param {number|Date} date
+ IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT
+ ; fixed length/zone/capitalization subset of the format
+ ; see Section 3.3 of [RFC5322]
+
+ day-name = %x4D.6F.6E ; "Mon", case-sensitive
+ / %x54.75.65 ; "Tue", case-sensitive
+ / %x57.65.64 ; "Wed", case-sensitive
+ / %x54.68.75 ; "Thu", case-sensitive
+ / %x46.72.69 ; "Fri", case-sensitive
+ / %x53.61.74 ; "Sat", case-sensitive
+ / %x53.75.6E ; "Sun", case-sensitive
+ date1 = day SP month SP year
+ ; e.g., 02 Jun 1982
+
+ day = 2DIGIT
+ month = %x4A.61.6E ; "Jan", case-sensitive
+ / %x46.65.62 ; "Feb", case-sensitive
+ / %x4D.61.72 ; "Mar", case-sensitive
+ / %x41.70.72 ; "Apr", case-sensitive
+ / %x4D.61.79 ; "May", case-sensitive
+ / %x4A.75.6E ; "Jun", case-sensitive
+ / %x4A.75.6C ; "Jul", case-sensitive
+ / %x41.75.67 ; "Aug", case-sensitive
+ / %x53.65.70 ; "Sep", case-sensitive
+ / %x4F.63.74 ; "Oct", case-sensitive
+ / %x4E.6F.76 ; "Nov", case-sensitive
+ / %x44.65.63 ; "Dec", case-sensitive
+ year = 4DIGIT
+
+ GMT = %x47.4D.54 ; "GMT", case-sensitive
+
+ time-of-day = hour ":" minute ":" second
+ ; 00:00:00 - 23:59:60 (leap second)
+
+ hour = 2DIGIT
+ minute = 2DIGIT
+ second = 2DIGIT
+ */
+function toIMFDate (date) {
+ if (typeof date === 'number') {
+ date = new Date(date)
+ }
+
+ return `${IMFDays[date.getUTCDay()]}, ${IMFPaddedNumbers[date.getUTCDate()]} ${IMFMonths[date.getUTCMonth()]} ${date.getUTCFullYear()} ${IMFPaddedNumbers[date.getUTCHours()]}:${IMFPaddedNumbers[date.getUTCMinutes()]}:${IMFPaddedNumbers[date.getUTCSeconds()]} GMT`
+}
+
+/**
+ max-age-av = "Max-Age=" non-zero-digit *DIGIT
+ ; In practice, both expires-av and max-age-av
+ ; are limited to dates representable by the
+ ; user agent.
+ * @param {number} maxAge
+ */
+function validateCookieMaxAge (maxAge) {
+ if (maxAge < 0) {
+ throw new Error('Invalid cookie max-age')
+ }
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
+ * @param {import('./index').Cookie} cookie
+ */
+function stringify (cookie) {
+ if (cookie.name.length === 0) {
+ return null
+ }
+
+ validateCookieName(cookie.name)
+ validateCookieValue(cookie.value)
+
+ const out = [`${cookie.name}=${cookie.value}`]
+
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1
+ // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2
+ if (cookie.name.startsWith('__Secure-')) {
+ cookie.secure = true
+ }
+
+ if (cookie.name.startsWith('__Host-')) {
+ cookie.secure = true
+ cookie.domain = null
+ cookie.path = '/'
+ }
+
+ if (cookie.secure) {
+ out.push('Secure')
+ }
+
+ if (cookie.httpOnly) {
+ out.push('HttpOnly')
+ }
+
+ if (typeof cookie.maxAge === 'number') {
+ validateCookieMaxAge(cookie.maxAge)
+ out.push(`Max-Age=${cookie.maxAge}`)
+ }
+
+ if (cookie.domain) {
+ validateCookieDomain(cookie.domain)
+ out.push(`Domain=${cookie.domain}`)
+ }
+
+ if (cookie.path) {
+ validateCookiePath(cookie.path)
+ out.push(`Path=${cookie.path}`)
+ }
+
+ if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') {
+ out.push(`Expires=${toIMFDate(cookie.expires)}`)
+ }
+
+ if (cookie.sameSite) {
+ out.push(`SameSite=${cookie.sameSite}`)
+ }
+
+ for (const part of cookie.unparsed) {
+ if (!part.includes('=')) {
+ throw new Error('Invalid unparsed')
+ }
+
+ const [key, ...value] = part.split('=')
+
+ out.push(`${key.trim()}=${value.join('=')}`)
+ }
+
+ return out.join('; ')
+}
+
+module.exports = {
+ isCTLExcludingHtab,
+ validateCookieName,
+ validateCookiePath,
+ validateCookieValue,
+ toIMFDate,
+ stringify
+}
diff --git a/vanilla/node_modules/undici/lib/web/eventsource/eventsource-stream.js b/vanilla/node_modules/undici/lib/web/eventsource/eventsource-stream.js
new file mode 100644
index 0000000..d24e8f6
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/eventsource/eventsource-stream.js
@@ -0,0 +1,399 @@
+'use strict'
+const { Transform } = require('node:stream')
+const { isASCIINumber, isValidLastEventId } = require('./util')
+
+/**
+ * @type {number[]} BOM
+ */
+const BOM = [0xEF, 0xBB, 0xBF]
+/**
+ * @type {10} LF
+ */
+const LF = 0x0A
+/**
+ * @type {13} CR
+ */
+const CR = 0x0D
+/**
+ * @type {58} COLON
+ */
+const COLON = 0x3A
+/**
+ * @type {32} SPACE
+ */
+const SPACE = 0x20
+
+/**
+ * @typedef {object} EventSourceStreamEvent
+ * @type {object}
+ * @property {string} [event] The event type.
+ * @property {string} [data] The data of the message.
+ * @property {string} [id] A unique ID for the event.
+ * @property {string} [retry] The reconnection time, in milliseconds.
+ */
+
+/**
+ * @typedef eventSourceSettings
+ * @type {object}
+ * @property {string} [lastEventId] The last event ID received from the server.
+ * @property {string} [origin] The origin of the event source.
+ * @property {number} [reconnectionTime] The reconnection time, in milliseconds.
+ */
+
+class EventSourceStream extends Transform {
+ /**
+ * @type {eventSourceSettings}
+ */
+ state
+
+ /**
+ * Leading byte-order-mark check.
+ * @type {boolean}
+ */
+ checkBOM = true
+
+ /**
+ * @type {boolean}
+ */
+ crlfCheck = false
+
+ /**
+ * @type {boolean}
+ */
+ eventEndCheck = false
+
+ /**
+ * @type {Buffer|null}
+ */
+ buffer = null
+
+ pos = 0
+
+ event = {
+ data: undefined,
+ event: undefined,
+ id: undefined,
+ retry: undefined
+ }
+
+ /**
+ * @param {object} options
+ * @param {boolean} [options.readableObjectMode]
+ * @param {eventSourceSettings} [options.eventSourceSettings]
+ * @param {(chunk: any, encoding?: BufferEncoding | undefined) => boolean} [options.push]
+ */
+ constructor (options = {}) {
+ // Enable object mode as EventSourceStream emits objects of shape
+ // EventSourceStreamEvent
+ options.readableObjectMode = true
+
+ super(options)
+
+ this.state = options.eventSourceSettings || {}
+ if (options.push) {
+ this.push = options.push
+ }
+ }
+
+ /**
+ * @param {Buffer} chunk
+ * @param {string} _encoding
+ * @param {Function} callback
+ * @returns {void}
+ */
+ _transform (chunk, _encoding, callback) {
+ if (chunk.length === 0) {
+ callback()
+ return
+ }
+
+ // Cache the chunk in the buffer, as the data might not be complete while
+ // processing it
+ // TODO: Investigate if there is a more performant way to handle
+ // incoming chunks
+ // see: https://github.com/nodejs/undici/issues/2630
+ if (this.buffer) {
+ this.buffer = Buffer.concat([this.buffer, chunk])
+ } else {
+ this.buffer = chunk
+ }
+
+ // Strip leading byte-order-mark if we opened the stream and started
+ // the processing of the incoming data
+ if (this.checkBOM) {
+ switch (this.buffer.length) {
+ case 1:
+ // Check if the first byte is the same as the first byte of the BOM
+ if (this.buffer[0] === BOM[0]) {
+ // If it is, we need to wait for more data
+ callback()
+ return
+ }
+ // Set the checkBOM flag to false as we don't need to check for the
+ // BOM anymore
+ this.checkBOM = false
+
+ // The buffer only contains one byte so we need to wait for more data
+ callback()
+ return
+ case 2:
+ // Check if the first two bytes are the same as the first two bytes
+ // of the BOM
+ if (
+ this.buffer[0] === BOM[0] &&
+ this.buffer[1] === BOM[1]
+ ) {
+ // If it is, we need to wait for more data, because the third byte
+ // is needed to determine if it is the BOM or not
+ callback()
+ return
+ }
+
+ // Set the checkBOM flag to false as we don't need to check for the
+ // BOM anymore
+ this.checkBOM = false
+ break
+ case 3:
+ // Check if the first three bytes are the same as the first three
+ // bytes of the BOM
+ if (
+ this.buffer[0] === BOM[0] &&
+ this.buffer[1] === BOM[1] &&
+ this.buffer[2] === BOM[2]
+ ) {
+ // If it is, we can drop the buffered data, as it is only the BOM
+ this.buffer = Buffer.alloc(0)
+ // Set the checkBOM flag to false as we don't need to check for the
+ // BOM anymore
+ this.checkBOM = false
+
+ // Await more data
+ callback()
+ return
+ }
+ // If it is not the BOM, we can start processing the data
+ this.checkBOM = false
+ break
+ default:
+ // The buffer is longer than 3 bytes, so we can drop the BOM if it is
+ // present
+ if (
+ this.buffer[0] === BOM[0] &&
+ this.buffer[1] === BOM[1] &&
+ this.buffer[2] === BOM[2]
+ ) {
+ // Remove the BOM from the buffer
+ this.buffer = this.buffer.subarray(3)
+ }
+
+ // Set the checkBOM flag to false as we don't need to check for the
+ this.checkBOM = false
+ break
+ }
+ }
+
+ while (this.pos < this.buffer.length) {
+ // If the previous line ended with an end-of-line, we need to check
+ // if the next character is also an end-of-line.
+ if (this.eventEndCheck) {
+ // If the the current character is an end-of-line, then the event
+ // is finished and we can process it
+
+ // If the previous line ended with a carriage return, we need to
+ // check if the current character is a line feed and remove it
+ // from the buffer.
+ if (this.crlfCheck) {
+ // If the current character is a line feed, we can remove it
+ // from the buffer and reset the crlfCheck flag
+ if (this.buffer[this.pos] === LF) {
+ this.buffer = this.buffer.subarray(this.pos + 1)
+ this.pos = 0
+ this.crlfCheck = false
+
+ // It is possible that the line feed is not the end of the
+ // event. We need to check if the next character is an
+ // end-of-line character to determine if the event is
+ // finished. We simply continue the loop to check the next
+ // character.
+
+ // As we removed the line feed from the buffer and set the
+ // crlfCheck flag to false, we basically don't make any
+ // distinction between a line feed and a carriage return.
+ continue
+ }
+ this.crlfCheck = false
+ }
+
+ if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
+ // If the current character is a carriage return, we need to
+ // set the crlfCheck flag to true, as we need to check if the
+ // next character is a line feed so we can remove it from the
+ // buffer
+ if (this.buffer[this.pos] === CR) {
+ this.crlfCheck = true
+ }
+
+ this.buffer = this.buffer.subarray(this.pos + 1)
+ this.pos = 0
+ if (
+ this.event.data !== undefined || this.event.event || this.event.id !== undefined || this.event.retry) {
+ this.processEvent(this.event)
+ }
+ this.clearEvent()
+ continue
+ }
+ // If the current character is not an end-of-line, then the event
+ // is not finished and we have to reset the eventEndCheck flag
+ this.eventEndCheck = false
+ continue
+ }
+
+ // If the current character is an end-of-line, we can process the
+ // line
+ if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
+ // If the current character is a carriage return, we need to
+ // set the crlfCheck flag to true, as we need to check if the
+ // next character is a line feed
+ if (this.buffer[this.pos] === CR) {
+ this.crlfCheck = true
+ }
+
+ // In any case, we can process the line as we reached an
+ // end-of-line character
+ this.parseLine(this.buffer.subarray(0, this.pos), this.event)
+
+ // Remove the processed line from the buffer
+ this.buffer = this.buffer.subarray(this.pos + 1)
+ // Reset the position as we removed the processed line from the buffer
+ this.pos = 0
+ // A line was processed and this could be the end of the event. We need
+ // to check if the next line is empty to determine if the event is
+ // finished.
+ this.eventEndCheck = true
+ continue
+ }
+
+ this.pos++
+ }
+
+ callback()
+ }
+
+ /**
+ * @param {Buffer} line
+ * @param {EventSourceStreamEvent} event
+ */
+ parseLine (line, event) {
+ // If the line is empty (a blank line)
+ // Dispatch the event, as defined below.
+ // This will be handled in the _transform method
+ if (line.length === 0) {
+ return
+ }
+
+ // If the line starts with a U+003A COLON character (:)
+ // Ignore the line.
+ const colonPosition = line.indexOf(COLON)
+ if (colonPosition === 0) {
+ return
+ }
+
+ let field = ''
+ let value = ''
+
+ // If the line contains a U+003A COLON character (:)
+ if (colonPosition !== -1) {
+ // Collect the characters on the line before the first U+003A COLON
+ // character (:), and let field be that string.
+ // TODO: Investigate if there is a more performant way to extract the
+ // field
+ // see: https://github.com/nodejs/undici/issues/2630
+ field = line.subarray(0, colonPosition).toString('utf8')
+
+ // Collect the characters on the line after the first U+003A COLON
+ // character (:), and let value be that string.
+ // If value starts with a U+0020 SPACE character, remove it from value.
+ let valueStart = colonPosition + 1
+ if (line[valueStart] === SPACE) {
+ ++valueStart
+ }
+ // TODO: Investigate if there is a more performant way to extract the
+ // value
+ // see: https://github.com/nodejs/undici/issues/2630
+ value = line.subarray(valueStart).toString('utf8')
+
+ // Otherwise, the string is not empty but does not contain a U+003A COLON
+ // character (:)
+ } else {
+ // Process the field using the steps described below, using the whole
+ // line as the field name, and the empty string as the field value.
+ field = line.toString('utf8')
+ value = ''
+ }
+
+ // Modify the event with the field name and value. The value is also
+ // decoded as UTF-8
+ switch (field) {
+ case 'data':
+ if (event[field] === undefined) {
+ event[field] = value
+ } else {
+ event[field] += `\n${value}`
+ }
+ break
+ case 'retry':
+ if (isASCIINumber(value)) {
+ event[field] = value
+ }
+ break
+ case 'id':
+ if (isValidLastEventId(value)) {
+ event[field] = value
+ }
+ break
+ case 'event':
+ if (value.length > 0) {
+ event[field] = value
+ }
+ break
+ }
+ }
+
+ /**
+ * @param {EventSourceStreamEvent} event
+ */
+ processEvent (event) {
+ if (event.retry && isASCIINumber(event.retry)) {
+ this.state.reconnectionTime = parseInt(event.retry, 10)
+ }
+
+ if (event.id !== undefined && isValidLastEventId(event.id)) {
+ this.state.lastEventId = event.id
+ }
+
+ // only dispatch event, when data is provided
+ if (event.data !== undefined) {
+ this.push({
+ type: event.event || 'message',
+ options: {
+ data: event.data,
+ lastEventId: this.state.lastEventId,
+ origin: this.state.origin
+ }
+ })
+ }
+ }
+
+ clearEvent () {
+ this.event = {
+ data: undefined,
+ event: undefined,
+ id: undefined,
+ retry: undefined
+ }
+ }
+}
+
+module.exports = {
+ EventSourceStream
+}
diff --git a/vanilla/node_modules/undici/lib/web/eventsource/eventsource.js b/vanilla/node_modules/undici/lib/web/eventsource/eventsource.js
new file mode 100644
index 0000000..32dcf0e
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/eventsource/eventsource.js
@@ -0,0 +1,501 @@
+'use strict'
+
+const { pipeline } = require('node:stream')
+const { fetching } = require('../fetch')
+const { makeRequest } = require('../fetch/request')
+const { webidl } = require('../webidl')
+const { EventSourceStream } = require('./eventsource-stream')
+const { parseMIMEType } = require('../fetch/data-url')
+const { createFastMessageEvent } = require('../websocket/events')
+const { isNetworkError } = require('../fetch/response')
+const { kEnumerableProperty } = require('../../core/util')
+const { environmentSettingsObject } = require('../fetch/util')
+
+let experimentalWarned = false
+
+/**
+ * A reconnection time, in milliseconds. This must initially be an implementation-defined value,
+ * probably in the region of a few seconds.
+ *
+ * In Comparison:
+ * - Chrome uses 3000ms.
+ * - Deno uses 5000ms.
+ *
+ * @type {3000}
+ */
+const defaultReconnectionTime = 3000
+
+/**
+ * The readyState attribute represents the state of the connection.
+ * @typedef ReadyState
+ * @type {0|1|2}
+ * @readonly
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev
+ */
+
+/**
+ * The connection has not yet been established, or it was closed and the user
+ * agent is reconnecting.
+ * @type {0}
+ */
+const CONNECTING = 0
+
+/**
+ * The user agent has an open connection and is dispatching events as it
+ * receives them.
+ * @type {1}
+ */
+const OPEN = 1
+
+/**
+ * The connection is not open, and the user agent is not trying to reconnect.
+ * @type {2}
+ */
+const CLOSED = 2
+
+/**
+ * Requests for the element will have their mode set to "cors" and their credentials mode set to "same-origin".
+ * @type {'anonymous'}
+ */
+const ANONYMOUS = 'anonymous'
+
+/**
+ * Requests for the element will have their mode set to "cors" and their credentials mode set to "include".
+ * @type {'use-credentials'}
+ */
+const USE_CREDENTIALS = 'use-credentials'
+
+/**
+ * The EventSource interface is used to receive server-sent events. It
+ * connects to a server over HTTP and receives events in text/event-stream
+ * format without closing the connection.
+ * @extends {EventTarget}
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
+ * @api public
+ */
+class EventSource extends EventTarget {
+ #events = {
+ open: null,
+ error: null,
+ message: null
+ }
+
+ #url
+ #withCredentials = false
+
+ /**
+ * @type {ReadyState}
+ */
+ #readyState = CONNECTING
+
+ #request = null
+ #controller = null
+
+ #dispatcher
+
+ /**
+ * @type {import('./eventsource-stream').eventSourceSettings}
+ */
+ #state
+
+ /**
+ * Creates a new EventSource object.
+ * @param {string} url
+ * @param {EventSourceInit} [eventSourceInitDict={}]
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface
+ */
+ constructor (url, eventSourceInitDict = {}) {
+ // 1. Let ev be a new EventSource object.
+ super()
+
+ webidl.util.markAsUncloneable(this)
+
+ const prefix = 'EventSource constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ if (!experimentalWarned) {
+ experimentalWarned = true
+ process.emitWarning('EventSource is experimental, expect them to change at any time.', {
+ code: 'UNDICI-ES'
+ })
+ }
+
+ url = webidl.converters.USVString(url)
+ eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, 'eventSourceInitDict')
+
+ this.#dispatcher = eventSourceInitDict.node.dispatcher || eventSourceInitDict.dispatcher
+ this.#state = {
+ lastEventId: '',
+ reconnectionTime: eventSourceInitDict.node.reconnectionTime
+ }
+
+ // 2. Let settings be ev's relevant settings object.
+ // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
+ const settings = environmentSettingsObject
+
+ let urlRecord
+
+ try {
+ // 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings.
+ urlRecord = new URL(url, settings.settingsObject.baseUrl)
+ this.#state.origin = urlRecord.origin
+ } catch (e) {
+ // 4. If urlRecord is failure, then throw a "SyntaxError" DOMException.
+ throw new DOMException(e, 'SyntaxError')
+ }
+
+ // 5. Set ev's url to urlRecord.
+ this.#url = urlRecord.href
+
+ // 6. Let corsAttributeState be Anonymous.
+ let corsAttributeState = ANONYMOUS
+
+ // 7. If the value of eventSourceInitDict's withCredentials member is true,
+ // then set corsAttributeState to Use Credentials and set ev's
+ // withCredentials attribute to true.
+ if (eventSourceInitDict.withCredentials === true) {
+ corsAttributeState = USE_CREDENTIALS
+ this.#withCredentials = true
+ }
+
+ // 8. Let request be the result of creating a potential-CORS request given
+ // urlRecord, the empty string, and corsAttributeState.
+ const initRequest = {
+ redirect: 'follow',
+ keepalive: true,
+ // @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
+ mode: 'cors',
+ credentials: corsAttributeState === 'anonymous'
+ ? 'same-origin'
+ : 'omit',
+ referrer: 'no-referrer'
+ }
+
+ // 9. Set request's client to settings.
+ initRequest.client = environmentSettingsObject.settingsObject
+
+ // 10. User agents may set (`Accept`, `text/event-stream`) in request's header list.
+ initRequest.headersList = [['accept', { name: 'accept', value: 'text/event-stream' }]]
+
+ // 11. Set request's cache mode to "no-store".
+ initRequest.cache = 'no-store'
+
+ // 12. Set request's initiator type to "other".
+ initRequest.initiator = 'other'
+
+ initRequest.urlList = [new URL(this.#url)]
+
+ // 13. Set ev's request to request.
+ this.#request = makeRequest(initRequest)
+
+ this.#connect()
+ }
+
+ /**
+ * Returns the state of this EventSource object's connection. It can have the
+ * values described below.
+ * @returns {ReadyState}
+ * @readonly
+ */
+ get readyState () {
+ return this.#readyState
+ }
+
+ /**
+ * Returns the URL providing the event stream.
+ * @readonly
+ * @returns {string}
+ */
+ get url () {
+ return this.#url
+ }
+
+ /**
+ * Returns a boolean indicating whether the EventSource object was
+ * instantiated with CORS credentials set (true), or not (false, the default).
+ */
+ get withCredentials () {
+ return this.#withCredentials
+ }
+
+ #connect () {
+ if (this.#readyState === CLOSED) return
+
+ this.#readyState = CONNECTING
+
+ const fetchParams = {
+ request: this.#request,
+ dispatcher: this.#dispatcher
+ }
+
+ // 14. Let processEventSourceEndOfBody given response res be the following step: if res is not a network error, then reestablish the connection.
+ const processEventSourceEndOfBody = (response) => {
+ if (!isNetworkError(response)) {
+ return this.#reconnect()
+ }
+ }
+
+ // 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody...
+ fetchParams.processResponseEndOfBody = processEventSourceEndOfBody
+
+ // and processResponse set to the following steps given response res:
+ fetchParams.processResponse = (response) => {
+ // 1. If res is an aborted network error, then fail the connection.
+
+ if (isNetworkError(response)) {
+ // 1. When a user agent is to fail the connection, the user agent
+ // must queue a task which, if the readyState attribute is set to a
+ // value other than CLOSED, sets the readyState attribute to CLOSED
+ // and fires an event named error at the EventSource object. Once the
+ // user agent has failed the connection, it does not attempt to
+ // reconnect.
+ if (response.aborted) {
+ this.close()
+ this.dispatchEvent(new Event('error'))
+ return
+ // 2. Otherwise, if res is a network error, then reestablish the
+ // connection, unless the user agent knows that to be futile, in
+ // which case the user agent may fail the connection.
+ } else {
+ this.#reconnect()
+ return
+ }
+ }
+
+ // 3. Otherwise, if res's status is not 200, or if res's `Content-Type`
+ // is not `text/event-stream`, then fail the connection.
+ const contentType = response.headersList.get('content-type', true)
+ const mimeType = contentType !== null ? parseMIMEType(contentType) : 'failure'
+ const contentTypeValid = mimeType !== 'failure' && mimeType.essence === 'text/event-stream'
+ if (
+ response.status !== 200 ||
+ contentTypeValid === false
+ ) {
+ this.close()
+ this.dispatchEvent(new Event('error'))
+ return
+ }
+
+ // 4. Otherwise, announce the connection and interpret res's body
+ // line by line.
+
+ // When a user agent is to announce the connection, the user agent
+ // must queue a task which, if the readyState attribute is set to a
+ // value other than CLOSED, sets the readyState attribute to OPEN
+ // and fires an event named open at the EventSource object.
+ // @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
+ this.#readyState = OPEN
+ this.dispatchEvent(new Event('open'))
+
+ // If redirected to a different origin, set the origin to the new origin.
+ this.#state.origin = response.urlList[response.urlList.length - 1].origin
+
+ const eventSourceStream = new EventSourceStream({
+ eventSourceSettings: this.#state,
+ push: (event) => {
+ this.dispatchEvent(createFastMessageEvent(
+ event.type,
+ event.options
+ ))
+ }
+ })
+
+ pipeline(response.body.stream,
+ eventSourceStream,
+ (error) => {
+ if (
+ error?.aborted === false
+ ) {
+ this.close()
+ this.dispatchEvent(new Event('error'))
+ }
+ })
+ }
+
+ this.#controller = fetching(fetchParams)
+ }
+
+ /**
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
+ * @returns {void}
+ */
+ #reconnect () {
+ // When a user agent is to reestablish the connection, the user agent must
+ // run the following steps. These steps are run in parallel, not as part of
+ // a task. (The tasks that it queues, of course, are run like normal tasks
+ // and not themselves in parallel.)
+
+ // 1. Queue a task to run the following steps:
+
+ // 1. If the readyState attribute is set to CLOSED, abort the task.
+ if (this.#readyState === CLOSED) return
+
+ // 2. Set the readyState attribute to CONNECTING.
+ this.#readyState = CONNECTING
+
+ // 3. Fire an event named error at the EventSource object.
+ this.dispatchEvent(new Event('error'))
+
+ // 2. Wait a delay equal to the reconnection time of the event source.
+ setTimeout(() => {
+ // 5. Queue a task to run the following steps:
+
+ // 1. If the EventSource object's readyState attribute is not set to
+ // CONNECTING, then return.
+ if (this.#readyState !== CONNECTING) return
+
+ // 2. Let request be the EventSource object's request.
+ // 3. If the EventSource object's last event ID string is not the empty
+ // string, then:
+ // 1. Let lastEventIDValue be the EventSource object's last event ID
+ // string, encoded as UTF-8.
+ // 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header
+ // list.
+ if (this.#state.lastEventId.length) {
+ this.#request.headersList.set('last-event-id', this.#state.lastEventId, true)
+ }
+
+ // 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.
+ this.#connect()
+ }, this.#state.reconnectionTime)?.unref()
+ }
+
+ /**
+ * Closes the connection, if any, and sets the readyState attribute to
+ * CLOSED.
+ */
+ close () {
+ webidl.brandCheck(this, EventSource)
+
+ if (this.#readyState === CLOSED) return
+ this.#readyState = CLOSED
+ this.#controller.abort()
+ this.#request = null
+ }
+
+ get onopen () {
+ return this.#events.open
+ }
+
+ set onopen (fn) {
+ if (this.#events.open) {
+ this.removeEventListener('open', this.#events.open)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('open', listener)
+ this.#events.open = fn
+ } else {
+ this.#events.open = null
+ }
+ }
+
+ get onmessage () {
+ return this.#events.message
+ }
+
+ set onmessage (fn) {
+ if (this.#events.message) {
+ this.removeEventListener('message', this.#events.message)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('message', listener)
+ this.#events.message = fn
+ } else {
+ this.#events.message = null
+ }
+ }
+
+ get onerror () {
+ return this.#events.error
+ }
+
+ set onerror (fn) {
+ if (this.#events.error) {
+ this.removeEventListener('error', this.#events.error)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('error', listener)
+ this.#events.error = fn
+ } else {
+ this.#events.error = null
+ }
+ }
+}
+
+const constantsPropertyDescriptors = {
+ CONNECTING: {
+ __proto__: null,
+ configurable: false,
+ enumerable: true,
+ value: CONNECTING,
+ writable: false
+ },
+ OPEN: {
+ __proto__: null,
+ configurable: false,
+ enumerable: true,
+ value: OPEN,
+ writable: false
+ },
+ CLOSED: {
+ __proto__: null,
+ configurable: false,
+ enumerable: true,
+ value: CLOSED,
+ writable: false
+ }
+}
+
+Object.defineProperties(EventSource, constantsPropertyDescriptors)
+Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors)
+
+Object.defineProperties(EventSource.prototype, {
+ close: kEnumerableProperty,
+ onerror: kEnumerableProperty,
+ onmessage: kEnumerableProperty,
+ onopen: kEnumerableProperty,
+ readyState: kEnumerableProperty,
+ url: kEnumerableProperty,
+ withCredentials: kEnumerableProperty
+})
+
+webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
+ {
+ key: 'withCredentials',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'dispatcher', // undici only
+ converter: webidl.converters.any
+ },
+ {
+ key: 'node', // undici only
+ converter: webidl.dictionaryConverter([
+ {
+ key: 'reconnectionTime',
+ converter: webidl.converters['unsigned long'],
+ defaultValue: () => defaultReconnectionTime
+ },
+ {
+ key: 'dispatcher',
+ converter: webidl.converters.any
+ }
+ ]),
+ defaultValue: () => ({})
+ }
+])
+
+module.exports = {
+ EventSource,
+ defaultReconnectionTime
+}
diff --git a/vanilla/node_modules/undici/lib/web/eventsource/util.js b/vanilla/node_modules/undici/lib/web/eventsource/util.js
new file mode 100644
index 0000000..a87cc83
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/eventsource/util.js
@@ -0,0 +1,29 @@
+'use strict'
+
+/**
+ * Checks if the given value is a valid LastEventId.
+ * @param {string} value
+ * @returns {boolean}
+ */
+function isValidLastEventId (value) {
+ // LastEventId should not contain U+0000 NULL
+ return value.indexOf('\u0000') === -1
+}
+
+/**
+ * Checks if the given value is a base 10 digit.
+ * @param {string} value
+ * @returns {boolean}
+ */
+function isASCIINumber (value) {
+ if (value.length === 0) return false
+ for (let i = 0; i < value.length; i++) {
+ if (value.charCodeAt(i) < 0x30 || value.charCodeAt(i) > 0x39) return false
+ }
+ return true
+}
+
+module.exports = {
+ isValidLastEventId,
+ isASCIINumber
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/LICENSE b/vanilla/node_modules/undici/lib/web/fetch/LICENSE
new file mode 100644
index 0000000..2943500
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Ethan Arrowood
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vanilla/node_modules/undici/lib/web/fetch/body.js b/vanilla/node_modules/undici/lib/web/fetch/body.js
new file mode 100644
index 0000000..5d17249
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/body.js
@@ -0,0 +1,509 @@
+'use strict'
+
+const util = require('../../core/util')
+const {
+ ReadableStreamFrom,
+ readableStreamClose,
+ fullyReadBody,
+ extractMimeType
+} = require('./util')
+const { FormData, setFormDataState } = require('./formdata')
+const { webidl } = require('../webidl')
+const assert = require('node:assert')
+const { isErrored, isDisturbed } = require('node:stream')
+const { isUint8Array } = require('node:util/types')
+const { serializeAMimeType } = require('./data-url')
+const { multipartFormDataParser } = require('./formdata-parser')
+const { createDeferredPromise } = require('../../util/promise')
+const { parseJSONFromBytes } = require('../infra')
+const { utf8DecodeBytes } = require('../../encoding')
+const { runtimeFeatures } = require('../../util/runtime-features.js')
+
+const random = runtimeFeatures.has('crypto')
+ ? require('node:crypto').randomInt
+ : (max) => Math.floor(Math.random() * max)
+
+const textEncoder = new TextEncoder()
+function noop () {}
+
+const streamRegistry = new FinalizationRegistry((weakRef) => {
+ const stream = weakRef.deref()
+ if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
+ stream.cancel('Response object has been garbage collected').catch(noop)
+ }
+})
+
+/**
+ * Extract a body with type from a byte sequence or BodyInit object
+ *
+ * @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
+ * @param {boolean} [keepalive=false] - If true, indicates that the body
+ * @returns {[{stream: ReadableStream, source: any, length: number | null}, string | null]} - Returns a tuple containing the body and its type
+ *
+ * @see https://fetch.spec.whatwg.org/#concept-bodyinit-extract
+ */
+function extractBody (object, keepalive = false) {
+ // 1. Let stream be null.
+ let stream = null
+ let controller = null
+
+ // 2. If object is a ReadableStream object, then set stream to object.
+ if (webidl.is.ReadableStream(object)) {
+ stream = object
+ } else if (webidl.is.Blob(object)) {
+ // 3. Otherwise, if object is a Blob object, set stream to the
+ // result of running object’s get stream.
+ stream = object.stream()
+ } else {
+ // 4. Otherwise, set stream to a new ReadableStream object, and set
+ // up stream with byte reading support.
+ stream = new ReadableStream({
+ pull () {},
+ start (c) {
+ controller = c
+ },
+ cancel () {},
+ type: 'bytes'
+ })
+ }
+
+ // 5. Assert: stream is a ReadableStream object.
+ assert(webidl.is.ReadableStream(stream))
+
+ // 6. Let action be null.
+ let action = null
+
+ // 7. Let source be null.
+ let source = null
+
+ // 8. Let length be null.
+ let length = null
+
+ // 9. Let type be null.
+ let type = null
+
+ // 10. Switch on object:
+ if (typeof object === 'string') {
+ // Set source to the UTF-8 encoding of object.
+ // Note: setting source to a Uint8Array here breaks some mocking assumptions.
+ source = object
+
+ // Set type to `text/plain;charset=UTF-8`.
+ type = 'text/plain;charset=UTF-8'
+ } else if (webidl.is.URLSearchParams(object)) {
+ // URLSearchParams
+
+ // spec says to run application/x-www-form-urlencoded on body.list
+ // this is implemented in Node.js as apart of an URLSearchParams instance toString method
+ // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
+ // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
+
+ // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
+ source = object.toString()
+
+ // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
+ type = 'application/x-www-form-urlencoded;charset=UTF-8'
+ } else if (webidl.is.BufferSource(object)) {
+ // Set source to a copy of the bytes held by object.
+ source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
+ } else if (webidl.is.FormData(object)) {
+ const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
+ const prefix = `--${boundary}\r\nContent-Disposition: form-data`
+
+ /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
+ const formdataEscape = (str) =>
+ str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
+ const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
+
+ // Set action to this step: run the multipart/form-data
+ // encoding algorithm, with object’s entry list and UTF-8.
+ // - This ensures that the body is immutable and can't be changed afterwords
+ // - That the content-length is calculated in advance.
+ // - And that all parts are pre-encoded and ready to be sent.
+
+ const blobParts = []
+ const rn = new Uint8Array([13, 10]) // '\r\n'
+ length = 0
+ let hasUnknownSizeValue = false
+
+ for (const [name, value] of object) {
+ if (typeof value === 'string') {
+ const chunk = textEncoder.encode(prefix +
+ `; name="${formdataEscape(normalizeLinefeeds(name))}"` +
+ `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
+ blobParts.push(chunk)
+ length += chunk.byteLength
+ } else {
+ const chunk = textEncoder.encode(`${prefix}; name="${formdataEscape(normalizeLinefeeds(name))}"` +
+ (value.name ? `; filename="${formdataEscape(value.name)}"` : '') + '\r\n' +
+ `Content-Type: ${
+ value.type || 'application/octet-stream'
+ }\r\n\r\n`)
+ blobParts.push(chunk, value, rn)
+ if (typeof value.size === 'number') {
+ length += chunk.byteLength + value.size + rn.byteLength
+ } else {
+ hasUnknownSizeValue = true
+ }
+ }
+ }
+
+ // CRLF is appended to the body to function with legacy servers and match other implementations.
+ // https://github.com/curl/curl/blob/3434c6b46e682452973972e8313613dfa58cd690/lib/mime.c#L1029-L1030
+ // https://github.com/form-data/form-data/issues/63
+ const chunk = textEncoder.encode(`--${boundary}--\r\n`)
+ blobParts.push(chunk)
+ length += chunk.byteLength
+ if (hasUnknownSizeValue) {
+ length = null
+ }
+
+ // Set source to object.
+ source = object
+
+ action = async function * () {
+ for (const part of blobParts) {
+ if (part.stream) {
+ yield * part.stream()
+ } else {
+ yield part
+ }
+ }
+ }
+
+ // Set type to `multipart/form-data; boundary=`,
+ // followed by the multipart/form-data boundary string generated
+ // by the multipart/form-data encoding algorithm.
+ type = `multipart/form-data; boundary=${boundary}`
+ } else if (webidl.is.Blob(object)) {
+ // Blob
+
+ // Set source to object.
+ source = object
+
+ // Set length to object’s size.
+ length = object.size
+
+ // If object’s type attribute is not the empty byte sequence, set
+ // type to its value.
+ if (object.type) {
+ type = object.type
+ }
+ } else if (typeof object[Symbol.asyncIterator] === 'function') {
+ // If keepalive is true, then throw a TypeError.
+ if (keepalive) {
+ throw new TypeError('keepalive')
+ }
+
+ // If object is disturbed or locked, then throw a TypeError.
+ if (util.isDisturbed(object) || object.locked) {
+ throw new TypeError(
+ 'Response body object should not be disturbed or locked'
+ )
+ }
+
+ stream =
+ webidl.is.ReadableStream(object) ? object : ReadableStreamFrom(object)
+ }
+
+ // 11. If source is a byte sequence, then set action to a
+ // step that returns source and length to source’s length.
+ if (typeof source === 'string' || isUint8Array(source)) {
+ action = () => {
+ length = typeof source === 'string' ? Buffer.byteLength(source) : source.length
+ return source
+ }
+ }
+
+ // 12. If action is non-null, then run these steps in parallel:
+ if (action != null) {
+ ;(async () => {
+ // 1. Run action.
+ const result = action()
+
+ // 2. Whenever one or more bytes are available and stream is not errored,
+ // enqueue the result of creating a Uint8Array from the available bytes into stream.
+ const iterator = result?.[Symbol.asyncIterator]?.()
+ if (iterator) {
+ for await (const bytes of iterator) {
+ if (isErrored(stream)) break
+ if (bytes.length) {
+ controller.enqueue(new Uint8Array(bytes))
+ }
+ }
+ } else if (result?.length && !isErrored(stream)) {
+ controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result))
+ }
+
+ // 3. When running action is done, close stream.
+ queueMicrotask(() => readableStreamClose(controller))
+ })()
+ }
+
+ // 13. Let body be a body whose stream is stream, source is source,
+ // and length is length.
+ const body = { stream, source, length }
+
+ // 14. Return (body, type).
+ return [body, type]
+}
+
+/**
+ * @typedef {object} ExtractBodyResult
+ * @property {ReadableStream<Uint8Array<ArrayBuffer>>} stream - The ReadableStream containing the body data
+ * @property {any} source - The original source of the body data
+ * @property {number | null} length - The length of the body data, or null
+ */
+
+/**
+ * Safely extract a body with type from a byte sequence or BodyInit object.
+ *
+ * @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
+ * @param {boolean} [keepalive=false] - If true, indicates that the body
+ * @returns {[ExtractBodyResult, string | null]} - Returns a tuple containing the body and its type
+ *
+ * @see https://fetch.spec.whatwg.org/#bodyinit-safely-extract
+ */
+function safelyExtractBody (object, keepalive = false) {
+ // To safely extract a body and a `Content-Type` value from
+ // a byte sequence or BodyInit object object, run these steps:
+
+ // 1. If object is a ReadableStream object, then:
+ if (webidl.is.ReadableStream(object)) {
+ // Assert: object is neither disturbed nor locked.
+ assert(!util.isDisturbed(object), 'The body has already been consumed.')
+ assert(!object.locked, 'The stream is locked.')
+ }
+
+ // 2. Return the results of extracting object.
+ return extractBody(object, keepalive)
+}
+
+function cloneBody (body) {
+ // To clone a body body, run these steps:
+
+ // https://fetch.spec.whatwg.org/#concept-body-clone
+
+ // 1. Let « out1, out2 » be the result of teeing body’s stream.
+ const { 0: out1, 1: out2 } = body.stream.tee()
+
+ // 2. Set body’s stream to out1.
+ body.stream = out1
+
+ // 3. Return a body whose stream is out2 and other members are copied from body.
+ return {
+ stream: out2,
+ length: body.length,
+ source: body.source
+ }
+}
+
+function bodyMixinMethods (instance, getInternalState) {
+ const methods = {
+ blob () {
+ // The blob() method steps are to return the result of
+ // running consume body with this and the following step
+ // given a byte sequence bytes: return a Blob whose
+ // contents are bytes and whose type attribute is this’s
+ // MIME type.
+ return consumeBody(this, (bytes) => {
+ let mimeType = bodyMimeType(getInternalState(this))
+
+ if (mimeType === null) {
+ mimeType = ''
+ } else if (mimeType) {
+ mimeType = serializeAMimeType(mimeType)
+ }
+
+ // Return a Blob whose contents are bytes and type attribute
+ // is mimeType.
+ return new Blob([bytes], { type: mimeType })
+ }, instance, getInternalState)
+ },
+
+ arrayBuffer () {
+ // The arrayBuffer() method steps are to return the result
+ // of running consume body with this and the following step
+ // given a byte sequence bytes: return a new ArrayBuffer
+ // whose contents are bytes.
+ return consumeBody(this, (bytes) => {
+ return new Uint8Array(bytes).buffer
+ }, instance, getInternalState)
+ },
+
+ text () {
+ // The text() method steps are to return the result of running
+ // consume body with this and UTF-8 decode.
+ return consumeBody(this, utf8DecodeBytes, instance, getInternalState)
+ },
+
+ json () {
+ // The json() method steps are to return the result of running
+ // consume body with this and parse JSON from bytes.
+ return consumeBody(this, parseJSONFromBytes, instance, getInternalState)
+ },
+
+ formData () {
+ // The formData() method steps are to return the result of running
+ // consume body with this and the following step given a byte sequence bytes:
+ return consumeBody(this, (value) => {
+ // 1. Let mimeType be the result of get the MIME type with this.
+ const mimeType = bodyMimeType(getInternalState(this))
+
+ // 2. If mimeType is non-null, then switch on mimeType’s essence and run
+ // the corresponding steps:
+ if (mimeType !== null) {
+ switch (mimeType.essence) {
+ case 'multipart/form-data': {
+ // 1. ... [long step]
+ // 2. If that fails for some reason, then throw a TypeError.
+ const parsed = multipartFormDataParser(value, mimeType)
+
+ // 3. Return a new FormData object, appending each entry,
+ // resulting from the parsing operation, to its entry list.
+ const fd = new FormData()
+ setFormDataState(fd, parsed)
+
+ return fd
+ }
+ case 'application/x-www-form-urlencoded': {
+ // 1. Let entries be the result of parsing bytes.
+ const entries = new URLSearchParams(value.toString())
+
+ // 2. If entries is failure, then throw a TypeError.
+
+ // 3. Return a new FormData object whose entry list is entries.
+ const fd = new FormData()
+
+ for (const [name, value] of entries) {
+ fd.append(name, value)
+ }
+
+ return fd
+ }
+ }
+ }
+
+ // 3. Throw a TypeError.
+ throw new TypeError(
+ 'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
+ )
+ }, instance, getInternalState)
+ },
+
+ bytes () {
+ // The bytes() method steps are to return the result of running consume body
+ // with this and the following step given a byte sequence bytes: return the
+ // result of creating a Uint8Array from bytes in this’s relevant realm.
+ return consumeBody(this, (bytes) => {
+ return new Uint8Array(bytes)
+ }, instance, getInternalState)
+ }
+ }
+
+ return methods
+}
+
+function mixinBody (prototype, getInternalState) {
+ Object.assign(prototype.prototype, bodyMixinMethods(prototype, getInternalState))
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-body-consume-body
+ * @param {any} object internal state
+ * @param {(value: unknown) => unknown} convertBytesToJSValue
+ * @param {any} instance
+ * @param {(target: any) => any} getInternalState
+ */
+function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
+ try {
+ webidl.brandCheck(object, instance)
+ } catch (e) {
+ return Promise.reject(e)
+ }
+
+ object = getInternalState(object)
+
+ // 1. If object is unusable, then return a promise rejected
+ // with a TypeError.
+ if (bodyUnusable(object)) {
+ return Promise.reject(new TypeError('Body is unusable: Body has already been read'))
+ }
+
+ // 2. Let promise be a new promise.
+ const promise = createDeferredPromise()
+
+ // 3. Let errorSteps given error be to reject promise with error.
+ const errorSteps = promise.reject
+
+ // 4. Let successSteps given a byte sequence data be to resolve
+ // promise with the result of running convertBytesToJSValue
+ // with data. If that threw an exception, then run errorSteps
+ // with that exception.
+ const successSteps = (data) => {
+ try {
+ promise.resolve(convertBytesToJSValue(data))
+ } catch (e) {
+ errorSteps(e)
+ }
+ }
+
+ // 5. If object’s body is null, then run successSteps with an
+ // empty byte sequence.
+ if (object.body == null) {
+ successSteps(Buffer.allocUnsafe(0))
+ return promise.promise
+ }
+
+ // 6. Otherwise, fully read object’s body given successSteps,
+ // errorSteps, and object’s relevant global object.
+ fullyReadBody(object.body, successSteps, errorSteps)
+
+ // 7. Return promise.
+ return promise.promise
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#body-unusable
+ * @param {any} object internal state
+ */
+function bodyUnusable (object) {
+ const body = object.body
+
+ // An object including the Body interface mixin is
+ // said to be unusable if its body is non-null and
+ // its body’s stream is disturbed or locked.
+ return body != null && (body.stream.locked || util.isDisturbed(body.stream))
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-body-mime-type
+ * @param {any} requestOrResponse internal state
+ */
+function bodyMimeType (requestOrResponse) {
+ // 1. Let headers be null.
+ // 2. If requestOrResponse is a Request object, then set headers to requestOrResponse’s request’s header list.
+ // 3. Otherwise, set headers to requestOrResponse’s response’s header list.
+ /** @type {import('./headers').HeadersList} */
+ const headers = requestOrResponse.headersList
+
+ // 4. Let mimeType be the result of extracting a MIME type from headers.
+ const mimeType = extractMimeType(headers)
+
+ // 5. If mimeType is failure, then return null.
+ if (mimeType === 'failure') {
+ return null
+ }
+
+ // 6. Return mimeType.
+ return mimeType
+}
+
+module.exports = {
+ extractBody,
+ safelyExtractBody,
+ cloneBody,
+ mixinBody,
+ streamRegistry,
+ bodyUnusable
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/constants.js b/vanilla/node_modules/undici/lib/web/fetch/constants.js
new file mode 100644
index 0000000..ef63b0c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/constants.js
@@ -0,0 +1,131 @@
+'use strict'
+
+const corsSafeListedMethods = /** @type {const} */ (['GET', 'HEAD', 'POST'])
+const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
+
+const nullBodyStatus = /** @type {const} */ ([101, 204, 205, 304])
+
+const redirectStatus = /** @type {const} */ ([301, 302, 303, 307, 308])
+const redirectStatusSet = new Set(redirectStatus)
+
+/**
+ * @see https://fetch.spec.whatwg.org/#block-bad-port
+ */
+const badPorts = /** @type {const} */ ([
+ '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
+ '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
+ '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
+ '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
+ '2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679',
+ '6697', '10080'
+])
+const badPortsSet = new Set(badPorts)
+
+/**
+ * @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header
+ */
+const referrerPolicyTokens = /** @type {const} */ ([
+ 'no-referrer',
+ 'no-referrer-when-downgrade',
+ 'same-origin',
+ 'origin',
+ 'strict-origin',
+ 'origin-when-cross-origin',
+ 'strict-origin-when-cross-origin',
+ 'unsafe-url'
+])
+
+/**
+ * @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
+ */
+const referrerPolicy = /** @type {const} */ ([
+ '',
+ ...referrerPolicyTokens
+])
+const referrerPolicyTokensSet = new Set(referrerPolicyTokens)
+
+const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error'])
+
+const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE'])
+const safeMethodsSet = new Set(safeMethods)
+
+const requestMode = /** @type {const} */ (['navigate', 'same-origin', 'no-cors', 'cors'])
+
+const requestCredentials = /** @type {const} */ (['omit', 'same-origin', 'include'])
+
+const requestCache = /** @type {const} */ ([
+ 'default',
+ 'no-store',
+ 'reload',
+ 'no-cache',
+ 'force-cache',
+ 'only-if-cached'
+])
+
+/**
+ * @see https://fetch.spec.whatwg.org/#request-body-header-name
+ */
+const requestBodyHeader = /** @type {const} */ ([
+ 'content-encoding',
+ 'content-language',
+ 'content-location',
+ 'content-type',
+ // See https://github.com/nodejs/undici/issues/2021
+ // 'Content-Length' is a forbidden header name, which is typically
+ // removed in the Headers implementation. However, undici doesn't
+ // filter out headers, so we add it here.
+ 'content-length'
+])
+
+/**
+ * @see https://fetch.spec.whatwg.org/#enumdef-requestduplex
+ */
+const requestDuplex = /** @type {const} */ ([
+ 'half'
+])
+
+/**
+ * @see http://fetch.spec.whatwg.org/#forbidden-method
+ */
+const forbiddenMethods = /** @type {const} */ (['CONNECT', 'TRACE', 'TRACK'])
+const forbiddenMethodsSet = new Set(forbiddenMethods)
+
+const subresource = /** @type {const} */ ([
+ 'audio',
+ 'audioworklet',
+ 'font',
+ 'image',
+ 'manifest',
+ 'paintworklet',
+ 'script',
+ 'style',
+ 'track',
+ 'video',
+ 'xslt',
+ ''
+])
+const subresourceSet = new Set(subresource)
+
+module.exports = {
+ subresource,
+ forbiddenMethods,
+ requestBodyHeader,
+ referrerPolicy,
+ requestRedirect,
+ requestMode,
+ requestCredentials,
+ requestCache,
+ redirectStatus,
+ corsSafeListedMethods,
+ nullBodyStatus,
+ safeMethods,
+ badPorts,
+ requestDuplex,
+ subresourceSet,
+ badPortsSet,
+ redirectStatusSet,
+ corsSafeListedMethodsSet,
+ safeMethodsSet,
+ forbiddenMethodsSet,
+ referrerPolicyTokens: referrerPolicyTokensSet
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/data-url.js b/vanilla/node_modules/undici/lib/web/fetch/data-url.js
new file mode 100644
index 0000000..27ad7ae
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/data-url.js
@@ -0,0 +1,596 @@
+'use strict'
+
+const assert = require('node:assert')
+const { forgivingBase64, collectASequenceOfCodePoints, collectASequenceOfCodePointsFast, isomorphicDecode, removeASCIIWhitespace, removeChars } = require('../infra')
+
+const encoder = new TextEncoder()
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#http-token-code-point
+ */
+const HTTP_TOKEN_CODEPOINTS = /^[-!#$%&'*+.^_|~A-Za-z0-9]+$/u
+const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/u // eslint-disable-line
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
+ */
+const HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/u // eslint-disable-line
+
+// https://fetch.spec.whatwg.org/#data-url-processor
+/** @param {URL} dataURL */
+function dataURLProcessor (dataURL) {
+ // 1. Assert: dataURL’s scheme is "data".
+ assert(dataURL.protocol === 'data:')
+
+ // 2. Let input be the result of running the URL
+ // serializer on dataURL with exclude fragment
+ // set to true.
+ let input = URLSerializer(dataURL, true)
+
+ // 3. Remove the leading "data:" string from input.
+ input = input.slice(5)
+
+ // 4. Let position point at the start of input.
+ const position = { position: 0 }
+
+ // 5. Let mimeType be the result of collecting a
+ // sequence of code points that are not equal
+ // to U+002C (,), given position.
+ let mimeType = collectASequenceOfCodePointsFast(
+ ',',
+ input,
+ position
+ )
+
+ // 6. Strip leading and trailing ASCII whitespace
+ // from mimeType.
+ // Undici implementation note: we need to store the
+ // length because if the mimetype has spaces removed,
+ // the wrong amount will be sliced from the input in
+ // step #9
+ const mimeTypeLength = mimeType.length
+ mimeType = removeASCIIWhitespace(mimeType, true, true)
+
+ // 7. If position is past the end of input, then
+ // return failure
+ if (position.position >= input.length) {
+ return 'failure'
+ }
+
+ // 8. Advance position by 1.
+ position.position++
+
+ // 9. Let encodedBody be the remainder of input.
+ const encodedBody = input.slice(mimeTypeLength + 1)
+
+ // 10. Let body be the percent-decoding of encodedBody.
+ let body = stringPercentDecode(encodedBody)
+
+ // 11. If mimeType ends with U+003B (;), followed by
+ // zero or more U+0020 SPACE, followed by an ASCII
+ // case-insensitive match for "base64", then:
+ if (/;(?:\u0020*)base64$/ui.test(mimeType)) {
+ // 1. Let stringBody be the isomorphic decode of body.
+ const stringBody = isomorphicDecode(body)
+
+ // 2. Set body to the forgiving-base64 decode of
+ // stringBody.
+ body = forgivingBase64(stringBody)
+
+ // 3. If body is failure, then return failure.
+ if (body === 'failure') {
+ return 'failure'
+ }
+
+ // 4. Remove the last 6 code points from mimeType.
+ mimeType = mimeType.slice(0, -6)
+
+ // 5. Remove trailing U+0020 SPACE code points from mimeType,
+ // if any.
+ mimeType = mimeType.replace(/(\u0020+)$/u, '')
+
+ // 6. Remove the last U+003B (;) code point from mimeType.
+ mimeType = mimeType.slice(0, -1)
+ }
+
+ // 12. If mimeType starts with U+003B (;), then prepend
+ // "text/plain" to mimeType.
+ if (mimeType.startsWith(';')) {
+ mimeType = 'text/plain' + mimeType
+ }
+
+ // 13. Let mimeTypeRecord be the result of parsing
+ // mimeType.
+ let mimeTypeRecord = parseMIMEType(mimeType)
+
+ // 14. If mimeTypeRecord is failure, then set
+ // mimeTypeRecord to text/plain;charset=US-ASCII.
+ if (mimeTypeRecord === 'failure') {
+ mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII')
+ }
+
+ // 15. Return a new data: URL struct whose MIME
+ // type is mimeTypeRecord and body is body.
+ // https://fetch.spec.whatwg.org/#data-url-struct
+ return { mimeType: mimeTypeRecord, body }
+}
+
+// https://url.spec.whatwg.org/#concept-url-serializer
+/**
+ * @param {URL} url
+ * @param {boolean} excludeFragment
+ */
+function URLSerializer (url, excludeFragment = false) {
+ if (!excludeFragment) {
+ return url.href
+ }
+
+ const href = url.href
+ const hashLength = url.hash.length
+
+ const serialized = hashLength === 0 ? href : href.substring(0, href.length - hashLength)
+
+ if (!hashLength && href.endsWith('#')) {
+ return serialized.slice(0, -1)
+ }
+
+ return serialized
+}
+
+// https://url.spec.whatwg.org/#string-percent-decode
+/** @param {string} input */
+function stringPercentDecode (input) {
+ // 1. Let bytes be the UTF-8 encoding of input.
+ const bytes = encoder.encode(input)
+
+ // 2. Return the percent-decoding of bytes.
+ return percentDecode(bytes)
+}
+
+/**
+ * @param {number} byte
+ */
+function isHexCharByte (byte) {
+ // 0-9 A-F a-f
+ return (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x46) || (byte >= 0x61 && byte <= 0x66)
+}
+
+/**
+ * @param {number} byte
+ */
+function hexByteToNumber (byte) {
+ return (
+ // 0-9
+ byte >= 0x30 && byte <= 0x39
+ ? (byte - 48)
+ // Convert to uppercase
+ // ((byte & 0xDF) - 65) + 10
+ : ((byte & 0xDF) - 55)
+ )
+}
+
+// https://url.spec.whatwg.org/#percent-decode
+/** @param {Uint8Array} input */
+function percentDecode (input) {
+ const length = input.length
+ // 1. Let output be an empty byte sequence.
+ /** @type {Uint8Array} */
+ const output = new Uint8Array(length)
+ let j = 0
+ let i = 0
+ // 2. For each byte byte in input:
+ while (i < length) {
+ const byte = input[i]
+
+ // 1. If byte is not 0x25 (%), then append byte to output.
+ if (byte !== 0x25) {
+ output[j++] = byte
+
+ // 2. Otherwise, if byte is 0x25 (%) and the next two bytes
+ // after byte in input are not in the ranges
+ // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F),
+ // and 0x61 (a) to 0x66 (f), all inclusive, append byte
+ // to output.
+ } else if (
+ byte === 0x25 &&
+ !(isHexCharByte(input[i + 1]) && isHexCharByte(input[i + 2]))
+ ) {
+ output[j++] = 0x25
+
+ // 3. Otherwise:
+ } else {
+ // 1. Let bytePoint be the two bytes after byte in input,
+ // decoded, and then interpreted as hexadecimal number.
+ // 2. Append a byte whose value is bytePoint to output.
+ output[j++] = (hexByteToNumber(input[i + 1]) << 4) | hexByteToNumber(input[i + 2])
+
+ // 3. Skip the next two bytes in input.
+ i += 2
+ }
+ ++i
+ }
+
+ // 3. Return output.
+ return length === j ? output : output.subarray(0, j)
+}
+
+// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+/** @param {string} input */
+function parseMIMEType (input) {
+ // 1. Remove any leading and trailing HTTP whitespace
+ // from input.
+ input = removeHTTPWhitespace(input, true, true)
+
+ // 2. Let position be a position variable for input,
+ // initially pointing at the start of input.
+ const position = { position: 0 }
+
+ // 3. Let type be the result of collecting a sequence
+ // of code points that are not U+002F (/) from
+ // input, given position.
+ const type = collectASequenceOfCodePointsFast(
+ '/',
+ input,
+ position
+ )
+
+ // 4. If type is the empty string or does not solely
+ // contain HTTP token code points, then return failure.
+ // https://mimesniff.spec.whatwg.org/#http-token-code-point
+ if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) {
+ return 'failure'
+ }
+
+ // 5. If position is past the end of input, then return
+ // failure
+ if (position.position >= input.length) {
+ return 'failure'
+ }
+
+ // 6. Advance position by 1. (This skips past U+002F (/).)
+ position.position++
+
+ // 7. Let subtype be the result of collecting a sequence of
+ // code points that are not U+003B (;) from input, given
+ // position.
+ let subtype = collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 8. Remove any trailing HTTP whitespace from subtype.
+ subtype = removeHTTPWhitespace(subtype, false, true)
+
+ // 9. If subtype is the empty string or does not solely
+ // contain HTTP token code points, then return failure.
+ if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) {
+ return 'failure'
+ }
+
+ const typeLowercase = type.toLowerCase()
+ const subtypeLowercase = subtype.toLowerCase()
+
+ // 10. Let mimeType be a new MIME type record whose type
+ // is type, in ASCII lowercase, and subtype is subtype,
+ // in ASCII lowercase.
+ // https://mimesniff.spec.whatwg.org/#mime-type
+ const mimeType = {
+ type: typeLowercase,
+ subtype: subtypeLowercase,
+ /** @type {Map<string, string>} */
+ parameters: new Map(),
+ // https://mimesniff.spec.whatwg.org/#mime-type-essence
+ essence: `${typeLowercase}/${subtypeLowercase}`
+ }
+
+ // 11. While position is not past the end of input:
+ while (position.position < input.length) {
+ // 1. Advance position by 1. (This skips past U+003B (;).)
+ position.position++
+
+ // 2. Collect a sequence of code points that are HTTP
+ // whitespace from input given position.
+ collectASequenceOfCodePoints(
+ // https://fetch.spec.whatwg.org/#http-whitespace
+ char => HTTP_WHITESPACE_REGEX.test(char),
+ input,
+ position
+ )
+
+ // 3. Let parameterName be the result of collecting a
+ // sequence of code points that are not U+003B (;)
+ // or U+003D (=) from input, given position.
+ let parameterName = collectASequenceOfCodePoints(
+ (char) => char !== ';' && char !== '=',
+ input,
+ position
+ )
+
+ // 4. Set parameterName to parameterName, in ASCII
+ // lowercase.
+ parameterName = parameterName.toLowerCase()
+
+ // 5. If position is not past the end of input, then:
+ if (position.position < input.length) {
+ // 1. If the code point at position within input is
+ // U+003B (;), then continue.
+ if (input[position.position] === ';') {
+ continue
+ }
+
+ // 2. Advance position by 1. (This skips past U+003D (=).)
+ position.position++
+ }
+
+ // 6. If position is past the end of input, then break.
+ if (position.position >= input.length) {
+ break
+ }
+
+ // 7. Let parameterValue be null.
+ let parameterValue = null
+
+ // 8. If the code point at position within input is
+ // U+0022 ("), then:
+ if (input[position.position] === '"') {
+ // 1. Set parameterValue to the result of collecting
+ // an HTTP quoted string from input, given position
+ // and the extract-value flag.
+ parameterValue = collectAnHTTPQuotedString(input, position, true)
+
+ // 2. Collect a sequence of code points that are not
+ // U+003B (;) from input, given position.
+ collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 9. Otherwise:
+ } else {
+ // 1. Set parameterValue to the result of collecting
+ // a sequence of code points that are not U+003B (;)
+ // from input, given position.
+ parameterValue = collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 2. Remove any trailing HTTP whitespace from parameterValue.
+ parameterValue = removeHTTPWhitespace(parameterValue, false, true)
+
+ // 3. If parameterValue is the empty string, then continue.
+ if (parameterValue.length === 0) {
+ continue
+ }
+ }
+
+ // 10. If all of the following are true
+ // - parameterName is not the empty string
+ // - parameterName solely contains HTTP token code points
+ // - parameterValue solely contains HTTP quoted-string token code points
+ // - mimeType’s parameters[parameterName] does not exist
+ // then set mimeType’s parameters[parameterName] to parameterValue.
+ if (
+ parameterName.length !== 0 &&
+ HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
+ (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
+ !mimeType.parameters.has(parameterName)
+ ) {
+ mimeType.parameters.set(parameterName, parameterValue)
+ }
+ }
+
+ // 12. Return mimeType.
+ return mimeType
+}
+
+// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
+// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string
+/**
+ * @param {string} input
+ * @param {{ position: number }} position
+ * @param {boolean} [extractValue=false]
+ */
+function collectAnHTTPQuotedString (input, position, extractValue = false) {
+ // 1. Let positionStart be position.
+ const positionStart = position.position
+
+ // 2. Let value be the empty string.
+ let value = ''
+
+ // 3. Assert: the code point at position within input
+ // is U+0022 (").
+ assert(input[position.position] === '"')
+
+ // 4. Advance position by 1.
+ position.position++
+
+ // 5. While true:
+ while (true) {
+ // 1. Append the result of collecting a sequence of code points
+ // that are not U+0022 (") or U+005C (\) from input, given
+ // position, to value.
+ value += collectASequenceOfCodePoints(
+ (char) => char !== '"' && char !== '\\',
+ input,
+ position
+ )
+
+ // 2. If position is past the end of input, then break.
+ if (position.position >= input.length) {
+ break
+ }
+
+ // 3. Let quoteOrBackslash be the code point at position within
+ // input.
+ const quoteOrBackslash = input[position.position]
+
+ // 4. Advance position by 1.
+ position.position++
+
+ // 5. If quoteOrBackslash is U+005C (\), then:
+ if (quoteOrBackslash === '\\') {
+ // 1. If position is past the end of input, then append
+ // U+005C (\) to value and break.
+ if (position.position >= input.length) {
+ value += '\\'
+ break
+ }
+
+ // 2. Append the code point at position within input to value.
+ value += input[position.position]
+
+ // 3. Advance position by 1.
+ position.position++
+
+ // 6. Otherwise:
+ } else {
+ // 1. Assert: quoteOrBackslash is U+0022 (").
+ assert(quoteOrBackslash === '"')
+
+ // 2. Break.
+ break
+ }
+ }
+
+ // 6. If the extract-value flag is set, then return value.
+ if (extractValue) {
+ return value
+ }
+
+ // 7. Return the code points from positionStart to position,
+ // inclusive, within input.
+ return input.slice(positionStart, position.position)
+}
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
+ */
+function serializeAMimeType (mimeType) {
+ assert(mimeType !== 'failure')
+ const { parameters, essence } = mimeType
+
+ // 1. Let serialization be the concatenation of mimeType’s
+ // type, U+002F (/), and mimeType’s subtype.
+ let serialization = essence
+
+ // 2. For each name → value of mimeType’s parameters:
+ for (let [name, value] of parameters.entries()) {
+ // 1. Append U+003B (;) to serialization.
+ serialization += ';'
+
+ // 2. Append name to serialization.
+ serialization += name
+
+ // 3. Append U+003D (=) to serialization.
+ serialization += '='
+
+ // 4. If value does not solely contain HTTP token code
+ // points or value is the empty string, then:
+ if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
+ // 1. Precede each occurrence of U+0022 (") or
+ // U+005C (\) in value with U+005C (\).
+ value = value.replace(/[\\"]/ug, '\\$&')
+
+ // 2. Prepend U+0022 (") to value.
+ value = '"' + value
+
+ // 3. Append U+0022 (") to value.
+ value += '"'
+ }
+
+ // 5. Append value to serialization.
+ serialization += value
+ }
+
+ // 3. Return serialization.
+ return serialization
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-whitespace
+ * @param {number} char
+ */
+function isHTTPWhiteSpace (char) {
+ // "\r\n\t "
+ return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x020
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-whitespace
+ * @param {string} str
+ * @param {boolean} [leading=true]
+ * @param {boolean} [trailing=true]
+ */
+function removeHTTPWhitespace (str, leading = true, trailing = true) {
+ return removeChars(str, leading, trailing, isHTTPWhiteSpace)
+}
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#minimize-a-supported-mime-type
+ * @param {Exclude<ReturnType<typeof parseMIMEType>, 'failure'>} mimeType
+ */
+function minimizeSupportedMimeType (mimeType) {
+ switch (mimeType.essence) {
+ case 'application/ecmascript':
+ case 'application/javascript':
+ case 'application/x-ecmascript':
+ case 'application/x-javascript':
+ case 'text/ecmascript':
+ case 'text/javascript':
+ case 'text/javascript1.0':
+ case 'text/javascript1.1':
+ case 'text/javascript1.2':
+ case 'text/javascript1.3':
+ case 'text/javascript1.4':
+ case 'text/javascript1.5':
+ case 'text/jscript':
+ case 'text/livescript':
+ case 'text/x-ecmascript':
+ case 'text/x-javascript':
+ // 1. If mimeType is a JavaScript MIME type, then return "text/javascript".
+ return 'text/javascript'
+ case 'application/json':
+ case 'text/json':
+ // 2. If mimeType is a JSON MIME type, then return "application/json".
+ return 'application/json'
+ case 'image/svg+xml':
+ // 3. If mimeType’s essence is "image/svg+xml", then return "image/svg+xml".
+ return 'image/svg+xml'
+ case 'text/xml':
+ case 'application/xml':
+ // 4. If mimeType is an XML MIME type, then return "application/xml".
+ return 'application/xml'
+ }
+
+ // 2. If mimeType is a JSON MIME type, then return "application/json".
+ if (mimeType.subtype.endsWith('+json')) {
+ return 'application/json'
+ }
+
+ // 4. If mimeType is an XML MIME type, then return "application/xml".
+ if (mimeType.subtype.endsWith('+xml')) {
+ return 'application/xml'
+ }
+
+ // 5. If mimeType is supported by the user agent, then return mimeType’s essence.
+ // Technically, node doesn't support any mimetypes.
+
+ // 6. Return the empty string.
+ return ''
+}
+
+module.exports = {
+ dataURLProcessor,
+ URLSerializer,
+ stringPercentDecode,
+ parseMIMEType,
+ collectAnHTTPQuotedString,
+ serializeAMimeType,
+ removeHTTPWhitespace,
+ minimizeSupportedMimeType,
+ HTTP_TOKEN_CODEPOINTS
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js b/vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js
new file mode 100644
index 0000000..4ba204c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js
@@ -0,0 +1,575 @@
+'use strict'
+
+const { bufferToLowerCasedHeaderName } = require('../../core/util')
+const { HTTP_TOKEN_CODEPOINTS } = require('./data-url')
+const { makeEntry } = require('./formdata')
+const { webidl } = require('../webidl')
+const assert = require('node:assert')
+const { isomorphicDecode } = require('../infra')
+const { utf8DecodeBytes } = require('../../encoding')
+
+const dd = Buffer.from('--')
+const decoder = new TextDecoder()
+
+/**
+ * @param {string} chars
+ */
+function isAsciiString (chars) {
+ for (let i = 0; i < chars.length; ++i) {
+ if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
+ * @param {string} boundary
+ */
+function validateBoundary (boundary) {
+ const length = boundary.length
+
+ // - its length is greater or equal to 27 and lesser or equal to 70, and
+ if (length < 27 || length > 70) {
+ return false
+ }
+
+ // - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
+ // 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
+ // 0x2D (-) or 0x5F (_).
+ for (let i = 0; i < length; ++i) {
+ const cp = boundary.charCodeAt(i)
+
+ if (!(
+ (cp >= 0x30 && cp <= 0x39) ||
+ (cp >= 0x41 && cp <= 0x5a) ||
+ (cp >= 0x61 && cp <= 0x7a) ||
+ cp === 0x27 ||
+ cp === 0x2d ||
+ cp === 0x5f
+ )) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
+ * @param {Buffer} input
+ * @param {ReturnType<import('./data-url')['parseMIMEType']>} mimeType
+ */
+function multipartFormDataParser (input, mimeType) {
+ // 1. Assert: mimeType’s essence is "multipart/form-data".
+ assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
+
+ const boundaryString = mimeType.parameters.get('boundary')
+
+ // 2. If mimeType’s parameters["boundary"] does not exist, return failure.
+ // Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
+ // parameters["boundary"].
+ if (boundaryString === undefined) {
+ throw parsingError('missing boundary in content-type header')
+ }
+
+ const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
+
+ // 3. Let entry list be an empty entry list.
+ const entryList = []
+
+ // 4. Let position be a pointer to a byte in input, initially pointing at
+ // the first byte.
+ const position = { position: 0 }
+
+ // Note: Per RFC 2046 Section 5.1.1, we must ignore anything before the
+ // first boundary delimiter line (preamble). Search for the first boundary.
+ const firstBoundaryIndex = input.indexOf(boundary)
+
+ if (firstBoundaryIndex === -1) {
+ throw parsingError('no boundary found in multipart body')
+ }
+
+ // Start parsing from the first boundary, ignoring any preamble
+ position.position = firstBoundaryIndex
+
+ // 5. While true:
+ while (true) {
+ // 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
+ // (`--`) followed by boundary, advance position by 2 + the length of
+ // boundary. Otherwise, return failure.
+ // Note: boundary is padded with 2 dashes already, no need to add 2.
+ if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
+ position.position += boundary.length
+ } else {
+ throw parsingError('expected a value starting with -- and the boundary')
+ }
+
+ // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
+ // (`--` followed by CR LF) followed by the end of input, return entry list.
+ // Note: Per RFC 2046 Section 5.1.1, we must ignore anything after the
+ // final boundary delimiter (epilogue). Check for -- or --CRLF and return
+ // regardless of what follows.
+ if (bufferStartsWith(input, dd, position)) {
+ // Found closing boundary delimiter (--), ignore any epilogue
+ return entryList
+ }
+
+ // 5.3. If position does not point to a sequence of bytes starting with 0x0D
+ // 0x0A (CR LF), return failure.
+ if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
+ throw parsingError('expected CRLF')
+ }
+
+ // 5.4. Advance position by 2. (This skips past the newline.)
+ position.position += 2
+
+ // 5.5. Let name, filename and contentType be the result of parsing
+ // multipart/form-data headers on input and position, if the result
+ // is not failure. Otherwise, return failure.
+ const result = parseMultipartFormDataHeaders(input, position)
+
+ let { name, filename, contentType, encoding } = result
+
+ // 5.6. Advance position by 2. (This skips past the empty line that marks
+ // the end of the headers.)
+ position.position += 2
+
+ // 5.7. Let body be the empty byte sequence.
+ let body
+
+ // 5.8. Body loop: While position is not past the end of input:
+ // TODO: the steps here are completely wrong
+ {
+ const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
+
+ if (boundaryIndex === -1) {
+ throw parsingError('expected boundary after body')
+ }
+
+ body = input.subarray(position.position, boundaryIndex - 4)
+
+ position.position += body.length
+
+ // Note: position must be advanced by the body's length before being
+ // decoded, otherwise the parsing will fail.
+ if (encoding === 'base64') {
+ body = Buffer.from(body.toString(), 'base64')
+ }
+ }
+
+ // 5.9. If position does not point to a sequence of bytes starting with
+ // 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
+ if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
+ throw parsingError('expected CRLF')
+ } else {
+ position.position += 2
+ }
+
+ // 5.10. If filename is not null:
+ let value
+
+ if (filename !== null) {
+ // 5.10.1. If contentType is null, set contentType to "text/plain".
+ contentType ??= 'text/plain'
+
+ // 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
+
+ // Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead.
+ // Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`.
+ if (!isAsciiString(contentType)) {
+ contentType = ''
+ }
+
+ // 5.10.3. Let value be a new File object with name filename, type contentType, and body body.
+ value = new File([body], filename, { type: contentType })
+ } else {
+ // 5.11. Otherwise:
+
+ // 5.11.1. Let value be the UTF-8 decoding without BOM of body.
+ value = utf8DecodeBytes(Buffer.from(body))
+ }
+
+ // 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
+ assert(webidl.is.USVString(name))
+ assert((typeof value === 'string' && webidl.is.USVString(value)) || webidl.is.File(value))
+
+ // 5.13. Create an entry with name and value, and append it to entry list.
+ entryList.push(makeEntry(name, value, filename))
+ }
+}
+
+/**
+ * Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
+ * @param {Buffer} input
+ * @param {{ position: number }} position
+ * @returns {{ name: string, value: string }}
+ */
+function parseContentDispositionAttribute (input, position) {
+ // Skip leading semicolon and whitespace
+ if (input[position.position] === 0x3b /* ; */) {
+ position.position++
+ }
+
+ // Skip whitespace
+ collectASequenceOfBytes(
+ (char) => char === 0x20 || char === 0x09,
+ input,
+ position
+ )
+
+ // Collect attribute name (token characters)
+ const attributeName = collectASequenceOfBytes(
+ (char) => isToken(char) && char !== 0x3d && char !== 0x2a, // not = or *
+ input,
+ position
+ )
+
+ if (attributeName.length === 0) {
+ return null
+ }
+
+ const attrNameStr = attributeName.toString('ascii').toLowerCase()
+
+ // Check for extended notation (attribute*)
+ const isExtended = input[position.position] === 0x2a /* * */
+ if (isExtended) {
+ position.position++ // skip *
+ }
+
+ // Expect = sign
+ if (input[position.position] !== 0x3d /* = */) {
+ return null
+ }
+ position.position++ // skip =
+
+ // Skip whitespace
+ collectASequenceOfBytes(
+ (char) => char === 0x20 || char === 0x09,
+ input,
+ position
+ )
+
+ let value
+
+ if (isExtended) {
+ // Extended attribute format: charset'language'encoded-value
+ const headerValue = collectASequenceOfBytes(
+ (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a && char !== 0x3b, // not space, CRLF, or ;
+ input,
+ position
+ )
+
+ // Check for utf-8'' prefix (case insensitive)
+ if (
+ (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
+ (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
+ (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
+ headerValue[3] !== 0x2d || // -
+ headerValue[4] !== 0x38 // 8
+ ) {
+ throw parsingError('unknown encoding, expected utf-8\'\'')
+ }
+
+ // Skip utf-8'' and decode the rest
+ value = decodeURIComponent(decoder.decode(headerValue.subarray(7)))
+ } else if (input[position.position] === 0x22 /* " */) {
+ // Quoted string
+ position.position++ // skip opening quote
+
+ const quotedValue = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d && char !== 0x22, // not LF, CR, or "
+ input,
+ position
+ )
+
+ if (input[position.position] !== 0x22) {
+ throw parsingError('Closing quote not found')
+ }
+ position.position++ // skip closing quote
+
+ value = decoder.decode(quotedValue)
+ .replace(/%0A/ig, '\n')
+ .replace(/%0D/ig, '\r')
+ .replace(/%22/g, '"')
+ } else {
+ // Token value (no quotes)
+ const tokenValue = collectASequenceOfBytes(
+ (char) => isToken(char) && char !== 0x3b, // not ;
+ input,
+ position
+ )
+
+ value = decoder.decode(tokenValue)
+ }
+
+ return { name: attrNameStr, value }
+}
+
+/**
+ * @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
+ * @param {Buffer} input
+ * @param {{ position: number }} position
+ */
+function parseMultipartFormDataHeaders (input, position) {
+ // 1. Let name, filename and contentType be null.
+ let name = null
+ let filename = null
+ let contentType = null
+ let encoding = null
+
+ // 2. While true:
+ while (true) {
+ // 2.1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF):
+ if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
+ // 2.1.1. If name is null, return failure.
+ if (name === null) {
+ throw parsingError('header name is null')
+ }
+
+ // 2.1.2. Return name, filename and contentType.
+ return { name, filename, contentType, encoding }
+ }
+
+ // 2.2. Let header name be the result of collecting a sequence of bytes that are
+ // not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position.
+ let headerName = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d && char !== 0x3a,
+ input,
+ position
+ )
+
+ // 2.3. Remove any HTTP tab or space bytes from the start or end of header name.
+ headerName = removeChars(headerName, true, true, (char) => char === 0x9 || char === 0x20)
+
+ // 2.4. If header name does not match the field-name token production, return failure.
+ if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
+ throw parsingError('header name does not match the field-name token production')
+ }
+
+ // 2.5. If the byte at position is not 0x3A (:), return failure.
+ if (input[position.position] !== 0x3a) {
+ throw parsingError('expected :')
+ }
+
+ // 2.6. Advance position by 1.
+ position.position++
+
+ // 2.7. Collect a sequence of bytes that are HTTP tab or space bytes given position.
+ // (Do nothing with those bytes.)
+ collectASequenceOfBytes(
+ (char) => char === 0x20 || char === 0x09,
+ input,
+ position
+ )
+
+ // 2.8. Byte-lowercase header name and switch on the result:
+ switch (bufferToLowerCasedHeaderName(headerName)) {
+ case 'content-disposition': {
+ name = filename = null
+
+ // Collect the disposition type (should be "form-data")
+ const dispositionType = collectASequenceOfBytes(
+ (char) => isToken(char),
+ input,
+ position
+ )
+
+ if (dispositionType.toString('ascii').toLowerCase() !== 'form-data') {
+ throw parsingError('expected form-data for content-disposition header')
+ }
+
+ // Parse attributes recursively until CRLF
+ while (
+ position.position < input.length &&
+ input[position.position] !== 0x0d &&
+ input[position.position + 1] !== 0x0a
+ ) {
+ const attribute = parseContentDispositionAttribute(input, position)
+
+ if (!attribute) {
+ break
+ }
+
+ if (attribute.name === 'name') {
+ name = attribute.value
+ } else if (attribute.name === 'filename') {
+ filename = attribute.value
+ }
+ }
+
+ if (name === null) {
+ throw parsingError('name attribute is required in content-disposition header')
+ }
+
+ break
+ }
+ case 'content-type': {
+ // 1. Let header value be the result of collecting a sequence of bytes that are
+ // not 0x0A (LF) or 0x0D (CR), given position.
+ let headerValue = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d,
+ input,
+ position
+ )
+
+ // 2. Remove any HTTP tab or space bytes from the end of header value.
+ headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
+
+ // 3. Set contentType to the isomorphic decoding of header value.
+ contentType = isomorphicDecode(headerValue)
+
+ break
+ }
+ case 'content-transfer-encoding': {
+ let headerValue = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d,
+ input,
+ position
+ )
+
+ headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
+
+ encoding = isomorphicDecode(headerValue)
+
+ break
+ }
+ default: {
+ // Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
+ // (Do nothing with those bytes.)
+ collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d,
+ input,
+ position
+ )
+ }
+ }
+
+ // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
+ // (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
+ if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
+ throw parsingError('expected CRLF')
+ } else {
+ position.position += 2
+ }
+ }
+}
+
+/**
+ * @param {(char: number) => boolean} condition
+ * @param {Buffer} input
+ * @param {{ position: number }} position
+ */
+function collectASequenceOfBytes (condition, input, position) {
+ let start = position.position
+
+ while (start < input.length && condition(input[start])) {
+ ++start
+ }
+
+ return input.subarray(position.position, (position.position = start))
+}
+
+/**
+ * @param {Buffer} buf
+ * @param {boolean} leading
+ * @param {boolean} trailing
+ * @param {(charCode: number) => boolean} predicate
+ * @returns {Buffer}
+ */
+function removeChars (buf, leading, trailing, predicate) {
+ let lead = 0
+ let trail = buf.length - 1
+
+ if (leading) {
+ while (lead < buf.length && predicate(buf[lead])) lead++
+ }
+
+ if (trailing) {
+ while (trail > 0 && predicate(buf[trail])) trail--
+ }
+
+ return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1)
+}
+
+/**
+ * Checks if {@param buffer} starts with {@param start}
+ * @param {Buffer} buffer
+ * @param {Buffer} start
+ * @param {{ position: number }} position
+ */
+function bufferStartsWith (buffer, start, position) {
+ if (buffer.length < start.length) {
+ return false
+ }
+
+ for (let i = 0; i < start.length; i++) {
+ if (start[i] !== buffer[position.position + i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+function parsingError (cause) {
+ return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
+}
+
+/**
+ * CTL = <any US-ASCII control character
+ * (octets 0 - 31) and DEL (127)>
+ * @param {number} char
+ */
+function isCTL (char) {
+ return char <= 0x1f || char === 0x7f
+}
+
+/**
+ * tspecials := "(" / ")" / "<" / ">" / "@" /
+ * "," / ";" / ":" / "\" / <">
+ * "/" / "[" / "]" / "?" / "="
+ * ; Must be in quoted-string,
+ * ; to use within parameter values
+ * @param {number} char
+ */
+function isTSpecial (char) {
+ return (
+ char === 0x28 || // (
+ char === 0x29 || // )
+ char === 0x3c || // <
+ char === 0x3e || // >
+ char === 0x40 || // @
+ char === 0x2c || // ,
+ char === 0x3b || // ;
+ char === 0x3a || // :
+ char === 0x5c || // \
+ char === 0x22 || // "
+ char === 0x2f || // /
+ char === 0x5b || // [
+ char === 0x5d || // ]
+ char === 0x3f || // ?
+ char === 0x3d // +
+ )
+}
+
+/**
+ * token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
+ * or tspecials>
+ * @param {number} char
+ */
+function isToken (char) {
+ return (
+ char <= 0x7f && // ascii
+ char !== 0x20 && // space
+ char !== 0x09 &&
+ !isCTL(char) &&
+ !isTSpecial(char)
+ )
+}
+
+module.exports = {
+ multipartFormDataParser,
+ validateBoundary
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/formdata.js b/vanilla/node_modules/undici/lib/web/fetch/formdata.js
new file mode 100644
index 0000000..c21fb06
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/formdata.js
@@ -0,0 +1,259 @@
+'use strict'
+
+const { iteratorMixin } = require('./util')
+const { kEnumerableProperty } = require('../../core/util')
+const { webidl } = require('../webidl')
+const nodeUtil = require('node:util')
+
+// https://xhr.spec.whatwg.org/#formdata
+class FormData {
+ #state = []
+
+ constructor (form = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (form !== undefined) {
+ throw webidl.errors.conversionFailed({
+ prefix: 'FormData constructor',
+ argument: 'Argument 1',
+ types: ['undefined']
+ })
+ }
+ }
+
+ append (name, value, filename = undefined) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.append'
+ webidl.argumentLengthCheck(arguments, 2, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ if (arguments.length === 3 || webidl.is.Blob(value)) {
+ value = webidl.converters.Blob(value, prefix, 'value')
+
+ if (filename !== undefined) {
+ filename = webidl.converters.USVString(filename)
+ }
+ } else {
+ value = webidl.converters.USVString(value)
+ }
+
+ // 1. Let value be value if given; otherwise blobValue.
+
+ // 2. Let entry be the result of creating an entry with
+ // name, value, and filename if given.
+ const entry = makeEntry(name, value, filename)
+
+ // 3. Append entry to this’s entry list.
+ this.#state.push(entry)
+ }
+
+ delete (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.delete'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ // The delete(name) method steps are to remove all entries whose name
+ // is name from this’s entry list.
+ this.#state = this.#state.filter(entry => entry.name !== name)
+ }
+
+ get (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.get'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ // 1. If there is no entry whose name is name in this’s entry list,
+ // then return null.
+ const idx = this.#state.findIndex((entry) => entry.name === name)
+ if (idx === -1) {
+ return null
+ }
+
+ // 2. Return the value of the first entry whose name is name from
+ // this’s entry list.
+ return this.#state[idx].value
+ }
+
+ getAll (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.getAll'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ // 1. If there is no entry whose name is name in this’s entry list,
+ // then return the empty list.
+ // 2. Return the values of all entries whose name is name, in order,
+ // from this’s entry list.
+ return this.#state
+ .filter((entry) => entry.name === name)
+ .map((entry) => entry.value)
+ }
+
+ has (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.has'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ // The has(name) method steps are to return true if there is an entry
+ // whose name is name in this’s entry list; otherwise false.
+ return this.#state.findIndex((entry) => entry.name === name) !== -1
+ }
+
+ set (name, value, filename = undefined) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.set'
+ webidl.argumentLengthCheck(arguments, 2, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ if (arguments.length === 3 || webidl.is.Blob(value)) {
+ value = webidl.converters.Blob(value, prefix, 'value')
+
+ if (filename !== undefined) {
+ filename = webidl.converters.USVString(filename)
+ }
+ } else {
+ value = webidl.converters.USVString(value)
+ }
+
+ // The set(name, value) and set(name, blobValue, filename) method steps
+ // are:
+
+ // 1. Let value be value if given; otherwise blobValue.
+
+ // 2. Let entry be the result of creating an entry with name, value, and
+ // filename if given.
+ const entry = makeEntry(name, value, filename)
+
+ // 3. If there are entries in this’s entry list whose name is name, then
+ // replace the first such entry with entry and remove the others.
+ const idx = this.#state.findIndex((entry) => entry.name === name)
+ if (idx !== -1) {
+ this.#state = [
+ ...this.#state.slice(0, idx),
+ entry,
+ ...this.#state.slice(idx + 1).filter((entry) => entry.name !== name)
+ ]
+ } else {
+ // 4. Otherwise, append entry to this’s entry list.
+ this.#state.push(entry)
+ }
+ }
+
+ [nodeUtil.inspect.custom] (depth, options) {
+ const state = this.#state.reduce((a, b) => {
+ if (a[b.name]) {
+ if (Array.isArray(a[b.name])) {
+ a[b.name].push(b.value)
+ } else {
+ a[b.name] = [a[b.name], b.value]
+ }
+ } else {
+ a[b.name] = b.value
+ }
+
+ return a
+ }, { __proto__: null })
+
+ options.depth ??= depth
+ options.colors ??= true
+
+ const output = nodeUtil.formatWithOptions(options, state)
+
+ // remove [Object null prototype]
+ return `FormData ${output.slice(output.indexOf(']') + 2)}`
+ }
+
+ /**
+ * @param {FormData} formData
+ */
+ static getFormDataState (formData) {
+ return formData.#state
+ }
+
+ /**
+ * @param {FormData} formData
+ * @param {any[]} newState
+ */
+ static setFormDataState (formData, newState) {
+ formData.#state = newState
+ }
+}
+
+const { getFormDataState, setFormDataState } = FormData
+Reflect.deleteProperty(FormData, 'getFormDataState')
+Reflect.deleteProperty(FormData, 'setFormDataState')
+
+iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')
+
+Object.defineProperties(FormData.prototype, {
+ append: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ get: kEnumerableProperty,
+ getAll: kEnumerableProperty,
+ has: kEnumerableProperty,
+ set: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'FormData',
+ configurable: true
+ }
+})
+
+/**
+ * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
+ * @param {string} name
+ * @param {string|Blob} value
+ * @param {?string} filename
+ * @returns
+ */
+function makeEntry (name, value, filename) {
+ // 1. Set name to the result of converting name into a scalar value string.
+ // Note: This operation was done by the webidl converter USVString.
+
+ // 2. If value is a string, then set value to the result of converting
+ // value into a scalar value string.
+ if (typeof value === 'string') {
+ // Note: This operation was done by the webidl converter USVString.
+ } else {
+ // 3. Otherwise:
+
+ // 1. If value is not a File object, then set value to a new File object,
+ // representing the same bytes, whose name attribute value is "blob"
+ if (!webidl.is.File(value)) {
+ value = new File([value], 'blob', { type: value.type })
+ }
+
+ // 2. If filename is given, then set value to a new File object,
+ // representing the same bytes, whose name attribute is filename.
+ if (filename !== undefined) {
+ /** @type {FilePropertyBag} */
+ const options = {
+ type: value.type,
+ lastModified: value.lastModified
+ }
+
+ value = new File([value], filename, options)
+ }
+ }
+
+ // 4. Return an entry whose name is name and whose value is value.
+ return { name, value }
+}
+
+webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)
+
+module.exports = { FormData, makeEntry, setFormDataState }
diff --git a/vanilla/node_modules/undici/lib/web/fetch/global.js b/vanilla/node_modules/undici/lib/web/fetch/global.js
new file mode 100644
index 0000000..1df6f12
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/global.js
@@ -0,0 +1,40 @@
+'use strict'
+
+// In case of breaking changes, increase the version
+// number to avoid conflicts.
+const globalOrigin = Symbol.for('undici.globalOrigin.1')
+
+function getGlobalOrigin () {
+ return globalThis[globalOrigin]
+}
+
+function setGlobalOrigin (newOrigin) {
+ if (newOrigin === undefined) {
+ Object.defineProperty(globalThis, globalOrigin, {
+ value: undefined,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+
+ return
+ }
+
+ const parsedURL = new URL(newOrigin)
+
+ if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
+ throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
+ }
+
+ Object.defineProperty(globalThis, globalOrigin, {
+ value: parsedURL,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+}
+
+module.exports = {
+ getGlobalOrigin,
+ setGlobalOrigin
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/headers.js b/vanilla/node_modules/undici/lib/web/fetch/headers.js
new file mode 100644
index 0000000..024d198
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/headers.js
@@ -0,0 +1,719 @@
+// https://github.com/Ethan-Arrowood/undici-fetch
+
+'use strict'
+
+const { kConstruct } = require('../../core/symbols')
+const { kEnumerableProperty } = require('../../core/util')
+const {
+ iteratorMixin,
+ isValidHeaderName,
+ isValidHeaderValue
+} = require('./util')
+const { webidl } = require('../webidl')
+const assert = require('node:assert')
+const util = require('node:util')
+
+/**
+ * @param {number} code
+ * @returns {code is (0x0a | 0x0d | 0x09 | 0x20)}
+ */
+function isHTTPWhiteSpaceCharCode (code) {
+ return code === 0x0a || code === 0x0d || code === 0x09 || code === 0x20
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
+ * @param {string} potentialValue
+ * @returns {string}
+ */
+function headerValueNormalize (potentialValue) {
+ // To normalize a byte sequence potentialValue, remove
+ // any leading and trailing HTTP whitespace bytes from
+ // potentialValue.
+ let i = 0; let j = potentialValue.length
+
+ while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
+ while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
+
+ return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
+}
+
+/**
+ * @param {Headers} headers
+ * @param {Array|Object} object
+ */
+function fill (headers, object) {
+ // To fill a Headers object headers with a given object object, run these steps:
+
+ // 1. If object is a sequence, then for each header in object:
+ // Note: webidl conversion to array has already been done.
+ if (Array.isArray(object)) {
+ for (let i = 0; i < object.length; ++i) {
+ const header = object[i]
+ // 1. If header does not contain exactly two items, then throw a TypeError.
+ if (header.length !== 2) {
+ throw webidl.errors.exception({
+ header: 'Headers constructor',
+ message: `expected name/value pair to be length 2, found ${header.length}.`
+ })
+ }
+
+ // 2. Append (header’s first item, header’s second item) to headers.
+ appendHeader(headers, header[0], header[1])
+ }
+ } else if (typeof object === 'object' && object !== null) {
+ // Note: null should throw
+
+ // 2. Otherwise, object is a record, then for each key → value in object,
+ // append (key, value) to headers
+ const keys = Object.keys(object)
+ for (let i = 0; i < keys.length; ++i) {
+ appendHeader(headers, keys[i], object[keys[i]])
+ }
+ } else {
+ throw webidl.errors.conversionFailed({
+ prefix: 'Headers constructor',
+ argument: 'Argument 1',
+ types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
+ })
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-headers-append
+ * @param {Headers} headers
+ * @param {string} name
+ * @param {string} value
+ */
+function appendHeader (headers, name, value) {
+ // 1. Normalize value.
+ value = headerValueNormalize(value)
+
+ // 2. If name is not a header name or value is not a
+ // header value, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.append',
+ value: name,
+ type: 'header name'
+ })
+ } else if (!isValidHeaderValue(value)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.append',
+ value,
+ type: 'header value'
+ })
+ }
+
+ // 3. If headers’s guard is "immutable", then throw a TypeError.
+ // 4. Otherwise, if headers’s guard is "request" and name is a
+ // forbidden header name, return.
+ // 5. Otherwise, if headers’s guard is "request-no-cors":
+ // TODO
+ // Note: undici does not implement forbidden header names
+ if (getHeadersGuard(headers) === 'immutable') {
+ throw new TypeError('immutable')
+ }
+
+ // 6. Otherwise, if headers’s guard is "response" and name is a
+ // forbidden response-header name, return.
+
+ // 7. Append (name, value) to headers’s header list.
+ return getHeadersList(headers).append(name, value, false)
+
+ // 8. If headers’s guard is "request-no-cors", then remove
+ // privileged no-CORS request headers from headers
+}
+
+// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+/**
+ * @param {Headers} target
+ */
+function headersListSortAndCombine (target) {
+ const headersList = getHeadersList(target)
+
+ if (!headersList) {
+ return []
+ }
+
+ if (headersList.sortedMap) {
+ return headersList.sortedMap
+ }
+
+ // 1. Let headers be an empty list of headers with the key being the name
+ // and value the value.
+ const headers = []
+
+ // 2. Let names be the result of convert header names to a sorted-lowercase
+ // set with all the names of the headers in list.
+ const names = headersList.toSortedArray()
+
+ const cookies = headersList.cookies
+
+ // fast-path
+ if (cookies === null || cookies.length === 1) {
+ // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
+ return (headersList.sortedMap = names)
+ }
+
+ // 3. For each name of names:
+ for (let i = 0; i < names.length; ++i) {
+ const { 0: name, 1: value } = names[i]
+ // 1. If name is `set-cookie`, then:
+ if (name === 'set-cookie') {
+ // 1. Let values be a list of all values of headers in list whose name
+ // is a byte-case-insensitive match for name, in order.
+
+ // 2. For each value of values:
+ // 1. Append (name, value) to headers.
+ for (let j = 0; j < cookies.length; ++j) {
+ headers.push([name, cookies[j]])
+ }
+ } else {
+ // 2. Otherwise:
+
+ // 1. Let value be the result of getting name from list.
+
+ // 2. Assert: value is non-null.
+ // Note: This operation was done by `HeadersList#toSortedArray`.
+
+ // 3. Append (name, value) to headers.
+ headers.push([name, value])
+ }
+ }
+
+ // 4. Return headers.
+ return (headersList.sortedMap = headers)
+}
+
+function compareHeaderName (a, b) {
+ return a[0] < b[0] ? -1 : 1
+}
+
+class HeadersList {
+ /** @type {[string, string][]|null} */
+ cookies = null
+
+ sortedMap
+ headersMap
+
+ constructor (init) {
+ if (init instanceof HeadersList) {
+ this.headersMap = new Map(init.headersMap)
+ this.sortedMap = init.sortedMap
+ this.cookies = init.cookies === null ? null : [...init.cookies]
+ } else {
+ this.headersMap = new Map(init)
+ this.sortedMap = null
+ }
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#header-list-contains
+ * @param {string} name
+ * @param {boolean} isLowerCase
+ */
+ contains (name, isLowerCase) {
+ // A header list list contains a header name name if list
+ // contains a header whose name is a byte-case-insensitive
+ // match for name.
+
+ return this.headersMap.has(isLowerCase ? name : name.toLowerCase())
+ }
+
+ clear () {
+ this.headersMap.clear()
+ this.sortedMap = null
+ this.cookies = null
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-append
+ * @param {string} name
+ * @param {string} value
+ * @param {boolean} isLowerCase
+ */
+ append (name, value, isLowerCase) {
+ this.sortedMap = null
+
+ // 1. If list contains name, then set name to the first such
+ // header’s name.
+ const lowercaseName = isLowerCase ? name : name.toLowerCase()
+ const exists = this.headersMap.get(lowercaseName)
+
+ // 2. Append (name, value) to list.
+ if (exists) {
+ const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
+ this.headersMap.set(lowercaseName, {
+ name: exists.name,
+ value: `${exists.value}${delimiter}${value}`
+ })
+ } else {
+ this.headersMap.set(lowercaseName, { name, value })
+ }
+
+ if (lowercaseName === 'set-cookie') {
+ (this.cookies ??= []).push(value)
+ }
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-set
+ * @param {string} name
+ * @param {string} value
+ * @param {boolean} isLowerCase
+ */
+ set (name, value, isLowerCase) {
+ this.sortedMap = null
+ const lowercaseName = isLowerCase ? name : name.toLowerCase()
+
+ if (lowercaseName === 'set-cookie') {
+ this.cookies = [value]
+ }
+
+ // 1. If list contains name, then set the value of
+ // the first such header to value and remove the
+ // others.
+ // 2. Otherwise, append header (name, value) to list.
+ this.headersMap.set(lowercaseName, { name, value })
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-delete
+ * @param {string} name
+ * @param {boolean} isLowerCase
+ */
+ delete (name, isLowerCase) {
+ this.sortedMap = null
+ if (!isLowerCase) name = name.toLowerCase()
+
+ if (name === 'set-cookie') {
+ this.cookies = null
+ }
+
+ this.headersMap.delete(name)
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-get
+ * @param {string} name
+ * @param {boolean} isLowerCase
+ * @returns {string | null}
+ */
+ get (name, isLowerCase) {
+ // 1. If list does not contain name, then return null.
+ // 2. Return the values of all headers in list whose name
+ // is a byte-case-insensitive match for name,
+ // separated from each other by 0x2C 0x20, in order.
+ return this.headersMap.get(isLowerCase ? name : name.toLowerCase())?.value ?? null
+ }
+
+ * [Symbol.iterator] () {
+ // use the lowercased name
+ for (const { 0: name, 1: { value } } of this.headersMap) {
+ yield [name, value]
+ }
+ }
+
+ get entries () {
+ const headers = {}
+
+ if (this.headersMap.size !== 0) {
+ for (const { name, value } of this.headersMap.values()) {
+ headers[name] = value
+ }
+ }
+
+ return headers
+ }
+
+ rawValues () {
+ return this.headersMap.values()
+ }
+
+ get entriesList () {
+ const headers = []
+
+ if (this.headersMap.size !== 0) {
+ for (const { 0: lowerName, 1: { name, value } } of this.headersMap) {
+ if (lowerName === 'set-cookie') {
+ for (const cookie of this.cookies) {
+ headers.push([name, cookie])
+ }
+ } else {
+ headers.push([name, value])
+ }
+ }
+ }
+
+ return headers
+ }
+
+ // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
+ toSortedArray () {
+ const size = this.headersMap.size
+ const array = new Array(size)
+ // In most cases, you will use the fast-path.
+ // fast-path: Use binary insertion sort for small arrays.
+ if (size <= 32) {
+ if (size === 0) {
+ // If empty, it is an empty array. To avoid the first index assignment.
+ return array
+ }
+ // Improve performance by unrolling loop and avoiding double-loop.
+ // Double-loop-less version of the binary insertion sort.
+ const iterator = this.headersMap[Symbol.iterator]()
+ const firstValue = iterator.next().value
+ // set [name, value] to first index.
+ array[0] = [firstValue[0], firstValue[1].value]
+ // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+ // 3.2.2. Assert: value is non-null.
+ assert(firstValue[1].value !== null)
+ for (
+ let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value;
+ i < size;
+ ++i
+ ) {
+ // get next value
+ value = iterator.next().value
+ // set [name, value] to current index.
+ x = array[i] = [value[0], value[1].value]
+ // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+ // 3.2.2. Assert: value is non-null.
+ assert(x[1] !== null)
+ left = 0
+ right = i
+ // binary search
+ while (left < right) {
+ // middle index
+ pivot = left + ((right - left) >> 1)
+ // compare header name
+ if (array[pivot][0] <= x[0]) {
+ left = pivot + 1
+ } else {
+ right = pivot
+ }
+ }
+ if (i !== pivot) {
+ j = i
+ while (j > left) {
+ array[j] = array[--j]
+ }
+ array[left] = x
+ }
+ }
+ /* c8 ignore next 4 */
+ if (!iterator.next().done) {
+ // This is for debugging and will never be called.
+ throw new TypeError('Unreachable')
+ }
+ return array
+ } else {
+ // This case would be a rare occurrence.
+ // slow-path: fallback
+ let i = 0
+ for (const { 0: name, 1: { value } } of this.headersMap) {
+ array[i++] = [name, value]
+ // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+ // 3.2.2. Assert: value is non-null.
+ assert(value !== null)
+ }
+ return array.sort(compareHeaderName)
+ }
+ }
+}
+
+// https://fetch.spec.whatwg.org/#headers-class
+class Headers {
+ #guard
+ /**
+ * @type {HeadersList}
+ */
+ #headersList
+
+ /**
+ * @param {HeadersInit|Symbol} [init]
+ * @returns
+ */
+ constructor (init = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (init === kConstruct) {
+ return
+ }
+
+ this.#headersList = new HeadersList()
+
+ // The new Headers(init) constructor steps are:
+
+ // 1. Set this’s guard to "none".
+ this.#guard = 'none'
+
+ // 2. If init is given, then fill this with init.
+ if (init !== undefined) {
+ init = webidl.converters.HeadersInit(init, 'Headers constructor', 'init')
+ fill(this, init)
+ }
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-append
+ append (name, value) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 2, 'Headers.append')
+
+ const prefix = 'Headers.append'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+ value = webidl.converters.ByteString(value, prefix, 'value')
+
+ return appendHeader(this, name, value)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-delete
+ delete (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, 'Headers.delete')
+
+ const prefix = 'Headers.delete'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.delete',
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. If this’s guard is "immutable", then throw a TypeError.
+ // 3. Otherwise, if this’s guard is "request" and name is a
+ // forbidden header name, return.
+ // 4. Otherwise, if this’s guard is "request-no-cors", name
+ // is not a no-CORS-safelisted request-header name, and
+ // name is not a privileged no-CORS request-header name,
+ // return.
+ // 5. Otherwise, if this’s guard is "response" and name is
+ // a forbidden response-header name, return.
+ // Note: undici does not implement forbidden header names
+ if (this.#guard === 'immutable') {
+ throw new TypeError('immutable')
+ }
+
+ // 6. If this’s header list does not contain name, then
+ // return.
+ if (!this.#headersList.contains(name, false)) {
+ return
+ }
+
+ // 7. Delete name from this’s header list.
+ // 8. If this’s guard is "request-no-cors", then remove
+ // privileged no-CORS request headers from this.
+ this.#headersList.delete(name, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-get
+ get (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, 'Headers.get')
+
+ const prefix = 'Headers.get'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. Return the result of getting name from this’s header
+ // list.
+ return this.#headersList.get(name, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-has
+ has (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, 'Headers.has')
+
+ const prefix = 'Headers.has'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. Return true if this’s header list contains name;
+ // otherwise false.
+ return this.#headersList.contains(name, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-set
+ set (name, value) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 2, 'Headers.set')
+
+ const prefix = 'Headers.set'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+ value = webidl.converters.ByteString(value, prefix, 'value')
+
+ // 1. Normalize value.
+ value = headerValueNormalize(value)
+
+ // 2. If name is not a header name or value is not a
+ // header value, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ value: name,
+ type: 'header name'
+ })
+ } else if (!isValidHeaderValue(value)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ value,
+ type: 'header value'
+ })
+ }
+
+ // 3. If this’s guard is "immutable", then throw a TypeError.
+ // 4. Otherwise, if this’s guard is "request" and name is a
+ // forbidden header name, return.
+ // 5. Otherwise, if this’s guard is "request-no-cors" and
+ // name/value is not a no-CORS-safelisted request-header,
+ // return.
+ // 6. Otherwise, if this’s guard is "response" and name is a
+ // forbidden response-header name, return.
+ // Note: undici does not implement forbidden header names
+ if (this.#guard === 'immutable') {
+ throw new TypeError('immutable')
+ }
+
+ // 7. Set (name, value) in this’s header list.
+ // 8. If this’s guard is "request-no-cors", then remove
+ // privileged no-CORS request headers from this
+ this.#headersList.set(name, value, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
+ getSetCookie () {
+ webidl.brandCheck(this, Headers)
+
+ // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
+ // 2. Return the values of all headers in this’s header list whose name is
+ // a byte-case-insensitive match for `Set-Cookie`, in order.
+
+ const list = this.#headersList.cookies
+
+ if (list) {
+ return [...list]
+ }
+
+ return []
+ }
+
+ [util.inspect.custom] (depth, options) {
+ options.depth ??= depth
+
+ return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
+ }
+
+ static getHeadersGuard (o) {
+ return o.#guard
+ }
+
+ static setHeadersGuard (o, guard) {
+ o.#guard = guard
+ }
+
+ /**
+ * @param {Headers} o
+ */
+ static getHeadersList (o) {
+ return o.#headersList
+ }
+
+ /**
+ * @param {Headers} target
+ * @param {HeadersList} list
+ */
+ static setHeadersList (target, list) {
+ target.#headersList = list
+ }
+}
+
+const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
+Reflect.deleteProperty(Headers, 'getHeadersGuard')
+Reflect.deleteProperty(Headers, 'setHeadersGuard')
+Reflect.deleteProperty(Headers, 'getHeadersList')
+Reflect.deleteProperty(Headers, 'setHeadersList')
+
+iteratorMixin('Headers', Headers, headersListSortAndCombine, 0, 1)
+
+Object.defineProperties(Headers.prototype, {
+ append: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ get: kEnumerableProperty,
+ has: kEnumerableProperty,
+ set: kEnumerableProperty,
+ getSetCookie: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Headers',
+ configurable: true
+ },
+ [util.inspect.custom]: {
+ enumerable: false
+ }
+})
+
+webidl.converters.HeadersInit = function (V, prefix, argument) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
+ const iterator = Reflect.get(V, Symbol.iterator)
+
+ // A work-around to ensure we send the properly-cased Headers when V is a Headers object.
+ // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
+ if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
+ try {
+ return getHeadersList(V).entriesList
+ } catch {
+ // fall-through
+ }
+ }
+
+ if (typeof iterator === 'function') {
+ return webidl.converters['sequence<sequence<ByteString>>'](V, prefix, argument, iterator.bind(V))
+ }
+
+ return webidl.converters['record<ByteString, ByteString>'](V, prefix, argument)
+ }
+
+ throw webidl.errors.conversionFailed({
+ prefix: 'Headers constructor',
+ argument: 'Argument 1',
+ types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
+ })
+}
+
+module.exports = {
+ fill,
+ // for test.
+ compareHeaderName,
+ Headers,
+ HeadersList,
+ getHeadersGuard,
+ setHeadersGuard,
+ setHeadersList,
+ getHeadersList
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/index.js b/vanilla/node_modules/undici/lib/web/fetch/index.js
new file mode 100644
index 0000000..f350035
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/index.js
@@ -0,0 +1,2372 @@
+// https://github.com/Ethan-Arrowood/undici-fetch
+
+'use strict'
+
+const {
+ makeNetworkError,
+ makeAppropriateNetworkError,
+ filterResponse,
+ makeResponse,
+ fromInnerResponse,
+ getResponseState
+} = require('./response')
+const { HeadersList } = require('./headers')
+const { Request, cloneRequest, getRequestDispatcher, getRequestState } = require('./request')
+const zlib = require('node:zlib')
+const {
+ makePolicyContainer,
+ clonePolicyContainer,
+ requestBadPort,
+ TAOCheck,
+ appendRequestOriginHeader,
+ responseLocationURL,
+ requestCurrentURL,
+ setRequestReferrerPolicyOnRedirect,
+ tryUpgradeRequestToAPotentiallyTrustworthyURL,
+ createOpaqueTimingInfo,
+ appendFetchMetadata,
+ corsCheck,
+ crossOriginResourcePolicyCheck,
+ determineRequestsReferrer,
+ coarsenedSharedCurrentTime,
+ sameOrigin,
+ isCancelled,
+ isAborted,
+ isErrorLike,
+ fullyReadBody,
+ readableStreamClose,
+ urlIsLocal,
+ urlIsHttpHttpsScheme,
+ urlHasHttpsScheme,
+ clampAndCoarsenConnectionTimingInfo,
+ simpleRangeHeaderValue,
+ buildContentRange,
+ createInflate,
+ extractMimeType,
+ hasAuthenticationEntry,
+ includesCredentials,
+ isTraversableNavigable
+} = require('./util')
+const assert = require('node:assert')
+const { safelyExtractBody, extractBody } = require('./body')
+const {
+ redirectStatusSet,
+ nullBodyStatus,
+ safeMethodsSet,
+ requestBodyHeader,
+ subresourceSet
+} = require('./constants')
+const EE = require('node:events')
+const { Readable, pipeline, finished, isErrored, isReadable } = require('node:stream')
+const { addAbortListener, bufferToLowerCasedHeaderName } = require('../../core/util')
+const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./data-url')
+const { getGlobalDispatcher } = require('../../global')
+const { webidl } = require('../webidl')
+const { STATUS_CODES } = require('node:http')
+const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
+const { createDeferredPromise } = require('../../util/promise')
+const { isomorphicEncode } = require('../infra')
+const { runtimeFeatures } = require('../../util/runtime-features')
+
+// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
+const hasZstd = runtimeFeatures.has('zstd')
+
+const GET_OR_HEAD = ['GET', 'HEAD']
+
+const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
+ ? 'node'
+ : 'undici'
+
+/** @type {import('buffer').resolveObjectURL} */
+let resolveObjectURL
+
+class Fetch extends EE {
+ constructor (dispatcher) {
+ super()
+
+ this.dispatcher = dispatcher
+ this.connection = null
+ this.dump = false
+ this.state = 'ongoing'
+ }
+
+ terminate (reason) {
+ if (this.state !== 'ongoing') {
+ return
+ }
+
+ this.state = 'terminated'
+ this.connection?.destroy(reason)
+ this.emit('terminated', reason)
+ }
+
+ // https://fetch.spec.whatwg.org/#fetch-controller-abort
+ abort (error) {
+ if (this.state !== 'ongoing') {
+ return
+ }
+
+ // 1. Set controller’s state to "aborted".
+ this.state = 'aborted'
+
+ // 2. Let fallbackError be an "AbortError" DOMException.
+ // 3. Set error to fallbackError if it is not given.
+ if (!error) {
+ error = new DOMException('The operation was aborted.', 'AbortError')
+ }
+
+ // 4. Let serializedError be StructuredSerialize(error).
+ // If that threw an exception, catch it, and let
+ // serializedError be StructuredSerialize(fallbackError).
+
+ // 5. Set controller’s serialized abort reason to serializedError.
+ this.serializedAbortReason = error
+
+ this.connection?.destroy(error)
+ this.emit('terminated', error)
+ }
+}
+
+function handleFetchDone (response) {
+ finalizeAndReportTiming(response, 'fetch')
+}
+
+// https://fetch.spec.whatwg.org/#fetch-method
+function fetch (input, init = undefined) {
+ webidl.argumentLengthCheck(arguments, 1, 'globalThis.fetch')
+
+ // 1. Let p be a new promise.
+ let p = createDeferredPromise()
+
+ // 2. Let requestObject be the result of invoking the initial value of
+ // Request as constructor with input and init as arguments. If this throws
+ // an exception, reject p with it and return p.
+ let requestObject
+
+ try {
+ requestObject = new Request(input, init)
+ } catch (e) {
+ p.reject(e)
+ return p.promise
+ }
+
+ // 3. Let request be requestObject’s request.
+ const request = getRequestState(requestObject)
+
+ // 4. If requestObject’s signal’s aborted flag is set, then:
+ if (requestObject.signal.aborted) {
+ // 1. Abort the fetch() call with p, request, null, and
+ // requestObject’s signal’s abort reason.
+ abortFetch(p, request, null, requestObject.signal.reason, null)
+
+ // 2. Return p.
+ return p.promise
+ }
+
+ // 5. Let globalObject be request’s client’s global object.
+ const globalObject = request.client.globalObject
+
+ // 6. If globalObject is a ServiceWorkerGlobalScope object, then set
+ // request’s service-workers mode to "none".
+ if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') {
+ request.serviceWorkers = 'none'
+ }
+
+ // 7. Let responseObject be null.
+ let responseObject = null
+
+ // 8. Let relevantRealm be this’s relevant Realm.
+
+ // 9. Let locallyAborted be false.
+ let locallyAborted = false
+
+ // 10. Let controller be null.
+ let controller = null
+
+ // 11. Add the following abort steps to requestObject’s signal:
+ addAbortListener(
+ requestObject.signal,
+ () => {
+ // 1. Set locallyAborted to true.
+ locallyAborted = true
+
+ // 2. Assert: controller is non-null.
+ assert(controller != null)
+
+ // 3. Abort controller with requestObject’s signal’s abort reason.
+ controller.abort(requestObject.signal.reason)
+
+ const realResponse = responseObject?.deref()
+
+ // 4. Abort the fetch() call with p, request, responseObject,
+ // and requestObject’s signal’s abort reason.
+ abortFetch(p, request, realResponse, requestObject.signal.reason, controller.controller)
+ }
+ )
+
+ // 12. Let handleFetchDone given response response be to finalize and
+ // report timing with response, globalObject, and "fetch".
+ // see function handleFetchDone
+
+ // 13. Set controller to the result of calling fetch given request,
+ // with processResponseEndOfBody set to handleFetchDone, and processResponse
+ // given response being these substeps:
+
+ const processResponse = (response) => {
+ // 1. If locallyAborted is true, terminate these substeps.
+ if (locallyAborted) {
+ return
+ }
+
+ // 2. If response’s aborted flag is set, then:
+ if (response.aborted) {
+ // 1. Let deserializedError be the result of deserialize a serialized
+ // abort reason given controller’s serialized abort reason and
+ // relevantRealm.
+
+ // 2. Abort the fetch() call with p, request, responseObject, and
+ // deserializedError.
+
+ abortFetch(p, request, responseObject, controller.serializedAbortReason, controller.controller)
+ return
+ }
+
+ // 3. If response is a network error, then reject p with a TypeError
+ // and terminate these substeps.
+ if (response.type === 'error') {
+ p.reject(new TypeError('fetch failed', { cause: response.error }))
+ return
+ }
+
+ // 4. Set responseObject to the result of creating a Response object,
+ // given response, "immutable", and relevantRealm.
+ responseObject = new WeakRef(fromInnerResponse(response, 'immutable'))
+
+ // 5. Resolve p with responseObject.
+ p.resolve(responseObject.deref())
+ p = null
+ }
+
+ controller = fetching({
+ request,
+ processResponseEndOfBody: handleFetchDone,
+ processResponse,
+ dispatcher: getRequestDispatcher(requestObject) // undici
+ })
+
+ // 14. Return p.
+ return p.promise
+}
+
+// https://fetch.spec.whatwg.org/#finalize-and-report-timing
+function finalizeAndReportTiming (response, initiatorType = 'other') {
+ // 1. If response is an aborted network error, then return.
+ if (response.type === 'error' && response.aborted) {
+ return
+ }
+
+ // 2. If response’s URL list is null or empty, then return.
+ if (!response.urlList?.length) {
+ return
+ }
+
+ // 3. Let originalURL be response’s URL list[0].
+ const originalURL = response.urlList[0]
+
+ // 4. Let timingInfo be response’s timing info.
+ let timingInfo = response.timingInfo
+
+ // 5. Let cacheState be response’s cache state.
+ let cacheState = response.cacheState
+
+ // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return.
+ if (!urlIsHttpHttpsScheme(originalURL)) {
+ return
+ }
+
+ // 7. If timingInfo is null, then return.
+ if (timingInfo === null) {
+ return
+ }
+
+ // 8. If response’s timing allow passed flag is not set, then:
+ if (!response.timingAllowPassed) {
+ // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo.
+ timingInfo = createOpaqueTimingInfo({
+ startTime: timingInfo.startTime
+ })
+
+ // 2. Set cacheState to the empty string.
+ cacheState = ''
+ }
+
+ // 9. Set timingInfo’s end time to the coarsened shared current time
+ // given global’s relevant settings object’s cross-origin isolated
+ // capability.
+ // TODO: given global’s relevant settings object’s cross-origin isolated
+ // capability?
+ timingInfo.endTime = coarsenedSharedCurrentTime()
+
+ // 10. Set response’s timing info to timingInfo.
+ response.timingInfo = timingInfo
+
+ // 11. Mark resource timing for timingInfo, originalURL, initiatorType,
+ // global, and cacheState.
+ markResourceTiming(
+ timingInfo,
+ originalURL.href,
+ initiatorType,
+ globalThis,
+ cacheState,
+ '', // bodyType
+ response.status
+ )
+}
+
+// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing
+const markResourceTiming = performance.markResourceTiming
+
+// https://fetch.spec.whatwg.org/#abort-fetch
+function abortFetch (p, request, responseObject, error, controller /* undici-specific */) {
+ // 1. Reject promise with error.
+ if (p) {
+ // We might have already resolved the promise at this stage
+ p.reject(error)
+ }
+
+ // 2. If request’s body is not null and is readable, then cancel request’s
+ // body with error.
+ if (request.body?.stream != null && isReadable(request.body.stream)) {
+ request.body.stream.cancel(error).catch((err) => {
+ if (err.code === 'ERR_INVALID_STATE') {
+ // Node bug?
+ return
+ }
+ throw err
+ })
+ }
+
+ // 3. If responseObject is null, then return.
+ if (responseObject == null) {
+ return
+ }
+
+ // 4. Let response be responseObject’s response.
+ const response = getResponseState(responseObject)
+
+ // 5. If response’s body is not null and is readable, then error response’s
+ // body with error.
+ if (response.body?.stream != null && isReadable(response.body.stream)) {
+ controller.error(error)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#fetching
+function fetching ({
+ request,
+ processRequestBodyChunkLength,
+ processRequestEndOfBody,
+ processResponse,
+ processResponseEndOfBody,
+ processResponseConsumeBody,
+ useParallelQueue = false,
+ dispatcher = getGlobalDispatcher() // undici
+}) {
+ // Ensure that the dispatcher is set accordingly
+ assert(dispatcher)
+
+ // 1. Let taskDestination be null.
+ let taskDestination = null
+
+ // 2. Let crossOriginIsolatedCapability be false.
+ let crossOriginIsolatedCapability = false
+
+ // 3. If request’s client is non-null, then:
+ if (request.client != null) {
+ // 1. Set taskDestination to request’s client’s global object.
+ taskDestination = request.client.globalObject
+
+ // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin
+ // isolated capability.
+ crossOriginIsolatedCapability =
+ request.client.crossOriginIsolatedCapability
+ }
+
+ // 4. If useParallelQueue is true, then set taskDestination to the result of
+ // starting a new parallel queue.
+ // TODO
+
+ // 5. Let timingInfo be a new fetch timing info whose start time and
+ // post-redirect start time are the coarsened shared current time given
+ // crossOriginIsolatedCapability.
+ const currentTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability)
+ const timingInfo = createOpaqueTimingInfo({
+ startTime: currentTime
+ })
+
+ // 6. Let fetchParams be a new fetch params whose
+ // request is request,
+ // timing info is timingInfo,
+ // process request body chunk length is processRequestBodyChunkLength,
+ // process request end-of-body is processRequestEndOfBody,
+ // process response is processResponse,
+ // process response consume body is processResponseConsumeBody,
+ // process response end-of-body is processResponseEndOfBody,
+ // task destination is taskDestination,
+ // and cross-origin isolated capability is crossOriginIsolatedCapability.
+ const fetchParams = {
+ controller: new Fetch(dispatcher),
+ request,
+ timingInfo,
+ processRequestBodyChunkLength,
+ processRequestEndOfBody,
+ processResponse,
+ processResponseConsumeBody,
+ processResponseEndOfBody,
+ taskDestination,
+ crossOriginIsolatedCapability
+ }
+
+ // 7. If request’s body is a byte sequence, then set request’s body to
+ // request’s body as a body.
+ // NOTE: Since fetching is only called from fetch, body should already be
+ // extracted.
+ assert(!request.body || request.body.stream)
+
+ // 8. If request’s window is "client", then set request’s window to request’s
+ // client, if request’s client’s global object is a Window object; otherwise
+ // "no-window".
+ if (request.window === 'client') {
+ // TODO: What if request.client is null?
+ request.window =
+ request.client?.globalObject?.constructor?.name === 'Window'
+ ? request.client
+ : 'no-window'
+ }
+
+ // 9. If request’s origin is "client", then set request’s origin to request’s
+ // client’s origin.
+ if (request.origin === 'client') {
+ request.origin = request.client.origin
+ }
+
+ // 10. If all of the following conditions are true:
+ // TODO
+
+ // 11. If request’s policy container is "client", then:
+ if (request.policyContainer === 'client') {
+ // 1. If request’s client is non-null, then set request’s policy
+ // container to a clone of request’s client’s policy container. [HTML]
+ if (request.client != null) {
+ request.policyContainer = clonePolicyContainer(
+ request.client.policyContainer
+ )
+ } else {
+ // 2. Otherwise, set request’s policy container to a new policy
+ // container.
+ request.policyContainer = makePolicyContainer()
+ }
+ }
+
+ // 12. If request’s header list does not contain `Accept`, then:
+ if (!request.headersList.contains('accept', true)) {
+ // 1. Let value be `*/*`.
+ const value = '*/*'
+
+ // 2. A user agent should set value to the first matching statement, if
+ // any, switching on request’s destination:
+ // "document"
+ // "frame"
+ // "iframe"
+ // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`
+ // "image"
+ // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5`
+ // "style"
+ // `text/css,*/*;q=0.1`
+ // TODO
+
+ // 3. Append `Accept`/value to request’s header list.
+ request.headersList.append('accept', value, true)
+ }
+
+ // 13. If request’s header list does not contain `Accept-Language`, then
+ // user agents should append `Accept-Language`/an appropriate value to
+ // request’s header list.
+ if (!request.headersList.contains('accept-language', true)) {
+ request.headersList.append('accept-language', '*', true)
+ }
+
+ // 14. If request’s priority is null, then use request’s initiator and
+ // destination appropriately in setting request’s priority to a
+ // user-agent-defined object.
+ if (request.priority === null) {
+ // TODO
+ }
+
+ // 15. If request is a subresource request, then:
+ if (subresourceSet.has(request.destination)) {
+ // TODO
+ }
+
+ // 16. Run main fetch given fetchParams.
+ mainFetch(fetchParams, false)
+
+ // 17. Return fetchParam's controller
+ return fetchParams.controller
+}
+
+// https://fetch.spec.whatwg.org/#concept-main-fetch
+async function mainFetch (fetchParams, recursive) {
+ try {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. If request’s local-URLs-only flag is set and request’s current URL is
+ // not local, then set response to a network error.
+ if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) {
+ response = makeNetworkError('local URLs only')
+ }
+
+ // 4. Run report Content Security Policy violations for request.
+ // TODO
+
+ // 5. Upgrade request to a potentially trustworthy URL, if appropriate.
+ tryUpgradeRequestToAPotentiallyTrustworthyURL(request)
+
+ // 6. If should request be blocked due to a bad port, should fetching request
+ // be blocked as mixed content, or should request be blocked by Content
+ // Security Policy returns blocked, then set response to a network error.
+ if (requestBadPort(request) === 'blocked') {
+ response = makeNetworkError('bad port')
+ }
+ // TODO: should fetching request be blocked as mixed content?
+ // TODO: should request be blocked by Content Security Policy?
+
+ // 7. If request’s referrer policy is the empty string, then set request’s
+ // referrer policy to request’s policy container’s referrer policy.
+ if (request.referrerPolicy === '') {
+ request.referrerPolicy = request.policyContainer.referrerPolicy
+ }
+
+ // 8. If request’s referrer is not "no-referrer", then set request’s
+ // referrer to the result of invoking determine request’s referrer.
+ if (request.referrer !== 'no-referrer') {
+ request.referrer = determineRequestsReferrer(request)
+ }
+
+ // 9. Set request’s current URL’s scheme to "https" if all of the following
+ // conditions are true:
+ // - request’s current URL’s scheme is "http"
+ // - request’s current URL’s host is a domain
+ // - Matching request’s current URL’s host per Known HSTS Host Domain Name
+ // Matching results in either a superdomain match with an asserted
+ // includeSubDomains directive or a congruent match (with or without an
+ // asserted includeSubDomains directive). [HSTS]
+ // TODO
+
+ // 10. If recursive is false, then run the remaining steps in parallel.
+ // TODO
+
+ // 11. If response is null, then set response to the result of running
+ // the steps corresponding to the first matching statement:
+ if (response === null) {
+ const currentURL = requestCurrentURL(request)
+ if (
+ // - request’s current URL’s origin is same origin with request’s origin,
+ // and request’s response tainting is "basic"
+ (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') ||
+ // request’s current URL’s scheme is "data"
+ (currentURL.protocol === 'data:') ||
+ // - request’s mode is "navigate" or "websocket"
+ (request.mode === 'navigate' || request.mode === 'websocket')
+ ) {
+ // 1. Set request’s response tainting to "basic".
+ request.responseTainting = 'basic'
+
+ // 2. Return the result of running scheme fetch given fetchParams.
+ response = await schemeFetch(fetchParams)
+
+ // request’s mode is "same-origin"
+ } else if (request.mode === 'same-origin') {
+ // 1. Return a network error.
+ response = makeNetworkError('request mode cannot be "same-origin"')
+
+ // request’s mode is "no-cors"
+ } else if (request.mode === 'no-cors') {
+ // 1. If request’s redirect mode is not "follow", then return a network
+ // error.
+ if (request.redirect !== 'follow') {
+ response = makeNetworkError(
+ 'redirect mode cannot be "follow" for "no-cors" request'
+ )
+ } else {
+ // 2. Set request’s response tainting to "opaque".
+ request.responseTainting = 'opaque'
+
+ // 3. Return the result of running scheme fetch given fetchParams.
+ response = await schemeFetch(fetchParams)
+ }
+ // request’s current URL’s scheme is not an HTTP(S) scheme
+ } else if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) {
+ // Return a network error.
+ response = makeNetworkError('URL scheme must be a HTTP(S) scheme')
+
+ // - request’s use-CORS-preflight flag is set
+ // - request’s unsafe-request flag is set and either request’s method is
+ // not a CORS-safelisted method or CORS-unsafe request-header names with
+ // request’s header list is not empty
+ // 1. Set request’s response tainting to "cors".
+ // 2. Let corsWithPreflightResponse be the result of running HTTP fetch
+ // given fetchParams and true.
+ // 3. If corsWithPreflightResponse is a network error, then clear cache
+ // entries using request.
+ // 4. Return corsWithPreflightResponse.
+ // TODO
+
+ // Otherwise
+ } else {
+ // 1. Set request’s response tainting to "cors".
+ request.responseTainting = 'cors'
+
+ // 2. Return the result of running HTTP fetch given fetchParams.
+ response = await httpFetch(fetchParams)
+ }
+ }
+
+ // 12. If recursive is true, then return response.
+ if (recursive) {
+ return response
+ }
+
+ // 13. If response is not a network error and response is not a filtered
+ // response, then:
+ if (response.status !== 0 && !response.internalResponse) {
+ // If request’s response tainting is "cors", then:
+ if (request.responseTainting === 'cors') {
+ // 1. Let headerNames be the result of extracting header list values
+ // given `Access-Control-Expose-Headers` and response’s header list.
+ // TODO
+ // 2. If request’s credentials mode is not "include" and headerNames
+ // contains `*`, then set response’s CORS-exposed header-name list to
+ // all unique header names in response’s header list.
+ // TODO
+ // 3. Otherwise, if headerNames is not null or failure, then set
+ // response’s CORS-exposed header-name list to headerNames.
+ // TODO
+ }
+
+ // Set response to the following filtered response with response as its
+ // internal response, depending on request’s response tainting:
+ if (request.responseTainting === 'basic') {
+ response = filterResponse(response, 'basic')
+ } else if (request.responseTainting === 'cors') {
+ response = filterResponse(response, 'cors')
+ } else if (request.responseTainting === 'opaque') {
+ response = filterResponse(response, 'opaque')
+ } else {
+ assert(false)
+ }
+ }
+
+ // 14. Let internalResponse be response, if response is a network error,
+ // and response’s internal response otherwise.
+ let internalResponse =
+ response.status === 0 ? response : response.internalResponse
+
+ // 15. If internalResponse’s URL list is empty, then set it to a clone of
+ // request’s URL list.
+ if (internalResponse.urlList.length === 0) {
+ internalResponse.urlList.push(...request.urlList)
+ }
+
+ // 16. If request’s timing allow failed flag is unset, then set
+ // internalResponse’s timing allow passed flag.
+ if (!request.timingAllowFailed) {
+ response.timingAllowPassed = true
+ }
+
+ // 17. If response is not a network error and any of the following returns
+ // blocked
+ // - should internalResponse to request be blocked as mixed content
+ // - should internalResponse to request be blocked by Content Security Policy
+ // - should internalResponse to request be blocked due to its MIME type
+ // - should internalResponse to request be blocked due to nosniff
+ // TODO
+
+ // 18. If response’s type is "opaque", internalResponse’s status is 206,
+ // internalResponse’s range-requested flag is set, and request’s header
+ // list does not contain `Range`, then set response and internalResponse
+ // to a network error.
+ if (
+ response.type === 'opaque' &&
+ internalResponse.status === 206 &&
+ internalResponse.rangeRequested &&
+ !request.headers.contains('range', true)
+ ) {
+ response = internalResponse = makeNetworkError()
+ }
+
+ // 19. If response is not a network error and either request’s method is
+ // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status,
+ // set internalResponse’s body to null and disregard any enqueuing toward
+ // it (if any).
+ if (
+ response.status !== 0 &&
+ (request.method === 'HEAD' ||
+ request.method === 'CONNECT' ||
+ nullBodyStatus.includes(internalResponse.status))
+ ) {
+ internalResponse.body = null
+ fetchParams.controller.dump = true
+ }
+
+ // 20. If request’s integrity metadata is not the empty string, then:
+ if (request.integrity) {
+ // 1. Let processBodyError be this step: run fetch finale given fetchParams
+ // and a network error.
+ const processBodyError = (reason) =>
+ fetchFinale(fetchParams, makeNetworkError(reason))
+
+ // 2. If request’s response tainting is "opaque", or response’s body is null,
+ // then run processBodyError and abort these steps.
+ if (request.responseTainting === 'opaque' || response.body == null) {
+ processBodyError(response.error)
+ return
+ }
+
+ // 3. Let processBody given bytes be these steps:
+ const processBody = (bytes) => {
+ // 1. If bytes do not match request’s integrity metadata,
+ // then run processBodyError and abort these steps. [SRI]
+ if (!bytesMatch(bytes, request.integrity)) {
+ processBodyError('integrity mismatch')
+ return
+ }
+
+ // 2. Set response’s body to bytes as a body.
+ response.body = safelyExtractBody(bytes)[0]
+
+ // 3. Run fetch finale given fetchParams and response.
+ fetchFinale(fetchParams, response)
+ }
+
+ // 4. Fully read response’s body given processBody and processBodyError.
+ fullyReadBody(response.body, processBody, processBodyError)
+ } else {
+ // 21. Otherwise, run fetch finale given fetchParams and response.
+ fetchFinale(fetchParams, response)
+ }
+ } catch (err) {
+ fetchParams.controller.terminate(err)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#concept-scheme-fetch
+// given a fetch params fetchParams
+function schemeFetch (fetchParams) {
+ // Note: since the connection is destroyed on redirect, which sets fetchParams to a
+ // cancelled state, we do not want this condition to trigger *unless* there have been
+ // no redirects. See https://github.com/nodejs/undici/issues/1776
+ // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) {
+ return Promise.resolve(makeAppropriateNetworkError(fetchParams))
+ }
+
+ // 2. Let request be fetchParams’s request.
+ const { request } = fetchParams
+
+ const { protocol: scheme } = requestCurrentURL(request)
+
+ // 3. Switch on request’s current URL’s scheme and run the associated steps:
+ switch (scheme) {
+ case 'about:': {
+ // If request’s current URL’s path is the string "blank", then return a new response
+ // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) »,
+ // and body is the empty byte sequence as a body.
+
+ // Otherwise, return a network error.
+ return Promise.resolve(makeNetworkError('about scheme is not supported'))
+ }
+ case 'blob:': {
+ if (!resolveObjectURL) {
+ resolveObjectURL = require('node:buffer').resolveObjectURL
+ }
+
+ // 1. Let blobURLEntry be request’s current URL’s blob URL entry.
+ const blobURLEntry = requestCurrentURL(request)
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56
+ // Buffer.resolveObjectURL does not ignore URL queries.
+ if (blobURLEntry.search.length !== 0) {
+ return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.'))
+ }
+
+ const blob = resolveObjectURL(blobURLEntry.toString())
+
+ // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s
+ // object is not a Blob object, then return a network error.
+ if (request.method !== 'GET' || !webidl.is.Blob(blob)) {
+ return Promise.resolve(makeNetworkError('invalid method'))
+ }
+
+ // 3. Let blob be blobURLEntry’s object.
+ // Note: done above
+
+ // 4. Let response be a new response.
+ const response = makeResponse()
+
+ // 5. Let fullLength be blob’s size.
+ const fullLength = blob.size
+
+ // 6. Let serializedFullLength be fullLength, serialized and isomorphic encoded.
+ const serializedFullLength = isomorphicEncode(`${fullLength}`)
+
+ // 7. Let type be blob’s type.
+ const type = blob.type
+
+ // 8. If request’s header list does not contain `Range`:
+ // 9. Otherwise:
+ if (!request.headersList.contains('range', true)) {
+ // 1. Let bodyWithType be the result of safely extracting blob.
+ // Note: in the FileAPI a blob "object" is a Blob *or* a MediaSource.
+ // In node, this can only ever be a Blob. Therefore we can safely
+ // use extractBody directly.
+ const bodyWithType = extractBody(blob)
+
+ // 2. Set response’s status message to `OK`.
+ response.statusText = 'OK'
+
+ // 3. Set response’s body to bodyWithType’s body.
+ response.body = bodyWithType[0]
+
+ // 4. Set response’s header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ».
+ response.headersList.set('content-length', serializedFullLength, true)
+ response.headersList.set('content-type', type, true)
+ } else {
+ // 1. Set response’s range-requested flag.
+ response.rangeRequested = true
+
+ // 2. Let rangeHeader be the result of getting `Range` from request’s header list.
+ const rangeHeader = request.headersList.get('range', true)
+
+ // 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true.
+ const rangeValue = simpleRangeHeaderValue(rangeHeader, true)
+
+ // 4. If rangeValue is failure, then return a network error.
+ if (rangeValue === 'failure') {
+ return Promise.resolve(makeNetworkError('failed to fetch the data URL'))
+ }
+
+ // 5. Let (rangeStart, rangeEnd) be rangeValue.
+ let { rangeStartValue: rangeStart, rangeEndValue: rangeEnd } = rangeValue
+
+ // 6. If rangeStart is null:
+ // 7. Otherwise:
+ if (rangeStart === null) {
+ // 1. Set rangeStart to fullLength − rangeEnd.
+ rangeStart = fullLength - rangeEnd
+
+ // 2. Set rangeEnd to rangeStart + rangeEnd − 1.
+ rangeEnd = rangeStart + rangeEnd - 1
+ } else {
+ // 1. If rangeStart is greater than or equal to fullLength, then return a network error.
+ if (rangeStart >= fullLength) {
+ return Promise.resolve(makeNetworkError('Range start is greater than the blob\'s size.'))
+ }
+
+ // 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set
+ // rangeEnd to fullLength − 1.
+ if (rangeEnd === null || rangeEnd >= fullLength) {
+ rangeEnd = fullLength - 1
+ }
+ }
+
+ // 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart,
+ // rangeEnd + 1, and type.
+ const slicedBlob = blob.slice(rangeStart, rangeEnd + 1, type)
+
+ // 9. Let slicedBodyWithType be the result of safely extracting slicedBlob.
+ // Note: same reason as mentioned above as to why we use extractBody
+ const slicedBodyWithType = extractBody(slicedBlob)
+
+ // 10. Set response’s body to slicedBodyWithType’s body.
+ response.body = slicedBodyWithType[0]
+
+ // 11. Let serializedSlicedLength be slicedBlob’s size, serialized and isomorphic encoded.
+ const serializedSlicedLength = isomorphicEncode(`${slicedBlob.size}`)
+
+ // 12. Let contentRange be the result of invoking build a content range given rangeStart,
+ // rangeEnd, and fullLength.
+ const contentRange = buildContentRange(rangeStart, rangeEnd, fullLength)
+
+ // 13. Set response’s status to 206.
+ response.status = 206
+
+ // 14. Set response’s status message to `Partial Content`.
+ response.statusText = 'Partial Content'
+
+ // 15. Set response’s header list to « (`Content-Length`, serializedSlicedLength),
+ // (`Content-Type`, type), (`Content-Range`, contentRange) ».
+ response.headersList.set('content-length', serializedSlicedLength, true)
+ response.headersList.set('content-type', type, true)
+ response.headersList.set('content-range', contentRange, true)
+ }
+
+ // 10. Return response.
+ return Promise.resolve(response)
+ }
+ case 'data:': {
+ // 1. Let dataURLStruct be the result of running the
+ // data: URL processor on request’s current URL.
+ const currentURL = requestCurrentURL(request)
+ const dataURLStruct = dataURLProcessor(currentURL)
+
+ // 2. If dataURLStruct is failure, then return a
+ // network error.
+ if (dataURLStruct === 'failure') {
+ return Promise.resolve(makeNetworkError('failed to fetch the data URL'))
+ }
+
+ // 3. Let mimeType be dataURLStruct’s MIME type, serialized.
+ const mimeType = serializeAMimeType(dataURLStruct.mimeType)
+
+ // 4. Return a response whose status message is `OK`,
+ // header list is « (`Content-Type`, mimeType) »,
+ // and body is dataURLStruct’s body as a body.
+ return Promise.resolve(makeResponse({
+ statusText: 'OK',
+ headersList: [
+ ['content-type', { name: 'Content-Type', value: mimeType }]
+ ],
+ body: safelyExtractBody(dataURLStruct.body)[0]
+ }))
+ }
+ case 'file:': {
+ // For now, unfortunate as it is, file URLs are left as an exercise for the reader.
+ // When in doubt, return a network error.
+ return Promise.resolve(makeNetworkError('not implemented... yet...'))
+ }
+ case 'http:':
+ case 'https:': {
+ // Return the result of running HTTP fetch given fetchParams.
+
+ return httpFetch(fetchParams)
+ .catch((err) => makeNetworkError(err))
+ }
+ default: {
+ return Promise.resolve(makeNetworkError('unknown scheme'))
+ }
+ }
+}
+
+// https://fetch.spec.whatwg.org/#finalize-response
+function finalizeResponse (fetchParams, response) {
+ // 1. Set fetchParams’s request’s done flag.
+ fetchParams.request.done = true
+
+ // 2, If fetchParams’s process response done is not null, then queue a fetch
+ // task to run fetchParams’s process response done given response, with
+ // fetchParams’s task destination.
+ if (fetchParams.processResponseDone != null) {
+ queueMicrotask(() => fetchParams.processResponseDone(response))
+ }
+}
+
+// https://fetch.spec.whatwg.org/#fetch-finale
+function fetchFinale (fetchParams, response) {
+ // 1. Let timingInfo be fetchParams’s timing info.
+ let timingInfo = fetchParams.timingInfo
+
+ // 2. If response is not a network error and fetchParams’s request’s client is a secure context,
+ // then set timingInfo’s server-timing headers to the result of getting, decoding, and splitting
+ // `Server-Timing` from response’s internal response’s header list.
+ // TODO
+
+ // 3. Let processResponseEndOfBody be the following steps:
+ const processResponseEndOfBody = () => {
+ // 1. Let unsafeEndTime be the unsafe shared current time.
+ const unsafeEndTime = Date.now() // ?
+
+ // 2. If fetchParams’s request’s destination is "document", then set fetchParams’s controller’s
+ // full timing info to fetchParams’s timing info.
+ if (fetchParams.request.destination === 'document') {
+ fetchParams.controller.fullTimingInfo = timingInfo
+ }
+
+ // 3. Set fetchParams’s controller’s report timing steps to the following steps given a global object global:
+ fetchParams.controller.reportTimingSteps = () => {
+ // 1. If fetchParams’s request’s URL’s scheme is not an HTTP(S) scheme, then return.
+ if (!urlIsHttpHttpsScheme(fetchParams.request.url)) {
+ return
+ }
+
+ // 2. Set timingInfo’s end time to the relative high resolution time given unsafeEndTime and global.
+ timingInfo.endTime = unsafeEndTime
+
+ // 3. Let cacheState be response’s cache state.
+ let cacheState = response.cacheState
+
+ // 4. Let bodyInfo be response’s body info.
+ const bodyInfo = response.bodyInfo
+
+ // 5. If response’s timing allow passed flag is not set, then set timingInfo to the result of creating an
+ // opaque timing info for timingInfo and set cacheState to the empty string.
+ if (!response.timingAllowPassed) {
+ timingInfo = createOpaqueTimingInfo(timingInfo)
+
+ cacheState = ''
+ }
+
+ // 6. Let responseStatus be 0.
+ let responseStatus = 0
+
+ // 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false:
+ if (fetchParams.request.mode !== 'navigator' || !response.hasCrossOriginRedirects) {
+ // 1. Set responseStatus to response’s status.
+ responseStatus = response.status
+
+ // 2. Let mimeType be the result of extracting a MIME type from response’s header list.
+ const mimeType = extractMimeType(response.headersList)
+
+ // 3. If mimeType is not failure, then set bodyInfo’s content type to the result of minimizing a supported MIME type given mimeType.
+ if (mimeType !== 'failure') {
+ bodyInfo.contentType = minimizeSupportedMimeType(mimeType)
+ }
+ }
+
+ // 8. If fetchParams’s request’s initiator type is non-null, then mark resource timing given timingInfo,
+ // fetchParams’s request’s URL, fetchParams’s request’s initiator type, global, cacheState, bodyInfo,
+ // and responseStatus.
+ if (fetchParams.request.initiatorType != null) {
+ markResourceTiming(timingInfo, fetchParams.request.url.href, fetchParams.request.initiatorType, globalThis, cacheState, bodyInfo, responseStatus)
+ }
+ }
+
+ // 4. Let processResponseEndOfBodyTask be the following steps:
+ const processResponseEndOfBodyTask = () => {
+ // 1. Set fetchParams’s request’s done flag.
+ fetchParams.request.done = true
+
+ // 2. If fetchParams’s process response end-of-body is non-null, then run fetchParams’s process
+ // response end-of-body given response.
+ if (fetchParams.processResponseEndOfBody != null) {
+ queueMicrotask(() => fetchParams.processResponseEndOfBody(response))
+ }
+
+ // 3. If fetchParams’s request’s initiator type is non-null and fetchParams’s request’s client’s
+ // global object is fetchParams’s task destination, then run fetchParams’s controller’s report
+ // timing steps given fetchParams’s request’s client’s global object.
+ if (fetchParams.request.initiatorType != null) {
+ fetchParams.controller.reportTimingSteps()
+ }
+ }
+
+ // 5. Queue a fetch task to run processResponseEndOfBodyTask with fetchParams’s task destination
+ queueMicrotask(() => processResponseEndOfBodyTask())
+ }
+
+ // 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s
+ // process response given response, with fetchParams’s task destination.
+ if (fetchParams.processResponse != null) {
+ queueMicrotask(() => {
+ fetchParams.processResponse(response)
+ fetchParams.processResponse = null
+ })
+ }
+
+ // 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response.
+ const internalResponse = response.type === 'error' ? response : (response.internalResponse ?? response)
+
+ // 6. If internalResponse’s body is null, then run processResponseEndOfBody.
+ // 7. Otherwise:
+ if (internalResponse.body == null) {
+ processResponseEndOfBody()
+ } else {
+ // mcollina: all the following steps of the specs are skipped.
+ // The internal transform stream is not needed.
+ // See https://github.com/nodejs/undici/pull/3093#issuecomment-2050198541
+
+ // 1. Let transformStream be a new TransformStream.
+ // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, enqueues chunk in transformStream.
+ // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm and flushAlgorithm
+ // set to processResponseEndOfBody.
+ // 4. Set internalResponse’s body’s stream to the result of internalResponse’s body’s stream piped through transformStream.
+
+ finished(internalResponse.body.stream, () => {
+ processResponseEndOfBody()
+ })
+ }
+}
+
+// https://fetch.spec.whatwg.org/#http-fetch
+async function httpFetch (fetchParams) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. Let actualResponse be null.
+ let actualResponse = null
+
+ // 4. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 5. If request’s service-workers mode is "all", then:
+ if (request.serviceWorkers === 'all') {
+ // TODO
+ }
+
+ // 6. If response is null, then:
+ if (response === null) {
+ // 1. If makeCORSPreflight is true and one of these conditions is true:
+ // TODO
+
+ // 2. If request’s redirect mode is "follow", then set request’s
+ // service-workers mode to "none".
+ if (request.redirect === 'follow') {
+ request.serviceWorkers = 'none'
+ }
+
+ // 3. Set response and actualResponse to the result of running
+ // HTTP-network-or-cache fetch given fetchParams.
+ actualResponse = response = await httpNetworkOrCacheFetch(fetchParams)
+
+ // 4. If request’s response tainting is "cors" and a CORS check
+ // for request and response returns failure, then return a network error.
+ if (
+ request.responseTainting === 'cors' &&
+ corsCheck(request, response) === 'failure'
+ ) {
+ return makeNetworkError('cors failure')
+ }
+
+ // 5. If the TAO check for request and response returns failure, then set
+ // request’s timing allow failed flag.
+ if (TAOCheck(request, response) === 'failure') {
+ request.timingAllowFailed = true
+ }
+ }
+
+ // 7. If either request’s response tainting or response’s type
+ // is "opaque", and the cross-origin resource policy check with
+ // request’s origin, request’s client, request’s destination,
+ // and actualResponse returns blocked, then return a network error.
+ if (
+ (request.responseTainting === 'opaque' || response.type === 'opaque') &&
+ crossOriginResourcePolicyCheck(
+ request.origin,
+ request.client,
+ request.destination,
+ actualResponse
+ ) === 'blocked'
+ ) {
+ return makeNetworkError('blocked')
+ }
+
+ // 8. If actualResponse’s status is a redirect status, then:
+ if (redirectStatusSet.has(actualResponse.status)) {
+ // 1. If actualResponse’s status is not 303, request’s body is not null,
+ // and the connection uses HTTP/2, then user agents may, and are even
+ // encouraged to, transmit an RST_STREAM frame.
+ // See, https://github.com/whatwg/fetch/issues/1288
+ if (request.redirect !== 'manual') {
+ fetchParams.controller.connection.destroy(undefined, false)
+ }
+
+ // 2. Switch on request’s redirect mode:
+ if (request.redirect === 'error') {
+ // Set response to a network error.
+ response = makeNetworkError('unexpected redirect')
+ } else if (request.redirect === 'manual') {
+ // Set response to an opaque-redirect filtered response whose internal
+ // response is actualResponse.
+ // NOTE(spec): On the web this would return an `opaqueredirect` response,
+ // but that doesn't make sense server side.
+ // See https://github.com/nodejs/undici/issues/1193.
+ response = actualResponse
+ } else if (request.redirect === 'follow') {
+ // Set response to the result of running HTTP-redirect fetch given
+ // fetchParams and response.
+ response = await httpRedirectFetch(fetchParams, response)
+ } else {
+ assert(false)
+ }
+ }
+
+ // 9. Set response’s timing info to timingInfo.
+ response.timingInfo = timingInfo
+
+ // 10. Return response.
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#http-redirect-fetch
+function httpRedirectFetch (fetchParams, response) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let actualResponse be response, if response is not a filtered response,
+ // and response’s internal response otherwise.
+ const actualResponse = response.internalResponse
+ ? response.internalResponse
+ : response
+
+ // 3. Let locationURL be actualResponse’s location URL given request’s current
+ // URL’s fragment.
+ let locationURL
+
+ try {
+ locationURL = responseLocationURL(
+ actualResponse,
+ requestCurrentURL(request).hash
+ )
+
+ // 4. If locationURL is null, then return response.
+ if (locationURL == null) {
+ return response
+ }
+ } catch (err) {
+ // 5. If locationURL is failure, then return a network error.
+ return Promise.resolve(makeNetworkError(err))
+ }
+
+ // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network
+ // error.
+ if (!urlIsHttpHttpsScheme(locationURL)) {
+ return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme'))
+ }
+
+ // 7. If request’s redirect count is 20, then return a network error.
+ if (request.redirectCount === 20) {
+ return Promise.resolve(makeNetworkError('redirect count exceeded'))
+ }
+
+ // 8. Increase request’s redirect count by 1.
+ request.redirectCount += 1
+
+ // 9. If request’s mode is "cors", locationURL includes credentials, and
+ // request’s origin is not same origin with locationURL’s origin, then return
+ // a network error.
+ if (
+ request.mode === 'cors' &&
+ (locationURL.username || locationURL.password) &&
+ !sameOrigin(request, locationURL)
+ ) {
+ return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"'))
+ }
+
+ // 10. If request’s response tainting is "cors" and locationURL includes
+ // credentials, then return a network error.
+ if (
+ request.responseTainting === 'cors' &&
+ (locationURL.username || locationURL.password)
+ ) {
+ return Promise.resolve(makeNetworkError(
+ 'URL cannot contain credentials for request mode "cors"'
+ ))
+ }
+
+ // 11. If actualResponse’s status is not 303, request’s body is non-null,
+ // and request’s body’s source is null, then return a network error.
+ if (
+ actualResponse.status !== 303 &&
+ request.body != null &&
+ request.body.source == null
+ ) {
+ return Promise.resolve(makeNetworkError())
+ }
+
+ // 12. If one of the following is true
+ // - actualResponse’s status is 301 or 302 and request’s method is `POST`
+ // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD`
+ if (
+ ([301, 302].includes(actualResponse.status) && request.method === 'POST') ||
+ (actualResponse.status === 303 &&
+ !GET_OR_HEAD.includes(request.method))
+ ) {
+ // then:
+ // 1. Set request’s method to `GET` and request’s body to null.
+ request.method = 'GET'
+ request.body = null
+
+ // 2. For each headerName of request-body-header name, delete headerName from
+ // request’s header list.
+ for (const headerName of requestBodyHeader) {
+ request.headersList.delete(headerName)
+ }
+ }
+
+ // 13. If request’s current URL’s origin is not same origin with locationURL’s
+ // origin, then for each headerName of CORS non-wildcard request-header name,
+ // delete headerName from request’s header list.
+ if (!sameOrigin(requestCurrentURL(request), locationURL)) {
+ // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
+ request.headersList.delete('authorization', true)
+
+ // https://fetch.spec.whatwg.org/#authentication-entries
+ request.headersList.delete('proxy-authorization', true)
+
+ // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
+ request.headersList.delete('cookie', true)
+ request.headersList.delete('host', true)
+ }
+
+ // 14. If request's body is non-null, then set request's body to the first return
+ // value of safely extracting request's body's source.
+ if (request.body != null) {
+ assert(request.body.source != null)
+ request.body = safelyExtractBody(request.body.source)[0]
+ }
+
+ // 15. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 16. Set timingInfo’s redirect end time and post-redirect start time to the
+ // coarsened shared current time given fetchParams’s cross-origin isolated
+ // capability.
+ timingInfo.redirectEndTime = timingInfo.postRedirectStartTime =
+ coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
+
+ // 17. If timingInfo’s redirect start time is 0, then set timingInfo’s
+ // redirect start time to timingInfo’s start time.
+ if (timingInfo.redirectStartTime === 0) {
+ timingInfo.redirectStartTime = timingInfo.startTime
+ }
+
+ // 18. Append locationURL to request’s URL list.
+ request.urlList.push(locationURL)
+
+ // 19. Invoke set request’s referrer policy on redirect on request and
+ // actualResponse.
+ setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ // 20. Return the result of running main fetch given fetchParams and true.
+ return mainFetch(fetchParams, true)
+}
+
+// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
+async function httpNetworkOrCacheFetch (
+ fetchParams,
+ isAuthenticationFetch = false,
+ isNewConnectionFetch = false
+) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let httpFetchParams be null.
+ let httpFetchParams = null
+
+ // 3. Let httpRequest be null.
+ let httpRequest = null
+
+ // 4. Let response be null.
+ let response = null
+
+ // 5. Let storedResponse be null.
+ // TODO: cache
+
+ // 6. Let httpCache be null.
+ const httpCache = null
+
+ // 7. Let the revalidatingFlag be unset.
+ const revalidatingFlag = false
+
+ // 8. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. If request’s window is "no-window" and request’s redirect mode is
+ // "error", then set httpFetchParams to fetchParams and httpRequest to
+ // request.
+ if (request.window === 'no-window' && request.redirect === 'error') {
+ httpFetchParams = fetchParams
+ httpRequest = request
+ } else {
+ // Otherwise:
+
+ // 1. Set httpRequest to a clone of request.
+ httpRequest = cloneRequest(request)
+
+ // 2. Set httpFetchParams to a copy of fetchParams.
+ httpFetchParams = { ...fetchParams }
+
+ // 3. Set httpFetchParams’s request to httpRequest.
+ httpFetchParams.request = httpRequest
+ }
+
+ // 3. Let includeCredentials be true if one of
+ const includeCredentials =
+ request.credentials === 'include' ||
+ (request.credentials === 'same-origin' &&
+ request.responseTainting === 'basic')
+
+ // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s
+ // body is non-null; otherwise null.
+ const contentLength = httpRequest.body ? httpRequest.body.length : null
+
+ // 5. Let contentLengthHeaderValue be null.
+ let contentLengthHeaderValue = null
+
+ // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or
+ // `PUT`, then set contentLengthHeaderValue to `0`.
+ if (
+ httpRequest.body == null &&
+ ['POST', 'PUT'].includes(httpRequest.method)
+ ) {
+ contentLengthHeaderValue = '0'
+ }
+
+ // 7. If contentLength is non-null, then set contentLengthHeaderValue to
+ // contentLength, serialized and isomorphic encoded.
+ if (contentLength != null) {
+ contentLengthHeaderValue = isomorphicEncode(`${contentLength}`)
+ }
+
+ // 8. If contentLengthHeaderValue is non-null, then append
+ // `Content-Length`/contentLengthHeaderValue to httpRequest’s header
+ // list.
+ if (contentLengthHeaderValue != null) {
+ httpRequest.headersList.append('content-length', contentLengthHeaderValue, true)
+ }
+
+ // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`,
+ // contentLengthHeaderValue) to httpRequest’s header list.
+
+ // 10. If contentLength is non-null and httpRequest’s keepalive is true,
+ // then:
+ if (contentLength != null && httpRequest.keepalive) {
+ // NOTE: keepalive is a noop outside of browser context.
+ }
+
+ // 11. If httpRequest’s referrer is a URL, then append
+ // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded,
+ // to httpRequest’s header list.
+ if (webidl.is.URL(httpRequest.referrer)) {
+ httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href), true)
+ }
+
+ // 12. Append a request `Origin` header for httpRequest.
+ appendRequestOriginHeader(httpRequest)
+
+ // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA]
+ appendFetchMetadata(httpRequest)
+
+ // 14. If httpRequest’s header list does not contain `User-Agent`, then
+ // user agents should append `User-Agent`/default `User-Agent` value to
+ // httpRequest’s header list.
+ if (!httpRequest.headersList.contains('user-agent', true)) {
+ httpRequest.headersList.append('user-agent', defaultUserAgent, true)
+ }
+
+ // 15. If httpRequest’s cache mode is "default" and httpRequest’s header
+ // list contains `If-Modified-Since`, `If-None-Match`,
+ // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set
+ // httpRequest’s cache mode to "no-store".
+ if (
+ httpRequest.cache === 'default' &&
+ (httpRequest.headersList.contains('if-modified-since', true) ||
+ httpRequest.headersList.contains('if-none-match', true) ||
+ httpRequest.headersList.contains('if-unmodified-since', true) ||
+ httpRequest.headersList.contains('if-match', true) ||
+ httpRequest.headersList.contains('if-range', true))
+ ) {
+ httpRequest.cache = 'no-store'
+ }
+
+ // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent
+ // no-cache cache-control header modification flag is unset, and
+ // httpRequest’s header list does not contain `Cache-Control`, then append
+ // `Cache-Control`/`max-age=0` to httpRequest’s header list.
+ if (
+ httpRequest.cache === 'no-cache' &&
+ !httpRequest.preventNoCacheCacheControlHeaderModification &&
+ !httpRequest.headersList.contains('cache-control', true)
+ ) {
+ httpRequest.headersList.append('cache-control', 'max-age=0', true)
+ }
+
+ // 17. If httpRequest’s cache mode is "no-store" or "reload", then:
+ if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') {
+ // 1. If httpRequest’s header list does not contain `Pragma`, then append
+ // `Pragma`/`no-cache` to httpRequest’s header list.
+ if (!httpRequest.headersList.contains('pragma', true)) {
+ httpRequest.headersList.append('pragma', 'no-cache', true)
+ }
+
+ // 2. If httpRequest’s header list does not contain `Cache-Control`,
+ // then append `Cache-Control`/`no-cache` to httpRequest’s header list.
+ if (!httpRequest.headersList.contains('cache-control', true)) {
+ httpRequest.headersList.append('cache-control', 'no-cache', true)
+ }
+ }
+
+ // 18. If httpRequest’s header list contains `Range`, then append
+ // `Accept-Encoding`/`identity` to httpRequest’s header list.
+ if (httpRequest.headersList.contains('range', true)) {
+ httpRequest.headersList.append('accept-encoding', 'identity', true)
+ }
+
+ // 19. Modify httpRequest’s header list per HTTP. Do not append a given
+ // header if httpRequest’s header list contains that header’s name.
+ // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
+ if (!httpRequest.headersList.contains('accept-encoding', true)) {
+ if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
+ httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate', true)
+ } else {
+ httpRequest.headersList.append('accept-encoding', 'gzip, deflate', true)
+ }
+ }
+
+ httpRequest.headersList.delete('host', true)
+
+ // 21. If includeCredentials is true, then:
+ if (includeCredentials) {
+ // 1. If the user agent is not configured to block cookies for httpRequest
+ // (see section 7 of [COOKIES]), then:
+ // TODO: credentials
+
+ // 2. If httpRequest’s header list does not contain `Authorization`, then:
+ if (!httpRequest.headersList.contains('authorization', true)) {
+ // 1. Let authorizationValue be null.
+ let authorizationValue = null
+
+ // 2. If there’s an authentication entry for httpRequest and either
+ // httpRequest’s use-URL-credentials flag is unset or httpRequest’s
+ // current URL does not include credentials, then set
+ // authorizationValue to authentication entry.
+ if (hasAuthenticationEntry(httpRequest) && (
+ httpRequest.useURLCredentials === undefined || !includesCredentials(requestCurrentURL(httpRequest))
+ )) {
+ // TODO
+ } else if (includesCredentials(requestCurrentURL(httpRequest)) && isAuthenticationFetch) {
+ // 3. Otherwise, if httpRequest’s current URL does include credentials
+ // and isAuthenticationFetch is true, set authorizationValue to
+ // httpRequest’s current URL, converted to an `Authorization` value
+ const { username, password } = requestCurrentURL(httpRequest)
+ authorizationValue = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
+ }
+
+ // 4. If authorizationValue is non-null, then append (`Authorization`,
+ // authorizationValue) to httpRequest’s header list.
+ if (authorizationValue !== null) {
+ httpRequest.headersList.append('Authorization', authorizationValue, false)
+ }
+ }
+ }
+
+ // 21. If there’s a proxy-authentication entry, use it as appropriate.
+ // TODO: proxy-authentication
+
+ // 22. Set httpCache to the result of determining the HTTP cache
+ // partition, given httpRequest.
+ // TODO: cache
+
+ // 23. If httpCache is null, then set httpRequest’s cache mode to
+ // "no-store".
+ if (httpCache == null) {
+ httpRequest.cache = 'no-store'
+ }
+
+ // 24. If httpRequest’s cache mode is neither "no-store" nor "reload",
+ // then:
+ if (httpRequest.cache !== 'no-store' && httpRequest.cache !== 'reload') {
+ // TODO: cache
+ }
+
+ // 9. If aborted, then return the appropriate network error for fetchParams.
+ // TODO
+
+ // 10. If response is null, then:
+ if (response == null) {
+ // 1. If httpRequest’s cache mode is "only-if-cached", then return a
+ // network error.
+ if (httpRequest.cache === 'only-if-cached') {
+ return makeNetworkError('only if cached')
+ }
+
+ // 2. Let forwardResponse be the result of running HTTP-network fetch
+ // given httpFetchParams, includeCredentials, and isNewConnectionFetch.
+ const forwardResponse = await httpNetworkFetch(
+ httpFetchParams,
+ includeCredentials,
+ isNewConnectionFetch
+ )
+
+ // 3. If httpRequest’s method is unsafe and forwardResponse’s status is
+ // in the range 200 to 399, inclusive, invalidate appropriate stored
+ // responses in httpCache, as per the "Invalidation" chapter of HTTP
+ // Caching, and set storedResponse to null. [HTTP-CACHING]
+ if (
+ !safeMethodsSet.has(httpRequest.method) &&
+ forwardResponse.status >= 200 &&
+ forwardResponse.status <= 399
+ ) {
+ // TODO: cache
+ }
+
+ // 4. If the revalidatingFlag is set and forwardResponse’s status is 304,
+ // then:
+ if (revalidatingFlag && forwardResponse.status === 304) {
+ // TODO: cache
+ }
+
+ // 5. If response is null, then:
+ if (response == null) {
+ // 1. Set response to forwardResponse.
+ response = forwardResponse
+
+ // 2. Store httpRequest and forwardResponse in httpCache, as per the
+ // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING]
+ // TODO: cache
+ }
+ }
+
+ // 11. Set response’s URL list to a clone of httpRequest’s URL list.
+ response.urlList = [...httpRequest.urlList]
+
+ // 12. If httpRequest’s header list contains `Range`, then set response’s
+ // range-requested flag.
+ if (httpRequest.headersList.contains('range', true)) {
+ response.rangeRequested = true
+ }
+
+ // 13. Set response’s request-includes-credentials to includeCredentials.
+ response.requestIncludesCredentials = includeCredentials
+
+ // 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
+ // includeCredentials is true, and request’s traversable for user prompts is
+ // a traversable navigable:
+ if (response.status === 401 && httpRequest.responseTainting !== 'cors' && includeCredentials && isTraversableNavigable(request.traversableForUserPrompts)) {
+ // 2. If request’s body is non-null, then:
+ if (request.body != null) {
+ // 1. If request’s body’s source is null, then return a network error.
+ if (request.body.source == null) {
+ return makeNetworkError('expected non-null body source')
+ }
+
+ // 2. Set request’s body to the body of the result of safely extracting
+ // request’s body’s source.
+ request.body = safelyExtractBody(request.body.source)[0]
+ }
+
+ // 3. If request’s use-URL-credentials flag is unset or isAuthenticationFetch is
+ // true, then:
+ if (request.useURLCredentials === undefined || isAuthenticationFetch) {
+ // 1. If fetchParams is canceled, then return the appropriate network error
+ // for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 2. Let username and password be the result of prompting the end user for a
+ // username and password, respectively, in request’s traversable for user prompts.
+ // TODO
+
+ // 3. Set the username given request’s current URL and username.
+ // requestCurrentURL(request).username = TODO
+
+ // 4. Set the password given request’s current URL and password.
+ // requestCurrentURL(request).password = TODO
+
+ // In browsers, the user will be prompted to enter a username/password before the request
+ // is re-sent. To prevent an infinite 401 loop, return the response for now.
+ // https://github.com/nodejs/undici/pull/4756
+ return response
+ }
+
+ // 4. Set response to the result of running HTTP-network-or-cache fetch given
+ // fetchParams and true.
+ fetchParams.controller.connection.destroy()
+
+ response = await httpNetworkOrCacheFetch(fetchParams, true)
+ }
+
+ // 15. If response’s status is 407, then:
+ if (response.status === 407) {
+ // 1. If request’s window is "no-window", then return a network error.
+ if (request.window === 'no-window') {
+ return makeNetworkError()
+ }
+
+ // 2. ???
+
+ // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 4. Prompt the end user as appropriate in request’s window and store
+ // the result as a proxy-authentication entry. [HTTP-AUTH]
+ // TODO: Invoke some kind of callback?
+
+ // 5. Set response to the result of running HTTP-network-or-cache fetch given
+ // fetchParams.
+ // TODO
+ return makeNetworkError('proxy authentication required')
+ }
+
+ // 16. If all of the following are true
+ if (
+ // response’s status is 421
+ response.status === 421 &&
+ // isNewConnectionFetch is false
+ !isNewConnectionFetch &&
+ // request’s body is null, or request’s body is non-null and request’s body’s source is non-null
+ (request.body == null || request.body.source != null)
+ ) {
+ // then:
+
+ // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 2. Set response to the result of running HTTP-network-or-cache
+ // fetch given fetchParams, isAuthenticationFetch, and true.
+
+ // TODO (spec): The spec doesn't specify this but we need to cancel
+ // the active response before we can start a new one.
+ // https://github.com/whatwg/fetch/issues/1293
+ fetchParams.controller.connection.destroy()
+
+ response = await httpNetworkOrCacheFetch(
+ fetchParams,
+ isAuthenticationFetch,
+ true
+ )
+ }
+
+ // 17. If isAuthenticationFetch is true, then create an authentication entry
+ if (isAuthenticationFetch) {
+ // TODO
+ }
+
+ // 18. Return response.
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#http-network-fetch
+async function httpNetworkFetch (
+ fetchParams,
+ includeCredentials = false,
+ forceNewConnection = false
+) {
+ assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed)
+
+ fetchParams.controller.connection = {
+ abort: null,
+ destroyed: false,
+ destroy (err, abort = true) {
+ if (!this.destroyed) {
+ this.destroyed = true
+ if (abort) {
+ this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
+ }
+ }
+ }
+ }
+
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 4. Let httpCache be the result of determining the HTTP cache partition,
+ // given request.
+ // TODO: cache
+ const httpCache = null
+
+ // 5. If httpCache is null, then set request’s cache mode to "no-store".
+ if (httpCache == null) {
+ request.cache = 'no-store'
+ }
+
+ // 6. Let networkPartitionKey be the result of determining the network
+ // partition key given request.
+ // TODO
+
+ // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise
+ // "no".
+ const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars
+
+ // 8. Switch on request’s mode:
+ if (request.mode === 'websocket') {
+ // Let connection be the result of obtaining a WebSocket connection,
+ // given request’s current URL.
+ // TODO
+ } else {
+ // Let connection be the result of obtaining a connection, given
+ // networkPartitionKey, request’s current URL’s origin,
+ // includeCredentials, and forceNewConnection.
+ // TODO
+ }
+
+ // 9. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. If connection is failure, then return a network error.
+
+ // 2. Set timingInfo’s final connection timing info to the result of
+ // calling clamp and coarsen connection timing info with connection’s
+ // timing info, timingInfo’s post-redirect start time, and fetchParams’s
+ // cross-origin isolated capability.
+
+ // 3. If connection is not an HTTP/2 connection, request’s body is non-null,
+ // and request’s body’s source is null, then append (`Transfer-Encoding`,
+ // `chunked`) to request’s header list.
+
+ // 4. Set timingInfo’s final network-request start time to the coarsened
+ // shared current time given fetchParams’s cross-origin isolated
+ // capability.
+
+ // 5. Set response to the result of making an HTTP request over connection
+ // using request with the following caveats:
+
+ // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS]
+ // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH]
+
+ // - If request’s body is non-null, and request’s body’s source is null,
+ // then the user agent may have a buffer of up to 64 kibibytes and store
+ // a part of request’s body in that buffer. If the user agent reads from
+ // request’s body beyond that buffer’s size and the user agent needs to
+ // resend request, then instead return a network error.
+
+ // - Set timingInfo’s final network-response start time to the coarsened
+ // shared current time given fetchParams’s cross-origin isolated capability,
+ // immediately after the user agent’s HTTP parser receives the first byte
+ // of the response (e.g., frame header bytes for HTTP/2 or response status
+ // line for HTTP/1.x).
+
+ // - Wait until all the headers are transmitted.
+
+ // - Any responses whose status is in the range 100 to 199, inclusive,
+ // and is not 101, are to be ignored, except for the purposes of setting
+ // timingInfo’s final network-response start time above.
+
+ // - If request’s header list contains `Transfer-Encoding`/`chunked` and
+ // response is transferred via HTTP/1.0 or older, then return a network
+ // error.
+
+ // - If the HTTP request results in a TLS client certificate dialog, then:
+
+ // 1. If request’s window is an environment settings object, make the
+ // dialog available in request’s window.
+
+ // 2. Otherwise, return a network error.
+
+ // To transmit request’s body body, run these steps:
+ let requestBody = null
+ // 1. If body is null and fetchParams’s process request end-of-body is
+ // non-null, then queue a fetch task given fetchParams’s process request
+ // end-of-body and fetchParams’s task destination.
+ if (request.body == null && fetchParams.processRequestEndOfBody) {
+ queueMicrotask(() => fetchParams.processRequestEndOfBody())
+ } else if (request.body != null) {
+ // 2. Otherwise, if body is non-null:
+
+ // 1. Let processBodyChunk given bytes be these steps:
+ const processBodyChunk = async function * (bytes) {
+ // 1. If the ongoing fetch is terminated, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. Run this step in parallel: transmit bytes.
+ yield bytes
+
+ // 3. If fetchParams’s process request body is non-null, then run
+ // fetchParams’s process request body given bytes’s length.
+ fetchParams.processRequestBodyChunkLength?.(bytes.byteLength)
+ }
+
+ // 2. Let processEndOfBody be these steps:
+ const processEndOfBody = () => {
+ // 1. If fetchParams is canceled, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. If fetchParams’s process request end-of-body is non-null,
+ // then run fetchParams’s process request end-of-body.
+ if (fetchParams.processRequestEndOfBody) {
+ fetchParams.processRequestEndOfBody()
+ }
+ }
+
+ // 3. Let processBodyError given e be these steps:
+ const processBodyError = (e) => {
+ // 1. If fetchParams is canceled, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller.
+ if (e.name === 'AbortError') {
+ fetchParams.controller.abort()
+ } else {
+ fetchParams.controller.terminate(e)
+ }
+ }
+
+ // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody,
+ // processBodyError, and fetchParams’s task destination.
+ requestBody = (async function * () {
+ try {
+ for await (const bytes of request.body.stream) {
+ yield * processBodyChunk(bytes)
+ }
+ processEndOfBody()
+ } catch (err) {
+ processBodyError(err)
+ }
+ })()
+ }
+
+ try {
+ // socket is only provided for websockets
+ const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody })
+
+ if (socket) {
+ response = makeResponse({ status, statusText, headersList, socket })
+ } else {
+ const iterator = body[Symbol.asyncIterator]()
+ fetchParams.controller.next = () => iterator.next()
+
+ response = makeResponse({ status, statusText, headersList })
+ }
+ } catch (err) {
+ // 10. If aborted, then:
+ if (err.name === 'AbortError') {
+ // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame.
+ fetchParams.controller.connection.destroy()
+
+ // 2. Return the appropriate network error for fetchParams.
+ return makeAppropriateNetworkError(fetchParams, err)
+ }
+
+ return makeNetworkError(err)
+ }
+
+ // 11. Let pullAlgorithm be an action that resumes the ongoing fetch
+ // if it is suspended.
+ const pullAlgorithm = () => {
+ return fetchParams.controller.resume()
+ }
+
+ // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s
+ // controller with reason, given reason.
+ const cancelAlgorithm = (reason) => {
+ // If the aborted fetch was already terminated, then we do not
+ // need to do anything.
+ if (!isCancelled(fetchParams)) {
+ fetchParams.controller.abort(reason)
+ }
+ }
+
+ // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by
+ // the user agent.
+ // TODO
+
+ // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object
+ // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent.
+ // TODO
+
+ // 15. Let stream be a new ReadableStream.
+ // 16. Set up stream with byte reading support with pullAlgorithm set to pullAlgorithm,
+ // cancelAlgorithm set to cancelAlgorithm.
+ const stream = new ReadableStream(
+ {
+ start (controller) {
+ fetchParams.controller.controller = controller
+ },
+ pull: pullAlgorithm,
+ cancel: cancelAlgorithm,
+ type: 'bytes'
+ }
+ )
+
+ // 17. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. Set response’s body to a new body whose stream is stream.
+ response.body = { stream, source: null, length: null }
+
+ // 2. If response is not a network error and request’s cache mode is
+ // not "no-store", then update response in httpCache for request.
+ // TODO
+
+ // 3. If includeCredentials is true and the user agent is not configured
+ // to block cookies for request (see section 7 of [COOKIES]), then run the
+ // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on
+ // the value of each header whose name is a byte-case-insensitive match for
+ // `Set-Cookie` in response’s header list, if any, and request’s current URL.
+ // TODO
+
+ // 18. If aborted, then:
+ // TODO
+
+ // 19. Run these steps in parallel:
+
+ // 1. Run these steps, but abort when fetchParams is canceled:
+ if (!fetchParams.controller.resume) {
+ fetchParams.controller.on('terminated', onAborted)
+ }
+
+ fetchParams.controller.resume = async () => {
+ // 1. While true
+ while (true) {
+ // 1-3. See onData...
+
+ // 4. Set bytes to the result of handling content codings given
+ // codings and bytes.
+ let bytes
+ let isFailure
+ try {
+ const { done, value } = await fetchParams.controller.next()
+
+ if (isAborted(fetchParams)) {
+ break
+ }
+
+ bytes = done ? undefined : value
+ } catch (err) {
+ if (fetchParams.controller.ended && !timingInfo.encodedBodySize) {
+ // zlib doesn't like empty streams.
+ bytes = undefined
+ } else {
+ bytes = err
+
+ // err may be propagated from the result of calling readablestream.cancel,
+ // which might not be an error. https://github.com/nodejs/undici/issues/2009
+ isFailure = true
+ }
+ }
+
+ if (bytes === undefined) {
+ // 2. Otherwise, if the bytes transmission for response’s message
+ // body is done normally and stream is readable, then close
+ // stream, finalize response for fetchParams and response, and
+ // abort these in-parallel steps.
+ readableStreamClose(fetchParams.controller.controller)
+
+ finalizeResponse(fetchParams, response)
+
+ return
+ }
+
+ // 5. Increase timingInfo’s decoded body size by bytes’s length.
+ timingInfo.decodedBodySize += bytes?.byteLength ?? 0
+
+ // 6. If bytes is failure, then terminate fetchParams’s controller.
+ if (isFailure) {
+ fetchParams.controller.terminate(bytes)
+ return
+ }
+
+ // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes
+ // into stream.
+ const buffer = new Uint8Array(bytes)
+ if (buffer.byteLength) {
+ fetchParams.controller.controller.enqueue(buffer)
+ }
+
+ // 8. If stream is errored, then terminate the ongoing fetch.
+ if (isErrored(stream)) {
+ fetchParams.controller.terminate()
+ return
+ }
+
+ // 9. If stream doesn’t need more data ask the user agent to suspend
+ // the ongoing fetch.
+ if (fetchParams.controller.controller.desiredSize <= 0) {
+ return
+ }
+ }
+ }
+
+ // 2. If aborted, then:
+ function onAborted (reason) {
+ // 2. If fetchParams is aborted, then:
+ if (isAborted(fetchParams)) {
+ // 1. Set response’s aborted flag.
+ response.aborted = true
+
+ // 2. If stream is readable, then error stream with the result of
+ // deserialize a serialized abort reason given fetchParams’s
+ // controller’s serialized abort reason and an
+ // implementation-defined realm.
+ if (isReadable(stream)) {
+ fetchParams.controller.controller.error(
+ fetchParams.controller.serializedAbortReason
+ )
+ }
+ } else {
+ // 3. Otherwise, if stream is readable, error stream with a TypeError.
+ if (isReadable(stream)) {
+ fetchParams.controller.controller.error(new TypeError('terminated', {
+ cause: isErrorLike(reason) ? reason : undefined
+ }))
+ }
+ }
+
+ // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame.
+ // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so.
+ fetchParams.controller.connection.destroy()
+ }
+
+ // 20. Return response.
+ return response
+
+ function dispatch ({ body }) {
+ const url = requestCurrentURL(request)
+ /** @type {import('../../..').Agent} */
+ const agent = fetchParams.controller.dispatcher
+
+ return new Promise((resolve, reject) => agent.dispatch(
+ {
+ path: url.pathname + url.search,
+ origin: url.origin,
+ method: request.method,
+ body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
+ headers: request.headersList.entries,
+ maxRedirections: 0,
+ upgrade: request.mode === 'websocket' ? 'websocket' : undefined
+ },
+ {
+ body: null,
+ abort: null,
+
+ onConnect (abort) {
+ // TODO (fix): Do we need connection here?
+ const { connection } = fetchParams.controller
+
+ // Set timingInfo’s final connection timing info to the result of calling clamp and coarsen
+ // connection timing info with connection’s timing info, timingInfo’s post-redirect start
+ // time, and fetchParams’s cross-origin isolated capability.
+ // TODO: implement connection timing
+ timingInfo.finalConnectionTimingInfo = clampAndCoarsenConnectionTimingInfo(undefined, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability)
+
+ if (connection.destroyed) {
+ abort(new DOMException('The operation was aborted.', 'AbortError'))
+ } else {
+ fetchParams.controller.on('terminated', abort)
+ this.abort = connection.abort = abort
+ }
+
+ // Set timingInfo’s final network-request start time to the coarsened shared current time given
+ // fetchParams’s cross-origin isolated capability.
+ timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
+ },
+
+ onResponseStarted () {
+ // Set timingInfo’s final network-response start time to the coarsened shared current
+ // time given fetchParams’s cross-origin isolated capability, immediately after the
+ // user agent’s HTTP parser receives the first byte of the response (e.g., frame header
+ // bytes for HTTP/2 or response status line for HTTP/1.x).
+ timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
+ },
+
+ onHeaders (status, rawHeaders, resume, statusText) {
+ if (status < 200) {
+ return false
+ }
+
+ const headersList = new HeadersList()
+
+ for (let i = 0; i < rawHeaders.length; i += 2) {
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
+ }
+ const location = headersList.get('location', true)
+
+ this.body = new Readable({ read: resume })
+
+ const willFollow = location && request.redirect === 'follow' &&
+ redirectStatusSet.has(status)
+
+ const decoders = []
+
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
+ if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
+ const contentEncoding = headersList.get('content-encoding', true)
+ // "All content-coding values are case-insensitive..."
+ /** @type {string[]} */
+ const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
+
+ // Limit the number of content-encodings to prevent resource exhaustion.
+ // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
+ const maxContentEncodings = 5
+ if (codings.length > maxContentEncodings) {
+ reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
+ return true
+ }
+
+ for (let i = codings.length - 1; i >= 0; --i) {
+ const coding = codings[i].trim()
+ // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
+ if (coding === 'x-gzip' || coding === 'gzip') {
+ decoders.push(zlib.createGunzip({
+ // Be less strict when decoding compressed responses, since sometimes
+ // servers send slightly invalid responses that are still accepted
+ // by common browsers.
+ // Always using Z_SYNC_FLUSH is what cURL does.
+ flush: zlib.constants.Z_SYNC_FLUSH,
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
+ }))
+ } else if (coding === 'deflate') {
+ decoders.push(createInflate({
+ flush: zlib.constants.Z_SYNC_FLUSH,
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
+ }))
+ } else if (coding === 'br') {
+ decoders.push(zlib.createBrotliDecompress({
+ flush: zlib.constants.BROTLI_OPERATION_FLUSH,
+ finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
+ }))
+ } else if (coding === 'zstd' && hasZstd) {
+ decoders.push(zlib.createZstdDecompress({
+ flush: zlib.constants.ZSTD_e_continue,
+ finishFlush: zlib.constants.ZSTD_e_end
+ }))
+ } else {
+ decoders.length = 0
+ break
+ }
+ }
+ }
+
+ const onError = this.onError.bind(this)
+
+ resolve({
+ status,
+ statusText,
+ headersList,
+ body: decoders.length
+ ? pipeline(this.body, ...decoders, (err) => {
+ if (err) {
+ this.onError(err)
+ }
+ }).on('error', onError)
+ : this.body.on('error', onError)
+ })
+
+ return true
+ },
+
+ onData (chunk) {
+ if (fetchParams.controller.dump) {
+ return
+ }
+
+ // 1. If one or more bytes have been transmitted from response’s
+ // message body, then:
+
+ // 1. Let bytes be the transmitted bytes.
+ const bytes = chunk
+
+ // 2. Let codings be the result of extracting header list values
+ // given `Content-Encoding` and response’s header list.
+ // See pullAlgorithm.
+
+ // 3. Increase timingInfo’s encoded body size by bytes’s length.
+ timingInfo.encodedBodySize += bytes.byteLength
+
+ // 4. See pullAlgorithm...
+
+ return this.body.push(bytes)
+ },
+
+ onComplete () {
+ if (this.abort) {
+ fetchParams.controller.off('terminated', this.abort)
+ }
+
+ fetchParams.controller.ended = true
+
+ this.body.push(null)
+ },
+
+ onError (error) {
+ if (this.abort) {
+ fetchParams.controller.off('terminated', this.abort)
+ }
+
+ this.body?.destroy(error)
+
+ fetchParams.controller.terminate(error)
+
+ reject(error)
+ },
+
+ onRequestUpgrade (_controller, status, headers, socket) {
+ // We need to support 200 for websocket over h2 as per RFC-8441
+ // Absence of session means H1
+ if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
+ return false
+ }
+
+ const headersList = new HeadersList()
+
+ for (const [name, value] of Object.entries(headers)) {
+ if (value == null) {
+ continue
+ }
+
+ const headerName = name.toLowerCase()
+
+ if (Array.isArray(value)) {
+ for (const entry of value) {
+ headersList.append(headerName, String(entry), true)
+ }
+ } else {
+ headersList.append(headerName, String(value), true)
+ }
+ }
+
+ resolve({
+ status,
+ statusText: STATUS_CODES[status],
+ headersList,
+ socket
+ })
+
+ return true
+ },
+
+ onUpgrade (status, rawHeaders, socket) {
+ // We need to support 200 for websocket over h2 as per RFC-8441
+ // Absence of session means H1
+ if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
+ return false
+ }
+
+ const headersList = new HeadersList()
+
+ for (let i = 0; i < rawHeaders.length; i += 2) {
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
+ }
+
+ resolve({
+ status,
+ statusText: STATUS_CODES[status],
+ headersList,
+ socket
+ })
+
+ return true
+ }
+ }
+ ))
+ }
+}
+
+module.exports = {
+ fetch,
+ Fetch,
+ fetching,
+ finalizeAndReportTiming
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/request.js b/vanilla/node_modules/undici/lib/web/fetch/request.js
new file mode 100644
index 0000000..6ef40f9
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/request.js
@@ -0,0 +1,1115 @@
+/* globals AbortController */
+
+'use strict'
+
+const { extractBody, mixinBody, cloneBody, bodyUnusable } = require('./body')
+const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers')
+const util = require('../../core/util')
+const nodeUtil = require('node:util')
+const {
+ isValidHTTPToken,
+ sameOrigin,
+ environmentSettingsObject
+} = require('./util')
+const {
+ forbiddenMethodsSet,
+ corsSafeListedMethodsSet,
+ referrerPolicy,
+ requestRedirect,
+ requestMode,
+ requestCredentials,
+ requestCache,
+ requestDuplex
+} = require('./constants')
+const { kEnumerableProperty, normalizedMethodRecordsBase, normalizedMethodRecords } = util
+const { webidl } = require('../webidl')
+const { URLSerializer } = require('./data-url')
+const { kConstruct } = require('../../core/symbols')
+const assert = require('node:assert')
+const { getMaxListeners, setMaxListeners, defaultMaxListeners } = require('node:events')
+
+const kAbortController = Symbol('abortController')
+
+const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
+ signal.removeEventListener('abort', abort)
+})
+
+const dependentControllerMap = new WeakMap()
+
+let abortSignalHasEventHandlerLeakWarning
+
+try {
+ abortSignalHasEventHandlerLeakWarning = getMaxListeners(new AbortController().signal) > 0
+} catch {
+ abortSignalHasEventHandlerLeakWarning = false
+}
+
+function buildAbort (acRef) {
+ return abort
+
+ function abort () {
+ const ac = acRef.deref()
+ if (ac !== undefined) {
+ // Currently, there is a problem with FinalizationRegistry.
+ // https://github.com/nodejs/node/issues/49344
+ // https://github.com/nodejs/node/issues/47748
+ // In the case of abort, the first step is to unregister from it.
+ // If the controller can refer to it, it is still registered.
+ // It will be removed in the future.
+ requestFinalizer.unregister(abort)
+
+ // Unsubscribe a listener.
+ // FinalizationRegistry will no longer be called, so this must be done.
+ this.removeEventListener('abort', abort)
+
+ ac.abort(this.reason)
+
+ const controllerList = dependentControllerMap.get(ac.signal)
+
+ if (controllerList !== undefined) {
+ if (controllerList.size !== 0) {
+ for (const ref of controllerList) {
+ const ctrl = ref.deref()
+ if (ctrl !== undefined) {
+ ctrl.abort(this.reason)
+ }
+ }
+ controllerList.clear()
+ }
+ dependentControllerMap.delete(ac.signal)
+ }
+ }
+ }
+}
+
+let patchMethodWarning = false
+
+// https://fetch.spec.whatwg.org/#request-class
+class Request {
+ /** @type {AbortSignal} */
+ #signal
+
+ /** @type {import('../../dispatcher/dispatcher')} */
+ #dispatcher
+
+ /** @type {Headers} */
+ #headers
+
+ #state
+
+ // https://fetch.spec.whatwg.org/#dom-request
+ constructor (input, init = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (input === kConstruct) {
+ return
+ }
+
+ const prefix = 'Request constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ input = webidl.converters.RequestInfo(input)
+ init = webidl.converters.RequestInit(init)
+
+ // 1. Let request be null.
+ let request = null
+
+ // 2. Let fallbackMode be null.
+ let fallbackMode = null
+
+ // 3. Let baseURL be this’s relevant settings object’s API base URL.
+ const baseUrl = environmentSettingsObject.settingsObject.baseUrl
+
+ // 4. Let signal be null.
+ let signal = null
+
+ // 5. If input is a string, then:
+ if (typeof input === 'string') {
+ this.#dispatcher = init.dispatcher
+
+ // 1. Let parsedURL be the result of parsing input with baseURL.
+ // 2. If parsedURL is failure, then throw a TypeError.
+ let parsedURL
+ try {
+ parsedURL = new URL(input, baseUrl)
+ } catch (err) {
+ throw new TypeError('Failed to parse URL from ' + input, { cause: err })
+ }
+
+ // 3. If parsedURL includes credentials, then throw a TypeError.
+ if (parsedURL.username || parsedURL.password) {
+ throw new TypeError(
+ 'Request cannot be constructed from a URL that includes credentials: ' +
+ input
+ )
+ }
+
+ // 4. Set request to a new request whose URL is parsedURL.
+ request = makeRequest({ urlList: [parsedURL] })
+
+ // 5. Set fallbackMode to "cors".
+ fallbackMode = 'cors'
+ } else {
+ // 6. Otherwise:
+
+ // 7. Assert: input is a Request object.
+ assert(webidl.is.Request(input))
+
+ // 8. Set request to input’s request.
+ request = input.#state
+
+ // 9. Set signal to input’s signal.
+ signal = input.#signal
+
+ this.#dispatcher = init.dispatcher || input.#dispatcher
+ }
+
+ // 7. Let origin be this’s relevant settings object’s origin.
+ const origin = environmentSettingsObject.settingsObject.origin
+
+ // 8. Let window be "client".
+ let window = 'client'
+
+ // 9. If request’s window is an environment settings object and its origin
+ // is same origin with origin, then set window to request’s window.
+ if (
+ request.window?.constructor?.name === 'EnvironmentSettingsObject' &&
+ sameOrigin(request.window, origin)
+ ) {
+ window = request.window
+ }
+
+ // 10. If init["window"] exists and is non-null, then throw a TypeError.
+ if (init.window != null) {
+ throw new TypeError(`'window' option '${window}' must be null`)
+ }
+
+ // 11. If init["window"] exists, then set window to "no-window".
+ if ('window' in init) {
+ window = 'no-window'
+ }
+
+ // 12. Set request to a new request with the following properties:
+ request = makeRequest({
+ // URL request’s URL.
+ // undici implementation note: this is set as the first item in request's urlList in makeRequest
+ // method request’s method.
+ method: request.method,
+ // header list A copy of request’s header list.
+ // undici implementation note: headersList is cloned in makeRequest
+ headersList: request.headersList,
+ // unsafe-request flag Set.
+ unsafeRequest: request.unsafeRequest,
+ // client This’s relevant settings object.
+ client: environmentSettingsObject.settingsObject,
+ // window window.
+ window,
+ // priority request’s priority.
+ priority: request.priority,
+ // origin request’s origin. The propagation of the origin is only significant for navigation requests
+ // being handled by a service worker. In this scenario a request can have an origin that is different
+ // from the current client.
+ origin: request.origin,
+ // referrer request’s referrer.
+ referrer: request.referrer,
+ // referrer policy request’s referrer policy.
+ referrerPolicy: request.referrerPolicy,
+ // mode request’s mode.
+ mode: request.mode,
+ // credentials mode request’s credentials mode.
+ credentials: request.credentials,
+ // cache mode request’s cache mode.
+ cache: request.cache,
+ // redirect mode request’s redirect mode.
+ redirect: request.redirect,
+ // integrity metadata request’s integrity metadata.
+ integrity: request.integrity,
+ // keepalive request’s keepalive.
+ keepalive: request.keepalive,
+ // reload-navigation flag request’s reload-navigation flag.
+ reloadNavigation: request.reloadNavigation,
+ // history-navigation flag request’s history-navigation flag.
+ historyNavigation: request.historyNavigation,
+ // URL list A clone of request’s URL list.
+ urlList: [...request.urlList]
+ })
+
+ const initHasKey = Object.keys(init).length !== 0
+
+ // 13. If init is not empty, then:
+ if (initHasKey) {
+ // 1. If request’s mode is "navigate", then set it to "same-origin".
+ if (request.mode === 'navigate') {
+ request.mode = 'same-origin'
+ }
+
+ // 2. Unset request’s reload-navigation flag.
+ request.reloadNavigation = false
+
+ // 3. Unset request’s history-navigation flag.
+ request.historyNavigation = false
+
+ // 4. Set request’s origin to "client".
+ request.origin = 'client'
+
+ // 5. Set request’s referrer to "client"
+ request.referrer = 'client'
+
+ // 6. Set request’s referrer policy to the empty string.
+ request.referrerPolicy = ''
+
+ // 7. Set request’s URL to request’s current URL.
+ request.url = request.urlList[request.urlList.length - 1]
+
+ // 8. Set request’s URL list to « request’s URL ».
+ request.urlList = [request.url]
+ }
+
+ // 14. If init["referrer"] exists, then:
+ if (init.referrer !== undefined) {
+ // 1. Let referrer be init["referrer"].
+ const referrer = init.referrer
+
+ // 2. If referrer is the empty string, then set request’s referrer to "no-referrer".
+ if (referrer === '') {
+ request.referrer = 'no-referrer'
+ } else {
+ // 1. Let parsedReferrer be the result of parsing referrer with
+ // baseURL.
+ // 2. If parsedReferrer is failure, then throw a TypeError.
+ let parsedReferrer
+ try {
+ parsedReferrer = new URL(referrer, baseUrl)
+ } catch (err) {
+ throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err })
+ }
+
+ // 3. If one of the following is true
+ // - parsedReferrer’s scheme is "about" and path is the string "client"
+ // - parsedReferrer’s origin is not same origin with origin
+ // then set request’s referrer to "client".
+ if (
+ (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') ||
+ (origin && !sameOrigin(parsedReferrer, environmentSettingsObject.settingsObject.baseUrl))
+ ) {
+ request.referrer = 'client'
+ } else {
+ // 4. Otherwise, set request’s referrer to parsedReferrer.
+ request.referrer = parsedReferrer
+ }
+ }
+ }
+
+ // 15. If init["referrerPolicy"] exists, then set request’s referrer policy
+ // to it.
+ if (init.referrerPolicy !== undefined) {
+ request.referrerPolicy = init.referrerPolicy
+ }
+
+ // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise.
+ let mode
+ if (init.mode !== undefined) {
+ mode = init.mode
+ } else {
+ mode = fallbackMode
+ }
+
+ // 17. If mode is "navigate", then throw a TypeError.
+ if (mode === 'navigate') {
+ throw webidl.errors.exception({
+ header: 'Request constructor',
+ message: 'invalid request mode navigate.'
+ })
+ }
+
+ // 18. If mode is non-null, set request’s mode to mode.
+ if (mode != null) {
+ request.mode = mode
+ }
+
+ // 19. If init["credentials"] exists, then set request’s credentials mode
+ // to it.
+ if (init.credentials !== undefined) {
+ request.credentials = init.credentials
+ }
+
+ // 18. If init["cache"] exists, then set request’s cache mode to it.
+ if (init.cache !== undefined) {
+ request.cache = init.cache
+ }
+
+ // 21. If request’s cache mode is "only-if-cached" and request’s mode is
+ // not "same-origin", then throw a TypeError.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ throw new TypeError(
+ "'only-if-cached' can be set only with 'same-origin' mode"
+ )
+ }
+
+ // 22. If init["redirect"] exists, then set request’s redirect mode to it.
+ if (init.redirect !== undefined) {
+ request.redirect = init.redirect
+ }
+
+ // 23. If init["integrity"] exists, then set request’s integrity metadata to it.
+ if (init.integrity != null) {
+ request.integrity = String(init.integrity)
+ }
+
+ // 24. If init["keepalive"] exists, then set request’s keepalive to it.
+ if (init.keepalive !== undefined) {
+ request.keepalive = Boolean(init.keepalive)
+ }
+
+ // 25. If init["method"] exists, then:
+ if (init.method !== undefined) {
+ // 1. Let method be init["method"].
+ let method = init.method
+
+ const mayBeNormalized = normalizedMethodRecords[method]
+
+ if (mayBeNormalized !== undefined) {
+ // Note: Bypass validation DELETE, GET, HEAD, OPTIONS, POST, PUT, PATCH and these lowercase ones
+ request.method = mayBeNormalized
+ } else {
+ // 2. If method is not a method or method is a forbidden method, then
+ // throw a TypeError.
+ if (!isValidHTTPToken(method)) {
+ throw new TypeError(`'${method}' is not a valid HTTP method.`)
+ }
+
+ const upperCase = method.toUpperCase()
+
+ if (forbiddenMethodsSet.has(upperCase)) {
+ throw new TypeError(`'${method}' HTTP method is unsupported.`)
+ }
+
+ // 3. Normalize method.
+ // https://fetch.spec.whatwg.org/#concept-method-normalize
+ // Note: must be in uppercase
+ method = normalizedMethodRecordsBase[upperCase] ?? method
+
+ // 4. Set request’s method to method.
+ request.method = method
+ }
+
+ if (!patchMethodWarning && request.method === 'patch') {
+ process.emitWarning('Using `patch` is highly likely to result in a `405 Method Not Allowed`. `PATCH` is much more likely to succeed.', {
+ code: 'UNDICI-FETCH-patch'
+ })
+
+ patchMethodWarning = true
+ }
+ }
+
+ // 26. If init["signal"] exists, then set signal to it.
+ if (init.signal !== undefined) {
+ signal = init.signal
+ }
+
+ // 27. Set this’s request to request.
+ this.#state = request
+
+ // 28. Set this’s signal to a new AbortSignal object with this’s relevant
+ // Realm.
+ // TODO: could this be simplified with AbortSignal.any
+ // (https://dom.spec.whatwg.org/#dom-abortsignal-any)
+ const ac = new AbortController()
+ this.#signal = ac.signal
+
+ // 29. If signal is not null, then make this’s signal follow signal.
+ if (signal != null) {
+ if (signal.aborted) {
+ ac.abort(signal.reason)
+ } else {
+ // Keep a strong ref to ac while request object
+ // is alive. This is needed to prevent AbortController
+ // from being prematurely garbage collected.
+ // See, https://github.com/nodejs/undici/issues/1926.
+ this[kAbortController] = ac
+
+ const acRef = new WeakRef(ac)
+ const abort = buildAbort(acRef)
+
+ // If the max amount of listeners is equal to the default, increase it
+ if (abortSignalHasEventHandlerLeakWarning && getMaxListeners(signal) === defaultMaxListeners) {
+ setMaxListeners(1500, signal)
+ }
+
+ util.addAbortListener(signal, abort)
+ // The third argument must be a registry key to be unregistered.
+ // Without it, you cannot unregister.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
+ // abort is used as the unregister key. (because it is unique)
+ requestFinalizer.register(ac, { signal, abort }, abort)
+ }
+ }
+
+ // 30. Set this’s headers to a new Headers object with this’s relevant
+ // Realm, whose header list is request’s header list and guard is
+ // "request".
+ this.#headers = new Headers(kConstruct)
+ setHeadersList(this.#headers, request.headersList)
+ setHeadersGuard(this.#headers, 'request')
+
+ // 31. If this’s request’s mode is "no-cors", then:
+ if (mode === 'no-cors') {
+ // 1. If this’s request’s method is not a CORS-safelisted method,
+ // then throw a TypeError.
+ if (!corsSafeListedMethodsSet.has(request.method)) {
+ throw new TypeError(
+ `'${request.method} is unsupported in no-cors mode.`
+ )
+ }
+
+ // 2. Set this’s headers’s guard to "request-no-cors".
+ setHeadersGuard(this.#headers, 'request-no-cors')
+ }
+
+ // 32. If init is not empty, then:
+ if (initHasKey) {
+ /** @type {HeadersList} */
+ const headersList = getHeadersList(this.#headers)
+ // 1. Let headers be a copy of this’s headers and its associated header
+ // list.
+ // 2. If init["headers"] exists, then set headers to init["headers"].
+ const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList)
+
+ // 3. Empty this’s headers’s header list.
+ headersList.clear()
+
+ // 4. If headers is a Headers object, then for each header in its header
+ // list, append header’s name/header’s value to this’s headers.
+ if (headers instanceof HeadersList) {
+ for (const { name, value } of headers.rawValues()) {
+ headersList.append(name, value, false)
+ }
+ // Note: Copy the `set-cookie` meta-data.
+ headersList.cookies = headers.cookies
+ } else {
+ // 5. Otherwise, fill this’s headers with headers.
+ fillHeaders(this.#headers, headers)
+ }
+ }
+
+ // 33. Let inputBody be input’s request’s body if input is a Request
+ // object; otherwise null.
+ const inputBody = webidl.is.Request(input) ? input.#state.body : null
+
+ // 34. If either init["body"] exists and is non-null or inputBody is
+ // non-null, and request’s method is `GET` or `HEAD`, then throw a
+ // TypeError.
+ if (
+ (init.body != null || inputBody != null) &&
+ (request.method === 'GET' || request.method === 'HEAD')
+ ) {
+ throw new TypeError('Request with GET/HEAD method cannot have body.')
+ }
+
+ // 35. Let initBody be null.
+ let initBody = null
+
+ // 36. If init["body"] exists and is non-null, then:
+ if (init.body != null) {
+ // 1. Let Content-Type be null.
+ // 2. Set initBody and Content-Type to the result of extracting
+ // init["body"], with keepalive set to request’s keepalive.
+ const [extractedBody, contentType] = extractBody(
+ init.body,
+ request.keepalive
+ )
+ initBody = extractedBody
+
+ // 3, If Content-Type is non-null and this’s headers’s header list does
+ // not contain `Content-Type`, then append `Content-Type`/Content-Type to
+ // this’s headers.
+ if (contentType && !getHeadersList(this.#headers).contains('content-type', true)) {
+ this.#headers.append('content-type', contentType, true)
+ }
+ }
+
+ // 37. Let inputOrInitBody be initBody if it is non-null; otherwise
+ // inputBody.
+ const inputOrInitBody = initBody ?? inputBody
+
+ // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is
+ // null, then:
+ if (inputOrInitBody != null && inputOrInitBody.source == null) {
+ // 1. If initBody is non-null and init["duplex"] does not exist,
+ // then throw a TypeError.
+ if (initBody != null && init.duplex == null) {
+ throw new TypeError('RequestInit: duplex option is required when sending a body.')
+ }
+
+ // 2. If this’s request’s mode is neither "same-origin" nor "cors",
+ // then throw a TypeError.
+ if (request.mode !== 'same-origin' && request.mode !== 'cors') {
+ throw new TypeError(
+ 'If request is made from ReadableStream, mode should be "same-origin" or "cors"'
+ )
+ }
+
+ // 3. Set this’s request’s use-CORS-preflight flag.
+ request.useCORSPreflightFlag = true
+ }
+
+ // 39. Let finalBody be inputOrInitBody.
+ let finalBody = inputOrInitBody
+
+ // 40. If initBody is null and inputBody is non-null, then:
+ if (initBody == null && inputBody != null) {
+ // 1. If input is unusable, then throw a TypeError.
+ if (bodyUnusable(input.#state)) {
+ throw new TypeError(
+ 'Cannot construct a Request with a Request object that has already been used.'
+ )
+ }
+
+ // 2. Set finalBody to the result of creating a proxy for inputBody.
+ // https://streams.spec.whatwg.org/#readablestream-create-a-proxy
+ const identityTransform = new TransformStream()
+ inputBody.stream.pipeThrough(identityTransform)
+ finalBody = {
+ source: inputBody.source,
+ length: inputBody.length,
+ stream: identityTransform.readable
+ }
+ }
+
+ // 41. Set this’s request’s body to finalBody.
+ this.#state.body = finalBody
+ }
+
+ // Returns request’s HTTP method, which is "GET" by default.
+ get method () {
+ webidl.brandCheck(this, Request)
+
+ // The method getter steps are to return this’s request’s method.
+ return this.#state.method
+ }
+
+ // Returns the URL of request as a string.
+ get url () {
+ webidl.brandCheck(this, Request)
+
+ // The url getter steps are to return this’s request’s URL, serialized.
+ return URLSerializer(this.#state.url)
+ }
+
+ // Returns a Headers object consisting of the headers associated with request.
+ // Note that headers added in the network layer by the user agent will not
+ // be accounted for in this object, e.g., the "Host" header.
+ get headers () {
+ webidl.brandCheck(this, Request)
+
+ // The headers getter steps are to return this’s headers.
+ return this.#headers
+ }
+
+ // Returns the kind of resource requested by request, e.g., "document"
+ // or "script".
+ get destination () {
+ webidl.brandCheck(this, Request)
+
+ // The destination getter are to return this’s request’s destination.
+ return this.#state.destination
+ }
+
+ // Returns the referrer of request. Its value can be a same-origin URL if
+ // explicitly set in init, the empty string to indicate no referrer, and
+ // "about:client" when defaulting to the global’s default. This is used
+ // during fetching to determine the value of the `Referer` header of the
+ // request being made.
+ get referrer () {
+ webidl.brandCheck(this, Request)
+
+ // 1. If this’s request’s referrer is "no-referrer", then return the
+ // empty string.
+ if (this.#state.referrer === 'no-referrer') {
+ return ''
+ }
+
+ // 2. If this’s request’s referrer is "client", then return
+ // "about:client".
+ if (this.#state.referrer === 'client') {
+ return 'about:client'
+ }
+
+ // Return this’s request’s referrer, serialized.
+ return this.#state.referrer.toString()
+ }
+
+ // Returns the referrer policy associated with request.
+ // This is used during fetching to compute the value of the request’s
+ // referrer.
+ get referrerPolicy () {
+ webidl.brandCheck(this, Request)
+
+ // The referrerPolicy getter steps are to return this’s request’s referrer policy.
+ return this.#state.referrerPolicy
+ }
+
+ // Returns the mode associated with request, which is a string indicating
+ // whether the request will use CORS, or will be restricted to same-origin
+ // URLs.
+ get mode () {
+ webidl.brandCheck(this, Request)
+
+ // The mode getter steps are to return this’s request’s mode.
+ return this.#state.mode
+ }
+
+ // Returns the credentials mode associated with request,
+ // which is a string indicating whether credentials will be sent with the
+ // request always, never, or only when sent to a same-origin URL.
+ get credentials () {
+ webidl.brandCheck(this, Request)
+
+ // The credentials getter steps are to return this’s request’s credentials mode.
+ return this.#state.credentials
+ }
+
+ // Returns the cache mode associated with request,
+ // which is a string indicating how the request will
+ // interact with the browser’s cache when fetching.
+ get cache () {
+ webidl.brandCheck(this, Request)
+
+ // The cache getter steps are to return this’s request’s cache mode.
+ return this.#state.cache
+ }
+
+ // Returns the redirect mode associated with request,
+ // which is a string indicating how redirects for the
+ // request will be handled during fetching. A request
+ // will follow redirects by default.
+ get redirect () {
+ webidl.brandCheck(this, Request)
+
+ // The redirect getter steps are to return this’s request’s redirect mode.
+ return this.#state.redirect
+ }
+
+ // Returns request’s subresource integrity metadata, which is a
+ // cryptographic hash of the resource being fetched. Its value
+ // consists of multiple hashes separated by whitespace. [SRI]
+ get integrity () {
+ webidl.brandCheck(this, Request)
+
+ // The integrity getter steps are to return this’s request’s integrity
+ // metadata.
+ return this.#state.integrity
+ }
+
+ // Returns a boolean indicating whether or not request can outlive the
+ // global in which it was created.
+ get keepalive () {
+ webidl.brandCheck(this, Request)
+
+ // The keepalive getter steps are to return this’s request’s keepalive.
+ return this.#state.keepalive
+ }
+
+ // Returns a boolean indicating whether or not request is for a reload
+ // navigation.
+ get isReloadNavigation () {
+ webidl.brandCheck(this, Request)
+
+ // The isReloadNavigation getter steps are to return true if this’s
+ // request’s reload-navigation flag is set; otherwise false.
+ return this.#state.reloadNavigation
+ }
+
+ // Returns a boolean indicating whether or not request is for a history
+ // navigation (a.k.a. back-forward navigation).
+ get isHistoryNavigation () {
+ webidl.brandCheck(this, Request)
+
+ // The isHistoryNavigation getter steps are to return true if this’s request’s
+ // history-navigation flag is set; otherwise false.
+ return this.#state.historyNavigation
+ }
+
+ // Returns the signal associated with request, which is an AbortSignal
+ // object indicating whether or not request has been aborted, and its
+ // abort event handler.
+ get signal () {
+ webidl.brandCheck(this, Request)
+
+ // The signal getter steps are to return this’s signal.
+ return this.#signal
+ }
+
+ get body () {
+ webidl.brandCheck(this, Request)
+
+ return this.#state.body ? this.#state.body.stream : null
+ }
+
+ get bodyUsed () {
+ webidl.brandCheck(this, Request)
+
+ return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
+ }
+
+ get duplex () {
+ webidl.brandCheck(this, Request)
+
+ return 'half'
+ }
+
+ // Returns a clone of request.
+ clone () {
+ webidl.brandCheck(this, Request)
+
+ // 1. If this is unusable, then throw a TypeError.
+ if (bodyUnusable(this.#state)) {
+ throw new TypeError('unusable')
+ }
+
+ // 2. Let clonedRequest be the result of cloning this’s request.
+ const clonedRequest = cloneRequest(this.#state)
+
+ // 3. Let clonedRequestObject be the result of creating a Request object,
+ // given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
+ // 4. Make clonedRequestObject’s signal follow this’s signal.
+ const ac = new AbortController()
+ if (this.signal.aborted) {
+ ac.abort(this.signal.reason)
+ } else {
+ let list = dependentControllerMap.get(this.signal)
+ if (list === undefined) {
+ list = new Set()
+ dependentControllerMap.set(this.signal, list)
+ }
+ const acRef = new WeakRef(ac)
+ list.add(acRef)
+ util.addAbortListener(
+ ac.signal,
+ buildAbort(acRef)
+ )
+ }
+
+ // 4. Return clonedRequestObject.
+ return fromInnerRequest(clonedRequest, this.#dispatcher, ac.signal, getHeadersGuard(this.#headers))
+ }
+
+ [nodeUtil.inspect.custom] (depth, options) {
+ if (options.depth === null) {
+ options.depth = 2
+ }
+
+ options.colors ??= true
+
+ const properties = {
+ method: this.method,
+ url: this.url,
+ headers: this.headers,
+ destination: this.destination,
+ referrer: this.referrer,
+ referrerPolicy: this.referrerPolicy,
+ mode: this.mode,
+ credentials: this.credentials,
+ cache: this.cache,
+ redirect: this.redirect,
+ integrity: this.integrity,
+ keepalive: this.keepalive,
+ isReloadNavigation: this.isReloadNavigation,
+ isHistoryNavigation: this.isHistoryNavigation,
+ signal: this.signal
+ }
+
+ return `Request ${nodeUtil.formatWithOptions(options, properties)}`
+ }
+
+ /**
+ * @param {Request} request
+ * @param {AbortSignal} newSignal
+ */
+ static setRequestSignal (request, newSignal) {
+ request.#signal = newSignal
+ return request
+ }
+
+ /**
+ * @param {Request} request
+ */
+ static getRequestDispatcher (request) {
+ return request.#dispatcher
+ }
+
+ /**
+ * @param {Request} request
+ * @param {import('../../dispatcher/dispatcher')} newDispatcher
+ */
+ static setRequestDispatcher (request, newDispatcher) {
+ request.#dispatcher = newDispatcher
+ }
+
+ /**
+ * @param {Request} request
+ * @param {Headers} newHeaders
+ */
+ static setRequestHeaders (request, newHeaders) {
+ request.#headers = newHeaders
+ }
+
+ /**
+ * @param {Request} request
+ */
+ static getRequestState (request) {
+ return request.#state
+ }
+
+ /**
+ * @param {Request} request
+ * @param {any} newState
+ */
+ static setRequestState (request, newState) {
+ request.#state = newState
+ }
+}
+
+const { setRequestSignal, getRequestDispatcher, setRequestDispatcher, setRequestHeaders, getRequestState, setRequestState } = Request
+Reflect.deleteProperty(Request, 'setRequestSignal')
+Reflect.deleteProperty(Request, 'getRequestDispatcher')
+Reflect.deleteProperty(Request, 'setRequestDispatcher')
+Reflect.deleteProperty(Request, 'setRequestHeaders')
+Reflect.deleteProperty(Request, 'getRequestState')
+Reflect.deleteProperty(Request, 'setRequestState')
+
+mixinBody(Request, getRequestState)
+
+// https://fetch.spec.whatwg.org/#requests
+function makeRequest (init) {
+ return {
+ method: init.method ?? 'GET',
+ localURLsOnly: init.localURLsOnly ?? false,
+ unsafeRequest: init.unsafeRequest ?? false,
+ body: init.body ?? null,
+ client: init.client ?? null,
+ reservedClient: init.reservedClient ?? null,
+ replacesClientId: init.replacesClientId ?? '',
+ window: init.window ?? 'client',
+ keepalive: init.keepalive ?? false,
+ serviceWorkers: init.serviceWorkers ?? 'all',
+ initiator: init.initiator ?? '',
+ destination: init.destination ?? '',
+ priority: init.priority ?? null,
+ origin: init.origin ?? 'client',
+ policyContainer: init.policyContainer ?? 'client',
+ referrer: init.referrer ?? 'client',
+ referrerPolicy: init.referrerPolicy ?? '',
+ mode: init.mode ?? 'no-cors',
+ useCORSPreflightFlag: init.useCORSPreflightFlag ?? false,
+ credentials: init.credentials ?? 'same-origin',
+ useCredentials: init.useCredentials ?? false,
+ cache: init.cache ?? 'default',
+ redirect: init.redirect ?? 'follow',
+ integrity: init.integrity ?? '',
+ cryptoGraphicsNonceMetadata: init.cryptoGraphicsNonceMetadata ?? '',
+ parserMetadata: init.parserMetadata ?? '',
+ reloadNavigation: init.reloadNavigation ?? false,
+ historyNavigation: init.historyNavigation ?? false,
+ userActivation: init.userActivation ?? false,
+ taintedOrigin: init.taintedOrigin ?? false,
+ redirectCount: init.redirectCount ?? 0,
+ responseTainting: init.responseTainting ?? 'basic',
+ preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false,
+ done: init.done ?? false,
+ timingAllowFailed: init.timingAllowFailed ?? false,
+ useURLCredentials: init.useURLCredentials ?? undefined,
+ traversableForUserPrompts: init.traversableForUserPrompts ?? 'client',
+ urlList: init.urlList,
+ url: init.urlList[0],
+ headersList: init.headersList
+ ? new HeadersList(init.headersList)
+ : new HeadersList()
+ }
+}
+
+// https://fetch.spec.whatwg.org/#concept-request-clone
+function cloneRequest (request) {
+ // To clone a request request, run these steps:
+
+ // 1. Let newRequest be a copy of request, except for its body.
+ const newRequest = makeRequest({ ...request, body: null })
+
+ // 2. If request’s body is non-null, set newRequest’s body to the
+ // result of cloning request’s body.
+ if (request.body != null) {
+ newRequest.body = cloneBody(request.body)
+ }
+
+ // 3. Return newRequest.
+ return newRequest
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#request-create
+ * @param {any} innerRequest
+ * @param {import('../../dispatcher/agent')} dispatcher
+ * @param {AbortSignal} signal
+ * @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
+ * @returns {Request}
+ */
+function fromInnerRequest (innerRequest, dispatcher, signal, guard) {
+ const request = new Request(kConstruct)
+ setRequestState(request, innerRequest)
+ setRequestDispatcher(request, dispatcher)
+ setRequestSignal(request, signal)
+ const headers = new Headers(kConstruct)
+ setRequestHeaders(request, headers)
+ setHeadersList(headers, innerRequest.headersList)
+ setHeadersGuard(headers, guard)
+ return request
+}
+
+Object.defineProperties(Request.prototype, {
+ method: kEnumerableProperty,
+ url: kEnumerableProperty,
+ headers: kEnumerableProperty,
+ redirect: kEnumerableProperty,
+ clone: kEnumerableProperty,
+ signal: kEnumerableProperty,
+ duplex: kEnumerableProperty,
+ destination: kEnumerableProperty,
+ body: kEnumerableProperty,
+ bodyUsed: kEnumerableProperty,
+ isHistoryNavigation: kEnumerableProperty,
+ isReloadNavigation: kEnumerableProperty,
+ keepalive: kEnumerableProperty,
+ integrity: kEnumerableProperty,
+ cache: kEnumerableProperty,
+ credentials: kEnumerableProperty,
+ attribute: kEnumerableProperty,
+ referrerPolicy: kEnumerableProperty,
+ referrer: kEnumerableProperty,
+ mode: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Request',
+ configurable: true
+ }
+})
+
+webidl.is.Request = webidl.util.MakeTypeAssertion(Request)
+
+/**
+ * @param {*} V
+ * @returns {import('../../../types/fetch').Request|string}
+ *
+ * @see https://fetch.spec.whatwg.org/#requestinfo
+ */
+webidl.converters.RequestInfo = function (V) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V)
+ }
+
+ if (webidl.is.Request(V)) {
+ return V
+ }
+
+ return webidl.converters.USVString(V)
+}
+
+/**
+ * @param {*} V
+ * @returns {import('../../../types/fetch').RequestInit}
+ * @see https://fetch.spec.whatwg.org/#requestinit
+ */
+webidl.converters.RequestInit = webidl.dictionaryConverter([
+ {
+ key: 'method',
+ converter: webidl.converters.ByteString
+ },
+ {
+ key: 'headers',
+ converter: webidl.converters.HeadersInit
+ },
+ {
+ key: 'body',
+ converter: webidl.nullableConverter(
+ webidl.converters.BodyInit
+ )
+ },
+ {
+ key: 'referrer',
+ converter: webidl.converters.USVString
+ },
+ {
+ key: 'referrerPolicy',
+ converter: webidl.converters.DOMString,
+ // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
+ allowedValues: referrerPolicy
+ },
+ {
+ key: 'mode',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#concept-request-mode
+ allowedValues: requestMode
+ },
+ {
+ key: 'credentials',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestcredentials
+ allowedValues: requestCredentials
+ },
+ {
+ key: 'cache',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestcache
+ allowedValues: requestCache
+ },
+ {
+ key: 'redirect',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestredirect
+ allowedValues: requestRedirect
+ },
+ {
+ key: 'integrity',
+ converter: webidl.converters.DOMString
+ },
+ {
+ key: 'keepalive',
+ converter: webidl.converters.boolean
+ },
+ {
+ key: 'signal',
+ converter: webidl.nullableConverter(
+ (signal) => webidl.converters.AbortSignal(
+ signal,
+ 'RequestInit',
+ 'signal'
+ )
+ )
+ },
+ {
+ key: 'window',
+ converter: webidl.converters.any
+ },
+ {
+ key: 'duplex',
+ converter: webidl.converters.DOMString,
+ allowedValues: requestDuplex
+ },
+ {
+ key: 'dispatcher', // undici specific option
+ converter: webidl.converters.any
+ },
+ {
+ key: 'priority',
+ converter: webidl.converters.DOMString,
+ allowedValues: ['high', 'low', 'auto'],
+ defaultValue: () => 'auto'
+ }
+])
+
+module.exports = {
+ Request,
+ makeRequest,
+ fromInnerRequest,
+ cloneRequest,
+ getRequestDispatcher,
+ getRequestState
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/response.js b/vanilla/node_modules/undici/lib/web/fetch/response.js
new file mode 100644
index 0000000..ffb7ce1
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/response.js
@@ -0,0 +1,641 @@
+'use strict'
+
+const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
+const { extractBody, cloneBody, mixinBody, streamRegistry, bodyUnusable } = require('./body')
+const util = require('../../core/util')
+const nodeUtil = require('node:util')
+const { kEnumerableProperty } = util
+const {
+ isValidReasonPhrase,
+ isCancelled,
+ isAborted,
+ isErrorLike,
+ environmentSettingsObject: relevantRealm
+} = require('./util')
+const {
+ redirectStatusSet,
+ nullBodyStatus
+} = require('./constants')
+const { webidl } = require('../webidl')
+const { URLSerializer } = require('./data-url')
+const { kConstruct } = require('../../core/symbols')
+const assert = require('node:assert')
+const { isomorphicEncode, serializeJavascriptValueToJSONString } = require('../infra')
+
+const textEncoder = new TextEncoder('utf-8')
+
+// https://fetch.spec.whatwg.org/#response-class
+class Response {
+ /** @type {Headers} */
+ #headers
+
+ #state
+
+ // Creates network error Response.
+ static error () {
+ // The static error() method steps are to return the result of creating a
+ // Response object, given a new network error, "immutable", and this’s
+ // relevant Realm.
+ const responseObject = fromInnerResponse(makeNetworkError(), 'immutable')
+
+ return responseObject
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-response-json
+ static json (data, init = undefined) {
+ webidl.argumentLengthCheck(arguments, 1, 'Response.json')
+
+ if (init !== null) {
+ init = webidl.converters.ResponseInit(init)
+ }
+
+ // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
+ const bytes = textEncoder.encode(
+ serializeJavascriptValueToJSONString(data)
+ )
+
+ // 2. Let body be the result of extracting bytes.
+ const body = extractBody(bytes)
+
+ // 3. Let responseObject be the result of creating a Response object, given a new response,
+ // "response", and this’s relevant Realm.
+ const responseObject = fromInnerResponse(makeResponse({}), 'response')
+
+ // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
+ initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
+
+ // 5. Return responseObject.
+ return responseObject
+ }
+
+ // Creates a redirect Response that redirects to url with status status.
+ static redirect (url, status = 302) {
+ webidl.argumentLengthCheck(arguments, 1, 'Response.redirect')
+
+ url = webidl.converters.USVString(url)
+ status = webidl.converters['unsigned short'](status)
+
+ // 1. Let parsedURL be the result of parsing url with current settings
+ // object’s API base URL.
+ // 2. If parsedURL is failure, then throw a TypeError.
+ // TODO: base-URL?
+ let parsedURL
+ try {
+ parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl)
+ } catch (err) {
+ throw new TypeError(`Failed to parse URL from ${url}`, { cause: err })
+ }
+
+ // 3. If status is not a redirect status, then throw a RangeError.
+ if (!redirectStatusSet.has(status)) {
+ throw new RangeError(`Invalid status code ${status}`)
+ }
+
+ // 4. Let responseObject be the result of creating a Response object,
+ // given a new response, "immutable", and this’s relevant Realm.
+ const responseObject = fromInnerResponse(makeResponse({}), 'immutable')
+
+ // 5. Set responseObject’s response’s status to status.
+ responseObject.#state.status = status
+
+ // 6. Let value be parsedURL, serialized and isomorphic encoded.
+ const value = isomorphicEncode(URLSerializer(parsedURL))
+
+ // 7. Append `Location`/value to responseObject’s response’s header list.
+ responseObject.#state.headersList.append('location', value, true)
+
+ // 8. Return responseObject.
+ return responseObject
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-response
+ constructor (body = null, init = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (body === kConstruct) {
+ return
+ }
+
+ if (body !== null) {
+ body = webidl.converters.BodyInit(body, 'Response', 'body')
+ }
+
+ init = webidl.converters.ResponseInit(init)
+
+ // 1. Set this’s response to a new response.
+ this.#state = makeResponse({})
+
+ // 2. Set this’s headers to a new Headers object with this’s relevant
+ // Realm, whose header list is this’s response’s header list and guard
+ // is "response".
+ this.#headers = new Headers(kConstruct)
+ setHeadersGuard(this.#headers, 'response')
+ setHeadersList(this.#headers, this.#state.headersList)
+
+ // 3. Let bodyWithType be null.
+ let bodyWithType = null
+
+ // 4. If body is non-null, then set bodyWithType to the result of extracting body.
+ if (body != null) {
+ const [extractedBody, type] = extractBody(body)
+ bodyWithType = { body: extractedBody, type }
+ }
+
+ // 5. Perform initialize a response given this, init, and bodyWithType.
+ initializeResponse(this, init, bodyWithType)
+ }
+
+ // Returns response’s type, e.g., "cors".
+ get type () {
+ webidl.brandCheck(this, Response)
+
+ // The type getter steps are to return this’s response’s type.
+ return this.#state.type
+ }
+
+ // Returns response’s URL, if it has one; otherwise the empty string.
+ get url () {
+ webidl.brandCheck(this, Response)
+
+ const urlList = this.#state.urlList
+
+ // The url getter steps are to return the empty string if this’s
+ // response’s URL is null; otherwise this’s response’s URL,
+ // serialized with exclude fragment set to true.
+ const url = urlList[urlList.length - 1] ?? null
+
+ if (url === null) {
+ return ''
+ }
+
+ return URLSerializer(url, true)
+ }
+
+ // Returns whether response was obtained through a redirect.
+ get redirected () {
+ webidl.brandCheck(this, Response)
+
+ // The redirected getter steps are to return true if this’s response’s URL
+ // list has more than one item; otherwise false.
+ return this.#state.urlList.length > 1
+ }
+
+ // Returns response’s status.
+ get status () {
+ webidl.brandCheck(this, Response)
+
+ // The status getter steps are to return this’s response’s status.
+ return this.#state.status
+ }
+
+ // Returns whether response’s status is an ok status.
+ get ok () {
+ webidl.brandCheck(this, Response)
+
+ // The ok getter steps are to return true if this’s response’s status is an
+ // ok status; otherwise false.
+ return this.#state.status >= 200 && this.#state.status <= 299
+ }
+
+ // Returns response’s status message.
+ get statusText () {
+ webidl.brandCheck(this, Response)
+
+ // The statusText getter steps are to return this’s response’s status
+ // message.
+ return this.#state.statusText
+ }
+
+ // Returns response’s headers as Headers.
+ get headers () {
+ webidl.brandCheck(this, Response)
+
+ // The headers getter steps are to return this’s headers.
+ return this.#headers
+ }
+
+ get body () {
+ webidl.brandCheck(this, Response)
+
+ return this.#state.body ? this.#state.body.stream : null
+ }
+
+ get bodyUsed () {
+ webidl.brandCheck(this, Response)
+
+ return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
+ }
+
+ // Returns a clone of response.
+ clone () {
+ webidl.brandCheck(this, Response)
+
+ // 1. If this is unusable, then throw a TypeError.
+ if (bodyUnusable(this.#state)) {
+ throw webidl.errors.exception({
+ header: 'Response.clone',
+ message: 'Body has already been consumed.'
+ })
+ }
+
+ // 2. Let clonedResponse be the result of cloning this’s response.
+ const clonedResponse = cloneResponse(this.#state)
+
+ // Note: To re-register because of a new stream.
+ // Don't set finalizers other than for fetch responses.
+ if (this.#state.urlList.length !== 0 && this.#state.body?.stream) {
+ streamRegistry.register(this, new WeakRef(this.#state.body.stream))
+ }
+
+ // 3. Return the result of creating a Response object, given
+ // clonedResponse, this’s headers’s guard, and this’s relevant Realm.
+ return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers))
+ }
+
+ [nodeUtil.inspect.custom] (depth, options) {
+ if (options.depth === null) {
+ options.depth = 2
+ }
+
+ options.colors ??= true
+
+ const properties = {
+ status: this.status,
+ statusText: this.statusText,
+ headers: this.headers,
+ body: this.body,
+ bodyUsed: this.bodyUsed,
+ ok: this.ok,
+ redirected: this.redirected,
+ type: this.type,
+ url: this.url
+ }
+
+ return `Response ${nodeUtil.formatWithOptions(options, properties)}`
+ }
+
+ /**
+ * @param {Response} response
+ */
+ static getResponseHeaders (response) {
+ return response.#headers
+ }
+
+ /**
+ * @param {Response} response
+ * @param {Headers} newHeaders
+ */
+ static setResponseHeaders (response, newHeaders) {
+ response.#headers = newHeaders
+ }
+
+ /**
+ * @param {Response} response
+ */
+ static getResponseState (response) {
+ return response.#state
+ }
+
+ /**
+ * @param {Response} response
+ * @param {any} newState
+ */
+ static setResponseState (response, newState) {
+ response.#state = newState
+ }
+}
+
+const { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response
+Reflect.deleteProperty(Response, 'getResponseHeaders')
+Reflect.deleteProperty(Response, 'setResponseHeaders')
+Reflect.deleteProperty(Response, 'getResponseState')
+Reflect.deleteProperty(Response, 'setResponseState')
+
+mixinBody(Response, getResponseState)
+
+Object.defineProperties(Response.prototype, {
+ type: kEnumerableProperty,
+ url: kEnumerableProperty,
+ status: kEnumerableProperty,
+ ok: kEnumerableProperty,
+ redirected: kEnumerableProperty,
+ statusText: kEnumerableProperty,
+ headers: kEnumerableProperty,
+ clone: kEnumerableProperty,
+ body: kEnumerableProperty,
+ bodyUsed: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Response',
+ configurable: true
+ }
+})
+
+Object.defineProperties(Response, {
+ json: kEnumerableProperty,
+ redirect: kEnumerableProperty,
+ error: kEnumerableProperty
+})
+
+// https://fetch.spec.whatwg.org/#concept-response-clone
+function cloneResponse (response) {
+ // To clone a response response, run these steps:
+
+ // 1. If response is a filtered response, then return a new identical
+ // filtered response whose internal response is a clone of response’s
+ // internal response.
+ if (response.internalResponse) {
+ return filterResponse(
+ cloneResponse(response.internalResponse),
+ response.type
+ )
+ }
+
+ // 2. Let newResponse be a copy of response, except for its body.
+ const newResponse = makeResponse({ ...response, body: null })
+
+ // 3. If response’s body is non-null, then set newResponse’s body to the
+ // result of cloning response’s body.
+ if (response.body != null) {
+ newResponse.body = cloneBody(response.body)
+ }
+
+ // 4. Return newResponse.
+ return newResponse
+}
+
+function makeResponse (init) {
+ return {
+ aborted: false,
+ rangeRequested: false,
+ timingAllowPassed: false,
+ requestIncludesCredentials: false,
+ type: 'default',
+ status: 200,
+ timingInfo: null,
+ cacheState: '',
+ statusText: '',
+ ...init,
+ headersList: init?.headersList
+ ? new HeadersList(init?.headersList)
+ : new HeadersList(),
+ urlList: init?.urlList ? [...init.urlList] : []
+ }
+}
+
+function makeNetworkError (reason) {
+ const isError = isErrorLike(reason)
+ return makeResponse({
+ type: 'error',
+ status: 0,
+ error: isError
+ ? reason
+ : new Error(reason ? String(reason) : reason),
+ aborted: reason && reason.name === 'AbortError'
+ })
+}
+
+// @see https://fetch.spec.whatwg.org/#concept-network-error
+function isNetworkError (response) {
+ return (
+ // A network error is a response whose type is "error",
+ response.type === 'error' &&
+ // status is 0
+ response.status === 0
+ )
+}
+
+function makeFilteredResponse (response, state) {
+ state = {
+ internalResponse: response,
+ ...state
+ }
+
+ return new Proxy(response, {
+ get (target, p) {
+ return p in state ? state[p] : target[p]
+ },
+ set (target, p, value) {
+ assert(!(p in state))
+ target[p] = value
+ return true
+ }
+ })
+}
+
+// https://fetch.spec.whatwg.org/#concept-filtered-response
+function filterResponse (response, type) {
+ // Set response to the following filtered response with response as its
+ // internal response, depending on request’s response tainting:
+ if (type === 'basic') {
+ // A basic filtered response is a filtered response whose type is "basic"
+ // and header list excludes any headers in internal response’s header list
+ // whose name is a forbidden response-header name.
+
+ // Note: undici does not implement forbidden response-header names
+ return makeFilteredResponse(response, {
+ type: 'basic',
+ headersList: response.headersList
+ })
+ } else if (type === 'cors') {
+ // A CORS filtered response is a filtered response whose type is "cors"
+ // and header list excludes any headers in internal response’s header
+ // list whose name is not a CORS-safelisted response-header name, given
+ // internal response’s CORS-exposed header-name list.
+
+ // Note: undici does not implement CORS-safelisted response-header names
+ return makeFilteredResponse(response, {
+ type: 'cors',
+ headersList: response.headersList
+ })
+ } else if (type === 'opaque') {
+ // An opaque filtered response is a filtered response whose type is
+ // "opaque", URL list is the empty list, status is 0, status message
+ // is the empty byte sequence, header list is empty, and body is null.
+
+ return makeFilteredResponse(response, {
+ type: 'opaque',
+ urlList: [],
+ status: 0,
+ statusText: '',
+ body: null
+ })
+ } else if (type === 'opaqueredirect') {
+ // An opaque-redirect filtered response is a filtered response whose type
+ // is "opaqueredirect", status is 0, status message is the empty byte
+ // sequence, header list is empty, and body is null.
+
+ return makeFilteredResponse(response, {
+ type: 'opaqueredirect',
+ status: 0,
+ statusText: '',
+ headersList: [],
+ body: null
+ })
+ } else {
+ assert(false)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#appropriate-network-error
+function makeAppropriateNetworkError (fetchParams, err = null) {
+ // 1. Assert: fetchParams is canceled.
+ assert(isCancelled(fetchParams))
+
+ // 2. Return an aborted network error if fetchParams is aborted;
+ // otherwise return a network error.
+ return isAborted(fetchParams)
+ ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
+ : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
+}
+
+// https://whatpr.org/fetch/1392.html#initialize-a-response
+function initializeResponse (response, init, body) {
+ // 1. If init["status"] is not in the range 200 to 599, inclusive, then
+ // throw a RangeError.
+ if (init.status !== null && (init.status < 200 || init.status > 599)) {
+ throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
+ }
+
+ // 2. If init["statusText"] does not match the reason-phrase token production,
+ // then throw a TypeError.
+ if ('statusText' in init && init.statusText != null) {
+ // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
+ // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
+ if (!isValidReasonPhrase(String(init.statusText))) {
+ throw new TypeError('Invalid statusText')
+ }
+ }
+
+ // 3. Set response’s response’s status to init["status"].
+ if ('status' in init && init.status != null) {
+ getResponseState(response).status = init.status
+ }
+
+ // 4. Set response’s response’s status message to init["statusText"].
+ if ('statusText' in init && init.statusText != null) {
+ getResponseState(response).statusText = init.statusText
+ }
+
+ // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
+ if ('headers' in init && init.headers != null) {
+ fill(getResponseHeaders(response), init.headers)
+ }
+
+ // 6. If body was given, then:
+ if (body) {
+ // 1. If response's status is a null body status, then throw a TypeError.
+ if (nullBodyStatus.includes(response.status)) {
+ throw webidl.errors.exception({
+ header: 'Response constructor',
+ message: `Invalid response status code ${response.status}`
+ })
+ }
+
+ // 2. Set response's body to body's body.
+ getResponseState(response).body = body.body
+
+ // 3. If body's type is non-null and response's header list does not contain
+ // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
+ if (body.type != null && !getResponseState(response).headersList.contains('content-type', true)) {
+ getResponseState(response).headersList.append('content-type', body.type, true)
+ }
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#response-create
+ * @param {any} innerResponse
+ * @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
+ * @returns {Response}
+ */
+function fromInnerResponse (innerResponse, guard) {
+ const response = new Response(kConstruct)
+ setResponseState(response, innerResponse)
+ const headers = new Headers(kConstruct)
+ setResponseHeaders(response, headers)
+ setHeadersList(headers, innerResponse.headersList)
+ setHeadersGuard(headers, guard)
+
+ // Note: If innerResponse's urlList contains a URL, it is a fetch response.
+ if (innerResponse.urlList.length !== 0 && innerResponse.body?.stream) {
+ // If the target (response) is reclaimed, the cleanup callback may be called at some point with
+ // the held value provided for it (innerResponse.body.stream). The held value can be any value:
+ // a primitive or an object, even undefined. If the held value is an object, the registry keeps
+ // a strong reference to it (so it can pass it to the cleanup callback later). Reworded from
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
+ streamRegistry.register(response, new WeakRef(innerResponse.body.stream))
+ }
+
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
+webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V, prefix, name)
+ }
+
+ if (webidl.is.Blob(V)) {
+ return V
+ }
+
+ if (webidl.is.BufferSource(V)) {
+ return V
+ }
+
+ if (webidl.is.FormData(V)) {
+ return V
+ }
+
+ if (webidl.is.URLSearchParams(V)) {
+ return V
+ }
+
+ return webidl.converters.DOMString(V, prefix, name)
+}
+
+// https://fetch.spec.whatwg.org/#bodyinit
+webidl.converters.BodyInit = function (V, prefix, argument) {
+ if (webidl.is.ReadableStream(V)) {
+ return V
+ }
+
+ // Note: the spec doesn't include async iterables,
+ // this is an undici extension.
+ if (V?.[Symbol.asyncIterator]) {
+ return V
+ }
+
+ return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument)
+}
+
+webidl.converters.ResponseInit = webidl.dictionaryConverter([
+ {
+ key: 'status',
+ converter: webidl.converters['unsigned short'],
+ defaultValue: () => 200
+ },
+ {
+ key: 'statusText',
+ converter: webidl.converters.ByteString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'headers',
+ converter: webidl.converters.HeadersInit
+ }
+])
+
+webidl.is.Response = webidl.util.MakeTypeAssertion(Response)
+
+module.exports = {
+ isNetworkError,
+ makeNetworkError,
+ makeResponse,
+ makeAppropriateNetworkError,
+ filterResponse,
+ Response,
+ cloneResponse,
+ fromInnerResponse,
+ getResponseState
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/util.js b/vanilla/node_modules/undici/lib/web/fetch/util.js
new file mode 100644
index 0000000..fe63cb3
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/util.js
@@ -0,0 +1,1520 @@
+'use strict'
+
+const { Transform } = require('node:stream')
+const zlib = require('node:zlib')
+const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants')
+const { getGlobalOrigin } = require('./global')
+const { collectAnHTTPQuotedString, parseMIMEType } = require('./data-url')
+const { performance } = require('node:perf_hooks')
+const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
+const assert = require('node:assert')
+const { isUint8Array } = require('node:util/types')
+const { webidl } = require('../webidl')
+const { isomorphicEncode, collectASequenceOfCodePoints, removeChars } = require('../infra')
+
+function responseURL (response) {
+ // https://fetch.spec.whatwg.org/#responses
+ // A response has an associated URL. It is a pointer to the last URL
+ // in response’s URL list and null if response’s URL list is empty.
+ const urlList = response.urlList
+ const length = urlList.length
+ return length === 0 ? null : urlList[length - 1].toString()
+}
+
+// https://fetch.spec.whatwg.org/#concept-response-location-url
+function responseLocationURL (response, requestFragment) {
+ // 1. If response’s status is not a redirect status, then return null.
+ if (!redirectStatusSet.has(response.status)) {
+ return null
+ }
+
+ // 2. Let location be the result of extracting header list values given
+ // `Location` and response’s header list.
+ let location = response.headersList.get('location', true)
+
+ // 3. If location is a header value, then set location to the result of
+ // parsing location with response’s URL.
+ if (location !== null && isValidHeaderValue(location)) {
+ if (!isValidEncodedURL(location)) {
+ // Some websites respond location header in UTF-8 form without encoding them as ASCII
+ // and major browsers redirect them to correctly UTF-8 encoded addresses.
+ // Here, we handle that behavior in the same way.
+ location = normalizeBinaryStringToUtf8(location)
+ }
+ location = new URL(location, responseURL(response))
+ }
+
+ // 4. If location is a URL whose fragment is null, then set location’s
+ // fragment to requestFragment.
+ if (location && !location.hash) {
+ location.hash = requestFragment
+ }
+
+ // 5. Return location.
+ return location
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2
+ * @param {string} url
+ * @returns {boolean}
+ */
+function isValidEncodedURL (url) {
+ for (let i = 0; i < url.length; ++i) {
+ const code = url.charCodeAt(i)
+
+ if (
+ code > 0x7E || // Non-US-ASCII + DEL
+ code < 0x20 // Control characters NUL - US
+ ) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it.
+ * Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well.
+ * @param {string} value
+ * @returns {string}
+ */
+function normalizeBinaryStringToUtf8 (value) {
+ return Buffer.from(value, 'binary').toString('utf8')
+}
+
+/** @returns {URL} */
+function requestCurrentURL (request) {
+ return request.urlList[request.urlList.length - 1]
+}
+
+function requestBadPort (request) {
+ // 1. Let url be request’s current URL.
+ const url = requestCurrentURL(request)
+
+ // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
+ // then return blocked.
+ if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
+ return 'blocked'
+ }
+
+ // 3. Return allowed.
+ return 'allowed'
+}
+
+function isErrorLike (object) {
+ return object instanceof Error || (
+ object?.constructor?.name === 'Error' ||
+ object?.constructor?.name === 'DOMException'
+ )
+}
+
+// Check whether |statusText| is a ByteString and
+// matches the Reason-Phrase token production.
+// RFC 2616: https://tools.ietf.org/html/rfc2616
+// RFC 7230: https://tools.ietf.org/html/rfc7230
+// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
+// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
+function isValidReasonPhrase (statusText) {
+ for (let i = 0; i < statusText.length; ++i) {
+ const c = statusText.charCodeAt(i)
+ if (
+ !(
+ (
+ c === 0x09 || // HTAB
+ (c >= 0x20 && c <= 0x7e) || // SP / VCHAR
+ (c >= 0x80 && c <= 0xff)
+ ) // obs-text
+ )
+ ) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#header-name
+ * @param {string} potentialValue
+ */
+const isValidHeaderName = isValidHTTPToken
+
+/**
+ * @see https://fetch.spec.whatwg.org/#header-value
+ * @param {string} potentialValue
+ */
+function isValidHeaderValue (potentialValue) {
+ // - Has no leading or trailing HTTP tab or space bytes.
+ // - Contains no 0x00 (NUL) or HTTP newline bytes.
+ return (
+ potentialValue[0] === '\t' ||
+ potentialValue[0] === ' ' ||
+ potentialValue[potentialValue.length - 1] === '\t' ||
+ potentialValue[potentialValue.length - 1] === ' ' ||
+ potentialValue.includes('\n') ||
+ potentialValue.includes('\r') ||
+ potentialValue.includes('\0')
+ ) === false
+}
+
+/**
+ * Parse a referrer policy from a Referrer-Policy header
+ * @see https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header
+ */
+function parseReferrerPolicy (actualResponse) {
+ // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list.
+ const policyHeader = (actualResponse.headersList.get('referrer-policy', true) ?? '').split(',')
+
+ // 2. Let policy be the empty string.
+ let policy = ''
+
+ // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
+
+ // Note: As the referrer-policy can contain multiple policies
+ // separated by comma, we need to loop through all of them
+ // and pick the first valid one.
+ // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
+ if (policyHeader.length) {
+ // The right-most policy takes precedence.
+ // The left-most policy is the fallback.
+ for (let i = policyHeader.length; i !== 0; i--) {
+ const token = policyHeader[i - 1].trim()
+ if (referrerPolicyTokens.has(token)) {
+ policy = token
+ break
+ }
+ }
+ }
+
+ // 4. Return policy.
+ return policy
+}
+
+/**
+ * Given a request request and a response actualResponse, this algorithm
+ * updates request’s referrer policy according to the Referrer-Policy
+ * header (if any) in actualResponse.
+ * @see https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
+ * @param {import('./request').Request} request
+ * @param {import('./response').Response} actualResponse
+ */
+function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
+ // 1. Let policy be the result of executing § 8.1 Parse a referrer policy
+ // from a Referrer-Policy header on actualResponse.
+ const policy = parseReferrerPolicy(actualResponse)
+
+ // 2. If policy is not the empty string, then set request’s referrer policy to policy.
+ if (policy !== '') {
+ request.referrerPolicy = policy
+ }
+}
+
+// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
+function crossOriginResourcePolicyCheck () {
+ // TODO
+ return 'allowed'
+}
+
+// https://fetch.spec.whatwg.org/#concept-cors-check
+function corsCheck () {
+ // TODO
+ return 'success'
+}
+
+// https://fetch.spec.whatwg.org/#concept-tao-check
+function TAOCheck () {
+ // TODO
+ return 'success'
+}
+
+function appendFetchMetadata (httpRequest) {
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header
+ // TODO
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header
+
+ // 1. Assert: r’s url is a potentially trustworthy URL.
+ // TODO
+
+ // 2. Let header be a Structured Header whose value is a token.
+ let header = null
+
+ // 3. Set header’s value to r’s mode.
+ header = httpRequest.mode
+
+ // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
+ httpRequest.headersList.set('sec-fetch-mode', header, true)
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
+ // TODO
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header
+ // TODO
+}
+
+// https://fetch.spec.whatwg.org/#append-a-request-origin-header
+function appendRequestOriginHeader (request) {
+ // 1. Let serializedOrigin be the result of byte-serializing a request origin
+ // with request.
+ // TODO: implement "byte-serializing a request origin"
+ let serializedOrigin = request.origin
+
+ // - "'client' is changed to an origin during fetching."
+ // This doesn't happen in undici (in most cases) because undici, by default,
+ // has no concept of origin.
+ // - request.origin can also be set to request.client.origin (client being
+ // an environment settings object), which is undefined without using
+ // setGlobalOrigin.
+ if (serializedOrigin === 'client' || serializedOrigin === undefined) {
+ return
+ }
+
+ // 2. If request’s response tainting is "cors" or request’s mode is "websocket",
+ // then append (`Origin`, serializedOrigin) to request’s header list.
+ // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
+ if (request.responseTainting === 'cors' || request.mode === 'websocket') {
+ request.headersList.append('origin', serializedOrigin, true)
+ } else if (request.method !== 'GET' && request.method !== 'HEAD') {
+ // 1. Switch on request’s referrer policy:
+ switch (request.referrerPolicy) {
+ case 'no-referrer':
+ // Set serializedOrigin to `null`.
+ serializedOrigin = null
+ break
+ case 'no-referrer-when-downgrade':
+ case 'strict-origin':
+ case 'strict-origin-when-cross-origin':
+ // If request’s origin is a tuple origin, its scheme is "https", and
+ // request’s current URL’s scheme is not "https", then set
+ // serializedOrigin to `null`.
+ if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
+ serializedOrigin = null
+ }
+ break
+ case 'same-origin':
+ // If request’s origin is not same origin with request’s current URL’s
+ // origin, then set serializedOrigin to `null`.
+ if (!sameOrigin(request, requestCurrentURL(request))) {
+ serializedOrigin = null
+ }
+ break
+ default:
+ // Do nothing.
+ }
+
+ // 2. Append (`Origin`, serializedOrigin) to request’s header list.
+ request.headersList.append('origin', serializedOrigin, true)
+ }
+}
+
+// https://w3c.github.io/hr-time/#dfn-coarsen-time
+function coarsenTime (timestamp, crossOriginIsolatedCapability) {
+ // TODO
+ return timestamp
+}
+
+// https://fetch.spec.whatwg.org/#clamp-and-coarsen-connection-timing-info
+function clampAndCoarsenConnectionTimingInfo (connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) {
+ if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) {
+ return {
+ domainLookupStartTime: defaultStartTime,
+ domainLookupEndTime: defaultStartTime,
+ connectionStartTime: defaultStartTime,
+ connectionEndTime: defaultStartTime,
+ secureConnectionStartTime: defaultStartTime,
+ ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol
+ }
+ }
+
+ return {
+ domainLookupStartTime: coarsenTime(connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability),
+ domainLookupEndTime: coarsenTime(connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability),
+ connectionStartTime: coarsenTime(connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability),
+ connectionEndTime: coarsenTime(connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability),
+ secureConnectionStartTime: coarsenTime(connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability),
+ ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol
+ }
+}
+
+// https://w3c.github.io/hr-time/#dfn-coarsened-shared-current-time
+function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
+ return coarsenTime(performance.now(), crossOriginIsolatedCapability)
+}
+
+// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
+function createOpaqueTimingInfo (timingInfo) {
+ return {
+ startTime: timingInfo.startTime ?? 0,
+ redirectStartTime: 0,
+ redirectEndTime: 0,
+ postRedirectStartTime: timingInfo.startTime ?? 0,
+ finalServiceWorkerStartTime: 0,
+ finalNetworkResponseStartTime: 0,
+ finalNetworkRequestStartTime: 0,
+ endTime: 0,
+ encodedBodySize: 0,
+ decodedBodySize: 0,
+ finalConnectionTimingInfo: null
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/origin.html#policy-container
+function makePolicyContainer () {
+ // Note: the fetch spec doesn't make use of embedder policy or CSP list
+ return {
+ referrerPolicy: 'strict-origin-when-cross-origin'
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
+function clonePolicyContainer (policyContainer) {
+ return {
+ referrerPolicy: policyContainer.referrerPolicy
+ }
+}
+
+/**
+ * Determine request’s Referrer
+ *
+ * @see https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
+ */
+function determineRequestsReferrer (request) {
+ // Given a request request, we can determine the correct referrer information
+ // to send by examining its referrer policy as detailed in the following
+ // steps, which return either no referrer or a URL:
+
+ // 1. Let policy be request's referrer policy.
+ const policy = request.referrerPolicy
+
+ // Note: policy cannot (shouldn't) be null or an empty string.
+ assert(policy)
+
+ // 2. Let environment be request’s client.
+
+ let referrerSource = null
+
+ // 3. Switch on request’s referrer:
+
+ // "client"
+ if (request.referrer === 'client') {
+ // Note: node isn't a browser and doesn't implement document/iframes,
+ // so we bypass this step and replace it with our own.
+
+ const globalOrigin = getGlobalOrigin()
+
+ if (!globalOrigin || globalOrigin.origin === 'null') {
+ return 'no-referrer'
+ }
+
+ // Note: we need to clone it as it's mutated
+ referrerSource = new URL(globalOrigin)
+ // a URL
+ } else if (webidl.is.URL(request.referrer)) {
+ // Let referrerSource be request’s referrer.
+ referrerSource = request.referrer
+ }
+
+ // 4. Let request’s referrerURL be the result of stripping referrerSource for
+ // use as a referrer.
+ let referrerURL = stripURLForReferrer(referrerSource)
+
+ // 5. Let referrerOrigin be the result of stripping referrerSource for use as
+ // a referrer, with the origin-only flag set to true.
+ const referrerOrigin = stripURLForReferrer(referrerSource, true)
+
+ // 6. If the result of serializing referrerURL is a string whose length is
+ // greater than 4096, set referrerURL to referrerOrigin.
+ if (referrerURL.toString().length > 4096) {
+ referrerURL = referrerOrigin
+ }
+
+ // 7. The user agent MAY alter referrerURL or referrerOrigin at this point
+ // to enforce arbitrary policy considerations in the interests of minimizing
+ // data leakage. For example, the user agent could strip the URL down to an
+ // origin, modify its host, replace it with an empty string, etc.
+
+ // 8. Execute the switch statements corresponding to the value of policy:
+ switch (policy) {
+ case 'no-referrer':
+ // Return no referrer
+ return 'no-referrer'
+ case 'origin':
+ // Return referrerOrigin
+ if (referrerOrigin != null) {
+ return referrerOrigin
+ }
+ return stripURLForReferrer(referrerSource, true)
+ case 'unsafe-url':
+ // Return referrerURL.
+ return referrerURL
+ case 'strict-origin': {
+ const currentURL = requestCurrentURL(request)
+
+ // 1. If referrerURL is a potentially trustworthy URL and request’s
+ // current URL is not a potentially trustworthy URL, then return no
+ // referrer.
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
+ return 'no-referrer'
+ }
+ // 2. Return referrerOrigin
+ return referrerOrigin
+ }
+ case 'strict-origin-when-cross-origin': {
+ const currentURL = requestCurrentURL(request)
+
+ // 1. If the origin of referrerURL and the origin of request’s current
+ // URL are the same, then return referrerURL.
+ if (sameOrigin(referrerURL, currentURL)) {
+ return referrerURL
+ }
+
+ // 2. If referrerURL is a potentially trustworthy URL and request’s
+ // current URL is not a potentially trustworthy URL, then return no
+ // referrer.
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
+ return 'no-referrer'
+ }
+
+ // 3. Return referrerOrigin.
+ return referrerOrigin
+ }
+ case 'same-origin':
+ // 1. If the origin of referrerURL and the origin of request’s current
+ // URL are the same, then return referrerURL.
+ if (sameOrigin(request, referrerURL)) {
+ return referrerURL
+ }
+ // 2. Return no referrer.
+ return 'no-referrer'
+ case 'origin-when-cross-origin':
+ // 1. If the origin of referrerURL and the origin of request’s current
+ // URL are the same, then return referrerURL.
+ if (sameOrigin(request, referrerURL)) {
+ return referrerURL
+ }
+ // 2. Return referrerOrigin.
+ return referrerOrigin
+ case 'no-referrer-when-downgrade': {
+ const currentURL = requestCurrentURL(request)
+
+ // 1. If referrerURL is a potentially trustworthy URL and request’s
+ // current URL is not a potentially trustworthy URL, then return no
+ // referrer.
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
+ return 'no-referrer'
+ }
+ // 2. Return referrerURL.
+ return referrerURL
+ }
+ }
+}
+
+/**
+ * Certain portions of URLs must not be included when sending a URL as the
+ * value of a `Referer` header: a URLs fragment, username, and password
+ * components must be stripped from the URL before it’s sent out. This
+ * algorithm accepts a origin-only flag, which defaults to false. If set to
+ * true, the algorithm will additionally remove the URL’s path and query
+ * components, leaving only the scheme, host, and port.
+ *
+ * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
+ * @param {URL} url
+ * @param {boolean} [originOnly=false]
+ */
+function stripURLForReferrer (url, originOnly = false) {
+ // 1. Assert: url is a URL.
+ assert(webidl.is.URL(url))
+
+ // Note: Create a new URL instance to avoid mutating the original URL.
+ url = new URL(url)
+
+ // 2. If url’s scheme is a local scheme, then return no referrer.
+ if (urlIsLocal(url)) {
+ return 'no-referrer'
+ }
+
+ // 3. Set url’s username to the empty string.
+ url.username = ''
+
+ // 4. Set url’s password to the empty string.
+ url.password = ''
+
+ // 5. Set url’s fragment to null.
+ url.hash = ''
+
+ // 6. If the origin-only flag is true, then:
+ if (originOnly === true) {
+ // 1. Set url’s path to « the empty string ».
+ url.pathname = ''
+
+ // 2. Set url’s query to null.
+ url.search = ''
+ }
+
+ // 7. Return url.
+ return url
+}
+
+const isPotentialleTrustworthyIPv4 = RegExp.prototype.test
+ .bind(/^127\.(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){2}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/)
+
+const isPotentiallyTrustworthyIPv6 = RegExp.prototype.test
+ .bind(/^(?:(?:0{1,4}:){7}|(?:0{1,4}:){1,6}:|::)0{0,3}1$/)
+
+/**
+ * Check if host matches one of the CIDR notations 127.0.0.0/8 or ::1/128.
+ *
+ * @param {string} origin
+ * @returns {boolean}
+ */
+function isOriginIPPotentiallyTrustworthy (origin) {
+ // IPv6
+ if (origin.includes(':')) {
+ // Remove brackets from IPv6 addresses
+ if (origin[0] === '[' && origin[origin.length - 1] === ']') {
+ origin = origin.slice(1, -1)
+ }
+ return isPotentiallyTrustworthyIPv6(origin)
+ }
+
+ // IPv4
+ return isPotentialleTrustworthyIPv4(origin)
+}
+
+/**
+ * A potentially trustworthy origin is one which a user agent can generally
+ * trust as delivering data securely.
+ *
+ * Return value `true` means `Potentially Trustworthy`.
+ * Return value `false` means `Not Trustworthy`.
+ *
+ * @see https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
+ * @param {string} origin
+ * @returns {boolean}
+ */
+function isOriginPotentiallyTrustworthy (origin) {
+ // 1. If origin is an opaque origin, return "Not Trustworthy".
+ if (origin == null || origin === 'null') {
+ return false
+ }
+
+ // 2. Assert: origin is a tuple origin.
+ origin = new URL(origin)
+
+ // 3. If origin’s scheme is either "https" or "wss",
+ // return "Potentially Trustworthy".
+ if (origin.protocol === 'https:' || origin.protocol === 'wss:') {
+ return true
+ }
+
+ // 4. If origin’s host matches one of the CIDR notations 127.0.0.0/8 or
+ // ::1/128 [RFC4632], return "Potentially Trustworthy".
+ if (isOriginIPPotentiallyTrustworthy(origin.hostname)) {
+ return true
+ }
+
+ // 5. If the user agent conforms to the name resolution rules in
+ // [let-localhost-be-localhost] and one of the following is true:
+
+ // origin’s host is "localhost" or "localhost."
+ if (origin.hostname === 'localhost' || origin.hostname === 'localhost.') {
+ return true
+ }
+
+ // origin’s host ends with ".localhost" or ".localhost."
+ if (origin.hostname.endsWith('.localhost') || origin.hostname.endsWith('.localhost.')) {
+ return true
+ }
+
+ // 6. If origin’s scheme is "file", return "Potentially Trustworthy".
+ if (origin.protocol === 'file:') {
+ return true
+ }
+
+ // 7. If origin’s scheme component is one which the user agent considers to
+ // be authenticated, return "Potentially Trustworthy".
+
+ // 8. If origin has been configured as a trustworthy origin, return
+ // "Potentially Trustworthy".
+
+ // 9. Return "Not Trustworthy".
+ return false
+}
+
+/**
+ * A potentially trustworthy URL is one which either inherits context from its
+ * creator (about:blank, about:srcdoc, data) or one whose origin is a
+ * potentially trustworthy origin.
+ *
+ * Return value `true` means `Potentially Trustworthy`.
+ * Return value `false` means `Not Trustworthy`.
+ *
+ * @see https://www.w3.org/TR/secure-contexts/#is-url-trustworthy
+ * @param {URL} url
+ * @returns {boolean}
+ */
+function isURLPotentiallyTrustworthy (url) {
+ // Given a URL record (url), the following algorithm returns "Potentially
+ // Trustworthy" or "Not Trustworthy" as appropriate:
+ if (!webidl.is.URL(url)) {
+ return false
+ }
+
+ // 1. If url is "about:blank" or "about:srcdoc",
+ // return "Potentially Trustworthy".
+ if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
+ return true
+ }
+
+ // 2. If url’s scheme is "data", return "Potentially Trustworthy".
+ if (url.protocol === 'data:') return true
+
+ // Note: The origin of blob: URLs is the origin of the context in which they
+ // were created. Therefore, blobs created in a trustworthy origin will
+ // themselves be potentially trustworthy.
+ if (url.protocol === 'blob:') return true
+
+ // 3. Return the result of executing § 3.1 Is origin potentially trustworthy?
+ // on url’s origin.
+ return isOriginPotentiallyTrustworthy(url.origin)
+}
+
+// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
+function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
+ // TODO
+}
+
+/**
+ * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin}
+ * @param {URL} A
+ * @param {URL} B
+ */
+function sameOrigin (A, B) {
+ // 1. If A and B are the same opaque origin, then return true.
+ if (A.origin === B.origin && A.origin === 'null') {
+ return true
+ }
+
+ // 2. If A and B are both tuple origins and their schemes,
+ // hosts, and port are identical, then return true.
+ if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) {
+ return true
+ }
+
+ // 3. Return false.
+ return false
+}
+
+function isAborted (fetchParams) {
+ return fetchParams.controller.state === 'aborted'
+}
+
+function isCancelled (fetchParams) {
+ return fetchParams.controller.state === 'aborted' ||
+ fetchParams.controller.state === 'terminated'
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-method-normalize
+ * @param {string} method
+ */
+function normalizeMethod (method) {
+ return normalizedMethodRecordsBase[method.toLowerCase()] ?? method
+}
+
+// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
+const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
+
+/**
+ * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ * @param {string} name name of the instance
+ * @param {((target: any) => any)} kInternalIterator
+ * @param {string | number} [keyIndex]
+ * @param {string | number} [valueIndex]
+ */
+function createIterator (name, kInternalIterator, keyIndex = 0, valueIndex = 1) {
+ class FastIterableIterator {
+ /** @type {any} */
+ #target
+ /** @type {'key' | 'value' | 'key+value'} */
+ #kind
+ /** @type {number} */
+ #index
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#dfn-default-iterator-object
+ * @param {unknown} target
+ * @param {'key' | 'value' | 'key+value'} kind
+ */
+ constructor (target, kind) {
+ this.#target = target
+ this.#kind = kind
+ this.#index = 0
+ }
+
+ next () {
+ // 1. Let interface be the interface for which the iterator prototype object exists.
+ // 2. Let thisValue be the this value.
+ // 3. Let object be ? ToObject(thisValue).
+ // 4. If object is a platform object, then perform a security
+ // check, passing:
+ // 5. If object is not a default iterator object for interface,
+ // then throw a TypeError.
+ if (typeof this !== 'object' || this === null || !(#target in this)) {
+ throw new TypeError(
+ `'next' called on an object that does not implement interface ${name} Iterator.`
+ )
+ }
+
+ // 6. Let index be object’s index.
+ // 7. Let kind be object’s kind.
+ // 8. Let values be object’s target's value pairs to iterate over.
+ const index = this.#index
+ const values = kInternalIterator(this.#target)
+
+ // 9. Let len be the length of values.
+ const len = values.length
+
+ // 10. If index is greater than or equal to len, then return
+ // CreateIterResultObject(undefined, true).
+ if (index >= len) {
+ return {
+ value: undefined,
+ done: true
+ }
+ }
+
+ // 11. Let pair be the entry in values at index index.
+ const { [keyIndex]: key, [valueIndex]: value } = values[index]
+
+ // 12. Set object’s index to index + 1.
+ this.#index = index + 1
+
+ // 13. Return the iterator result for pair and kind.
+
+ // https://webidl.spec.whatwg.org/#iterator-result
+
+ // 1. Let result be a value determined by the value of kind:
+ let result
+ switch (this.#kind) {
+ case 'key':
+ // 1. Let idlKey be pair’s key.
+ // 2. Let key be the result of converting idlKey to an
+ // ECMAScript value.
+ // 3. result is key.
+ result = key
+ break
+ case 'value':
+ // 1. Let idlValue be pair’s value.
+ // 2. Let value be the result of converting idlValue to
+ // an ECMAScript value.
+ // 3. result is value.
+ result = value
+ break
+ case 'key+value':
+ // 1. Let idlKey be pair’s key.
+ // 2. Let idlValue be pair’s value.
+ // 3. Let key be the result of converting idlKey to an
+ // ECMAScript value.
+ // 4. Let value be the result of converting idlValue to
+ // an ECMAScript value.
+ // 5. Let array be ! ArrayCreate(2).
+ // 6. Call ! CreateDataProperty(array, "0", key).
+ // 7. Call ! CreateDataProperty(array, "1", value).
+ // 8. result is array.
+ result = [key, value]
+ break
+ }
+
+ // 2. Return CreateIterResultObject(result, false).
+ return {
+ value: result,
+ done: false
+ }
+ }
+ }
+
+ // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ // @ts-ignore
+ delete FastIterableIterator.prototype.constructor
+
+ Object.setPrototypeOf(FastIterableIterator.prototype, esIteratorPrototype)
+
+ Object.defineProperties(FastIterableIterator.prototype, {
+ [Symbol.toStringTag]: {
+ writable: false,
+ enumerable: false,
+ configurable: true,
+ value: `${name} Iterator`
+ },
+ next: { writable: true, enumerable: true, configurable: true }
+ })
+
+ /**
+ * @param {unknown} target
+ * @param {'key' | 'value' | 'key+value'} kind
+ * @returns {IterableIterator<any>}
+ */
+ return function (target, kind) {
+ return new FastIterableIterator(target, kind)
+ }
+}
+
+/**
+ * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ * @param {string} name name of the instance
+ * @param {any} object class
+ * @param {(target: any) => any} kInternalIterator
+ * @param {string | number} [keyIndex]
+ * @param {string | number} [valueIndex]
+ */
+function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueIndex = 1) {
+ const makeIterator = createIterator(name, kInternalIterator, keyIndex, valueIndex)
+
+ const properties = {
+ keys: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function keys () {
+ webidl.brandCheck(this, object)
+ return makeIterator(this, 'key')
+ }
+ },
+ values: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function values () {
+ webidl.brandCheck(this, object)
+ return makeIterator(this, 'value')
+ }
+ },
+ entries: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function entries () {
+ webidl.brandCheck(this, object)
+ return makeIterator(this, 'key+value')
+ }
+ },
+ forEach: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function forEach (callbackfn, thisArg = globalThis) {
+ webidl.brandCheck(this, object)
+ webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`)
+ if (typeof callbackfn !== 'function') {
+ throw new TypeError(
+ `Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.`
+ )
+ }
+ for (const { 0: key, 1: value } of makeIterator(this, 'key+value')) {
+ callbackfn.call(thisArg, value, key, this)
+ }
+ }
+ }
+ }
+
+ return Object.defineProperties(object.prototype, {
+ ...properties,
+ [Symbol.iterator]: {
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ value: properties.entries.value
+ }
+ })
+}
+
+/**
+ * @param {import('./body').ExtractBodyResult} body
+ * @param {(bytes: Uint8Array) => void} processBody
+ * @param {(error: Error) => void} processBodyError
+ * @returns {void}
+ *
+ * @see https://fetch.spec.whatwg.org/#body-fully-read
+ */
+function fullyReadBody (body, processBody, processBodyError) {
+ // 1. If taskDestination is null, then set taskDestination to
+ // the result of starting a new parallel queue.
+
+ // 2. Let successSteps given a byte sequence bytes be to queue a
+ // fetch task to run processBody given bytes, with taskDestination.
+ const successSteps = processBody
+
+ // 3. Let errorSteps be to queue a fetch task to run processBodyError,
+ // with taskDestination.
+ const errorSteps = processBodyError
+
+ try {
+ // 4. Let reader be the result of getting a reader for body’s stream.
+ // If that threw an exception, then run errorSteps with that
+ // exception and return.
+ const reader = body.stream.getReader()
+
+ // 5. Read all bytes from reader, given successSteps and errorSteps.
+ readAllBytes(reader, successSteps, errorSteps)
+ } catch (e) {
+ errorSteps(e)
+ }
+}
+
+/**
+ * @param {ReadableStreamController<Uint8Array>} controller
+ */
+function readableStreamClose (controller) {
+ try {
+ controller.close()
+ controller.byobRequest?.respond(0)
+ } catch (err) {
+ // TODO: add comment explaining why this error occurs.
+ if (!err.message.includes('Controller is already closed') && !err.message.includes('ReadableStream is already closed')) {
+ throw err
+ }
+ }
+}
+
+/**
+ * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
+ * @see https://streams.spec.whatwg.org/#read-loop
+ * @param {ReadableStream<Uint8Array<ArrayBuffer>>} reader
+ * @param {(bytes: Uint8Array) => void} successSteps
+ * @param {(error: Error) => void} failureSteps
+ * @returns {Promise<void>}
+ */
+async function readAllBytes (reader, successSteps, failureSteps) {
+ try {
+ const bytes = []
+ let byteLength = 0
+
+ do {
+ const { done, value: chunk } = await reader.read()
+
+ if (done) {
+ // 1. Call successSteps with bytes.
+ successSteps(Buffer.concat(bytes, byteLength))
+ return
+ }
+
+ // 1. If chunk is not a Uint8Array object, call failureSteps
+ // with a TypeError and abort these steps.
+ if (!isUint8Array(chunk)) {
+ failureSteps(new TypeError('Received non-Uint8Array chunk'))
+ return
+ }
+
+ // 2. Append the bytes represented by chunk to bytes.
+ bytes.push(chunk)
+ byteLength += chunk.length
+
+ // 3. Read-loop given reader, bytes, successSteps, and failureSteps.
+ } while (true)
+ } catch (e) {
+ // 1. Call failureSteps with e.
+ failureSteps(e)
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#is-local
+ * @param {URL} url
+ * @returns {boolean}
+ */
+function urlIsLocal (url) {
+ assert('protocol' in url) // ensure it's a url object
+
+ const protocol = url.protocol
+
+ // A URL is local if its scheme is a local scheme.
+ // A local scheme is "about", "blob", or "data".
+ return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
+}
+
+/**
+ * @param {string|URL} url
+ * @returns {boolean}
+ */
+function urlHasHttpsScheme (url) {
+ return (
+ (
+ typeof url === 'string' &&
+ url[5] === ':' &&
+ url[0] === 'h' &&
+ url[1] === 't' &&
+ url[2] === 't' &&
+ url[3] === 'p' &&
+ url[4] === 's'
+ ) ||
+ url.protocol === 'https:'
+ )
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-scheme
+ * @param {URL} url
+ */
+function urlIsHttpHttpsScheme (url) {
+ assert('protocol' in url) // ensure it's a url object
+
+ const protocol = url.protocol
+
+ return protocol === 'http:' || protocol === 'https:'
+}
+
+/**
+ * @typedef {Object} RangeHeaderValue
+ * @property {number|null} rangeStartValue
+ * @property {number|null} rangeEndValue
+ */
+
+/**
+ * @see https://fetch.spec.whatwg.org/#simple-range-header-value
+ * @param {string} value
+ * @param {boolean} allowWhitespace
+ * @return {RangeHeaderValue|'failure'}
+ */
+function simpleRangeHeaderValue (value, allowWhitespace) {
+ // 1. Let data be the isomorphic decoding of value.
+ // Note: isomorphic decoding takes a sequence of bytes (ie. a Uint8Array) and turns it into a string,
+ // nothing more. We obviously don't need to do that if value is a string already.
+ const data = value
+
+ // 2. If data does not start with "bytes", then return failure.
+ if (!data.startsWith('bytes')) {
+ return 'failure'
+ }
+
+ // 3. Let position be a position variable for data, initially pointing at the 5th code point of data.
+ const position = { position: 5 }
+
+ // 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space,
+ // from data given position.
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 5. If the code point at position within data is not U+003D (=), then return failure.
+ if (data.charCodeAt(position.position) !== 0x3D) {
+ return 'failure'
+ }
+
+ // 6. Advance position by 1.
+ position.position++
+
+ // 7. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from
+ // data given position.
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 8. Let rangeStart be the result of collecting a sequence of code points that are ASCII digits,
+ // from data given position.
+ const rangeStart = collectASequenceOfCodePoints(
+ (char) => {
+ const code = char.charCodeAt(0)
+
+ return code >= 0x30 && code <= 0x39
+ },
+ data,
+ position
+ )
+
+ // 9. Let rangeStartValue be rangeStart, interpreted as decimal number, if rangeStart is not the
+ // empty string; otherwise null.
+ const rangeStartValue = rangeStart.length ? Number(rangeStart) : null
+
+ // 10. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space,
+ // from data given position.
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 11. If the code point at position within data is not U+002D (-), then return failure.
+ if (data.charCodeAt(position.position) !== 0x2D) {
+ return 'failure'
+ }
+
+ // 12. Advance position by 1.
+ position.position++
+
+ // 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab
+ // or space, from data given position.
+ // Note from Khafra: its the same step as in #8 again lol
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 14. Let rangeEnd be the result of collecting a sequence of code points that are
+ // ASCII digits, from data given position.
+ // Note from Khafra: you wouldn't guess it, but this is also the same step as #8
+ const rangeEnd = collectASequenceOfCodePoints(
+ (char) => {
+ const code = char.charCodeAt(0)
+
+ return code >= 0x30 && code <= 0x39
+ },
+ data,
+ position
+ )
+
+ // 15. Let rangeEndValue be rangeEnd, interpreted as decimal number, if rangeEnd
+ // is not the empty string; otherwise null.
+ // Note from Khafra: THE SAME STEP, AGAIN!!!
+ // Note: why interpret as a decimal if we only collect ascii digits?
+ const rangeEndValue = rangeEnd.length ? Number(rangeEnd) : null
+
+ // 16. If position is not past the end of data, then return failure.
+ if (position.position < data.length) {
+ return 'failure'
+ }
+
+ // 17. If rangeEndValue and rangeStartValue are null, then return failure.
+ if (rangeEndValue === null && rangeStartValue === null) {
+ return 'failure'
+ }
+
+ // 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is
+ // greater than rangeEndValue, then return failure.
+ // Note: ... when can they not be numbers?
+ if (rangeStartValue > rangeEndValue) {
+ return 'failure'
+ }
+
+ // 19. Return (rangeStartValue, rangeEndValue).
+ return { rangeStartValue, rangeEndValue }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#build-a-content-range
+ * @param {number} rangeStart
+ * @param {number} rangeEnd
+ * @param {number} fullLength
+ */
+function buildContentRange (rangeStart, rangeEnd, fullLength) {
+ // 1. Let contentRange be `bytes `.
+ let contentRange = 'bytes '
+
+ // 2. Append rangeStart, serialized and isomorphic encoded, to contentRange.
+ contentRange += isomorphicEncode(`${rangeStart}`)
+
+ // 3. Append 0x2D (-) to contentRange.
+ contentRange += '-'
+
+ // 4. Append rangeEnd, serialized and isomorphic encoded to contentRange.
+ contentRange += isomorphicEncode(`${rangeEnd}`)
+
+ // 5. Append 0x2F (/) to contentRange.
+ contentRange += '/'
+
+ // 6. Append fullLength, serialized and isomorphic encoded to contentRange.
+ contentRange += isomorphicEncode(`${fullLength}`)
+
+ // 7. Return contentRange.
+ return contentRange
+}
+
+// A Stream, which pipes the response to zlib.createInflate() or
+// zlib.createInflateRaw() depending on the first byte of the Buffer.
+// If the lower byte of the first byte is 0x08, then the stream is
+// interpreted as a zlib stream, otherwise it's interpreted as a
+// raw deflate stream.
+class InflateStream extends Transform {
+ #zlibOptions
+
+ /** @param {zlib.ZlibOptions} [zlibOptions] */
+ constructor (zlibOptions) {
+ super()
+ this.#zlibOptions = zlibOptions
+ }
+
+ _transform (chunk, encoding, callback) {
+ if (!this._inflateStream) {
+ if (chunk.length === 0) {
+ callback()
+ return
+ }
+ this._inflateStream = (chunk[0] & 0x0F) === 0x08
+ ? zlib.createInflate(this.#zlibOptions)
+ : zlib.createInflateRaw(this.#zlibOptions)
+
+ this._inflateStream.on('data', this.push.bind(this))
+ this._inflateStream.on('end', () => this.push(null))
+ this._inflateStream.on('error', (err) => this.destroy(err))
+ }
+
+ this._inflateStream.write(chunk, encoding, callback)
+ }
+
+ _final (callback) {
+ if (this._inflateStream) {
+ this._inflateStream.end()
+ this._inflateStream = null
+ }
+ callback()
+ }
+}
+
+/**
+ * @param {zlib.ZlibOptions} [zlibOptions]
+ * @returns {InflateStream}
+ */
+function createInflate (zlibOptions) {
+ return new InflateStream(zlibOptions)
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-header-extract-mime-type
+ * @param {import('./headers').HeadersList} headers
+ */
+function extractMimeType (headers) {
+ // 1. Let charset be null.
+ let charset = null
+
+ // 2. Let essence be null.
+ let essence = null
+
+ // 3. Let mimeType be null.
+ let mimeType = null
+
+ // 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers.
+ const values = getDecodeSplit('content-type', headers)
+
+ // 5. If values is null, then return failure.
+ if (values === null) {
+ return 'failure'
+ }
+
+ // 6. For each value of values:
+ for (const value of values) {
+ // 6.1. Let temporaryMimeType be the result of parsing value.
+ const temporaryMimeType = parseMIMEType(value)
+
+ // 6.2. If temporaryMimeType is failure or its essence is "*/*", then continue.
+ if (temporaryMimeType === 'failure' || temporaryMimeType.essence === '*/*') {
+ continue
+ }
+
+ // 6.3. Set mimeType to temporaryMimeType.
+ mimeType = temporaryMimeType
+
+ // 6.4. If mimeType’s essence is not essence, then:
+ if (mimeType.essence !== essence) {
+ // 6.4.1. Set charset to null.
+ charset = null
+
+ // 6.4.2. If mimeType’s parameters["charset"] exists, then set charset to
+ // mimeType’s parameters["charset"].
+ if (mimeType.parameters.has('charset')) {
+ charset = mimeType.parameters.get('charset')
+ }
+
+ // 6.4.3. Set essence to mimeType’s essence.
+ essence = mimeType.essence
+ } else if (!mimeType.parameters.has('charset') && charset !== null) {
+ // 6.5. Otherwise, if mimeType’s parameters["charset"] does not exist, and
+ // charset is non-null, set mimeType’s parameters["charset"] to charset.
+ mimeType.parameters.set('charset', charset)
+ }
+ }
+
+ // 7. If mimeType is null, then return failure.
+ if (mimeType == null) {
+ return 'failure'
+ }
+
+ // 8. Return mimeType.
+ return mimeType
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#header-value-get-decode-and-split
+ * @param {string|null} value
+ */
+function gettingDecodingSplitting (value) {
+ // 1. Let input be the result of isomorphic decoding value.
+ const input = value
+
+ // 2. Let position be a position variable for input, initially pointing at the start of input.
+ const position = { position: 0 }
+
+ // 3. Let values be a list of strings, initially empty.
+ const values = []
+
+ // 4. Let temporaryValue be the empty string.
+ let temporaryValue = ''
+
+ // 5. While position is not past the end of input:
+ while (position.position < input.length) {
+ // 5.1. Append the result of collecting a sequence of code points that are not U+0022 (")
+ // or U+002C (,) from input, given position, to temporaryValue.
+ temporaryValue += collectASequenceOfCodePoints(
+ (char) => char !== '"' && char !== ',',
+ input,
+ position
+ )
+
+ // 5.2. If position is not past the end of input, then:
+ if (position.position < input.length) {
+ // 5.2.1. If the code point at position within input is U+0022 ("), then:
+ if (input.charCodeAt(position.position) === 0x22) {
+ // 5.2.1.1. Append the result of collecting an HTTP quoted string from input, given position, to temporaryValue.
+ temporaryValue += collectAnHTTPQuotedString(
+ input,
+ position
+ )
+
+ // 5.2.1.2. If position is not past the end of input, then continue.
+ if (position.position < input.length) {
+ continue
+ }
+ } else {
+ // 5.2.2. Otherwise:
+
+ // 5.2.2.1. Assert: the code point at position within input is U+002C (,).
+ assert(input.charCodeAt(position.position) === 0x2C)
+
+ // 5.2.2.2. Advance position by 1.
+ position.position++
+ }
+ }
+
+ // 5.3. Remove all HTTP tab or space from the start and end of temporaryValue.
+ temporaryValue = removeChars(temporaryValue, true, true, (char) => char === 0x9 || char === 0x20)
+
+ // 5.4. Append temporaryValue to values.
+ values.push(temporaryValue)
+
+ // 5.6. Set temporaryValue to the empty string.
+ temporaryValue = ''
+ }
+
+ // 6. Return values.
+ return values
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split
+ * @param {string} name lowercase header name
+ * @param {import('./headers').HeadersList} list
+ */
+function getDecodeSplit (name, list) {
+ // 1. Let value be the result of getting name from list.
+ const value = list.get(name, true)
+
+ // 2. If value is null, then return null.
+ if (value === null) {
+ return null
+ }
+
+ // 3. Return the result of getting, decoding, and splitting value.
+ return gettingDecodingSplitting(value)
+}
+
+function hasAuthenticationEntry (request) {
+ return false
+}
+
+/**
+ * @see https://url.spec.whatwg.org/#include-credentials
+ * @param {URL} url
+ */
+function includesCredentials (url) {
+ // A URL includes credentials if its username or password is not the empty string.
+ return !!(url.username || url.password)
+}
+
+/**
+ * @see https://html.spec.whatwg.org/multipage/document-sequences.html#traversable-navigable
+ * @param {object|string} navigable
+ */
+function isTraversableNavigable (navigable) {
+ // TODO
+ return true
+}
+
+class EnvironmentSettingsObjectBase {
+ get baseUrl () {
+ return getGlobalOrigin()
+ }
+
+ get origin () {
+ return this.baseUrl?.origin
+ }
+
+ policyContainer = makePolicyContainer()
+}
+
+class EnvironmentSettingsObject {
+ settingsObject = new EnvironmentSettingsObjectBase()
+}
+
+const environmentSettingsObject = new EnvironmentSettingsObject()
+
+module.exports = {
+ isAborted,
+ isCancelled,
+ isValidEncodedURL,
+ ReadableStreamFrom,
+ tryUpgradeRequestToAPotentiallyTrustworthyURL,
+ clampAndCoarsenConnectionTimingInfo,
+ coarsenedSharedCurrentTime,
+ determineRequestsReferrer,
+ makePolicyContainer,
+ clonePolicyContainer,
+ appendFetchMetadata,
+ appendRequestOriginHeader,
+ TAOCheck,
+ corsCheck,
+ crossOriginResourcePolicyCheck,
+ createOpaqueTimingInfo,
+ setRequestReferrerPolicyOnRedirect,
+ isValidHTTPToken,
+ requestBadPort,
+ requestCurrentURL,
+ responseURL,
+ responseLocationURL,
+ isURLPotentiallyTrustworthy,
+ isValidReasonPhrase,
+ sameOrigin,
+ normalizeMethod,
+ iteratorMixin,
+ createIterator,
+ isValidHeaderName,
+ isValidHeaderValue,
+ isErrorLike,
+ fullyReadBody,
+ readableStreamClose,
+ urlIsLocal,
+ urlHasHttpsScheme,
+ urlIsHttpHttpsScheme,
+ readAllBytes,
+ simpleRangeHeaderValue,
+ buildContentRange,
+ createInflate,
+ extractMimeType,
+ getDecodeSplit,
+ environmentSettingsObject,
+ isOriginIPPotentiallyTrustworthy,
+ hasAuthenticationEntry,
+ includesCredentials,
+ isTraversableNavigable
+}
diff --git a/vanilla/node_modules/undici/lib/web/infra/index.js b/vanilla/node_modules/undici/lib/web/infra/index.js
new file mode 100644
index 0000000..391bb2e
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/infra/index.js
@@ -0,0 +1,229 @@
+'use strict'
+
+const assert = require('node:assert')
+const { utf8DecodeBytes } = require('../../encoding')
+
+/**
+ * @param {(char: string) => boolean} condition
+ * @param {string} input
+ * @param {{ position: number }} position
+ * @returns {string}
+ *
+ * @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
+ */
+function collectASequenceOfCodePoints (condition, input, position) {
+ // 1. Let result be the empty string.
+ let result = ''
+
+ // 2. While position doesn’t point past the end of input and the
+ // code point at position within input meets the condition condition:
+ while (position.position < input.length && condition(input[position.position])) {
+ // 1. Append that code point to the end of result.
+ result += input[position.position]
+
+ // 2. Advance position by 1.
+ position.position++
+ }
+
+ // 3. Return result.
+ return result
+}
+
+/**
+ * A faster collectASequenceOfCodePoints that only works when comparing a single character.
+ * @param {string} char
+ * @param {string} input
+ * @param {{ position: number }} position
+ * @returns {string}
+ *
+ * @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
+ */
+function collectASequenceOfCodePointsFast (char, input, position) {
+ const idx = input.indexOf(char, position.position)
+ const start = position.position
+
+ if (idx === -1) {
+ position.position = input.length
+ return input.slice(start)
+ }
+
+ position.position = idx
+ return input.slice(start, position.position)
+}
+
+const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line no-control-regex
+
+/**
+ * @param {string} data
+ * @returns {Uint8Array | 'failure'}
+ *
+ * @see https://infra.spec.whatwg.org/#forgiving-base64-decode
+ */
+function forgivingBase64 (data) {
+ // 1. Remove all ASCII whitespace from data.
+ data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '')
+
+ let dataLength = data.length
+ // 2. If data’s code point length divides by 4 leaving
+ // no remainder, then:
+ if (dataLength % 4 === 0) {
+ // 1. If data ends with one or two U+003D (=) code points,
+ // then remove them from data.
+ if (data.charCodeAt(dataLength - 1) === 0x003D) {
+ --dataLength
+ if (data.charCodeAt(dataLength - 1) === 0x003D) {
+ --dataLength
+ }
+ }
+ }
+
+ // 3. If data’s code point length divides by 4 leaving
+ // a remainder of 1, then return failure.
+ if (dataLength % 4 === 1) {
+ return 'failure'
+ }
+
+ // 4. If data contains a code point that is not one of
+ // U+002B (+)
+ // U+002F (/)
+ // ASCII alphanumeric
+ // then return failure.
+ if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) {
+ return 'failure'
+ }
+
+ const buffer = Buffer.from(data, 'base64')
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
+}
+
+/**
+ * @param {number} char
+ * @returns {boolean}
+ *
+ * @see https://infra.spec.whatwg.org/#ascii-whitespace
+ */
+function isASCIIWhitespace (char) {
+ return (
+ char === 0x09 || // \t
+ char === 0x0a || // \n
+ char === 0x0c || // \f
+ char === 0x0d || // \r
+ char === 0x20 // space
+ )
+}
+
+/**
+ * @param {Uint8Array} input
+ * @returns {string}
+ *
+ * @see https://infra.spec.whatwg.org/#isomorphic-decode
+ */
+function isomorphicDecode (input) {
+ // 1. To isomorphic decode a byte sequence input, return a string whose code point
+ // length is equal to input’s length and whose code points have the same values
+ // as the values of input’s bytes, in the same order.
+ const length = input.length
+ if ((2 << 15) - 1 > length) {
+ return String.fromCharCode.apply(null, input)
+ }
+ let result = ''
+ let i = 0
+ let addition = (2 << 15) - 1
+ while (i < length) {
+ if (i + addition > length) {
+ addition = length - i
+ }
+ result += String.fromCharCode.apply(null, input.subarray(i, i += addition))
+ }
+ return result
+}
+
+const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line no-control-regex
+
+/**
+ * @param {string} input
+ * @returns {string}
+ *
+ * @see https://infra.spec.whatwg.org/#isomorphic-encode
+ */
+function isomorphicEncode (input) {
+ // 1. Assert: input contains no code points greater than U+00FF.
+ assert(!invalidIsomorphicEncodeValueRegex.test(input))
+
+ // 2. Return a byte sequence whose length is equal to input’s code
+ // point length and whose bytes have the same values as the
+ // values of input’s code points, in the same order
+ return input
+}
+
+/**
+ * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
+ * @param {Uint8Array} bytes
+ */
+function parseJSONFromBytes (bytes) {
+ return JSON.parse(utf8DecodeBytes(bytes))
+}
+
+/**
+ * @param {string} str
+ * @param {boolean} [leading=true]
+ * @param {boolean} [trailing=true]
+ * @returns {string}
+ *
+ * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
+ */
+function removeASCIIWhitespace (str, leading = true, trailing = true) {
+ return removeChars(str, leading, trailing, isASCIIWhitespace)
+}
+
+/**
+ * @param {string} str
+ * @param {boolean} leading
+ * @param {boolean} trailing
+ * @param {(charCode: number) => boolean} predicate
+ * @returns {string}
+ */
+function removeChars (str, leading, trailing, predicate) {
+ let lead = 0
+ let trail = str.length - 1
+
+ if (leading) {
+ while (lead < str.length && predicate(str.charCodeAt(lead))) lead++
+ }
+
+ if (trailing) {
+ while (trail > 0 && predicate(str.charCodeAt(trail))) trail--
+ }
+
+ return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
+}
+
+// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
+function serializeJavascriptValueToJSONString (value) {
+ // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
+ const result = JSON.stringify(value)
+
+ // 2. If result is undefined, then throw a TypeError.
+ if (result === undefined) {
+ throw new TypeError('Value is not JSON serializable')
+ }
+
+ // 3. Assert: result is a string.
+ assert(typeof result === 'string')
+
+ // 4. Return result.
+ return result
+}
+
+module.exports = {
+ collectASequenceOfCodePoints,
+ collectASequenceOfCodePointsFast,
+ forgivingBase64,
+ isASCIIWhitespace,
+ isomorphicDecode,
+ isomorphicEncode,
+ parseJSONFromBytes,
+ removeASCIIWhitespace,
+ removeChars,
+ serializeJavascriptValueToJSONString
+}
diff --git a/vanilla/node_modules/undici/lib/web/subresource-integrity/Readme.md b/vanilla/node_modules/undici/lib/web/subresource-integrity/Readme.md
new file mode 100644
index 0000000..289a2b8
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/subresource-integrity/Readme.md
@@ -0,0 +1,9 @@
+# Subresource Integrity
+
+based on Editor’s Draft, 12 June 2025
+
+This module provides support for Subresource Integrity (SRI) in the context of web fetch operations. SRI is a security feature that allows clients to verify that fetched resources are delivered without unexpected manipulation.
+
+## Links
+
+- [Subresource Integrity](https://w3c.github.io/webappsec-subresource-integrity/) \ No newline at end of file
diff --git a/vanilla/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js b/vanilla/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js
new file mode 100644
index 0000000..8c8f6c4
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js
@@ -0,0 +1,307 @@
+'use strict'
+
+const assert = require('node:assert')
+const { runtimeFeatures } = require('../../util/runtime-features.js')
+
+/**
+ * @typedef {object} Metadata
+ * @property {SRIHashAlgorithm} alg - The algorithm used for the hash.
+ * @property {string} val - The base64-encoded hash value.
+ */
+
+/**
+ * @typedef {Metadata[]} MetadataList
+ */
+
+/**
+ * @typedef {('sha256' | 'sha384' | 'sha512')} SRIHashAlgorithm
+ */
+
+/**
+ * @type {Map<SRIHashAlgorithm, number>}
+ *
+ * The valid SRI hash algorithm token set is the ordered set « "sha256",
+ * "sha384", "sha512" » (corresponding to SHA-256, SHA-384, and SHA-512
+ * respectively). The ordering of this set is meaningful, with stronger
+ * algorithms appearing later in the set.
+ *
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token-set
+ */
+const validSRIHashAlgorithmTokenSet = new Map([['sha256', 0], ['sha384', 1], ['sha512', 2]])
+
+// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
+/** @type {import('node:crypto')} */
+let crypto
+
+if (runtimeFeatures.has('crypto')) {
+ crypto = require('node:crypto')
+ const cryptoHashes = crypto.getHashes()
+
+ // If no hashes are available, we cannot support SRI.
+ if (cryptoHashes.length === 0) {
+ validSRIHashAlgorithmTokenSet.clear()
+ }
+
+ for (const algorithm of validSRIHashAlgorithmTokenSet.keys()) {
+ // If the algorithm is not supported, remove it from the list.
+ if (cryptoHashes.includes(algorithm) === false) {
+ validSRIHashAlgorithmTokenSet.delete(algorithm)
+ }
+ }
+} else {
+ // If crypto is not available, we cannot support SRI.
+ validSRIHashAlgorithmTokenSet.clear()
+}
+
+/**
+ * @typedef GetSRIHashAlgorithmIndex
+ * @type {(algorithm: SRIHashAlgorithm) => number}
+ * @param {SRIHashAlgorithm} algorithm
+ * @returns {number} The index of the algorithm in the valid SRI hash algorithm
+ * token set.
+ */
+
+const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.prototype.get.bind(
+ validSRIHashAlgorithmTokenSet))
+
+/**
+ * @typedef IsValidSRIHashAlgorithm
+ * @type {(algorithm: string) => algorithm is SRIHashAlgorithm}
+ * @param {*} algorithm
+ * @returns {algorithm is SRIHashAlgorithm}
+ */
+
+const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ (
+ Map.prototype.has.bind(validSRIHashAlgorithmTokenSet)
+)
+
+/**
+ * @param {Uint8Array} bytes
+ * @param {string} metadataList
+ * @returns {boolean}
+ *
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
+ */
+const bytesMatch = runtimeFeatures.has('crypto') === false || validSRIHashAlgorithmTokenSet.size === 0
+ // If node is not built with OpenSSL support, we cannot check
+ // a request's integrity, so allow it by default (the spec will
+ // allow requests if an invalid hash is given, as precedence).
+ ? () => true
+ : (bytes, metadataList) => {
+ // 1. Let parsedMetadata be the result of parsing metadataList.
+ const parsedMetadata = parseMetadata(metadataList)
+
+ // 2. If parsedMetadata is empty set, return true.
+ if (parsedMetadata.length === 0) {
+ return true
+ }
+
+ // 3. Let metadata be the result of getting the strongest
+ // metadata from parsedMetadata.
+ const metadata = getStrongestMetadata(parsedMetadata)
+
+ // 4. For each item in metadata:
+ for (const item of metadata) {
+ // 1. Let algorithm be the item["alg"].
+ const algorithm = item.alg
+
+ // 2. Let expectedValue be the item["val"].
+ const expectedValue = item.val
+
+ // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
+ // "be liberal with padding". This is annoying, and it's not even in the spec.
+
+ // 3. Let actualValue be the result of applying algorithm to bytes .
+ const actualValue = applyAlgorithmToBytes(algorithm, bytes)
+
+ // 4. If actualValue is a case-sensitive match for expectedValue,
+ // return true.
+ if (caseSensitiveMatch(actualValue, expectedValue)) {
+ return true
+ }
+ }
+
+ // 5. Return false.
+ return false
+ }
+
+/**
+ * @param {MetadataList} metadataList
+ * @returns {MetadataList} The strongest hash algorithm from the metadata list.
+ */
+function getStrongestMetadata (metadataList) {
+ // 1. Let result be the empty set and strongest be the empty string.
+ const result = []
+ /** @type {Metadata|null} */
+ let strongest = null
+
+ // 2. For each item in set:
+ for (const item of metadataList) {
+ // 1. Assert: item["alg"] is a valid SRI hash algorithm token.
+ assert(isValidSRIHashAlgorithm(item.alg), 'Invalid SRI hash algorithm token')
+
+ // 2. If result is the empty set, then:
+ if (result.length === 0) {
+ // 1. Append item to result.
+ result.push(item)
+
+ // 2. Set strongest to item.
+ strongest = item
+
+ // 3. Continue.
+ continue
+ }
+
+ // 3. Let currentAlgorithm be strongest["alg"], and currentAlgorithmIndex be
+ // the index of currentAlgorithm in the valid SRI hash algorithm token set.
+ const currentAlgorithm = /** @type {Metadata} */ (strongest).alg
+ const currentAlgorithmIndex = getSRIHashAlgorithmIndex(currentAlgorithm)
+
+ // 4. Let newAlgorithm be the item["alg"], and newAlgorithmIndex be the
+ // index of newAlgorithm in the valid SRI hash algorithm token set.
+ const newAlgorithm = item.alg
+ const newAlgorithmIndex = getSRIHashAlgorithmIndex(newAlgorithm)
+
+ // 5. If newAlgorithmIndex is less than currentAlgorithmIndex, then continue.
+ if (newAlgorithmIndex < currentAlgorithmIndex) {
+ continue
+
+ // 6. Otherwise, if newAlgorithmIndex is greater than
+ // currentAlgorithmIndex:
+ } else if (newAlgorithmIndex > currentAlgorithmIndex) {
+ // 1. Set strongest to item.
+ strongest = item
+
+ // 2. Set result to « item ».
+ result[0] = item
+ result.length = 1
+
+ // 7. Otherwise, newAlgorithmIndex and currentAlgorithmIndex are the same
+ // value. Append item to result.
+ } else {
+ result.push(item)
+ }
+ }
+
+ // 3. Return result.
+ return result
+}
+
+/**
+ * @param {string} metadata
+ * @returns {MetadataList}
+ *
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
+ */
+function parseMetadata (metadata) {
+ // 1. Let result be the empty set.
+ /** @type {MetadataList} */
+ const result = []
+
+ // 2. For each item returned by splitting metadata on spaces:
+ for (const item of metadata.split(' ')) {
+ // 1. Let expression-and-options be the result of splitting item on U+003F (?).
+ const expressionAndOptions = item.split('?', 1)
+
+ // 2. Let algorithm-expression be expression-and-options[0].
+ const algorithmExpression = expressionAndOptions[0]
+
+ // 3. Let base64-value be the empty string.
+ let base64Value = ''
+
+ // 4. Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-).
+ const algorithmAndValue = [algorithmExpression.slice(0, 6), algorithmExpression.slice(7)]
+
+ // 5. Let algorithm be algorithm-and-value[0].
+ const algorithm = algorithmAndValue[0]
+
+ // 6. If algorithm is not a valid SRI hash algorithm token, then continue.
+ if (!isValidSRIHashAlgorithm(algorithm)) {
+ continue
+ }
+
+ // 7. If algorithm-and-value[1] exists, set base64-value to
+ // algorithm-and-value[1].
+ if (algorithmAndValue[1]) {
+ base64Value = algorithmAndValue[1]
+ }
+
+ // 8. Let metadata be the ordered map
+ // «["alg" → algorithm, "val" → base64-value]».
+ const metadata = {
+ alg: algorithm,
+ val: base64Value
+ }
+
+ // 9. Append metadata to result.
+ result.push(metadata)
+ }
+
+ // 3. Return result.
+ return result
+}
+
+/**
+ * Applies the specified hash algorithm to the given bytes
+ *
+ * @typedef {(algorithm: SRIHashAlgorithm, bytes: Uint8Array) => string} ApplyAlgorithmToBytes
+ * @param {SRIHashAlgorithm} algorithm
+ * @param {Uint8Array} bytes
+ * @returns {string}
+ */
+const applyAlgorithmToBytes = (algorithm, bytes) => {
+ return crypto.hash(algorithm, bytes, 'base64')
+}
+
+/**
+ * Compares two base64 strings, allowing for base64url
+ * in the second string.
+ *
+ * @param {string} actualValue base64 encoded string
+ * @param {string} expectedValue base64 or base64url encoded string
+ * @returns {boolean}
+ */
+function caseSensitiveMatch (actualValue, expectedValue) {
+ // Ignore padding characters from the end of the strings by
+ // decreasing the length by 1 or 2 if the last characters are `=`.
+ let actualValueLength = actualValue.length
+ if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') {
+ actualValueLength -= 1
+ }
+ if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') {
+ actualValueLength -= 1
+ }
+ let expectedValueLength = expectedValue.length
+ if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') {
+ expectedValueLength -= 1
+ }
+ if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') {
+ expectedValueLength -= 1
+ }
+
+ if (actualValueLength !== expectedValueLength) {
+ return false
+ }
+
+ for (let i = 0; i < actualValueLength; ++i) {
+ if (
+ actualValue[i] === expectedValue[i] ||
+ (actualValue[i] === '+' && expectedValue[i] === '-') ||
+ (actualValue[i] === '/' && expectedValue[i] === '_')
+ ) {
+ continue
+ }
+ return false
+ }
+
+ return true
+}
+
+module.exports = {
+ applyAlgorithmToBytes,
+ bytesMatch,
+ caseSensitiveMatch,
+ isValidSRIHashAlgorithm,
+ getStrongestMetadata,
+ parseMetadata
+}
diff --git a/vanilla/node_modules/undici/lib/web/webidl/index.js b/vanilla/node_modules/undici/lib/web/webidl/index.js
new file mode 100644
index 0000000..c783432
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/webidl/index.js
@@ -0,0 +1,1003 @@
+'use strict'
+
+const assert = require('node:assert')
+const { types, inspect } = require('node:util')
+const { runtimeFeatures } = require('../../util/runtime-features')
+
+const UNDEFINED = 1
+const BOOLEAN = 2
+const STRING = 3
+const SYMBOL = 4
+const NUMBER = 5
+const BIGINT = 6
+const NULL = 7
+const OBJECT = 8 // function and object
+
+const FunctionPrototypeSymbolHasInstance = Function.call.bind(Function.prototype[Symbol.hasInstance])
+
+/** @type {import('../../../types/webidl').Webidl} */
+const webidl = {
+ converters: {},
+ util: {},
+ errors: {},
+ is: {}
+}
+
+/**
+ * @description Instantiate an error.
+ *
+ * @param {Object} opts
+ * @param {string} opts.header
+ * @param {string} opts.message
+ * @returns {TypeError}
+ */
+webidl.errors.exception = function (message) {
+ return new TypeError(`${message.header}: ${message.message}`)
+}
+
+/**
+ * @description Instantiate an error when conversion from one type to another has failed.
+ *
+ * @param {Object} opts
+ * @param {string} opts.prefix
+ * @param {string} opts.argument
+ * @param {string[]} opts.types
+ * @returns {TypeError}
+ */
+webidl.errors.conversionFailed = function (opts) {
+ const plural = opts.types.length === 1 ? '' : ' one of'
+ const message =
+ `${opts.argument} could not be converted to` +
+ `${plural}: ${opts.types.join(', ')}.`
+
+ return webidl.errors.exception({
+ header: opts.prefix,
+ message
+ })
+}
+
+/**
+ * @description Instantiate an error when an invalid argument is provided
+ *
+ * @param {Object} context
+ * @param {string} context.prefix
+ * @param {string} context.value
+ * @param {string} context.type
+ * @returns {TypeError}
+ */
+webidl.errors.invalidArgument = function (context) {
+ return webidl.errors.exception({
+ header: context.prefix,
+ message: `"${context.value}" is an invalid ${context.type}.`
+ })
+}
+
+// https://webidl.spec.whatwg.org/#implements
+webidl.brandCheck = function (V, I) {
+ if (!FunctionPrototypeSymbolHasInstance(I, V)) {
+ const err = new TypeError('Illegal invocation')
+ err.code = 'ERR_INVALID_THIS' // node compat.
+ throw err
+ }
+}
+
+webidl.brandCheckMultiple = function (List) {
+ const prototypes = List.map((c) => webidl.util.MakeTypeAssertion(c))
+
+ return (V) => {
+ if (prototypes.every(typeCheck => !typeCheck(V))) {
+ const err = new TypeError('Illegal invocation')
+ err.code = 'ERR_INVALID_THIS' // node compat.
+ throw err
+ }
+ }
+}
+
+webidl.argumentLengthCheck = function ({ length }, min, ctx) {
+ if (length < min) {
+ throw webidl.errors.exception({
+ message: `${min} argument${min !== 1 ? 's' : ''} required, ` +
+ `but${length ? ' only' : ''} ${length} found.`,
+ header: ctx
+ })
+ }
+}
+
+webidl.illegalConstructor = function () {
+ throw webidl.errors.exception({
+ header: 'TypeError',
+ message: 'Illegal constructor'
+ })
+}
+
+webidl.util.MakeTypeAssertion = function (I) {
+ return (O) => FunctionPrototypeSymbolHasInstance(I, O)
+}
+
+// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values
+webidl.util.Type = function (V) {
+ switch (typeof V) {
+ case 'undefined': return UNDEFINED
+ case 'boolean': return BOOLEAN
+ case 'string': return STRING
+ case 'symbol': return SYMBOL
+ case 'number': return NUMBER
+ case 'bigint': return BIGINT
+ case 'function':
+ case 'object': {
+ if (V === null) {
+ return NULL
+ }
+
+ return OBJECT
+ }
+ }
+}
+
+webidl.util.Types = {
+ UNDEFINED,
+ BOOLEAN,
+ STRING,
+ SYMBOL,
+ NUMBER,
+ BIGINT,
+ NULL,
+ OBJECT
+}
+
+webidl.util.TypeValueToString = function (o) {
+ switch (webidl.util.Type(o)) {
+ case UNDEFINED: return 'Undefined'
+ case BOOLEAN: return 'Boolean'
+ case STRING: return 'String'
+ case SYMBOL: return 'Symbol'
+ case NUMBER: return 'Number'
+ case BIGINT: return 'BigInt'
+ case NULL: return 'Null'
+ case OBJECT: return 'Object'
+ }
+}
+
+webidl.util.markAsUncloneable = runtimeFeatures.has('markAsUncloneable')
+ ? require('node:worker_threads').markAsUncloneable
+ : () => {}
+
+// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
+webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) {
+ let upperBound
+ let lowerBound
+
+ // 1. If bitLength is 64, then:
+ if (bitLength === 64) {
+ // 1. Let upperBound be 2^53 − 1.
+ upperBound = Math.pow(2, 53) - 1
+
+ // 2. If signedness is "unsigned", then let lowerBound be 0.
+ if (signedness === 'unsigned') {
+ lowerBound = 0
+ } else {
+ // 3. Otherwise let lowerBound be −2^53 + 1.
+ lowerBound = Math.pow(-2, 53) + 1
+ }
+ } else if (signedness === 'unsigned') {
+ // 2. Otherwise, if signedness is "unsigned", then:
+
+ // 1. Let lowerBound be 0.
+ lowerBound = 0
+
+ // 2. Let upperBound be 2^bitLength − 1.
+ upperBound = Math.pow(2, bitLength) - 1
+ } else {
+ // 3. Otherwise:
+
+ // 1. Let lowerBound be -2^bitLength − 1.
+ lowerBound = Math.pow(-2, bitLength) - 1
+
+ // 2. Let upperBound be 2^bitLength − 1 − 1.
+ upperBound = Math.pow(2, bitLength - 1) - 1
+ }
+
+ // 4. Let x be ? ToNumber(V).
+ let x = Number(V)
+
+ // 5. If x is −0, then set x to +0.
+ if (x === 0) {
+ x = 0
+ }
+
+ // 6. If the conversion is to an IDL type associated
+ // with the [EnforceRange] extended attribute, then:
+ if (webidl.util.HasFlag(flags, webidl.attributes.EnforceRange)) {
+ // 1. If x is NaN, +∞, or −∞, then throw a TypeError.
+ if (
+ Number.isNaN(x) ||
+ x === Number.POSITIVE_INFINITY ||
+ x === Number.NEGATIVE_INFINITY
+ ) {
+ throw webidl.errors.exception({
+ header: 'Integer conversion',
+ message: `Could not convert ${webidl.util.Stringify(V)} to an integer.`
+ })
+ }
+
+ // 2. Set x to IntegerPart(x).
+ x = webidl.util.IntegerPart(x)
+
+ // 3. If x < lowerBound or x > upperBound, then
+ // throw a TypeError.
+ if (x < lowerBound || x > upperBound) {
+ throw webidl.errors.exception({
+ header: 'Integer conversion',
+ message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.`
+ })
+ }
+
+ // 4. Return x.
+ return x
+ }
+
+ // 7. If x is not NaN and the conversion is to an IDL
+ // type associated with the [Clamp] extended
+ // attribute, then:
+ if (!Number.isNaN(x) && webidl.util.HasFlag(flags, webidl.attributes.Clamp)) {
+ // 1. Set x to min(max(x, lowerBound), upperBound).
+ x = Math.min(Math.max(x, lowerBound), upperBound)
+
+ // 2. Round x to the nearest integer, choosing the
+ // even integer if it lies halfway between two,
+ // and choosing +0 rather than −0.
+ if (Math.floor(x) % 2 === 0) {
+ x = Math.floor(x)
+ } else {
+ x = Math.ceil(x)
+ }
+
+ // 3. Return x.
+ return x
+ }
+
+ // 8. If x is NaN, +0, +∞, or −∞, then return +0.
+ if (
+ Number.isNaN(x) ||
+ (x === 0 && Object.is(0, x)) ||
+ x === Number.POSITIVE_INFINITY ||
+ x === Number.NEGATIVE_INFINITY
+ ) {
+ return 0
+ }
+
+ // 9. Set x to IntegerPart(x).
+ x = webidl.util.IntegerPart(x)
+
+ // 10. Set x to x modulo 2^bitLength.
+ x = x % Math.pow(2, bitLength)
+
+ // 11. If signedness is "signed" and x ≥ 2^bitLength − 1,
+ // then return x − 2^bitLength.
+ if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) {
+ return x - Math.pow(2, bitLength)
+ }
+
+ // 12. Otherwise, return x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
+webidl.util.IntegerPart = function (n) {
+ // 1. Let r be floor(abs(n)).
+ const r = Math.floor(Math.abs(n))
+
+ // 2. If n < 0, then return -1 × r.
+ if (n < 0) {
+ return -1 * r
+ }
+
+ // 3. Otherwise, return r.
+ return r
+}
+
+webidl.util.Stringify = function (V) {
+ const type = webidl.util.Type(V)
+
+ switch (type) {
+ case SYMBOL:
+ return `Symbol(${V.description})`
+ case OBJECT:
+ return inspect(V)
+ case STRING:
+ return `"${V}"`
+ case BIGINT:
+ return `${V}n`
+ default:
+ return `${V}`
+ }
+}
+
+webidl.util.IsResizableArrayBuffer = function (V) {
+ if (types.isArrayBuffer(V)) {
+ return V.resizable
+ }
+
+ if (types.isSharedArrayBuffer(V)) {
+ return V.growable
+ }
+
+ throw webidl.errors.exception({
+ header: 'IsResizableArrayBuffer',
+ message: `"${webidl.util.Stringify(V)}" is not an array buffer.`
+ })
+}
+
+webidl.util.HasFlag = function (flags, attributes) {
+ return typeof flags === 'number' && (flags & attributes) === attributes
+}
+
+// https://webidl.spec.whatwg.org/#es-sequence
+webidl.sequenceConverter = function (converter) {
+ return (V, prefix, argument, Iterable) => {
+ // 1. If Type(V) is not Object, throw a TypeError.
+ if (webidl.util.Type(V) !== OBJECT) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} (${webidl.util.Stringify(V)}) is not iterable.`
+ })
+ }
+
+ // 2. Let method be ? GetMethod(V, @@iterator).
+ /** @type {Generator} */
+ const method = typeof Iterable === 'function' ? Iterable() : V?.[Symbol.iterator]?.()
+ const seq = []
+ let index = 0
+
+ // 3. If method is undefined, throw a TypeError.
+ if (
+ method === undefined ||
+ typeof method.next !== 'function'
+ ) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} is not iterable.`
+ })
+ }
+
+ // https://webidl.spec.whatwg.org/#create-sequence-from-iterable
+ while (true) {
+ const { done, value } = method.next()
+
+ if (done) {
+ break
+ }
+
+ seq.push(converter(value, prefix, `${argument}[${index++}]`))
+ }
+
+ return seq
+ }
+}
+
+// https://webidl.spec.whatwg.org/#es-to-record
+webidl.recordConverter = function (keyConverter, valueConverter) {
+ return (O, prefix, argument) => {
+ // 1. If Type(O) is not Object, throw a TypeError.
+ if (webidl.util.Type(O) !== OBJECT) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} ("${webidl.util.TypeValueToString(O)}") is not an Object.`
+ })
+ }
+
+ // 2. Let result be a new empty instance of record<K, V>.
+ const result = {}
+
+ if (!types.isProxy(O)) {
+ // 1. Let desc be ? O.[[GetOwnProperty]](key).
+ const keys = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]
+
+ for (const key of keys) {
+ const keyName = webidl.util.Stringify(key)
+
+ // 1. Let typedKey be key converted to an IDL value of type K.
+ const typedKey = keyConverter(key, prefix, `Key ${keyName} in ${argument}`)
+
+ // 2. Let value be ? Get(O, key).
+ // 3. Let typedValue be value converted to an IDL value of type V.
+ const typedValue = valueConverter(O[key], prefix, `${argument}[${keyName}]`)
+
+ // 4. Set result[typedKey] to typedValue.
+ result[typedKey] = typedValue
+ }
+
+ // 5. Return result.
+ return result
+ }
+
+ // 3. Let keys be ? O.[[OwnPropertyKeys]]().
+ const keys = Reflect.ownKeys(O)
+
+ // 4. For each key of keys.
+ for (const key of keys) {
+ // 1. Let desc be ? O.[[GetOwnProperty]](key).
+ const desc = Reflect.getOwnPropertyDescriptor(O, key)
+
+ // 2. If desc is not undefined and desc.[[Enumerable]] is true:
+ if (desc?.enumerable) {
+ // 1. Let typedKey be key converted to an IDL value of type K.
+ const typedKey = keyConverter(key, prefix, argument)
+
+ // 2. Let value be ? Get(O, key).
+ // 3. Let typedValue be value converted to an IDL value of type V.
+ const typedValue = valueConverter(O[key], prefix, argument)
+
+ // 4. Set result[typedKey] to typedValue.
+ result[typedKey] = typedValue
+ }
+ }
+
+ // 5. Return result.
+ return result
+ }
+}
+
+webidl.interfaceConverter = function (TypeCheck, name) {
+ return (V, prefix, argument) => {
+ if (!TypeCheck(V)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${name}.`
+ })
+ }
+
+ return V
+ }
+}
+
+webidl.dictionaryConverter = function (converters) {
+ return (dictionary, prefix, argument) => {
+ const dict = {}
+
+ if (dictionary != null && webidl.util.Type(dictionary) !== OBJECT) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`
+ })
+ }
+
+ for (const options of converters) {
+ const { key, defaultValue, required, converter } = options
+
+ if (required === true) {
+ if (dictionary == null || !Object.hasOwn(dictionary, key)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `Missing required key "${key}".`
+ })
+ }
+ }
+
+ let value = dictionary?.[key]
+ const hasDefault = defaultValue !== undefined
+
+ // Only use defaultValue if value is undefined and
+ // a defaultValue options was provided.
+ if (hasDefault && value === undefined) {
+ value = defaultValue()
+ }
+
+ // A key can be optional and have no default value.
+ // When this happens, do not perform a conversion,
+ // and do not assign the key a value.
+ if (required || hasDefault || value !== undefined) {
+ value = converter(value, prefix, `${argument}.${key}`)
+
+ if (
+ options.allowedValues &&
+ !options.allowedValues.includes(value)
+ ) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.`
+ })
+ }
+
+ dict[key] = value
+ }
+ }
+
+ return dict
+ }
+}
+
+webidl.nullableConverter = function (converter) {
+ return (V, prefix, argument) => {
+ if (V === null) {
+ return V
+ }
+
+ return converter(V, prefix, argument)
+ }
+}
+
+/**
+ * @param {*} value
+ * @returns {boolean}
+ */
+webidl.is.USVString = function (value) {
+ return (
+ typeof value === 'string' &&
+ value.isWellFormed()
+ )
+}
+
+webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
+webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
+webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
+webidl.is.File = webidl.util.MakeTypeAssertion(File)
+webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
+webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
+webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)
+
+webidl.is.BufferSource = function (V) {
+ return types.isArrayBuffer(V) || (
+ ArrayBuffer.isView(V) &&
+ types.isArrayBuffer(V.buffer)
+ )
+}
+
+// https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
+webidl.util.getCopyOfBytesHeldByBufferSource = function (bufferSource) {
+ // 1. Let jsBufferSource be the result of converting bufferSource to a JavaScript value.
+ const jsBufferSource = bufferSource
+
+ // 2. Let jsArrayBuffer be jsBufferSource.
+ let jsArrayBuffer = jsBufferSource
+
+ // 3. Let offset be 0.
+ let offset = 0
+
+ // 4. Let length be 0.
+ let length = 0
+
+ // 5. If jsBufferSource has a [[ViewedArrayBuffer]] internal slot, then:
+ if (types.isTypedArray(jsBufferSource) || types.isDataView(jsBufferSource)) {
+ // 5.1. Set jsArrayBuffer to jsBufferSource.[[ViewedArrayBuffer]].
+ jsArrayBuffer = jsBufferSource.buffer
+
+ // 5.2. Set offset to jsBufferSource.[[ByteOffset]].
+ offset = jsBufferSource.byteOffset
+
+ // 5.3. Set length to jsBufferSource.[[ByteLength]].
+ length = jsBufferSource.byteLength
+ } else {
+ // 6. Otherwise:
+
+ // 6.1. Assert: jsBufferSource is an ArrayBuffer or SharedArrayBuffer object.
+ assert(types.isAnyArrayBuffer(jsBufferSource))
+
+ // 6.2. Set length to jsBufferSource.[[ArrayBufferByteLength]].
+ length = jsBufferSource.byteLength
+ }
+
+ // 7. If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence.
+ if (jsArrayBuffer.detached) {
+ return new Uint8Array(0)
+ }
+
+ // 8. Let bytes be a new byte sequence of length equal to length.
+ const bytes = new Uint8Array(length)
+
+ // 9. For i in the range offset to offset + length − 1, inclusive,
+ // set bytes[i − offset] to GetValueFromBuffer(jsArrayBuffer, i, Uint8, true, Unordered).
+ const view = new Uint8Array(jsArrayBuffer, offset, length)
+ bytes.set(view)
+
+ // 10. Return bytes.
+ return bytes
+}
+
+// https://webidl.spec.whatwg.org/#es-DOMString
+webidl.converters.DOMString = function (V, prefix, argument, flags) {
+ // 1. If V is null and the conversion is to an IDL type
+ // associated with the [LegacyNullToEmptyString]
+ // extended attribute, then return the DOMString value
+ // that represents the empty string.
+ if (V === null && webidl.util.HasFlag(flags, webidl.attributes.LegacyNullToEmptyString)) {
+ return ''
+ }
+
+ // 2. Let x be ? ToString(V).
+ if (typeof V === 'symbol') {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} is a symbol, which cannot be converted to a DOMString.`
+ })
+ }
+
+ // 3. Return the IDL DOMString value that represents the
+ // same sequence of code units as the one the
+ // ECMAScript String value x represents.
+ return String(V)
+}
+
+// https://webidl.spec.whatwg.org/#es-ByteString
+webidl.converters.ByteString = function (V, prefix, argument) {
+ // 1. Let x be ? ToString(V).
+ if (typeof V === 'symbol') {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} is a symbol, which cannot be converted to a ByteString.`
+ })
+ }
+
+ const x = String(V)
+
+ // 2. If the value of any element of x is greater than
+ // 255, then throw a TypeError.
+ for (let index = 0; index < x.length; index++) {
+ if (x.charCodeAt(index) > 255) {
+ throw new TypeError(
+ 'Cannot convert argument to a ByteString because the character at ' +
+ `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.`
+ )
+ }
+ }
+
+ // 3. Return an IDL ByteString value whose length is the
+ // length of x, and where the value of each element is
+ // the value of the corresponding element of x.
+ return x
+}
+
+/**
+ * @param {unknown} value
+ * @returns {string}
+ * @see https://webidl.spec.whatwg.org/#es-USVString
+ */
+webidl.converters.USVString = function (value) {
+ // TODO: rewrite this so we can control the errors thrown
+ if (typeof value === 'string') {
+ return value.toWellFormed()
+ }
+ return `${value}`.toWellFormed()
+}
+
+// https://webidl.spec.whatwg.org/#es-boolean
+webidl.converters.boolean = function (V) {
+ // 1. Let x be the result of computing ToBoolean(V).
+ // https://262.ecma-international.org/10.0/index.html#table-10
+ const x = Boolean(V)
+
+ // 2. Return the IDL boolean value that is the one that represents
+ // the same truth value as the ECMAScript Boolean value x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-any
+webidl.converters.any = function (V) {
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#es-long-long
+webidl.converters['long long'] = function (V, prefix, argument) {
+ // 1. Let x be ? ConvertToInt(V, 64, "signed").
+ const x = webidl.util.ConvertToInt(V, 64, 'signed', 0, prefix, argument)
+
+ // 2. Return the IDL long long value that represents
+ // the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-unsigned-long-long
+webidl.converters['unsigned long long'] = function (V, prefix, argument) {
+ // 1. Let x be ? ConvertToInt(V, 64, "unsigned").
+ const x = webidl.util.ConvertToInt(V, 64, 'unsigned', 0, prefix, argument)
+
+ // 2. Return the IDL unsigned long long value that
+ // represents the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-unsigned-long
+webidl.converters['unsigned long'] = function (V, prefix, argument) {
+ // 1. Let x be ? ConvertToInt(V, 32, "unsigned").
+ const x = webidl.util.ConvertToInt(V, 32, 'unsigned', 0, prefix, argument)
+
+ // 2. Return the IDL unsigned long value that
+ // represents the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#es-unsigned-short
+webidl.converters['unsigned short'] = function (V, prefix, argument, flags) {
+ // 1. Let x be ? ConvertToInt(V, 16, "unsigned").
+ const x = webidl.util.ConvertToInt(V, 16, 'unsigned', flags, prefix, argument)
+
+ // 2. Return the IDL unsigned short value that represents
+ // the same numeric value as x.
+ return x
+}
+
+// https://webidl.spec.whatwg.org/#idl-ArrayBuffer
+webidl.converters.ArrayBuffer = function (V, prefix, argument, flags) {
+ // 1. If V is not an Object, or V does not have an
+ // [[ArrayBufferData]] internal slot, then throw a
+ // TypeError.
+ // 2. If IsSharedArrayBuffer(V) is true, then throw a
+ // TypeError.
+ // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances
+ if (
+ webidl.util.Type(V) !== OBJECT ||
+ !types.isArrayBuffer(V)
+ ) {
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: `${argument} ("${webidl.util.Stringify(V)}")`,
+ types: ['ArrayBuffer']
+ })
+ }
+
+ // 3. If the conversion is not to an IDL type associated
+ // with the [AllowResizable] extended attribute, and
+ // IsResizableArrayBuffer(V) is true, then throw a
+ // TypeError.
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a resizable ArrayBuffer.`
+ })
+ }
+
+ // 4. Return the IDL ArrayBuffer value that is a
+ // reference to the same object as V.
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#idl-SharedArrayBuffer
+webidl.converters.SharedArrayBuffer = function (V, prefix, argument, flags) {
+ // 1. If V is not an Object, or V does not have an
+ // [[ArrayBufferData]] internal slot, then throw a
+ // TypeError.
+ // 2. If IsSharedArrayBuffer(V) is false, then throw a
+ // TypeError.
+ // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances
+ if (
+ webidl.util.Type(V) !== OBJECT ||
+ !types.isSharedArrayBuffer(V)
+ ) {
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: `${argument} ("${webidl.util.Stringify(V)}")`,
+ types: ['SharedArrayBuffer']
+ })
+ }
+
+ // 3. If the conversion is not to an IDL type associated
+ // with the [AllowResizable] extended attribute, and
+ // IsResizableArrayBuffer(V) is true, then throw a
+ // TypeError.
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a resizable SharedArrayBuffer.`
+ })
+ }
+
+ // 4. Return the IDL SharedArrayBuffer value that is a
+ // reference to the same object as V.
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#dfn-typed-array-type
+webidl.converters.TypedArray = function (V, T, prefix, argument, flags) {
+ // 1. Let T be the IDL type V is being converted to.
+
+ // 2. If Type(V) is not Object, or V does not have a
+ // [[TypedArrayName]] internal slot with a value
+ // equal to T’s name, then throw a TypeError.
+ if (
+ webidl.util.Type(V) !== OBJECT ||
+ !types.isTypedArray(V) ||
+ V.constructor.name !== T.name
+ ) {
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: `${argument} ("${webidl.util.Stringify(V)}")`,
+ types: [T.name]
+ })
+ }
+
+ // 3. If the conversion is not to an IDL type associated
+ // with the [AllowShared] extended attribute, and
+ // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is
+ // true, then throw a TypeError.
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a view on a shared array buffer.`
+ })
+ }
+
+ // 4. If the conversion is not to an IDL type associated
+ // with the [AllowResizable] extended attribute, and
+ // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
+ // true, then throw a TypeError.
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a view on a resizable array buffer.`
+ })
+ }
+
+ // 5. Return the IDL value of type T that is a reference
+ // to the same object as V.
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#idl-DataView
+webidl.converters.DataView = function (V, prefix, argument, flags) {
+ // 1. If Type(V) is not Object, or V does not have a
+ // [[DataView]] internal slot, then throw a TypeError.
+ if (webidl.util.Type(V) !== OBJECT || !types.isDataView(V)) {
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: `${argument} ("${webidl.util.Stringify(V)}")`,
+ types: ['DataView']
+ })
+ }
+
+ // 2. If the conversion is not to an IDL type associated
+ // with the [AllowShared] extended attribute, and
+ // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true,
+ // then throw a TypeError.
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a view on a shared array buffer.`
+ })
+ }
+
+ // 3. If the conversion is not to an IDL type associated
+ // with the [AllowResizable] extended attribute, and
+ // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is
+ // true, then throw a TypeError.
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a view on a resizable array buffer.`
+ })
+ }
+
+ // 4. Return the IDL DataView value that is a reference
+ // to the same object as V.
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#ArrayBufferView
+webidl.converters.ArrayBufferView = function (V, prefix, argument, flags) {
+ if (
+ webidl.util.Type(V) !== OBJECT ||
+ !types.isArrayBufferView(V)
+ ) {
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: `${argument} ("${webidl.util.Stringify(V)}")`,
+ types: ['ArrayBufferView']
+ })
+ }
+
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowShared) && types.isSharedArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a view on a shared array buffer.`
+ })
+ }
+
+ if (!webidl.util.HasFlag(flags, webidl.attributes.AllowResizable) && webidl.util.IsResizableArrayBuffer(V.buffer)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a view on a resizable array buffer.`
+ })
+ }
+
+ return V
+}
+
+// https://webidl.spec.whatwg.org/#BufferSource
+webidl.converters.BufferSource = function (V, prefix, argument, flags) {
+ if (types.isArrayBuffer(V)) {
+ return webidl.converters.ArrayBuffer(V, prefix, argument, flags)
+ }
+
+ if (types.isArrayBufferView(V)) {
+ flags &= ~webidl.attributes.AllowShared
+
+ return webidl.converters.ArrayBufferView(V, prefix, argument, flags)
+ }
+
+ // Make this explicit for easier debugging
+ if (types.isSharedArrayBuffer(V)) {
+ throw webidl.errors.exception({
+ header: prefix,
+ message: `${argument} cannot be a SharedArrayBuffer.`
+ })
+ }
+
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: `${argument} ("${webidl.util.Stringify(V)}")`,
+ types: ['ArrayBuffer', 'ArrayBufferView']
+ })
+}
+
+// https://webidl.spec.whatwg.org/#AllowSharedBufferSource
+webidl.converters.AllowSharedBufferSource = function (V, prefix, argument, flags) {
+ if (types.isArrayBuffer(V)) {
+ return webidl.converters.ArrayBuffer(V, prefix, argument, flags)
+ }
+
+ if (types.isSharedArrayBuffer(V)) {
+ return webidl.converters.SharedArrayBuffer(V, prefix, argument, flags)
+ }
+
+ if (types.isArrayBufferView(V)) {
+ flags |= webidl.attributes.AllowShared
+ return webidl.converters.ArrayBufferView(V, prefix, argument, flags)
+ }
+
+ throw webidl.errors.conversionFailed({
+ prefix,
+ argument: `${argument} ("${webidl.util.Stringify(V)}")`,
+ types: ['ArrayBuffer', 'SharedArrayBuffer', 'ArrayBufferView']
+ })
+}
+
+webidl.converters['sequence<ByteString>'] = webidl.sequenceConverter(
+ webidl.converters.ByteString
+)
+
+webidl.converters['sequence<sequence<ByteString>>'] = webidl.sequenceConverter(
+ webidl.converters['sequence<ByteString>']
+)
+
+webidl.converters['record<ByteString, ByteString>'] = webidl.recordConverter(
+ webidl.converters.ByteString,
+ webidl.converters.ByteString
+)
+
+webidl.converters.Blob = webidl.interfaceConverter(webidl.is.Blob, 'Blob')
+
+webidl.converters.AbortSignal = webidl.interfaceConverter(
+ webidl.is.AbortSignal,
+ 'AbortSignal'
+)
+
+/**
+ * [LegacyTreatNonObjectAsNull]
+ * callback EventHandlerNonNull = any (Event event);
+ * typedef EventHandlerNonNull? EventHandler;
+ * @param {*} V
+ */
+webidl.converters.EventHandlerNonNull = function (V) {
+ if (webidl.util.Type(V) !== OBJECT) {
+ return null
+ }
+
+ // [I]f the value is not an object, it will be converted to null, and if the value is not callable,
+ // it will be converted to a callback function value that does nothing when called.
+ if (typeof V === 'function') {
+ return V
+ }
+
+ return () => {}
+}
+
+webidl.attributes = {
+ Clamp: 1 << 0,
+ EnforceRange: 1 << 1,
+ AllowShared: 1 << 2,
+ AllowResizable: 1 << 3,
+ LegacyNullToEmptyString: 1 << 4
+}
+
+module.exports = {
+ webidl
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/connection.js b/vanilla/node_modules/undici/lib/web/websocket/connection.js
new file mode 100644
index 0000000..4ecc8a1
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/connection.js
@@ -0,0 +1,329 @@
+'use strict'
+
+const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
+const { parseExtensions, isClosed, isClosing, isEstablished, isConnecting, validateCloseCodeAndReason } = require('./util')
+const { makeRequest } = require('../fetch/request')
+const { fetching } = require('../fetch/index')
+const { Headers, getHeadersList } = require('../fetch/headers')
+const { getDecodeSplit } = require('../fetch/util')
+const { WebsocketFrameSend } = require('./frame')
+const assert = require('node:assert')
+const { runtimeFeatures } = require('../../util/runtime-features')
+
+const crypto = runtimeFeatures.has('crypto')
+ ? require('node:crypto')
+ : null
+
+let warningEmitted = false
+
+/**
+ * @see https://websockets.spec.whatwg.org/#concept-websocket-establish
+ * @param {URL} url
+ * @param {string|string[]} protocols
+ * @param {import('./websocket').Handler} handler
+ * @param {Partial<import('../../../types/websocket').WebSocketInit>} options
+ */
+function establishWebSocketConnection (url, protocols, client, handler, options) {
+ // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
+ // scheme is "ws", and to "https" otherwise.
+ const requestURL = url
+
+ requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
+
+ // 2. Let request be a new request, whose URL is requestURL, client is client,
+ // service-workers mode is "none", referrer is "no-referrer", mode is
+ // "websocket", credentials mode is "include", cache mode is "no-store" ,
+ // redirect mode is "error", and use-URL-credentials flag is set.
+ const request = makeRequest({
+ urlList: [requestURL],
+ client,
+ serviceWorkers: 'none',
+ referrer: 'no-referrer',
+ mode: 'websocket',
+ credentials: 'include',
+ cache: 'no-store',
+ redirect: 'error',
+ useURLCredentials: true
+ })
+
+ // Note: undici extension, allow setting custom headers.
+ if (options.headers) {
+ const headersList = getHeadersList(new Headers(options.headers))
+
+ request.headersList = headersList
+ }
+
+ // 3. Append (`Upgrade`, `websocket`) to request’s header list.
+ // 4. Append (`Connection`, `Upgrade`) to request’s header list.
+ // Note: both of these are handled by undici currently.
+ // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397
+
+ // 5. Let keyValue be a nonce consisting of a randomly selected
+ // 16-byte value that has been forgiving-base64-encoded and
+ // isomorphic encoded.
+ const keyValue = crypto.randomBytes(16).toString('base64')
+
+ // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s
+ // header list.
+ request.headersList.append('sec-websocket-key', keyValue, true)
+
+ // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s
+ // header list.
+ request.headersList.append('sec-websocket-version', '13', true)
+
+ // 8. For each protocol in protocols, combine
+ // (`Sec-WebSocket-Protocol`, protocol) in request’s header
+ // list.
+ for (const protocol of protocols) {
+ request.headersList.append('sec-websocket-protocol', protocol, true)
+ }
+
+ // 9. Let permessageDeflate be a user-agent defined
+ // "permessage-deflate" extension header value.
+ // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673
+ const permessageDeflate = 'permessage-deflate; client_max_window_bits'
+
+ // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to
+ // request’s header list.
+ request.headersList.append('sec-websocket-extensions', permessageDeflate, true)
+
+ // 11. Fetch request with useParallelQueue set to true, and
+ // processResponse given response being these steps:
+ const controller = fetching({
+ request,
+ useParallelQueue: true,
+ dispatcher: options.dispatcher,
+ processResponse (response) {
+ // 1. If response is a network error or its status is not 101,
+ // fail the WebSocket connection.
+ // if (response.type === 'error' || ((response.socket?.session != null && response.status !== 200) && response.status !== 101)) {
+ if (response.type === 'error' || response.status !== 101) {
+ // The presence of a session property on the socket indicates HTTP2
+ // HTTP1
+ if (response.socket?.session == null) {
+ failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error)
+ return
+ }
+
+ // HTTP2
+ if (response.status !== 200) {
+ failWebsocketConnection(handler, 1002, 'Received network error or non-200 status code.', response.error)
+ return
+ }
+ }
+
+ if (warningEmitted === false && response.socket?.session != null) {
+ process.emitWarning('WebSocket over HTTP2 is experimental, and subject to change.', 'ExperimentalWarning')
+ warningEmitted = true
+ }
+
+ // 2. If protocols is not the empty list and extracting header
+ // list values given `Sec-WebSocket-Protocol` and response’s
+ // header list results in null, failure, or the empty byte
+ // sequence, then fail the WebSocket connection.
+ if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
+ failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
+ return
+ }
+
+ // 3. Follow the requirements stated step 2 to step 6, inclusive,
+ // of the last set of steps in section 4.1 of The WebSocket
+ // Protocol to validate response. This either results in fail
+ // the WebSocket connection or the WebSocket connection is
+ // established.
+
+ // 2. If the response lacks an |Upgrade| header field or the |Upgrade|
+ // header field contains a value that is not an ASCII case-
+ // insensitive match for the value "websocket", the client MUST
+ // _Fail the WebSocket Connection_.
+ // For H2, no upgrade header is expected.
+ if (response.socket.session == null && response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
+ failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
+ return
+ }
+
+ // 3. If the response lacks a |Connection| header field or the
+ // |Connection| header field doesn't contain a token that is an
+ // ASCII case-insensitive match for the value "Upgrade", the client
+ // MUST _Fail the WebSocket Connection_.
+ // For H2, no connection header is expected.
+ if (response.socket.session == null && response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
+ failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
+ return
+ }
+
+ // 4. If the response lacks a |Sec-WebSocket-Accept| header field or
+ // the |Sec-WebSocket-Accept| contains a value other than the
+ // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket-
+ // Key| (as a string, not base64-decoded) with the string "258EAFA5-
+ // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
+ // trailing whitespace, the client MUST _Fail the WebSocket
+ // Connection_.
+ const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
+ const digest = crypto.hash('sha1', keyValue + uid, 'base64')
+ if (secWSAccept !== digest) {
+ failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
+ return
+ }
+
+ // 5. If the response includes a |Sec-WebSocket-Extensions| header
+ // field and this header field indicates the use of an extension
+ // that was not present in the client's handshake (the server has
+ // indicated an extension not requested by the client), the client
+ // MUST _Fail the WebSocket Connection_. (The parsing of this
+ // header field to determine which extensions are requested is
+ // discussed in Section 9.1.)
+ const secExtension = response.headersList.get('Sec-WebSocket-Extensions')
+ let extensions
+
+ if (secExtension !== null) {
+ extensions = parseExtensions(secExtension)
+
+ if (!extensions.has('permessage-deflate')) {
+ failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
+ return
+ }
+ }
+
+ // 6. If the response includes a |Sec-WebSocket-Protocol| header field
+ // and this header field indicates the use of a subprotocol that was
+ // not present in the client's handshake (the server has indicated a
+ // subprotocol not requested by the client), the client MUST _Fail
+ // the WebSocket Connection_.
+ const secProtocol = response.headersList.get('Sec-WebSocket-Protocol')
+
+ if (secProtocol !== null) {
+ const requestProtocols = getDecodeSplit('sec-websocket-protocol', request.headersList)
+
+ // The client can request that the server use a specific subprotocol by
+ // including the |Sec-WebSocket-Protocol| field in its handshake. If it
+ // is specified, the server needs to include the same field and one of
+ // the selected subprotocol values in its response for the connection to
+ // be established.
+ if (!requestProtocols.includes(secProtocol)) {
+ failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
+ return
+ }
+ }
+
+ response.socket.on('data', handler.onSocketData)
+ response.socket.on('close', handler.onSocketClose)
+ response.socket.on('error', handler.onSocketError)
+
+ handler.wasEverConnected = true
+ handler.onConnectionEstablished(response, extensions)
+ }
+ })
+
+ return controller
+}
+
+/**
+ * @see https://whatpr.org/websockets/48.html#close-the-websocket
+ * @param {import('./websocket').Handler} object
+ * @param {number} [code=null]
+ * @param {string} [reason='']
+ */
+function closeWebSocketConnection (object, code, reason, validate = false) {
+ // 1. If code was not supplied, let code be null.
+ code ??= null
+
+ // 2. If reason was not supplied, let reason be the empty string.
+ reason ??= ''
+
+ // 3. Validate close code and reason with code and reason.
+ if (validate) validateCloseCodeAndReason(code, reason)
+
+ // 4. Run the first matching steps from the following list:
+ // - If object’s ready state is CLOSING (2) or CLOSED (3)
+ // - If the WebSocket connection is not yet established [WSP]
+ // - If the WebSocket closing handshake has not yet been started [WSP]
+ // - Otherwise
+ if (isClosed(object.readyState) || isClosing(object.readyState)) {
+ // Do nothing.
+ } else if (!isEstablished(object.readyState)) {
+ // Fail the WebSocket connection and set object’s ready state to CLOSING (2). [WSP]
+ failWebsocketConnection(object)
+ object.readyState = states.CLOSING
+ } else if (!object.closeState.has(sentCloseFrameState.SENT) && !object.closeState.has(sentCloseFrameState.RECEIVED)) {
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+
+ const frame = new WebsocketFrameSend()
+
+ // If neither code nor reason is present, the WebSocket Close
+ // message must not have a body.
+
+ // If code is present, then the status code to use in the
+ // WebSocket Close message must be the integer given by code.
+ // If code is null and reason is the empty string, the WebSocket Close frame must not have a body.
+ // If reason is non-empty but code is null, then set code to 1000 ("Normal Closure").
+ if (reason.length !== 0 && code === null) {
+ code = 1000
+ }
+
+ // If code is set, then the status code to use in the WebSocket Close frame must be the integer given by code.
+ assert(code === null || Number.isInteger(code))
+
+ if (code === null && reason.length === 0) {
+ frame.frameData = emptyBuffer
+ } else if (code !== null && reason === null) {
+ frame.frameData = Buffer.allocUnsafe(2)
+ frame.frameData.writeUInt16BE(code, 0)
+ } else if (code !== null && reason !== null) {
+ // If reason is also present, then reasonBytes must be
+ // provided in the Close message after the status code.
+ frame.frameData = Buffer.allocUnsafe(2 + Buffer.byteLength(reason))
+ frame.frameData.writeUInt16BE(code, 0)
+ // the body MAY contain UTF-8-encoded data with value /reason/
+ frame.frameData.write(reason, 2, 'utf-8')
+ } else {
+ frame.frameData = emptyBuffer
+ }
+
+ object.socket.write(frame.createFrame(opcodes.CLOSE))
+
+ object.closeState.add(sentCloseFrameState.SENT)
+
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ object.readyState = states.CLOSING
+ } else {
+ // Set object’s ready state to CLOSING (2).
+ object.readyState = states.CLOSING
+ }
+}
+
+/**
+ * @param {import('./websocket').Handler} handler
+ * @param {number} code
+ * @param {string|undefined} reason
+ * @param {unknown} cause
+ * @returns {void}
+ */
+function failWebsocketConnection (handler, code, reason, cause) {
+ // If _The WebSocket Connection is Established_ prior to the point where
+ // the endpoint is required to _Fail the WebSocket Connection_, the
+ // endpoint SHOULD send a Close frame with an appropriate status code
+ // (Section 7.4) before proceeding to _Close the WebSocket Connection_.
+ if (isEstablished(handler.readyState)) {
+ closeWebSocketConnection(handler, code, reason, false)
+ }
+
+ handler.controller.abort()
+
+ if (isConnecting(handler.readyState)) {
+ // If the connection was not established, we must still emit an 'error' and 'close' events
+ handler.onSocketClose()
+ } else if (handler.socket?.destroyed === false) {
+ handler.socket.destroy()
+ }
+}
+
+module.exports = {
+ establishWebSocketConnection,
+ failWebsocketConnection,
+ closeWebSocketConnection
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/constants.js b/vanilla/node_modules/undici/lib/web/websocket/constants.js
new file mode 100644
index 0000000..e4e6990
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/constants.js
@@ -0,0 +1,126 @@
+'use strict'
+
+/**
+ * This is a Globally Unique Identifier unique used to validate that the
+ * endpoint accepts websocket connections.
+ * @see https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3
+ * @type {'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'}
+ */
+const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
+
+/**
+ * @type {PropertyDescriptor}
+ */
+const staticPropertyDescriptors = {
+ enumerable: true,
+ writable: false,
+ configurable: false
+}
+
+/**
+ * The states of the WebSocket connection.
+ *
+ * @readonly
+ * @enum
+ * @property {0} CONNECTING
+ * @property {1} OPEN
+ * @property {2} CLOSING
+ * @property {3} CLOSED
+ */
+const states = {
+ CONNECTING: 0,
+ OPEN: 1,
+ CLOSING: 2,
+ CLOSED: 3
+}
+
+/**
+ * @readonly
+ * @enum
+ * @property {0} NOT_SENT
+ * @property {1} PROCESSING
+ * @property {2} SENT
+ */
+const sentCloseFrameState = {
+ SENT: 1,
+ RECEIVED: 2
+}
+
+/**
+ * The WebSocket opcodes.
+ *
+ * @readonly
+ * @enum
+ * @property {0x0} CONTINUATION
+ * @property {0x1} TEXT
+ * @property {0x2} BINARY
+ * @property {0x8} CLOSE
+ * @property {0x9} PING
+ * @property {0xA} PONG
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
+ */
+const opcodes = {
+ CONTINUATION: 0x0,
+ TEXT: 0x1,
+ BINARY: 0x2,
+ CLOSE: 0x8,
+ PING: 0x9,
+ PONG: 0xA
+}
+
+/**
+ * The maximum value for an unsigned 16-bit integer.
+ *
+ * @type {65535} 2 ** 16 - 1
+ */
+const maxUnsigned16Bit = 65535
+
+/**
+ * The states of the parser.
+ *
+ * @readonly
+ * @enum
+ * @property {0} INFO
+ * @property {2} PAYLOADLENGTH_16
+ * @property {3} PAYLOADLENGTH_64
+ * @property {4} READ_DATA
+ */
+const parserStates = {
+ INFO: 0,
+ PAYLOADLENGTH_16: 2,
+ PAYLOADLENGTH_64: 3,
+ READ_DATA: 4
+}
+
+/**
+ * An empty buffer.
+ *
+ * @type {Buffer}
+ */
+const emptyBuffer = Buffer.allocUnsafe(0)
+
+/**
+ * @readonly
+ * @property {1} text
+ * @property {2} typedArray
+ * @property {3} arrayBuffer
+ * @property {4} blob
+ */
+const sendHints = {
+ text: 1,
+ typedArray: 2,
+ arrayBuffer: 3,
+ blob: 4
+}
+
+module.exports = {
+ uid,
+ sentCloseFrameState,
+ staticPropertyDescriptors,
+ states,
+ opcodes,
+ maxUnsigned16Bit,
+ parserStates,
+ emptyBuffer,
+ sendHints
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/events.js b/vanilla/node_modules/undici/lib/web/websocket/events.js
new file mode 100644
index 0000000..7ac9566
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/events.js
@@ -0,0 +1,331 @@
+'use strict'
+
+const { webidl } = require('../webidl')
+const { kEnumerableProperty } = require('../../core/util')
+const { kConstruct } = require('../../core/symbols')
+
+/**
+ * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent
+ */
+class MessageEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict = {}) {
+ if (type === kConstruct) {
+ super(arguments[1], arguments[2])
+ webidl.util.markAsUncloneable(this)
+ return
+ }
+
+ const prefix = 'MessageEvent constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ type = webidl.converters.DOMString(type, prefix, 'type')
+ eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, 'eventInitDict')
+
+ super(type, eventInitDict)
+
+ this.#eventInit = eventInitDict
+ webidl.util.markAsUncloneable(this)
+ }
+
+ get data () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.data
+ }
+
+ get origin () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.origin
+ }
+
+ get lastEventId () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.lastEventId
+ }
+
+ get source () {
+ webidl.brandCheck(this, MessageEvent)
+
+ return this.#eventInit.source
+ }
+
+ get ports () {
+ webidl.brandCheck(this, MessageEvent)
+
+ if (!Object.isFrozen(this.#eventInit.ports)) {
+ Object.freeze(this.#eventInit.ports)
+ }
+
+ return this.#eventInit.ports
+ }
+
+ initMessageEvent (
+ type,
+ bubbles = false,
+ cancelable = false,
+ data = null,
+ origin = '',
+ lastEventId = '',
+ source = null,
+ ports = []
+ ) {
+ webidl.brandCheck(this, MessageEvent)
+
+ webidl.argumentLengthCheck(arguments, 1, 'MessageEvent.initMessageEvent')
+
+ return new MessageEvent(type, {
+ bubbles, cancelable, data, origin, lastEventId, source, ports
+ })
+ }
+
+ static createFastMessageEvent (type, init) {
+ const messageEvent = new MessageEvent(kConstruct, type, init)
+ messageEvent.#eventInit = init
+ messageEvent.#eventInit.data ??= null
+ messageEvent.#eventInit.origin ??= ''
+ messageEvent.#eventInit.lastEventId ??= ''
+ messageEvent.#eventInit.source ??= null
+ messageEvent.#eventInit.ports ??= []
+ return messageEvent
+ }
+}
+
+const { createFastMessageEvent } = MessageEvent
+delete MessageEvent.createFastMessageEvent
+
+/**
+ * @see https://websockets.spec.whatwg.org/#the-closeevent-interface
+ */
+class CloseEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict = {}) {
+ const prefix = 'CloseEvent constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ type = webidl.converters.DOMString(type, prefix, 'type')
+ eventInitDict = webidl.converters.CloseEventInit(eventInitDict)
+
+ super(type, eventInitDict)
+
+ this.#eventInit = eventInitDict
+ webidl.util.markAsUncloneable(this)
+ }
+
+ get wasClean () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.wasClean
+ }
+
+ get code () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.code
+ }
+
+ get reason () {
+ webidl.brandCheck(this, CloseEvent)
+
+ return this.#eventInit.reason
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface
+class ErrorEvent extends Event {
+ #eventInit
+
+ constructor (type, eventInitDict) {
+ const prefix = 'ErrorEvent constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ super(type, eventInitDict)
+ webidl.util.markAsUncloneable(this)
+
+ type = webidl.converters.DOMString(type, prefix, 'type')
+ eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {})
+
+ this.#eventInit = eventInitDict
+ }
+
+ get message () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.message
+ }
+
+ get filename () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.filename
+ }
+
+ get lineno () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.lineno
+ }
+
+ get colno () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.colno
+ }
+
+ get error () {
+ webidl.brandCheck(this, ErrorEvent)
+
+ return this.#eventInit.error
+ }
+}
+
+Object.defineProperties(MessageEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'MessageEvent',
+ configurable: true
+ },
+ data: kEnumerableProperty,
+ origin: kEnumerableProperty,
+ lastEventId: kEnumerableProperty,
+ source: kEnumerableProperty,
+ ports: kEnumerableProperty,
+ initMessageEvent: kEnumerableProperty
+})
+
+Object.defineProperties(CloseEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'CloseEvent',
+ configurable: true
+ },
+ reason: kEnumerableProperty,
+ code: kEnumerableProperty,
+ wasClean: kEnumerableProperty
+})
+
+Object.defineProperties(ErrorEvent.prototype, {
+ [Symbol.toStringTag]: {
+ value: 'ErrorEvent',
+ configurable: true
+ },
+ message: kEnumerableProperty,
+ filename: kEnumerableProperty,
+ lineno: kEnumerableProperty,
+ colno: kEnumerableProperty,
+ error: kEnumerableProperty
+})
+
+webidl.converters.MessagePort = webidl.interfaceConverter(
+ webidl.is.MessagePort,
+ 'MessagePort'
+)
+
+webidl.converters['sequence<MessagePort>'] = webidl.sequenceConverter(
+ webidl.converters.MessagePort
+)
+
+const eventInit = [
+ {
+ key: 'bubbles',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'cancelable',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'composed',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ }
+]
+
+webidl.converters.MessageEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'data',
+ converter: webidl.converters.any,
+ defaultValue: () => null
+ },
+ {
+ key: 'origin',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'lastEventId',
+ converter: webidl.converters.DOMString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'source',
+ // Node doesn't implement WindowProxy or ServiceWorker, so the only
+ // valid value for source is a MessagePort.
+ converter: webidl.nullableConverter(webidl.converters.MessagePort),
+ defaultValue: () => null
+ },
+ {
+ key: 'ports',
+ converter: webidl.converters['sequence<MessagePort>'],
+ defaultValue: () => []
+ }
+])
+
+webidl.converters.CloseEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'wasClean',
+ converter: webidl.converters.boolean,
+ defaultValue: () => false
+ },
+ {
+ key: 'code',
+ converter: webidl.converters['unsigned short'],
+ defaultValue: () => 0
+ },
+ {
+ key: 'reason',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ }
+])
+
+webidl.converters.ErrorEventInit = webidl.dictionaryConverter([
+ ...eventInit,
+ {
+ key: 'message',
+ converter: webidl.converters.DOMString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'filename',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ },
+ {
+ key: 'lineno',
+ converter: webidl.converters['unsigned long'],
+ defaultValue: () => 0
+ },
+ {
+ key: 'colno',
+ converter: webidl.converters['unsigned long'],
+ defaultValue: () => 0
+ },
+ {
+ key: 'error',
+ converter: webidl.converters.any
+ }
+])
+
+module.exports = {
+ MessageEvent,
+ CloseEvent,
+ ErrorEvent,
+ createFastMessageEvent
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/frame.js b/vanilla/node_modules/undici/lib/web/websocket/frame.js
new file mode 100644
index 0000000..e397c87
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/frame.js
@@ -0,0 +1,133 @@
+'use strict'
+
+const { runtimeFeatures } = require('../../util/runtime-features')
+const { maxUnsigned16Bit, opcodes } = require('./constants')
+
+const BUFFER_SIZE = 8 * 1024
+
+let buffer = null
+let bufIdx = BUFFER_SIZE
+
+const randomFillSync = runtimeFeatures.has('crypto')
+ ? require('node:crypto').randomFillSync
+ // not full compatibility, but minimum.
+ : function randomFillSync (buffer, _offset, _size) {
+ for (let i = 0; i < buffer.length; ++i) {
+ buffer[i] = Math.random() * 255 | 0
+ }
+ return buffer
+ }
+
+function generateMask () {
+ if (bufIdx === BUFFER_SIZE) {
+ bufIdx = 0
+ randomFillSync((buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)), 0, BUFFER_SIZE)
+ }
+ return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]
+}
+
+class WebsocketFrameSend {
+ /**
+ * @param {Buffer|undefined} data
+ */
+ constructor (data) {
+ this.frameData = data
+ }
+
+ createFrame (opcode) {
+ const frameData = this.frameData
+ const maskKey = generateMask()
+ const bodyLength = frameData?.byteLength ?? 0
+
+ /** @type {number} */
+ let payloadLength = bodyLength // 0-125
+ let offset = 6
+
+ if (bodyLength > maxUnsigned16Bit) {
+ offset += 8 // payload length is next 8 bytes
+ payloadLength = 127
+ } else if (bodyLength > 125) {
+ offset += 2 // payload length is next 2 bytes
+ payloadLength = 126
+ }
+
+ const buffer = Buffer.allocUnsafe(bodyLength + offset)
+
+ // Clear first 2 bytes, everything else is overwritten
+ buffer[0] = buffer[1] = 0
+ buffer[0] |= 0x80 // FIN
+ buffer[0] = (buffer[0] & 0xF0) + opcode // opcode
+
+ /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
+ buffer[offset - 4] = maskKey[0]
+ buffer[offset - 3] = maskKey[1]
+ buffer[offset - 2] = maskKey[2]
+ buffer[offset - 1] = maskKey[3]
+
+ buffer[1] = payloadLength
+
+ if (payloadLength === 126) {
+ buffer.writeUInt16BE(bodyLength, 2)
+ } else if (payloadLength === 127) {
+ // Clear extended payload length
+ buffer[2] = buffer[3] = 0
+ buffer.writeUIntBE(bodyLength, 4, 6)
+ }
+
+ buffer[1] |= 0x80 // MASK
+
+ // mask body
+ for (let i = 0; i < bodyLength; ++i) {
+ buffer[offset + i] = frameData[i] ^ maskKey[i & 3]
+ }
+
+ return buffer
+ }
+
+ /**
+ * @param {Uint8Array} buffer
+ */
+ static createFastTextFrame (buffer) {
+ const maskKey = generateMask()
+
+ const bodyLength = buffer.length
+
+ // mask body
+ for (let i = 0; i < bodyLength; ++i) {
+ buffer[i] ^= maskKey[i & 3]
+ }
+
+ let payloadLength = bodyLength
+ let offset = 6
+
+ if (bodyLength > maxUnsigned16Bit) {
+ offset += 8 // payload length is next 8 bytes
+ payloadLength = 127
+ } else if (bodyLength > 125) {
+ offset += 2 // payload length is next 2 bytes
+ payloadLength = 126
+ }
+ const head = Buffer.allocUnsafeSlow(offset)
+
+ head[0] = 0x80 /* FIN */ | opcodes.TEXT /* opcode TEXT */
+ head[1] = payloadLength | 0x80 /* MASK */
+ head[offset - 4] = maskKey[0]
+ head[offset - 3] = maskKey[1]
+ head[offset - 2] = maskKey[2]
+ head[offset - 1] = maskKey[3]
+
+ if (payloadLength === 126) {
+ head.writeUInt16BE(bodyLength, 2)
+ } else if (payloadLength === 127) {
+ head[2] = head[3] = 0
+ head.writeUIntBE(bodyLength, 4, 6)
+ }
+
+ return [head, buffer]
+ }
+}
+
+module.exports = {
+ WebsocketFrameSend,
+ generateMask // for benchmark
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js b/vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js
new file mode 100644
index 0000000..76cb366
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/permessage-deflate.js
@@ -0,0 +1,70 @@
+'use strict'
+
+const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
+const { isValidClientWindowBits } = require('./util')
+
+const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
+const kBuffer = Symbol('kBuffer')
+const kLength = Symbol('kLength')
+
+class PerMessageDeflate {
+ /** @type {import('node:zlib').InflateRaw} */
+ #inflate
+
+ #options = {}
+
+ constructor (extensions) {
+ this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
+ this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
+ }
+
+ decompress (chunk, fin, callback) {
+ // An endpoint uses the following algorithm to decompress a message.
+ // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
+ // payload of the message.
+ // 2. Decompress the resulting data using DEFLATE.
+
+ if (!this.#inflate) {
+ let windowBits = Z_DEFAULT_WINDOWBITS
+
+ if (this.#options.serverMaxWindowBits) { // empty values default to Z_DEFAULT_WINDOWBITS
+ if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) {
+ callback(new Error('Invalid server_max_window_bits'))
+ return
+ }
+
+ windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
+ }
+
+ this.#inflate = createInflateRaw({ windowBits })
+ this.#inflate[kBuffer] = []
+ this.#inflate[kLength] = 0
+
+ this.#inflate.on('data', (data) => {
+ this.#inflate[kBuffer].push(data)
+ this.#inflate[kLength] += data.length
+ })
+
+ this.#inflate.on('error', (err) => {
+ this.#inflate = null
+ callback(err)
+ })
+ }
+
+ this.#inflate.write(chunk)
+ if (fin) {
+ this.#inflate.write(tail)
+ }
+
+ this.#inflate.flush(() => {
+ const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
+
+ this.#inflate[kBuffer].length = 0
+ this.#inflate[kLength] = 0
+
+ callback(null, full)
+ })
+ }
+}
+
+module.exports = { PerMessageDeflate }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/receiver.js b/vanilla/node_modules/undici/lib/web/websocket/receiver.js
new file mode 100644
index 0000000..ba0a5aa
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/receiver.js
@@ -0,0 +1,444 @@
+'use strict'
+
+const { Writable } = require('node:stream')
+const assert = require('node:assert')
+const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
+const {
+ isValidStatusCode,
+ isValidOpcode,
+ websocketMessageReceived,
+ utf8Decode,
+ isControlFrame,
+ isTextBinaryFrame,
+ isContinuationFrame
+} = require('./util')
+const { failWebsocketConnection } = require('./connection')
+const { WebsocketFrameSend } = require('./frame')
+const { PerMessageDeflate } = require('./permessage-deflate')
+
+// This code was influenced by ws released under the MIT license.
+// Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+// Copyright (c) 2013 Arnout Kazemier and contributors
+// Copyright (c) 2016 Luigi Pinca and contributors
+
+class ByteParser extends Writable {
+ #buffers = []
+ #fragmentsBytes = 0
+ #byteOffset = 0
+ #loop = false
+
+ #state = parserStates.INFO
+
+ #info = {}
+ #fragments = []
+
+ /** @type {Map<string, PerMessageDeflate>} */
+ #extensions
+
+ /** @type {import('./websocket').Handler} */
+ #handler
+
+ constructor (handler, extensions) {
+ super()
+
+ this.#handler = handler
+ this.#extensions = extensions == null ? new Map() : extensions
+
+ if (this.#extensions.has('permessage-deflate')) {
+ this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
+ }
+ }
+
+ /**
+ * @param {Buffer} chunk
+ * @param {() => void} callback
+ */
+ _write (chunk, _, callback) {
+ this.#buffers.push(chunk)
+ this.#byteOffset += chunk.length
+ this.#loop = true
+
+ this.run(callback)
+ }
+
+ /**
+ * Runs whenever a new chunk is received.
+ * Callback is called whenever there are no more chunks buffering,
+ * or not enough bytes are buffered to parse.
+ */
+ run (callback) {
+ while (this.#loop) {
+ if (this.#state === parserStates.INFO) {
+ // If there aren't enough bytes to parse the payload length, etc.
+ if (this.#byteOffset < 2) {
+ return callback()
+ }
+
+ const buffer = this.consume(2)
+ const fin = (buffer[0] & 0x80) !== 0
+ const opcode = buffer[0] & 0x0F
+ const masked = (buffer[1] & 0x80) === 0x80
+
+ const fragmented = !fin && opcode !== opcodes.CONTINUATION
+ const payloadLength = buffer[1] & 0x7F
+
+ const rsv1 = buffer[0] & 0x40
+ const rsv2 = buffer[0] & 0x20
+ const rsv3 = buffer[0] & 0x10
+
+ if (!isValidOpcode(opcode)) {
+ failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received')
+ return callback()
+ }
+
+ if (masked) {
+ failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked')
+ return callback()
+ }
+
+ // MUST be 0 unless an extension is negotiated that defines meanings
+ // for non-zero values. If a nonzero value is received and none of
+ // the negotiated extensions defines the meaning of such a nonzero
+ // value, the receiving endpoint MUST _Fail the WebSocket
+ // Connection_.
+ // This document allocates the RSV1 bit of the WebSocket header for
+ // PMCEs and calls the bit the "Per-Message Compressed" bit. On a
+ // WebSocket connection where a PMCE is in use, this bit indicates
+ // whether a message is compressed or not.
+ if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) {
+ failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.')
+ return
+ }
+
+ if (rsv2 !== 0 || rsv3 !== 0) {
+ failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear')
+ return
+ }
+
+ if (fragmented && !isTextBinaryFrame(opcode)) {
+ // Only text and binary frames can be fragmented
+ failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.')
+ return
+ }
+
+ // If we are already parsing a text/binary frame and do not receive either
+ // a continuation frame or close frame, fail the connection.
+ if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
+ failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame')
+ return
+ }
+
+ if (this.#info.fragmented && fragmented) {
+ // A fragmented frame can't be fragmented itself
+ failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.')
+ return
+ }
+
+ // "All control frames MUST have a payload length of 125 bytes or less
+ // and MUST NOT be fragmented."
+ if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
+ failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented')
+ return
+ }
+
+ if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) {
+ failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame')
+ return
+ }
+
+ if (payloadLength <= 125) {
+ this.#info.payloadLength = payloadLength
+ this.#state = parserStates.READ_DATA
+ } else if (payloadLength === 126) {
+ this.#state = parserStates.PAYLOADLENGTH_16
+ } else if (payloadLength === 127) {
+ this.#state = parserStates.PAYLOADLENGTH_64
+ }
+
+ if (isTextBinaryFrame(opcode)) {
+ this.#info.binaryType = opcode
+ this.#info.compressed = rsv1 !== 0
+ }
+
+ this.#info.opcode = opcode
+ this.#info.masked = masked
+ this.#info.fin = fin
+ this.#info.fragmented = fragmented
+ } else if (this.#state === parserStates.PAYLOADLENGTH_16) {
+ if (this.#byteOffset < 2) {
+ return callback()
+ }
+
+ const buffer = this.consume(2)
+
+ this.#info.payloadLength = buffer.readUInt16BE(0)
+ this.#state = parserStates.READ_DATA
+ } else if (this.#state === parserStates.PAYLOADLENGTH_64) {
+ if (this.#byteOffset < 8) {
+ return callback()
+ }
+
+ const buffer = this.consume(8)
+ const upper = buffer.readUInt32BE(0)
+
+ // 2^31 is the maximum bytes an arraybuffer can contain
+ // on 32-bit systems. Although, on 64-bit systems, this is
+ // 2^53-1 bytes.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
+ // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
+ // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
+ if (upper > 2 ** 31 - 1) {
+ failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.')
+ return
+ }
+
+ const lower = buffer.readUInt32BE(4)
+
+ this.#info.payloadLength = (upper << 8) + lower
+ this.#state = parserStates.READ_DATA
+ } else if (this.#state === parserStates.READ_DATA) {
+ if (this.#byteOffset < this.#info.payloadLength) {
+ return callback()
+ }
+
+ const body = this.consume(this.#info.payloadLength)
+
+ if (isControlFrame(this.#info.opcode)) {
+ this.#loop = this.parseControlFrame(body)
+ this.#state = parserStates.INFO
+ } else {
+ if (!this.#info.compressed) {
+ this.writeFragments(body)
+
+ // If the frame is not fragmented, a message has been received.
+ // If the frame is fragmented, it will terminate with a fin bit set
+ // and an opcode of 0 (continuation), therefore we handle that when
+ // parsing continuation frames, not here.
+ if (!this.#info.fragmented && this.#info.fin) {
+ websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
+ }
+
+ this.#state = parserStates.INFO
+ } else {
+ this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
+ if (error) {
+ failWebsocketConnection(this.#handler, 1007, error.message)
+ return
+ }
+
+ this.writeFragments(data)
+
+ if (!this.#info.fin) {
+ this.#state = parserStates.INFO
+ this.#loop = true
+ this.run(callback)
+ return
+ }
+
+ websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
+
+ this.#loop = true
+ this.#state = parserStates.INFO
+ this.run(callback)
+ })
+
+ this.#loop = false
+ break
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Take n bytes from the buffered Buffers
+ * @param {number} n
+ * @returns {Buffer}
+ */
+ consume (n) {
+ if (n > this.#byteOffset) {
+ throw new Error('Called consume() before buffers satiated.')
+ } else if (n === 0) {
+ return emptyBuffer
+ }
+
+ this.#byteOffset -= n
+
+ const first = this.#buffers[0]
+
+ if (first.length > n) {
+ // replace with remaining buffer
+ this.#buffers[0] = first.subarray(n, first.length)
+ return first.subarray(0, n)
+ } else if (first.length === n) {
+ // prefect match
+ return this.#buffers.shift()
+ } else {
+ let offset = 0
+ // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
+ const buffer = Buffer.allocUnsafeSlow(n)
+ while (offset !== n) {
+ const next = this.#buffers[0]
+ const length = next.length
+
+ if (length + offset === n) {
+ buffer.set(this.#buffers.shift(), offset)
+ break
+ } else if (length + offset > n) {
+ buffer.set(next.subarray(0, n - offset), offset)
+ this.#buffers[0] = next.subarray(n - offset)
+ break
+ } else {
+ buffer.set(this.#buffers.shift(), offset)
+ offset += length
+ }
+ }
+
+ return buffer
+ }
+ }
+
+ writeFragments (fragment) {
+ this.#fragmentsBytes += fragment.length
+ this.#fragments.push(fragment)
+ }
+
+ consumeFragments () {
+ const fragments = this.#fragments
+
+ if (fragments.length === 1) {
+ // single fragment
+ this.#fragmentsBytes = 0
+ return fragments.shift()
+ }
+
+ let offset = 0
+ // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
+ const output = Buffer.allocUnsafeSlow(this.#fragmentsBytes)
+
+ for (let i = 0; i < fragments.length; ++i) {
+ const buffer = fragments[i]
+ output.set(buffer, offset)
+ offset += buffer.length
+ }
+
+ this.#fragments = []
+ this.#fragmentsBytes = 0
+
+ return output
+ }
+
+ parseCloseBody (data) {
+ assert(data.length !== 1)
+
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
+ /** @type {number|undefined} */
+ let code
+
+ if (data.length >= 2) {
+ // _The WebSocket Connection Close Code_ is
+ // defined as the status code (Section 7.4) contained in the first Close
+ // control frame received by the application
+ code = data.readUInt16BE(0)
+ }
+
+ if (code !== undefined && !isValidStatusCode(code)) {
+ return { code: 1002, reason: 'Invalid status code', error: true }
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
+ /** @type {Buffer} */
+ let reason = data.subarray(2)
+
+ // Remove BOM
+ if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) {
+ reason = reason.subarray(3)
+ }
+
+ try {
+ reason = utf8Decode(reason)
+ } catch {
+ return { code: 1007, reason: 'Invalid UTF-8', error: true }
+ }
+
+ return { code, reason, error: false }
+ }
+
+ /**
+ * Parses control frames.
+ * @param {Buffer} body
+ */
+ parseControlFrame (body) {
+ const { opcode, payloadLength } = this.#info
+
+ if (opcode === opcodes.CLOSE) {
+ if (payloadLength === 1) {
+ failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.')
+ return false
+ }
+
+ this.#info.closeInfo = this.parseCloseBody(body)
+
+ if (this.#info.closeInfo.error) {
+ const { code, reason } = this.#info.closeInfo
+
+ failWebsocketConnection(this.#handler, code, reason)
+ return false
+ }
+
+ // Upon receiving such a frame, the other peer sends a
+ // Close frame in response, if it hasn't already sent one.
+ if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ // If an endpoint receives a Close frame and did not previously send a
+ // Close frame, the endpoint MUST send a Close frame in response. (When
+ // sending a Close frame in response, the endpoint typically echos the
+ // status code it received.)
+ let body = emptyBuffer
+ if (this.#info.closeInfo.code) {
+ body = Buffer.allocUnsafe(2)
+ body.writeUInt16BE(this.#info.closeInfo.code, 0)
+ }
+ const closeFrame = new WebsocketFrameSend(body)
+
+ this.#handler.socket.write(closeFrame.createFrame(opcodes.CLOSE))
+ this.#handler.closeState.add(sentCloseFrameState.SENT)
+ }
+
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ this.#handler.readyState = states.CLOSING
+ this.#handler.closeState.add(sentCloseFrameState.RECEIVED)
+
+ return false
+ } else if (opcode === opcodes.PING) {
+ // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
+ // response, unless it already received a Close frame.
+ // A Pong frame sent in response to a Ping frame must have identical
+ // "Application data"
+
+ if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ const frame = new WebsocketFrameSend(body)
+
+ this.#handler.socket.write(frame.createFrame(opcodes.PONG))
+
+ this.#handler.onPing(body)
+ }
+ } else if (opcode === opcodes.PONG) {
+ // A Pong frame MAY be sent unsolicited. This serves as a
+ // unidirectional heartbeat. A response to an unsolicited Pong frame is
+ // not expected.
+ this.#handler.onPong(body)
+ }
+
+ return true
+ }
+
+ get closingInfo () {
+ return this.#info.closeInfo
+ }
+}
+
+module.exports = {
+ ByteParser
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/sender.js b/vanilla/node_modules/undici/lib/web/websocket/sender.js
new file mode 100644
index 0000000..c647bf6
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/sender.js
@@ -0,0 +1,109 @@
+'use strict'
+
+const { WebsocketFrameSend } = require('./frame')
+const { opcodes, sendHints } = require('./constants')
+const FixedQueue = require('../../dispatcher/fixed-queue')
+
+/**
+ * @typedef {object} SendQueueNode
+ * @property {Promise<void> | null} promise
+ * @property {((...args: any[]) => any)} callback
+ * @property {Buffer | null} frame
+ */
+
+class SendQueue {
+ /**
+ * @type {FixedQueue}
+ */
+ #queue = new FixedQueue()
+
+ /**
+ * @type {boolean}
+ */
+ #running = false
+
+ /** @type {import('node:net').Socket} */
+ #socket
+
+ constructor (socket) {
+ this.#socket = socket
+ }
+
+ add (item, cb, hint) {
+ if (hint !== sendHints.blob) {
+ if (!this.#running) {
+ // TODO(@tsctx): support fast-path for string on running
+ if (hint === sendHints.text) {
+ // special fast-path for string
+ const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item)
+ this.#socket.cork()
+ this.#socket.write(head)
+ this.#socket.write(body, cb)
+ this.#socket.uncork()
+ } else {
+ // direct writing
+ this.#socket.write(createFrame(item, hint), cb)
+ }
+ } else {
+ /** @type {SendQueueNode} */
+ const node = {
+ promise: null,
+ callback: cb,
+ frame: createFrame(item, hint)
+ }
+ this.#queue.push(node)
+ }
+ return
+ }
+
+ /** @type {SendQueueNode} */
+ const node = {
+ promise: item.arrayBuffer().then((ab) => {
+ node.promise = null
+ node.frame = createFrame(ab, hint)
+ }),
+ callback: cb,
+ frame: null
+ }
+
+ this.#queue.push(node)
+
+ if (!this.#running) {
+ this.#run()
+ }
+ }
+
+ async #run () {
+ this.#running = true
+ const queue = this.#queue
+ while (!queue.isEmpty()) {
+ const node = queue.shift()
+ // wait pending promise
+ if (node.promise !== null) {
+ await node.promise
+ }
+ // write
+ this.#socket.write(node.frame, node.callback)
+ // cleanup
+ node.callback = node.frame = null
+ }
+ this.#running = false
+ }
+}
+
+function createFrame (data, hint) {
+ return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY)
+}
+
+function toBuffer (data, hint) {
+ switch (hint) {
+ case sendHints.text:
+ case sendHints.typedArray:
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
+ case sendHints.arrayBuffer:
+ case sendHints.blob:
+ return new Uint8Array(data)
+ }
+}
+
+module.exports = { SendQueue }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js
new file mode 100644
index 0000000..a34c521
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketerror.js
@@ -0,0 +1,104 @@
+'use strict'
+
+const { webidl } = require('../../webidl')
+const { validateCloseCodeAndReason } = require('../util')
+const { kConstruct } = require('../../../core/symbols')
+const { kEnumerableProperty } = require('../../../core/util')
+
+function createInheritableDOMException () {
+ // https://github.com/nodejs/node/issues/59677
+ class Test extends DOMException {
+ get reason () {
+ return ''
+ }
+ }
+
+ if (new Test().reason !== undefined) {
+ return DOMException
+ }
+
+ return new Proxy(DOMException, {
+ construct (target, args, newTarget) {
+ const instance = Reflect.construct(target, args, target)
+ Object.setPrototypeOf(instance, newTarget.prototype)
+ return instance
+ }
+ })
+}
+
+class WebSocketError extends createInheritableDOMException() {
+ #closeCode
+ #reason
+
+ constructor (message = '', init = undefined) {
+ message = webidl.converters.DOMString(message, 'WebSocketError', 'message')
+
+ // 1. Set this 's name to " WebSocketError ".
+ // 2. Set this 's message to message .
+ super(message, 'WebSocketError')
+
+ if (init === kConstruct) {
+ return
+ } else if (init !== null) {
+ init = webidl.converters.WebSocketCloseInfo(init)
+ }
+
+ // 3. Let code be init [" closeCode "] if it exists , or null otherwise.
+ let code = init.closeCode ?? null
+
+ // 4. Let reason be init [" reason "] if it exists , or the empty string otherwise.
+ const reason = init.reason ?? ''
+
+ // 5. Validate close code and reason with code and reason .
+ validateCloseCodeAndReason(code, reason)
+
+ // 6. If reason is non-empty, but code is not set, then set code to 1000 ("Normal Closure").
+ if (reason.length !== 0 && code === null) {
+ code = 1000
+ }
+
+ // 7. Set this 's closeCode to code .
+ this.#closeCode = code
+
+ // 8. Set this 's reason to reason .
+ this.#reason = reason
+ }
+
+ get closeCode () {
+ return this.#closeCode
+ }
+
+ get reason () {
+ return this.#reason
+ }
+
+ /**
+ * @param {string} message
+ * @param {number|null} code
+ * @param {string} reason
+ */
+ static createUnvalidatedWebSocketError (message, code, reason) {
+ const error = new WebSocketError(message, kConstruct)
+ error.#closeCode = code
+ error.#reason = reason
+ return error
+ }
+}
+
+const { createUnvalidatedWebSocketError } = WebSocketError
+delete WebSocketError.createUnvalidatedWebSocketError
+
+Object.defineProperties(WebSocketError.prototype, {
+ closeCode: kEnumerableProperty,
+ reason: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'WebSocketError',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+webidl.is.WebSocketError = webidl.util.MakeTypeAssertion(WebSocketError)
+
+module.exports = { WebSocketError, createUnvalidatedWebSocketError }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js
new file mode 100644
index 0000000..ce3be84
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/stream/websocketstream.js
@@ -0,0 +1,497 @@
+'use strict'
+
+const { createDeferredPromise } = require('../../../util/promise')
+const { environmentSettingsObject } = require('../../fetch/util')
+const { states, opcodes, sentCloseFrameState } = require('../constants')
+const { webidl } = require('../../webidl')
+const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
+const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
+const { channels } = require('../../../core/diagnostics')
+const { WebsocketFrameSend } = require('../frame')
+const { ByteParser } = require('../receiver')
+const { WebSocketError, createUnvalidatedWebSocketError } = require('./websocketerror')
+const { kEnumerableProperty } = require('../../../core/util')
+const { utf8DecodeBytes } = require('../../../encoding')
+
+let emittedExperimentalWarning = false
+
+class WebSocketStream {
+ // Each WebSocketStream object has an associated url , which is a URL record .
+ /** @type {URL} */
+ #url
+
+ // Each WebSocketStream object has an associated opened promise , which is a promise.
+ /** @type {import('../../../util/promise').DeferredPromise} */
+ #openedPromise
+
+ // Each WebSocketStream object has an associated closed promise , which is a promise.
+ /** @type {import('../../../util/promise').DeferredPromise} */
+ #closedPromise
+
+ // Each WebSocketStream object has an associated readable stream , which is a ReadableStream .
+ /** @type {ReadableStream} */
+ #readableStream
+ /** @type {ReadableStreamDefaultController} */
+ #readableStreamController
+
+ // Each WebSocketStream object has an associated writable stream , which is a WritableStream .
+ /** @type {WritableStream} */
+ #writableStream
+
+ // Each WebSocketStream object has an associated boolean handshake aborted , which is initially false.
+ #handshakeAborted = false
+
+ /** @type {import('../websocket').Handler} */
+ #handler = {
+ // https://whatpr.org/websockets/48/7b748d3...d5570f3.html#feedback-to-websocket-stream-from-the-protocol
+ onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
+ onMessage: (opcode, data) => this.#onMessage(opcode, data),
+ onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
+ onParserDrain: () => this.#handler.socket.resume(),
+ onSocketData: (chunk) => {
+ if (!this.#parser.write(chunk)) {
+ this.#handler.socket.pause()
+ }
+ },
+ onSocketError: (err) => {
+ this.#handler.readyState = states.CLOSING
+
+ if (channels.socketError.hasSubscribers) {
+ channels.socketError.publish(err)
+ }
+
+ this.#handler.socket.destroy()
+ },
+ onSocketClose: () => this.#onSocketClose(),
+ onPing: () => {},
+ onPong: () => {},
+
+ readyState: states.CONNECTING,
+ socket: null,
+ closeState: new Set(),
+ controller: null,
+ wasEverConnected: false
+ }
+
+ /** @type {import('../receiver').ByteParser} */
+ #parser
+
+ constructor (url, options = undefined) {
+ if (!emittedExperimentalWarning) {
+ process.emitWarning('WebSocketStream is experimental! Expect it to change at any time.', {
+ code: 'UNDICI-WSS'
+ })
+ emittedExperimentalWarning = true
+ }
+
+ webidl.argumentLengthCheck(arguments, 1, 'WebSocket')
+
+ url = webidl.converters.USVString(url)
+ if (options !== null) {
+ options = webidl.converters.WebSocketStreamOptions(options)
+ }
+
+ // 1. Let baseURL be this 's relevant settings object 's API base URL .
+ const baseURL = environmentSettingsObject.settingsObject.baseUrl
+
+ // 2. Let urlRecord be the result of getting a URL record given url and baseURL .
+ const urlRecord = getURLRecord(url, baseURL)
+
+ // 3. Let protocols be options [" protocols "] if it exists , otherwise an empty sequence.
+ const protocols = options.protocols
+
+ // 4. If any of the values in protocols occur more than once or otherwise fail to match the requirements for elements that comprise the value of ` Sec-WebSocket-Protocol ` fields as defined by The WebSocket Protocol , then throw a " SyntaxError " DOMException . [WSP]
+ if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ // 5. Set this 's url to urlRecord .
+ this.#url = urlRecord.toString()
+
+ // 6. Set this 's opened promise and closed promise to new promises.
+ this.#openedPromise = createDeferredPromise()
+ this.#closedPromise = createDeferredPromise()
+
+ // 7. Apply backpressure to the WebSocket.
+ // TODO
+
+ // 8. If options [" signal "] exists ,
+ if (options.signal != null) {
+ // 8.1. Let signal be options [" signal "].
+ const signal = options.signal
+
+ // 8.2. If signal is aborted , then reject this 's opened promise and closed promise with signal ’s abort reason
+ // and return.
+ if (signal.aborted) {
+ this.#openedPromise.reject(signal.reason)
+ this.#closedPromise.reject(signal.reason)
+ return
+ }
+
+ // 8.3. Add the following abort steps to signal :
+ signal.addEventListener('abort', () => {
+ // 8.3.1. If the WebSocket connection is not yet established : [WSP]
+ if (!isEstablished(this.#handler.readyState)) {
+ // 8.3.1.1. Fail the WebSocket connection .
+ failWebsocketConnection(this.#handler)
+
+ // Set this 's ready state to CLOSING .
+ this.#handler.readyState = states.CLOSING
+
+ // Reject this 's opened promise and closed promise with signal ’s abort reason .
+ this.#openedPromise.reject(signal.reason)
+ this.#closedPromise.reject(signal.reason)
+
+ // Set this 's handshake aborted to true.
+ this.#handshakeAborted = true
+ }
+ }, { once: true })
+ }
+
+ // 9. Let client be this 's relevant settings object .
+ const client = environmentSettingsObject.settingsObject
+
+ // 10. Run this step in parallel :
+ // 10.1. Establish a WebSocket connection given urlRecord , protocols , and client . [FETCH]
+ this.#handler.controller = establishWebSocketConnection(
+ urlRecord,
+ protocols,
+ client,
+ this.#handler,
+ options
+ )
+ }
+
+ // The url getter steps are to return this 's url , serialized .
+ get url () {
+ return this.#url.toString()
+ }
+
+ // The opened getter steps are to return this 's opened promise .
+ get opened () {
+ return this.#openedPromise.promise
+ }
+
+ // The closed getter steps are to return this 's closed promise .
+ get closed () {
+ return this.#closedPromise.promise
+ }
+
+ // The close( closeInfo ) method steps are:
+ close (closeInfo = undefined) {
+ if (closeInfo !== null) {
+ closeInfo = webidl.converters.WebSocketCloseInfo(closeInfo)
+ }
+
+ // 1. Let code be closeInfo [" closeCode "] if present, or null otherwise.
+ const code = closeInfo.closeCode ?? null
+
+ // 2. Let reason be closeInfo [" reason "].
+ const reason = closeInfo.reason
+
+ // 3. Close the WebSocket with this , code , and reason .
+ closeWebSocketConnection(this.#handler, code, reason, true)
+ }
+
+ #write (chunk) {
+ // See /websockets/stream/tentative/write.any.html
+ chunk = webidl.converters.WebSocketStreamWrite(chunk)
+
+ // 1. Let promise be a new promise created in stream ’s relevant realm .
+ const promise = createDeferredPromise()
+
+ // 2. Let data be null.
+ let data = null
+
+ // 3. Let opcode be null.
+ let opcode = null
+
+ // 4. If chunk is a BufferSource ,
+ if (webidl.is.BufferSource(chunk)) {
+ // 4.1. Set data to a copy of the bytes given chunk .
+ data = new Uint8Array(ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : chunk.slice())
+
+ // 4.2. Set opcode to a binary frame opcode.
+ opcode = opcodes.BINARY
+ } else {
+ // 5. Otherwise,
+
+ // 5.1. Let string be the result of converting chunk to an IDL USVString .
+ // If this throws an exception, return a promise rejected with the exception.
+ let string
+
+ try {
+ string = webidl.converters.DOMString(chunk)
+ } catch (e) {
+ promise.reject(e)
+ return promise.promise
+ }
+
+ // 5.2. Set data to the result of UTF-8 encoding string .
+ data = new TextEncoder().encode(string)
+
+ // 5.3. Set opcode to a text frame opcode.
+ opcode = opcodes.TEXT
+ }
+
+ // 6. In parallel,
+ // 6.1. Wait until there is sufficient buffer space in stream to send the message.
+
+ // 6.2. If the closing handshake has not yet started , Send a WebSocket Message to stream comprised of data using opcode .
+ if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ const frame = new WebsocketFrameSend(data)
+
+ this.#handler.socket.write(frame.createFrame(opcode), () => {
+ promise.resolve(undefined)
+ })
+ }
+
+ // 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined.
+ return promise.promise
+ }
+
+ /** @type {import('../websocket').Handler['onConnectionEstablished']} */
+ #onConnectionEstablished (response, parsedExtensions) {
+ this.#handler.socket = response.socket
+
+ const parser = new ByteParser(this.#handler, parsedExtensions)
+ parser.on('drain', () => this.#handler.onParserDrain())
+ parser.on('error', (err) => this.#handler.onParserError(err))
+
+ this.#parser = parser
+
+ // 1. Change stream ’s ready state to OPEN (1).
+ this.#handler.readyState = states.OPEN
+
+ // 2. Set stream ’s was ever connected to true.
+ // This is done in the opening handshake.
+
+ // 3. Let extensions be the extensions in use .
+ const extensions = parsedExtensions ?? ''
+
+ // 4. Let protocol be the subprotocol in use .
+ const protocol = response.headersList.get('sec-websocket-protocol') ?? ''
+
+ // 5. Let pullAlgorithm be an action that pulls bytes from stream .
+ // 6. Let cancelAlgorithm be an action that cancels stream with reason , given reason .
+ // 7. Let readable be a new ReadableStream .
+ // 8. Set up readable with pullAlgorithm and cancelAlgorithm .
+ const readable = new ReadableStream({
+ start: (controller) => {
+ this.#readableStreamController = controller
+ },
+ pull (controller) {
+ let chunk
+ while (controller.desiredSize > 0 && (chunk = response.socket.read()) !== null) {
+ controller.enqueue(chunk)
+ }
+ },
+ cancel: (reason) => this.#cancel(reason)
+ })
+
+ // 9. Let writeAlgorithm be an action that writes chunk to stream , given chunk .
+ // 10. Let closeAlgorithm be an action that closes stream .
+ // 11. Let abortAlgorithm be an action that aborts stream with reason , given reason .
+ // 12. Let writable be a new WritableStream .
+ // 13. Set up writable with writeAlgorithm , closeAlgorithm , and abortAlgorithm .
+ const writable = new WritableStream({
+ write: (chunk) => this.#write(chunk),
+ close: () => closeWebSocketConnection(this.#handler, null, null),
+ abort: (reason) => this.#closeUsingReason(reason)
+ })
+
+ // Set stream ’s readable stream to readable .
+ this.#readableStream = readable
+
+ // Set stream ’s writable stream to writable .
+ this.#writableStream = writable
+
+ // Resolve stream ’s opened promise with WebSocketOpenInfo «[ " extensions " → extensions , " protocol " → protocol , " readable " → readable , " writable " → writable ]».
+ this.#openedPromise.resolve({
+ extensions,
+ protocol,
+ readable,
+ writable
+ })
+ }
+
+ /** @type {import('../websocket').Handler['onMessage']} */
+ #onMessage (type, data) {
+ // 1. If stream’s ready state is not OPEN (1), then return.
+ if (this.#handler.readyState !== states.OPEN) {
+ return
+ }
+
+ // 2. Let chunk be determined by switching on type:
+ // - type indicates that the data is Text
+ // a new DOMString containing data
+ // - type indicates that the data is Binary
+ // a new Uint8Array object, created in the relevant Realm of the
+ // WebSocketStream object, whose contents are data
+ let chunk
+
+ if (type === opcodes.TEXT) {
+ try {
+ chunk = utf8Decode(data)
+ } catch {
+ failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
+ return
+ }
+ } else if (type === opcodes.BINARY) {
+ chunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
+ }
+
+ // 3. Enqueue chunk into stream’s readable stream.
+ this.#readableStreamController.enqueue(chunk)
+
+ // 4. Apply backpressure to the WebSocket.
+ }
+
+ /** @type {import('../websocket').Handler['onSocketClose']} */
+ #onSocketClose () {
+ const wasClean =
+ this.#handler.closeState.has(sentCloseFrameState.SENT) &&
+ this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
+
+ // 1. Change the ready state to CLOSED (3).
+ this.#handler.readyState = states.CLOSED
+
+ // 2. If stream ’s handshake aborted is true, then return.
+ if (this.#handshakeAborted) {
+ return
+ }
+
+ // 3. If stream ’s was ever connected is false, then reject stream ’s opened promise with a new WebSocketError.
+ if (!this.#handler.wasEverConnected) {
+ this.#openedPromise.reject(new WebSocketError('Socket never opened'))
+ }
+
+ const result = this.#parser?.closingInfo
+
+ // 4. Let code be the WebSocket connection close code .
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
+ // If this Close control frame contains no status code, _The WebSocket
+ // Connection Close Code_ is considered to be 1005. If _The WebSocket
+ // Connection is Closed_ and no Close control frame was received by the
+ // endpoint (such as could occur if the underlying transport connection
+ // is lost), _The WebSocket Connection Close Code_ is considered to be
+ // 1006.
+ let code = result?.code ?? 1005
+
+ if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ code = 1006
+ }
+
+ // 5. Let reason be the result of applying UTF-8 decode without BOM to the WebSocket connection close reason .
+ const reason = result?.reason == null ? '' : utf8DecodeBytes(Buffer.from(result.reason))
+
+ // 6. If the connection was closed cleanly ,
+ if (wasClean) {
+ // 6.1. Close stream ’s readable stream .
+ this.#readableStreamController.close()
+
+ // 6.2. Error stream ’s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
+ if (!this.#writableStream.locked) {
+ this.#writableStream.abort(new DOMException('A closed WebSocketStream cannot be written to', 'InvalidStateError'))
+ }
+
+ // 6.3. Resolve stream ’s closed promise with WebSocketCloseInfo «[ " closeCode " → code , " reason " → reason ]».
+ this.#closedPromise.resolve({
+ closeCode: code,
+ reason
+ })
+ } else {
+ // 7. Otherwise,
+
+ // 7.1. Let error be a new WebSocketError whose closeCode is code and reason is reason .
+ const error = createUnvalidatedWebSocketError('unclean close', code, reason)
+
+ // 7.2. Error stream ’s readable stream with error .
+ this.#readableStreamController?.error(error)
+
+ // 7.3. Error stream ’s writable stream with error .
+ this.#writableStream?.abort(error)
+
+ // 7.4. Reject stream ’s closed promise with error .
+ this.#closedPromise.reject(error)
+ }
+ }
+
+ #closeUsingReason (reason) {
+ // 1. Let code be null.
+ let code = null
+
+ // 2. Let reasonString be the empty string.
+ let reasonString = ''
+
+ // 3. If reason implements WebSocketError ,
+ if (webidl.is.WebSocketError(reason)) {
+ // 3.1. Set code to reason ’s closeCode .
+ code = reason.closeCode
+
+ // 3.2. Set reasonString to reason ’s reason .
+ reasonString = reason.reason
+ }
+
+ // 4. Close the WebSocket with stream , code , and reasonString . If this throws an exception,
+ // discard code and reasonString and close the WebSocket with stream .
+ closeWebSocketConnection(this.#handler, code, reasonString)
+ }
+
+ // To cancel a WebSocketStream stream given reason , close using reason giving stream and reason .
+ #cancel (reason) {
+ this.#closeUsingReason(reason)
+ }
+}
+
+Object.defineProperties(WebSocketStream.prototype, {
+ url: kEnumerableProperty,
+ opened: kEnumerableProperty,
+ closed: kEnumerableProperty,
+ close: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'WebSocketStream',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+webidl.converters.WebSocketStreamOptions = webidl.dictionaryConverter([
+ {
+ key: 'protocols',
+ converter: webidl.sequenceConverter(webidl.converters.USVString),
+ defaultValue: () => []
+ },
+ {
+ key: 'signal',
+ converter: webidl.nullableConverter(webidl.converters.AbortSignal),
+ defaultValue: () => null
+ }
+])
+
+webidl.converters.WebSocketCloseInfo = webidl.dictionaryConverter([
+ {
+ key: 'closeCode',
+ converter: (V) => webidl.converters['unsigned short'](V, webidl.attributes.EnforceRange)
+ },
+ {
+ key: 'reason',
+ converter: webidl.converters.USVString,
+ defaultValue: () => ''
+ }
+])
+
+webidl.converters.WebSocketStreamWrite = function (V) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V)
+ }
+
+ return webidl.converters.BufferSource(V)
+}
+
+module.exports = { WebSocketStream }
diff --git a/vanilla/node_modules/undici/lib/web/websocket/util.js b/vanilla/node_modules/undici/lib/web/websocket/util.js
new file mode 100644
index 0000000..eaa5e7a
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/util.js
@@ -0,0 +1,339 @@
+'use strict'
+
+const { states, opcodes } = require('./constants')
+const { isUtf8 } = require('node:buffer')
+const { removeHTTPWhitespace } = require('../fetch/data-url')
+const { collectASequenceOfCodePointsFast } = require('../infra')
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isConnecting (readyState) {
+ // If the WebSocket connection is not yet established, and the connection
+ // is not yet closed, then the WebSocket connection is in the CONNECTING state.
+ return readyState === states.CONNECTING
+}
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isEstablished (readyState) {
+ // If the server's response is validated as provided for above, it is
+ // said that _The WebSocket Connection is Established_ and that the
+ // WebSocket Connection is in the OPEN state.
+ return readyState === states.OPEN
+}
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isClosing (readyState) {
+ // Upon either sending or receiving a Close control frame, it is said
+ // that _The WebSocket Closing Handshake is Started_ and that the
+ // WebSocket connection is in the CLOSING state.
+ return readyState === states.CLOSING
+}
+
+/**
+ * @param {number} readyState
+ * @returns {boolean}
+ */
+function isClosed (readyState) {
+ return readyState === states.CLOSED
+}
+
+/**
+ * @see https://dom.spec.whatwg.org/#concept-event-fire
+ * @param {string} e
+ * @param {EventTarget} target
+ * @param {(...args: ConstructorParameters<typeof Event>) => Event} eventFactory
+ * @param {EventInit | undefined} eventInitDict
+ * @returns {void}
+ */
+function fireEvent (e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) {
+ // 1. If eventConstructor is not given, then let eventConstructor be Event.
+
+ // 2. Let event be the result of creating an event given eventConstructor,
+ // in the relevant realm of target.
+ // 3. Initialize event’s type attribute to e.
+ const event = eventFactory(e, eventInitDict)
+
+ // 4. Initialize any other IDL attributes of event as described in the
+ // invocation of this algorithm.
+
+ // 5. Return the result of dispatching event at target, with legacy target
+ // override flag set if set.
+ target.dispatchEvent(event)
+}
+
+/**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ * @param {import('./websocket').Handler} handler
+ * @param {number} type Opcode
+ * @param {Buffer} data application data
+ * @returns {void}
+ */
+function websocketMessageReceived (handler, type, data) {
+ handler.onMessage(type, data)
+}
+
+/**
+ * @param {Buffer} buffer
+ * @returns {ArrayBuffer}
+ */
+function toArrayBuffer (buffer) {
+ if (buffer.byteLength === buffer.buffer.byteLength) {
+ return buffer.buffer
+ }
+ return new Uint8Array(buffer).buffer
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455
+ * @see https://datatracker.ietf.org/doc/html/rfc2616
+ * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407
+ * @param {string} protocol
+ * @returns {boolean}
+ */
+function isValidSubprotocol (protocol) {
+ // If present, this value indicates one
+ // or more comma-separated subprotocol the client wishes to speak,
+ // ordered by preference. The elements that comprise this value
+ // MUST be non-empty strings with characters in the range U+0021 to
+ // U+007E not including separator characters as defined in
+ // [RFC2616] and MUST all be unique strings.
+ if (protocol.length === 0) {
+ return false
+ }
+
+ for (let i = 0; i < protocol.length; ++i) {
+ const code = protocol.charCodeAt(i)
+
+ if (
+ code < 0x21 || // CTL, contains SP (0x20) and HT (0x09)
+ code > 0x7E ||
+ code === 0x22 || // "
+ code === 0x28 || // (
+ code === 0x29 || // )
+ code === 0x2C || // ,
+ code === 0x2F || // /
+ code === 0x3A || // :
+ code === 0x3B || // ;
+ code === 0x3C || // <
+ code === 0x3D || // =
+ code === 0x3E || // >
+ code === 0x3F || // ?
+ code === 0x40 || // @
+ code === 0x5B || // [
+ code === 0x5C || // \
+ code === 0x5D || // ]
+ code === 0x7B || // {
+ code === 0x7D // }
+ ) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4
+ * @param {number} code
+ * @returns {boolean}
+ */
+function isValidStatusCode (code) {
+ if (code >= 1000 && code < 1015) {
+ return (
+ code !== 1004 && // reserved
+ code !== 1005 && // "MUST NOT be set as a status code"
+ code !== 1006 // "MUST NOT be set as a status code"
+ )
+ }
+
+ return code >= 3000 && code <= 4999
+}
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isControlFrame (opcode) {
+ return (
+ opcode === opcodes.CLOSE ||
+ opcode === opcodes.PING ||
+ opcode === opcodes.PONG
+ )
+}
+
+/**
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isContinuationFrame (opcode) {
+ return opcode === opcodes.CONTINUATION
+}
+
+/**
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isTextBinaryFrame (opcode) {
+ return opcode === opcodes.TEXT || opcode === opcodes.BINARY
+}
+
+/**
+ *
+ * @param {number} opcode
+ * @returns {boolean}
+ */
+function isValidOpcode (opcode) {
+ return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
+}
+
+/**
+ * Parses a Sec-WebSocket-Extensions header value.
+ * @param {string} extensions
+ * @returns {Map<string, string>}
+ */
+// TODO(@Uzlopak, @KhafraDev): make compliant https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
+function parseExtensions (extensions) {
+ const position = { position: 0 }
+ const extensionList = new Map()
+
+ while (position.position < extensions.length) {
+ const pair = collectASequenceOfCodePointsFast(';', extensions, position)
+ const [name, value = ''] = pair.split('=', 2)
+
+ extensionList.set(
+ removeHTTPWhitespace(name, true, false),
+ removeHTTPWhitespace(value, false, true)
+ )
+
+ position.position++
+ }
+
+ return extensionList
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc7692#section-7.1.2.2
+ * @description "client-max-window-bits = 1*DIGIT"
+ * @param {string} value
+ * @returns {boolean}
+ */
+function isValidClientWindowBits (value) {
+ for (let i = 0; i < value.length; i++) {
+ const byte = value.charCodeAt(i)
+
+ if (byte < 0x30 || byte > 0x39) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @see https://whatpr.org/websockets/48/7b748d3...d5570f3.html#get-a-url-record
+ * @param {string} url
+ * @param {string} [baseURL]
+ */
+function getURLRecord (url, baseURL) {
+ // 1. Let urlRecord be the result of applying the URL parser to url with baseURL .
+ // 2. If urlRecord is failure, then throw a " SyntaxError " DOMException .
+ let urlRecord
+
+ try {
+ urlRecord = new URL(url, baseURL)
+ } catch (e) {
+ throw new DOMException(e, 'SyntaxError')
+ }
+
+ // 3. If urlRecord ’s scheme is " http ", then set urlRecord ’s scheme to " ws ".
+ // 4. Otherwise, if urlRecord ’s scheme is " https ", set urlRecord ’s scheme to " wss ".
+ if (urlRecord.protocol === 'http:') {
+ urlRecord.protocol = 'ws:'
+ } else if (urlRecord.protocol === 'https:') {
+ urlRecord.protocol = 'wss:'
+ }
+
+ // 5. If urlRecord ’s scheme is not " ws " or " wss ", then throw a " SyntaxError " DOMException .
+ if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') {
+ throw new DOMException('expected a ws: or wss: url', 'SyntaxError')
+ }
+
+ // If urlRecord ’s fragment is non-null, then throw a " SyntaxError " DOMException .
+ if (urlRecord.hash.length || urlRecord.href.endsWith('#')) {
+ throw new DOMException('hash', 'SyntaxError')
+ }
+
+ // Return urlRecord .
+ return urlRecord
+}
+
+// https://whatpr.org/websockets/48.html#validate-close-code-and-reason
+function validateCloseCodeAndReason (code, reason) {
+ // 1. If code is not null, but is neither an integer equal to
+ // 1000 nor an integer in the range 3000 to 4999, inclusive,
+ // throw an "InvalidAccessError" DOMException.
+ if (code !== null) {
+ if (code !== 1000 && (code < 3000 || code > 4999)) {
+ throw new DOMException('invalid code', 'InvalidAccessError')
+ }
+ }
+
+ // 2. If reason is not null, then:
+ if (reason !== null) {
+ // 2.1. Let reasonBytes be the result of UTF-8 encoding reason.
+ // 2.2. If reasonBytes is longer than 123 bytes, then throw a
+ // "SyntaxError" DOMException.
+ const reasonBytesLength = Buffer.byteLength(reason)
+
+ if (reasonBytesLength > 123) {
+ throw new DOMException(`Reason must be less than 123 bytes; received ${reasonBytesLength}`, 'SyntaxError')
+ }
+ }
+}
+
+/**
+ * Converts a Buffer to utf-8, even on platforms without icu.
+ * @type {(buffer: Buffer) => string}
+ */
+const utf8Decode = (() => {
+ if (typeof process.versions.icu === 'string') {
+ const fatalDecoder = new TextDecoder('utf-8', { fatal: true })
+ return fatalDecoder.decode.bind(fatalDecoder)
+ }
+ return function (buffer) {
+ if (isUtf8(buffer)) {
+ return buffer.toString('utf-8')
+ }
+ throw new TypeError('Invalid utf-8 received.')
+ }
+})()
+
+module.exports = {
+ isConnecting,
+ isEstablished,
+ isClosing,
+ isClosed,
+ fireEvent,
+ isValidSubprotocol,
+ isValidStatusCode,
+ websocketMessageReceived,
+ utf8Decode,
+ isControlFrame,
+ isContinuationFrame,
+ isTextBinaryFrame,
+ isValidOpcode,
+ parseExtensions,
+ isValidClientWindowBits,
+ toArrayBuffer,
+ getURLRecord,
+ validateCloseCodeAndReason
+}
diff --git a/vanilla/node_modules/undici/lib/web/websocket/websocket.js b/vanilla/node_modules/undici/lib/web/websocket/websocket.js
new file mode 100644
index 0000000..3314976
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/websocket/websocket.js
@@ -0,0 +1,739 @@
+'use strict'
+
+const { isArrayBuffer } = require('node:util/types')
+const { webidl } = require('../webidl')
+const { URLSerializer } = require('../fetch/data-url')
+const { environmentSettingsObject } = require('../fetch/util')
+const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
+const {
+ isConnecting,
+ isEstablished,
+ isClosing,
+ isClosed,
+ isValidSubprotocol,
+ fireEvent,
+ utf8Decode,
+ toArrayBuffer,
+ getURLRecord
+} = require('./util')
+const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketConnection } = require('./connection')
+const { ByteParser } = require('./receiver')
+const { kEnumerableProperty } = require('../../core/util')
+const { getGlobalDispatcher } = require('../../global')
+const { ErrorEvent, CloseEvent, createFastMessageEvent } = require('./events')
+const { SendQueue } = require('./sender')
+const { WebsocketFrameSend } = require('./frame')
+const { channels } = require('../../core/diagnostics')
+
+/**
+ * @typedef {object} Handler
+ * @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
+ * @property {(opcode: number, data: Buffer) => void} onMessage
+ * @property {(error: Error) => void} onParserError
+ * @property {() => void} onParserDrain
+ * @property {(chunk: Buffer) => void} onSocketData
+ * @property {(err: Error) => void} onSocketError
+ * @property {() => void} onSocketClose
+ * @property {(body: Buffer) => void} onPing
+ * @property {(body: Buffer) => void} onPong
+ *
+ * @property {number} readyState
+ * @property {import('stream').Duplex} socket
+ * @property {Set<number>} closeState
+ * @property {import('../fetch/index').Fetch} controller
+ * @property {boolean} [wasEverConnected=false]
+ */
+
+// https://websockets.spec.whatwg.org/#interface-definition
+class WebSocket extends EventTarget {
+ #events = {
+ open: null,
+ error: null,
+ close: null,
+ message: null
+ }
+
+ #bufferedAmount = 0
+ #protocol = ''
+ #extensions = ''
+
+ /** @type {SendQueue} */
+ #sendQueue
+
+ /** @type {Handler} */
+ #handler = {
+ onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
+ onMessage: (opcode, data) => this.#onMessage(opcode, data),
+ onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
+ onParserDrain: () => this.#onParserDrain(),
+ onSocketData: (chunk) => {
+ if (!this.#parser.write(chunk)) {
+ this.#handler.socket.pause()
+ }
+ },
+ onSocketError: (err) => {
+ this.#handler.readyState = states.CLOSING
+
+ if (channels.socketError.hasSubscribers) {
+ channels.socketError.publish(err)
+ }
+
+ this.#handler.socket.destroy()
+ },
+ onSocketClose: () => this.#onSocketClose(),
+ onPing: (body) => {
+ if (channels.ping.hasSubscribers) {
+ channels.ping.publish({
+ payload: body,
+ websocket: this
+ })
+ }
+ },
+ onPong: (body) => {
+ if (channels.pong.hasSubscribers) {
+ channels.pong.publish({
+ payload: body,
+ websocket: this
+ })
+ }
+ },
+
+ readyState: states.CONNECTING,
+ socket: null,
+ closeState: new Set(),
+ controller: null,
+ wasEverConnected: false
+ }
+
+ #url
+ #binaryType
+ /** @type {import('./receiver').ByteParser} */
+ #parser
+
+ /**
+ * @param {string} url
+ * @param {string|string[]} protocols
+ */
+ constructor (url, protocols = []) {
+ super()
+
+ webidl.util.markAsUncloneable(this)
+
+ const prefix = 'WebSocket constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols, prefix, 'options')
+
+ url = webidl.converters.USVString(url)
+ protocols = options.protocols
+
+ // 1. Let baseURL be this's relevant settings object's API base URL.
+ const baseURL = environmentSettingsObject.settingsObject.baseUrl
+
+ // 2. Let urlRecord be the result of getting a URL record given url and baseURL.
+ const urlRecord = getURLRecord(url, baseURL)
+
+ // 3. If protocols is a string, set protocols to a sequence consisting
+ // of just that string.
+ if (typeof protocols === 'string') {
+ protocols = [protocols]
+ }
+
+ // 4. If any of the values in protocols occur more than once or otherwise
+ // fail to match the requirements for elements that comprise the value
+ // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket
+ // protocol, then throw a "SyntaxError" DOMException.
+ if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) {
+ throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError')
+ }
+
+ // 5. Set this's url to urlRecord.
+ this.#url = new URL(urlRecord.href)
+
+ // 6. Let client be this's relevant settings object.
+ const client = environmentSettingsObject.settingsObject
+
+ // 7. Run this step in parallel:
+ // 7.1. Establish a WebSocket connection given urlRecord, protocols,
+ // and client.
+ this.#handler.controller = establishWebSocketConnection(
+ urlRecord,
+ protocols,
+ client,
+ this.#handler,
+ options
+ )
+
+ // Each WebSocket object has an associated ready state, which is a
+ // number representing the state of the connection. Initially it must
+ // be CONNECTING (0).
+ this.#handler.readyState = WebSocket.CONNECTING
+
+ // The extensions attribute must initially return the empty string.
+
+ // The protocol attribute must initially return the empty string.
+
+ // Each WebSocket object has an associated binary type, which is a
+ // BinaryType. Initially it must be "blob".
+ this.#binaryType = 'blob'
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#dom-websocket-close
+ * @param {number|undefined} code
+ * @param {string|undefined} reason
+ */
+ close (code = undefined, reason = undefined) {
+ webidl.brandCheck(this, WebSocket)
+
+ const prefix = 'WebSocket.close'
+
+ if (code !== undefined) {
+ code = webidl.converters['unsigned short'](code, prefix, 'code', webidl.attributes.Clamp)
+ }
+
+ if (reason !== undefined) {
+ reason = webidl.converters.USVString(reason)
+ }
+
+ // 1. If code is the special value "missing", then set code to null.
+ code ??= null
+
+ // 2. If reason is the special value "missing", then set reason to the empty string.
+ reason ??= ''
+
+ // 3. Close the WebSocket with this, code, and reason.
+ closeWebSocketConnection(this.#handler, code, reason, true)
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#dom-websocket-send
+ * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data
+ */
+ send (data) {
+ webidl.brandCheck(this, WebSocket)
+
+ const prefix = 'WebSocket.send'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ data = webidl.converters.WebSocketSendData(data, prefix, 'data')
+
+ // 1. If this's ready state is CONNECTING, then throw an
+ // "InvalidStateError" DOMException.
+ if (isConnecting(this.#handler.readyState)) {
+ throw new DOMException('Sent before connected.', 'InvalidStateError')
+ }
+
+ // 2. Run the appropriate set of steps from the following list:
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
+
+ if (!isEstablished(this.#handler.readyState) || isClosing(this.#handler.readyState)) {
+ return
+ }
+
+ // If data is a string
+ if (typeof data === 'string') {
+ // If the WebSocket connection is established and the WebSocket
+ // closing handshake has not yet started, then the user agent
+ // must send a WebSocket Message comprised of the data argument
+ // using a text frame opcode; if the data cannot be sent, e.g.
+ // because it would need to be buffered but the buffer is full,
+ // the user agent must flag the WebSocket as full and then close
+ // the WebSocket connection. Any invocation of this method with a
+ // string argument that does not throw an exception must increase
+ // the bufferedAmount attribute by the number of bytes needed to
+ // express the argument as UTF-8.
+
+ const buffer = Buffer.from(data)
+
+ this.#bufferedAmount += buffer.byteLength
+ this.#sendQueue.add(buffer, () => {
+ this.#bufferedAmount -= buffer.byteLength
+ }, sendHints.text)
+ } else if (isArrayBuffer(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need
+ // to be buffered but the buffer is full, the user agent must flag
+ // the WebSocket as full and then close the WebSocket connection.
+ // The data to be sent is the data stored in the buffer described
+ // by the ArrayBuffer object. Any invocation of this method with an
+ // ArrayBuffer argument that does not throw an exception must
+ // increase the bufferedAmount attribute by the length of the
+ // ArrayBuffer in bytes.
+
+ this.#bufferedAmount += data.byteLength
+ this.#sendQueue.add(data, () => {
+ this.#bufferedAmount -= data.byteLength
+ }, sendHints.arrayBuffer)
+ } else if (ArrayBuffer.isView(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need to
+ // be buffered but the buffer is full, the user agent must flag the
+ // WebSocket as full and then close the WebSocket connection. The
+ // data to be sent is the data stored in the section of the buffer
+ // described by the ArrayBuffer object that data references. Any
+ // invocation of this method with this kind of argument that does
+ // not throw an exception must increase the bufferedAmount attribute
+ // by the length of data’s buffer in bytes.
+
+ this.#bufferedAmount += data.byteLength
+ this.#sendQueue.add(data, () => {
+ this.#bufferedAmount -= data.byteLength
+ }, sendHints.typedArray)
+ } else if (webidl.is.Blob(data)) {
+ // If the WebSocket connection is established, and the WebSocket
+ // closing handshake has not yet started, then the user agent must
+ // send a WebSocket Message comprised of data using a binary frame
+ // opcode; if the data cannot be sent, e.g. because it would need to
+ // be buffered but the buffer is full, the user agent must flag the
+ // WebSocket as full and then close the WebSocket connection. The data
+ // to be sent is the raw data represented by the Blob object. Any
+ // invocation of this method with a Blob argument that does not throw
+ // an exception must increase the bufferedAmount attribute by the size
+ // of the Blob object’s raw data, in bytes.
+
+ this.#bufferedAmount += data.size
+ this.#sendQueue.add(data, () => {
+ this.#bufferedAmount -= data.size
+ }, sendHints.blob)
+ }
+ }
+
+ get readyState () {
+ webidl.brandCheck(this, WebSocket)
+
+ // The readyState getter steps are to return this's ready state.
+ return this.#handler.readyState
+ }
+
+ get bufferedAmount () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#bufferedAmount
+ }
+
+ get url () {
+ webidl.brandCheck(this, WebSocket)
+
+ // The url getter steps are to return this's url, serialized.
+ return URLSerializer(this.#url)
+ }
+
+ get extensions () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#extensions
+ }
+
+ get protocol () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#protocol
+ }
+
+ get onopen () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.open
+ }
+
+ set onopen (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.open) {
+ this.removeEventListener('open', this.#events.open)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('open', listener)
+ this.#events.open = fn
+ } else {
+ this.#events.open = null
+ }
+ }
+
+ get onerror () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.error
+ }
+
+ set onerror (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.error) {
+ this.removeEventListener('error', this.#events.error)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('error', listener)
+ this.#events.error = fn
+ } else {
+ this.#events.error = null
+ }
+ }
+
+ get onclose () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.close
+ }
+
+ set onclose (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.close) {
+ this.removeEventListener('close', this.#events.close)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('close', listener)
+ this.#events.close = fn
+ } else {
+ this.#events.close = null
+ }
+ }
+
+ get onmessage () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#events.message
+ }
+
+ set onmessage (fn) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (this.#events.message) {
+ this.removeEventListener('message', this.#events.message)
+ }
+
+ const listener = webidl.converters.EventHandlerNonNull(fn)
+
+ if (listener !== null) {
+ this.addEventListener('message', listener)
+ this.#events.message = fn
+ } else {
+ this.#events.message = null
+ }
+ }
+
+ get binaryType () {
+ webidl.brandCheck(this, WebSocket)
+
+ return this.#binaryType
+ }
+
+ set binaryType (type) {
+ webidl.brandCheck(this, WebSocket)
+
+ if (type !== 'blob' && type !== 'arraybuffer') {
+ this.#binaryType = 'blob'
+ } else {
+ this.#binaryType = type
+ }
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ */
+ #onConnectionEstablished (response, parsedExtensions) {
+ // processResponse is called when the "response’s header list has been received and initialized."
+ // once this happens, the connection is open
+ this.#handler.socket = response.socket
+
+ const parser = new ByteParser(this.#handler, parsedExtensions)
+ parser.on('drain', () => this.#handler.onParserDrain())
+ parser.on('error', (err) => this.#handler.onParserError(err))
+
+ this.#parser = parser
+ this.#sendQueue = new SendQueue(response.socket)
+
+ // 1. Change the ready state to OPEN (1).
+ this.#handler.readyState = states.OPEN
+
+ // 2. Change the extensions attribute’s value to the extensions in use, if
+ // it is not the null value.
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
+ const extensions = response.headersList.get('sec-websocket-extensions')
+
+ if (extensions !== null) {
+ this.#extensions = extensions
+ }
+
+ // 3. Change the protocol attribute’s value to the subprotocol in use, if
+ // it is not the null value.
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
+ const protocol = response.headersList.get('sec-websocket-protocol')
+
+ if (protocol !== null) {
+ this.#protocol = protocol
+ }
+
+ // 4. Fire an event named open at the WebSocket object.
+ fireEvent('open', this)
+
+ if (channels.open.hasSubscribers) {
+ // Convert headers to a plain object for the event
+ const headers = response.headersList.entries
+ channels.open.publish({
+ address: response.socket.address(),
+ protocol: this.#protocol,
+ extensions: this.#extensions,
+ websocket: this,
+ handshakeResponse: {
+ status: response.status,
+ statusText: response.statusText,
+ headers
+ }
+ })
+ }
+ }
+
+ #onMessage (type, data) {
+ // 1. If ready state is not OPEN (1), then return.
+ if (this.#handler.readyState !== states.OPEN) {
+ return
+ }
+
+ // 2. Let dataForEvent be determined by switching on type and binary type:
+ let dataForEvent
+
+ if (type === opcodes.TEXT) {
+ // -> type indicates that the data is Text
+ // a new DOMString containing data
+ try {
+ dataForEvent = utf8Decode(data)
+ } catch {
+ failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
+ return
+ }
+ } else if (type === opcodes.BINARY) {
+ if (this.#binaryType === 'blob') {
+ // -> type indicates that the data is Binary and binary type is "blob"
+ // a new Blob object, created in the relevant Realm of the WebSocket
+ // object, that represents data as its raw data
+ dataForEvent = new Blob([data])
+ } else {
+ // -> type indicates that the data is Binary and binary type is "arraybuffer"
+ // a new ArrayBuffer object, created in the relevant Realm of the
+ // WebSocket object, whose contents are data
+ dataForEvent = toArrayBuffer(data)
+ }
+ }
+
+ // 3. Fire an event named message at the WebSocket object, using MessageEvent,
+ // with the origin attribute initialized to the serialization of the WebSocket
+ // object’s url's origin, and the data attribute initialized to dataForEvent.
+ fireEvent('message', this, createFastMessageEvent, {
+ origin: this.#url.origin,
+ data: dataForEvent
+ })
+ }
+
+ #onParserDrain () {
+ this.#handler.socket.resume()
+ }
+
+ /**
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
+ */
+ #onSocketClose () {
+ // If the TCP connection was closed after the
+ // WebSocket closing handshake was completed, the WebSocket connection
+ // is said to have been closed _cleanly_.
+ const wasClean =
+ this.#handler.closeState.has(sentCloseFrameState.SENT) &&
+ this.#handler.closeState.has(sentCloseFrameState.RECEIVED)
+
+ let code = 1005
+ let reason = ''
+
+ const result = this.#parser?.closingInfo
+
+ if (result && !result.error) {
+ code = result.code ?? 1005
+ reason = result.reason
+ }
+
+ // 1. Change the ready state to CLOSED (3).
+ this.#handler.readyState = states.CLOSED
+
+ // 2. If the user agent was required to fail the WebSocket
+ // connection, or if the WebSocket connection was closed
+ // after being flagged as full, fire an event named error
+ // at the WebSocket object.
+ if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
+ // If _The WebSocket
+ // Connection is Closed_ and no Close control frame was received by the
+ // endpoint (such as could occur if the underlying transport connection
+ // is lost), _The WebSocket Connection Close Code_ is considered to be
+ // 1006.
+ code = 1006
+
+ fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
+ error: new TypeError(reason)
+ })
+ }
+
+ // 3. Fire an event named close at the WebSocket object,
+ // using CloseEvent, with the wasClean attribute
+ // initialized to true if the connection closed cleanly
+ // and false otherwise, the code attribute initialized to
+ // the WebSocket connection close code, and the reason
+ // attribute initialized to the result of applying UTF-8
+ // decode without BOM to the WebSocket connection close
+ // reason.
+ // TODO: process.nextTick
+ fireEvent('close', this, (type, init) => new CloseEvent(type, init), {
+ wasClean, code, reason
+ })
+
+ if (channels.close.hasSubscribers) {
+ channels.close.publish({
+ websocket: this,
+ code,
+ reason
+ })
+ }
+ }
+
+ /**
+ * @param {WebSocket} ws
+ * @param {Buffer|undefined} buffer
+ */
+ static ping (ws, buffer) {
+ if (Buffer.isBuffer(buffer)) {
+ if (buffer.length > 125) {
+ throw new TypeError('A PING frame cannot have a body larger than 125 bytes.')
+ }
+ } else if (buffer !== undefined) {
+ throw new TypeError('Expected buffer payload')
+ }
+
+ // An endpoint MAY send a Ping frame any time after the connection is
+ // established and before the connection is closed.
+ const readyState = ws.#handler.readyState
+
+ if (isEstablished(readyState) && !isClosing(readyState) && !isClosed(readyState)) {
+ const frame = new WebsocketFrameSend(buffer)
+ ws.#handler.socket.write(frame.createFrame(opcodes.PING))
+ }
+ }
+}
+
+const { ping } = WebSocket
+Reflect.deleteProperty(WebSocket, 'ping')
+
+// https://websockets.spec.whatwg.org/#dom-websocket-connecting
+WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING
+// https://websockets.spec.whatwg.org/#dom-websocket-open
+WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN
+// https://websockets.spec.whatwg.org/#dom-websocket-closing
+WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING
+// https://websockets.spec.whatwg.org/#dom-websocket-closed
+WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED
+
+Object.defineProperties(WebSocket.prototype, {
+ CONNECTING: staticPropertyDescriptors,
+ OPEN: staticPropertyDescriptors,
+ CLOSING: staticPropertyDescriptors,
+ CLOSED: staticPropertyDescriptors,
+ url: kEnumerableProperty,
+ readyState: kEnumerableProperty,
+ bufferedAmount: kEnumerableProperty,
+ onopen: kEnumerableProperty,
+ onerror: kEnumerableProperty,
+ onclose: kEnumerableProperty,
+ close: kEnumerableProperty,
+ onmessage: kEnumerableProperty,
+ binaryType: kEnumerableProperty,
+ send: kEnumerableProperty,
+ extensions: kEnumerableProperty,
+ protocol: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'WebSocket',
+ writable: false,
+ enumerable: false,
+ configurable: true
+ }
+})
+
+Object.defineProperties(WebSocket, {
+ CONNECTING: staticPropertyDescriptors,
+ OPEN: staticPropertyDescriptors,
+ CLOSING: staticPropertyDescriptors,
+ CLOSED: staticPropertyDescriptors
+})
+
+webidl.converters['sequence<DOMString>'] = webidl.sequenceConverter(
+ webidl.converters.DOMString
+)
+
+webidl.converters['DOMString or sequence<DOMString>'] = function (V, prefix, argument) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT && Symbol.iterator in V) {
+ return webidl.converters['sequence<DOMString>'](V)
+ }
+
+ return webidl.converters.DOMString(V, prefix, argument)
+}
+
+// This implements the proposal made in https://github.com/whatwg/websockets/issues/42
+webidl.converters.WebSocketInit = webidl.dictionaryConverter([
+ {
+ key: 'protocols',
+ converter: webidl.converters['DOMString or sequence<DOMString>'],
+ defaultValue: () => []
+ },
+ {
+ key: 'dispatcher',
+ converter: webidl.converters.any,
+ defaultValue: () => getGlobalDispatcher()
+ },
+ {
+ key: 'headers',
+ converter: webidl.nullableConverter(webidl.converters.HeadersInit)
+ }
+])
+
+webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT && !(Symbol.iterator in V)) {
+ return webidl.converters.WebSocketInit(V)
+ }
+
+ return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) }
+}
+
+webidl.converters.WebSocketSendData = function (V) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
+ if (webidl.is.Blob(V)) {
+ return V
+ }
+
+ if (webidl.is.BufferSource(V)) {
+ return V
+ }
+ }
+
+ return webidl.converters.USVString(V)
+}
+
+module.exports = {
+ WebSocket,
+ ping
+}