Comparing version 7.0.0-alpha.6 to 7.0.0-alpha.7
@@ -16,5 +16,17 @@ # Cache Store | ||
- `maxEntries` - The maximum amount of responses to store. Default `Infinity`. | ||
- `maxCount` - The maximum amount of responses to store. Default `Infinity`. | ||
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. | ||
### `SqliteCacheStore` | ||
The `SqliteCacheStore` stores the responses in a SQLite database. | ||
Under the hood, it uses Node.js' [`node:sqlite`](https://nodejs.org/api/sqlite.html) api. | ||
The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present. | ||
**Options** | ||
- `location` - The location of the SQLite database to use. Default `:memory:`. | ||
- `maxCount` - The maximum number of entries to store in the database. Default `Infinity`. | ||
- `maxEntrySize` - The maximum size in bytes that a resposne's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`. | ||
## Defining a Custom Cache Store | ||
@@ -21,0 +33,0 @@ |
@@ -208,10 +208,8 @@ # Dispatcher | ||
* **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. | ||
* **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw. | ||
* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`. | ||
* **onResponseStarted** `() => void` (optional) - Invoked when response is received, before headers have been read. | ||
* **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests. | ||
* **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests. | ||
* **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests. | ||
* **onBodySent** `(chunk: string | Buffer | Uint8Array) => void` - Invoked when a body chunk is sent to the server. Not required. For a stream or iterable body this will be invoked for every chunk. For other body types, it will be invoked once after the body is sent. | ||
* **onRequestStart** `(controller: DispatchController, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. | ||
* **onRequestUpgrade** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string[]>, socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`. | ||
* **onResponseStart** `(controller: DispatchController, statusCode: number, statusMessage?: string, headers: Record<string, string | string []>) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests. | ||
* **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests. | ||
* **onResponseEnd** `(controller: DispatchController, trailers: Record<string, string | string[]>) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests. | ||
* **onResponseError** `(error: Error) => void` - Invoked when an error has occurred. May not throw. | ||
@@ -218,0 +216,0 @@ #### Example 1 - Dispatch GET request |
@@ -19,3 +19,3 @@ # Class: RedirectHandler | ||
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection. | ||
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection. | ||
- **maxRedirections** `number` (required) - Maximum number of redirections allowed. | ||
@@ -22,0 +22,0 @@ - **opts** `object` (required) - Options for handling redirection. |
@@ -46,4 +46,4 @@ # Class: RetryHandler | ||
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry. | ||
- **handler** Extends [`Dispatch.DispatchHandlers`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted. | ||
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry. | ||
- **handler** Extends [`Dispatch.DispatchHandler`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted. | ||
@@ -50,0 +50,0 @@ >__Note__: The `RetryHandler` does not retry over stateful bodies (e.g. streams, AsyncIterable) as those, once consumed, are left in a state that cannot be reutilized. For these situations the `RetryHandler` will identify |
@@ -51,2 +51,11 @@ 'use strict' | ||
try { | ||
const SqliteCacheStore = require('./lib/cache/sqlite-cache-store') | ||
module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore | ||
} catch (err) { | ||
if (err.code !== 'ERR_UNKNOWN_BUILTIN_MODULE') { | ||
throw err | ||
} | ||
} | ||
module.exports.buildConnector = buildConnector | ||
@@ -53,0 +62,0 @@ module.exports.errors = errors |
'use strict' | ||
const { Writable } = require('node:stream') | ||
const { assertCacheKey, assertCacheValue } = require('../util/cache.js') | ||
@@ -73,5 +74,3 @@ /** | ||
get (key) { | ||
if (typeof key !== 'object') { | ||
throw new TypeError(`expected key to be object, got ${typeof key}`) | ||
} | ||
assertCacheKey(key) | ||
@@ -92,3 +91,3 @@ const topLevelKey = `${key.origin}:${key.path}` | ||
statusCode: entry.statusCode, | ||
rawHeaders: entry.rawHeaders, | ||
headers: entry.headers, | ||
body: entry.body, | ||
@@ -108,8 +107,4 @@ etag: entry.etag, | ||
createWriteStream (key, val) { | ||
if (typeof key !== 'object') { | ||
throw new TypeError(`expected key to be object, got ${typeof key}`) | ||
} | ||
if (typeof val !== 'object') { | ||
throw new TypeError(`expected value to be object, got ${typeof val}`) | ||
} | ||
assertCacheKey(key) | ||
assertCacheValue(val) | ||
@@ -116,0 +111,0 @@ const topLevelKey = `${key.origin}:${key.path}` |
@@ -514,2 +514,7 @@ 'use strict' | ||
if (typeof handler.onRequestStart === 'function') { | ||
// TODO (fix): More checks... | ||
return | ||
} | ||
if (typeof handler.onConnect !== 'function') { | ||
@@ -516,0 +521,0 @@ throw new InvalidArgumentError('invalid onConnect method') |
@@ -772,6 +772,17 @@ 'use strict' | ||
if (!llhttpInstance) { | ||
const noop = () => {} | ||
socket.on('error', noop) | ||
llhttpInstance = await llhttpPromise | ||
llhttpPromise = null | ||
socket.off('error', noop) | ||
} | ||
if (socket.errored) { | ||
throw socket.errored | ||
} | ||
if (socket.destroyed) { | ||
throw new SocketError('destroyed') | ||
} | ||
socket[kNoRef] = false | ||
@@ -778,0 +789,0 @@ socket[kWriting] = false |
@@ -35,2 +35,4 @@ 'use strict' | ||
let extractBody | ||
// Experimental | ||
@@ -205,8 +207,9 @@ let h2ExperimentalWarned = false | ||
// Fail head of pipeline. | ||
const request = client[kQueue][client[kRunningIdx]] | ||
client[kQueue][client[kRunningIdx]++] = null | ||
util.errorRequest(client, request, err) | ||
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] | ||
} | ||
client[kPendingIdx] = client[kRunningIdx] | ||
assert(client[kRunning] === 0) | ||
@@ -284,3 +287,4 @@ | ||
const session = client[kHTTP2Session] | ||
const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request | ||
const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request | ||
let { body } = request | ||
@@ -413,2 +417,12 @@ if (upgrade) { | ||
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) { | ||
@@ -415,0 +429,0 @@ contentLength = request.contentLength |
'use strict' | ||
const Dispatcher = require('./dispatcher') | ||
const UnwrapHandler = require('../handler/unwrap-handler') | ||
const { | ||
@@ -145,3 +146,3 @@ ClientDestroyedError, | ||
return this[kDispatch](opts, handler) | ||
return this[kDispatch](opts, UnwrapHandler.unwrap(handler)) | ||
} catch (err) { | ||
@@ -148,0 +149,0 @@ if (typeof handler.onError !== 'function') { |
'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 { | ||
@@ -32,2 +35,3 @@ dispatch () { | ||
dispatch = interceptor(dispatch) | ||
dispatch = wrapInterceptor(dispatch) | ||
@@ -34,0 +38,0 @@ if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) { |
'use strict' | ||
const util = require('../core/util') | ||
const DecoratorHandler = require('../handler/decorator-handler') | ||
const { | ||
@@ -14,5 +13,5 @@ parseCacheControlHeader, | ||
/** | ||
* Writes a response to a CacheStore and then passes it on to the next handler | ||
* @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler} | ||
*/ | ||
class CacheHandler extends DecoratorHandler { | ||
class CacheHandler { | ||
/** | ||
@@ -29,3 +28,3 @@ * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} | ||
/** | ||
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers} | ||
* @type {import('../../types/dispatcher.d.ts').default.DispatchHandler} | ||
*/ | ||
@@ -40,5 +39,5 @@ #handler | ||
/** | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts | ||
* @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.DispatchHandlers} handler | ||
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler | ||
*/ | ||
@@ -48,4 +47,2 @@ constructor (opts, cacheKey, handler) { | ||
super(handler) | ||
this.#store = store | ||
@@ -56,40 +53,25 @@ this.#cacheKey = cacheKey | ||
onConnect (abort) { | ||
if (this.#writeStream) { | ||
this.#writeStream.destroy() | ||
this.#writeStream = undefined | ||
} | ||
onRequestStart (controller, context) { | ||
this.#writeStream?.destroy() | ||
this.#writeStream = undefined | ||
this.#handler.onRequestStart?.(controller, context) | ||
} | ||
if (typeof this.#handler.onConnect === 'function') { | ||
this.#handler.onConnect(abort) | ||
} | ||
onRequestUpgrade (controller, statusCode, headers, socket) { | ||
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket) | ||
} | ||
/** | ||
* @see {DispatchHandlers.onHeaders} | ||
* | ||
* @param {number} statusCode | ||
* @param {Buffer[]} rawHeaders | ||
* @param {() => void} resume | ||
* @param {string} statusMessage | ||
* @returns {boolean} | ||
*/ | ||
onHeaders ( | ||
onResponseStart ( | ||
controller, | ||
statusCode, | ||
rawHeaders, | ||
resume, | ||
statusMessage | ||
statusMessage, | ||
headers | ||
) { | ||
const downstreamOnHeaders = () => { | ||
if (typeof this.#handler.onHeaders === 'function') { | ||
return this.#handler.onHeaders( | ||
statusCode, | ||
rawHeaders, | ||
resume, | ||
statusMessage | ||
) | ||
} else { | ||
return true | ||
} | ||
} | ||
const downstreamOnHeaders = () => | ||
this.#handler.onResponseStart?.( | ||
controller, | ||
statusCode, | ||
statusMessage, | ||
headers | ||
) | ||
@@ -110,17 +92,8 @@ if ( | ||
const parsedRawHeaders = util.parseRawHeaders(rawHeaders) | ||
const headers = util.parseHeaders(parsedRawHeaders) | ||
const cacheControlHeader = headers['cache-control'] | ||
const isCacheFull = typeof this.#store.isFull !== 'undefined' | ||
? this.#store.isFull | ||
: false | ||
if ( | ||
!cacheControlHeader || | ||
isCacheFull | ||
) { | ||
if (!cacheControlHeader) { | ||
// Don't have the cache control header or the cache is full | ||
return downstreamOnHeaders() | ||
} | ||
const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader) | ||
@@ -139,7 +112,3 @@ if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) { | ||
const strippedHeaders = stripNecessaryHeaders( | ||
rawHeaders, | ||
parsedRawHeaders, | ||
cacheControlDirectives | ||
) | ||
const strippedHeaders = stripNecessaryHeaders(headers, cacheControlDirectives) | ||
@@ -152,3 +121,3 @@ /** | ||
statusMessage, | ||
rawHeaders: strippedHeaders, | ||
headers: strippedHeaders, | ||
vary: varyDirectives, | ||
@@ -169,3 +138,3 @@ cachedAt: now, | ||
this.#writeStream | ||
.on('drain', resume) | ||
.on('drain', () => controller.resume()) | ||
.on('error', function () { | ||
@@ -180,3 +149,3 @@ // TODO (fix): Make error somehow observable? | ||
// TODO (fix): Should we resume even if was paused downstream? | ||
resume() | ||
controller.resume() | ||
}) | ||
@@ -189,51 +158,19 @@ } | ||
/** | ||
* @see {DispatchHandlers.onData} | ||
* | ||
* @param {Buffer} chunk | ||
* @returns {boolean} | ||
*/ | ||
onData (chunk) { | ||
let paused = false | ||
if (this.#writeStream) { | ||
paused ||= this.#writeStream.write(chunk) === false | ||
onResponseData (controller, chunk) { | ||
if (this.#writeStream?.write(chunk) === false) { | ||
controller.pause() | ||
} | ||
if (typeof this.#handler.onData === 'function') { | ||
paused ||= this.#handler.onData(chunk) === false | ||
} | ||
return !paused | ||
this.#handler.onResponseData?.(controller, chunk) | ||
} | ||
/** | ||
* @see {DispatchHandlers.onComplete} | ||
* | ||
* @param {string[] | null} rawTrailers | ||
*/ | ||
onComplete (rawTrailers) { | ||
if (this.#writeStream) { | ||
this.#writeStream.end() | ||
} | ||
if (typeof this.#handler.onComplete === 'function') { | ||
return this.#handler.onComplete(rawTrailers) | ||
} | ||
onResponseEnd (controller, trailers) { | ||
this.#writeStream?.end() | ||
this.#handler.onResponseEnd?.(controller, trailers) | ||
} | ||
/** | ||
* @see {DispatchHandlers.onError} | ||
* | ||
* @param {Error} err | ||
*/ | ||
onError (err) { | ||
if (this.#writeStream) { | ||
this.#writeStream.destroy(err) | ||
this.#writeStream = undefined | ||
} | ||
if (typeof this.#handler.onError === 'function') { | ||
this.#handler.onError(err) | ||
} | ||
onResponseError (controller, err) { | ||
this.#writeStream?.destroy(err) | ||
this.#writeStream = undefined | ||
this.#handler.onResponseError?.(controller, err) | ||
} | ||
@@ -250,6 +187,3 @@ } | ||
function canCacheResponse (statusCode, headers, cacheControlDirectives) { | ||
if ( | ||
statusCode !== 200 && | ||
statusCode !== 307 | ||
) { | ||
if (statusCode !== 200 && statusCode !== 307) { | ||
return false | ||
@@ -324,3 +258,3 @@ } | ||
const expiresDate = new Date(headers.expire) | ||
if (expiresDate instanceof Date && !isNaN(expiresDate)) { | ||
if (expiresDate instanceof Date && Number.isFinite(expiresDate.valueOf())) { | ||
return now + (Date.now() - expiresDate.getTime()) | ||
@@ -348,8 +282,7 @@ } | ||
* Strips headers required to be removed in cached responses | ||
* @param {Buffer[]} rawHeaders | ||
* @param {string[]} parsedRawHeaders | ||
* @param {Record<string, string | string[]>} headers | ||
* @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives | ||
* @returns {Buffer[]} | ||
* @returns {Record<string, string | string []>} | ||
*/ | ||
function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirectives) { | ||
function stripNecessaryHeaders (headers, cacheControlDirectives) { | ||
const headersToRemove = ['connection'] | ||
@@ -366,49 +299,11 @@ | ||
let strippedHeaders | ||
let offset = 0 | ||
for (let i = 0; i < parsedRawHeaders.length; i += 2) { | ||
const headerName = parsedRawHeaders[i] | ||
for (const headerName of Object.keys(headers)) { | ||
if (headersToRemove.includes(headerName)) { | ||
// We have at least one header we want to remove | ||
if (!strippedHeaders) { | ||
// This is the first header we want to remove, let's create the array | ||
// Since we're stripping headers, this will over allocate. We'll trim | ||
// it later. | ||
strippedHeaders = new Array(parsedRawHeaders.length) | ||
// Backfill the previous headers into it | ||
for (let j = 0; j < i; j += 2) { | ||
strippedHeaders[j] = parsedRawHeaders[j] | ||
strippedHeaders[j + 1] = parsedRawHeaders[j + 1] | ||
} | ||
} | ||
// We can't map indices 1:1 from stripped headers to rawHeaders without | ||
// creating holes (if we skip a header, we now have two holes where at | ||
// element should be). So, let's keep an offset to keep strippedHeaders | ||
// flattened. We can also use this at the end for trimming the empty | ||
// elements off of strippedHeaders. | ||
offset += 2 | ||
continue | ||
strippedHeaders ??= { ...headers } | ||
delete headers[headerName] | ||
} | ||
// We want to keep this header. Let's add it to strippedHeaders if it exists | ||
if (strippedHeaders) { | ||
strippedHeaders[i - offset] = parsedRawHeaders[i] | ||
strippedHeaders[i + 1 - offset] = parsedRawHeaders[i + 1] | ||
} | ||
} | ||
if (strippedHeaders) { | ||
// Trim off the empty values at the end | ||
strippedHeaders.length -= offset | ||
} | ||
return strippedHeaders | ||
? util.encodeRawHeaders(strippedHeaders) | ||
: rawHeaders | ||
return strippedHeaders ?? headers | ||
} | ||
module.exports = CacheHandler |
'use strict' | ||
const assert = require('node:assert') | ||
const DecoratorHandler = require('../handler/decorator-handler') | ||
@@ -17,21 +16,20 @@ /** | ||
* | ||
* @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers | ||
* @implements {DispatchHandlers} | ||
* @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler} | ||
*/ | ||
class CacheRevalidationHandler extends DecoratorHandler { | ||
class CacheRevalidationHandler { | ||
#successful = false | ||
/** | ||
* @type {((boolean) => void) | null} | ||
* @type {((boolean, any) => void) | null} | ||
*/ | ||
#callback | ||
/** | ||
* @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)} | ||
* @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)} | ||
*/ | ||
#handler | ||
#abort | ||
#context | ||
/** | ||
* @param {(boolean) => void} callback Function to call if the cached value is valid | ||
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler | ||
* @param {(boolean, any) => void} callback Function to call if the cached value is valid | ||
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler | ||
*/ | ||
@@ -43,4 +41,2 @@ constructor (callback, handler) { | ||
super(handler) | ||
this.#callback = callback | ||
@@ -50,21 +46,16 @@ this.#handler = handler | ||
onConnect (abort) { | ||
onRequestStart (controller, context) { | ||
this.#successful = false | ||
this.#abort = abort | ||
this.#context = context | ||
} | ||
/** | ||
* @see {DispatchHandlers.onHeaders} | ||
* | ||
* @param {number} statusCode | ||
* @param {Buffer[]} rawHeaders | ||
* @param {() => void} resume | ||
* @param {string} statusMessage | ||
* @returns {boolean} | ||
*/ | ||
onHeaders ( | ||
onRequestUpgrade (controller, statusCode, headers, socket) { | ||
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket) | ||
} | ||
onResponseStart ( | ||
controller, | ||
statusCode, | ||
rawHeaders, | ||
resume, | ||
statusMessage | ||
statusMessage, | ||
headers | ||
) { | ||
@@ -75,3 +66,3 @@ assert(this.#callback != null) | ||
this.#successful = statusCode === 304 | ||
this.#callback(this.#successful) | ||
this.#callback(this.#successful, this.#context) | ||
this.#callback = null | ||
@@ -83,42 +74,20 @@ | ||
if (typeof this.#handler.onConnect === 'function') { | ||
this.#handler.onConnect(this.#abort) | ||
} | ||
if (typeof this.#handler.onHeaders === 'function') { | ||
return this.#handler.onHeaders( | ||
statusCode, | ||
rawHeaders, | ||
resume, | ||
statusMessage | ||
) | ||
} | ||
return true | ||
this.#handler.onRequestStart?.(controller, this.#context) | ||
this.#handler.onResponseStart?.( | ||
controller, | ||
statusCode, | ||
statusMessage, | ||
headers | ||
) | ||
} | ||
/** | ||
* @see {DispatchHandlers.onData} | ||
* | ||
* @param {Buffer} chunk | ||
* @returns {boolean} | ||
*/ | ||
onData (chunk) { | ||
onResponseData (controller, chunk) { | ||
if (this.#successful) { | ||
return true | ||
return | ||
} | ||
if (typeof this.#handler.onData === 'function') { | ||
return this.#handler.onData(chunk) | ||
} | ||
return true | ||
return this.#handler.onResponseData(controller, chunk) | ||
} | ||
/** | ||
* @see {DispatchHandlers.onComplete} | ||
* | ||
* @param {string[] | null} rawTrailers | ||
*/ | ||
onComplete (rawTrailers) { | ||
onResponseEnd (controller, trailers) { | ||
if (this.#successful) { | ||
@@ -128,13 +97,6 @@ return | ||
if (typeof this.#handler.onComplete === 'function') { | ||
this.#handler.onComplete(rawTrailers) | ||
} | ||
this.#handler.onResponseEnd?.(controller, trailers) | ||
} | ||
/** | ||
* @see {DispatchHandlers.onError} | ||
* | ||
* @param {Error} err | ||
*/ | ||
onError (err) { | ||
onResponseError (controller, err) { | ||
if (this.#successful) { | ||
@@ -149,4 +111,4 @@ return | ||
if (typeof this.#handler.onError === 'function') { | ||
this.#handler.onError(err) | ||
if (typeof this.#handler.onResponseError === 'function') { | ||
this.#handler.onResponseError(controller, err) | ||
} else { | ||
@@ -153,0 +115,0 @@ throw err |
@@ -13,2 +13,4 @@ 'use strict' | ||
const noop = () => {} | ||
class BodyAsyncIterable { | ||
@@ -42,4 +44,2 @@ constructor (body) { | ||
util.assertRequestHandler(handler, opts.method, opts.upgrade) | ||
this.dispatch = dispatch | ||
@@ -52,3 +52,2 @@ this.location = null | ||
this.history = [] | ||
this.redirectionLimitReached = false | ||
@@ -103,17 +102,32 @@ if (util.isStream(this.opts.body)) { | ||
onHeaders (statusCode, headers, resume, statusText) { | ||
this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) | ||
? null | ||
: parseLocation(statusCode, headers) | ||
onHeaders (statusCode, rawHeaders, resume, statusText) { | ||
if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) { | ||
throw new Error('max redirects') | ||
} | ||
if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) { | ||
if (this.request) { | ||
this.request.abort(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 | ||
} | ||
this.redirectionLimitReached = true | ||
this.abort(new Error('max redirects')) | ||
return | ||
// 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) | ||
? null | ||
: parseLocation(statusCode, rawHeaders) | ||
if (this.opts.origin) { | ||
@@ -124,3 +138,3 @@ this.history.push(new URL(this.opts.path, this.opts.origin)) | ||
if (!this.location) { | ||
return this.handler.onHeaders(statusCode, headers, resume, statusText) | ||
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText) | ||
} | ||
@@ -139,9 +153,2 @@ | ||
this.opts.query = 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' | ||
this.opts.body = null | ||
} | ||
} | ||
@@ -200,3 +207,3 @@ | ||
function parseLocation (statusCode, headers) { | ||
function parseLocation (statusCode, rawHeaders) { | ||
if (redirectableStatusCodes.indexOf(statusCode) === -1) { | ||
@@ -206,5 +213,5 @@ return null | ||
for (let i = 0; i < headers.length; i += 2) { | ||
if (headers[i].length === 8 && util.headerNameToString(headers[i]) === 'location') { | ||
return headers[i + 1] | ||
for (let i = 0; i < rawHeaders.length; i += 2) { | ||
if (rawHeaders[i].length === 8 && util.headerNameToString(rawHeaders[i]) === 'location') { | ||
return rawHeaders[i + 1] | ||
} | ||
@@ -211,0 +218,0 @@ } |
@@ -40,2 +40,3 @@ 'use strict' | ||
this.aborted = false | ||
this.connectCalled = false | ||
this.retryOpts = { | ||
@@ -72,12 +73,2 @@ retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], | ||
this.resume = null | ||
// Handle possible onConnect duplication | ||
this.handler.onConnect(reason => { | ||
this.aborted = true | ||
if (this.abort) { | ||
this.abort(reason) | ||
} else { | ||
this.reason = reason | ||
} | ||
}) | ||
} | ||
@@ -97,7 +88,10 @@ | ||
onConnect (abort) { | ||
if (this.aborted) { | ||
abort(this.reason) | ||
} else { | ||
this.abort = abort | ||
onConnect (abort, context) { | ||
this.abort = abort | ||
if (!this.connectCalled) { | ||
this.connectCalled = true | ||
this.handler.onConnect(reason => { | ||
this.aborted = true | ||
this.abort(reason) | ||
}, context) | ||
} | ||
@@ -104,0 +98,0 @@ } |
@@ -10,7 +10,6 @@ 'use strict' | ||
const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js') | ||
const { AbortError } = require('../core/errors.js') | ||
const AGE_HEADER = Buffer.from('age') | ||
/** | ||
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler | ||
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler | ||
*/ | ||
@@ -152,3 +151,3 @@ function sendGatewayTimeout (handler) { | ||
*/ | ||
const respondWithCachedValue = ({ rawHeaders, statusCode, statusMessage, body }, age) => { | ||
const respondWithCachedValue = ({ headers, statusCode, statusMessage, body }, age, context) => { | ||
const stream = util.isStream(body) | ||
@@ -161,7 +160,28 @@ ? body | ||
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.onError === 'function') { | ||
handler.onError(err) | ||
if (typeof handler.onResponseError === 'function') { | ||
handler.onResponseError(controller, err) | ||
} else { | ||
@@ -173,30 +193,20 @@ throw err | ||
.on('close', function () { | ||
if (!this.errored && typeof handler.onComplete === 'function') { | ||
handler.onComplete([]) | ||
if (!this.errored) { | ||
handler.onResponseEnd?.(controller, {}) | ||
} | ||
}) | ||
if (typeof handler.onConnect === 'function') { | ||
handler.onConnect((err) => { | ||
stream.destroy(err) | ||
}) | ||
handler.onRequestStart?.(controller, context) | ||
if (stream.destroyed) { | ||
return | ||
} | ||
if (stream.destroyed) { | ||
return | ||
} | ||
if (typeof handler.onHeaders === 'function') { | ||
// Add the age header | ||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age | ||
const age = Math.round((Date.now() - result.cachedAt) / 1000) | ||
// Add the age header | ||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age | ||
// TODO (fix): What if headers.age already exists? | ||
headers = age != null ? { ...headers, age: String(age) } : headers | ||
// TODO (fix): What if rawHeaders already contains age header? | ||
rawHeaders = [...rawHeaders, AGE_HEADER, Buffer.from(`${age}`)] | ||
handler.onResponseStart?.(controller, statusCode, statusMessage, headers) | ||
if (handler.onHeaders(statusCode, rawHeaders, () => stream?.resume(), statusMessage) === false) { | ||
stream.pause() | ||
} | ||
} | ||
if (opts.method === 'HEAD') { | ||
@@ -206,5 +216,3 @@ stream.destroy() | ||
stream.on('data', function (chunk) { | ||
if (typeof handler.onData === 'function' && !handler.onData(chunk)) { | ||
stream.pause() | ||
} | ||
handler.onResponseData?.(controller, chunk) | ||
}) | ||
@@ -250,5 +258,5 @@ } | ||
new CacheRevalidationHandler( | ||
(success) => { | ||
(success, context) => { | ||
if (success) { | ||
respondWithCachedValue(result, age) | ||
respondWithCachedValue(result, age, context) | ||
} else if (util.isStream(result.body)) { | ||
@@ -267,3 +275,4 @@ result.body.on('error', () => {}).destroy() | ||
} | ||
respondWithCachedValue(result, age) | ||
respondWithCachedValue(result, age, null) | ||
} | ||
@@ -270,0 +279,0 @@ |
@@ -16,16 +16,83 @@ 'use strict' | ||
/** | ||
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} | ||
*/ | ||
const cacheKey = { | ||
/** @type {Record<string, string[] | string>} */ | ||
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] = val | ||
} | ||
} else if (typeof opts.headers === 'object') { | ||
headers = opts.headers | ||
} else { | ||
throw new Error('opts.headers is not an object') | ||
} | ||
return { | ||
origin: opts.origin.toString(), | ||
method: opts.method, | ||
path: opts.path, | ||
headers: opts.headers | ||
headers | ||
} | ||
} | ||
return cacheKey | ||
/** | ||
* @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 | ||
@@ -253,6 +320,2 @@ * @see https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml | ||
} | ||
if (typeof store.isFull !== 'undefined' && typeof store.isFull !== 'boolean') { | ||
throw new TypeError(`${name} needs a isFull getter with type boolean or undefined, current type: ${typeof store.isFull}`) | ||
} | ||
} | ||
@@ -281,2 +344,4 @@ /** | ||
makeCacheKey, | ||
assertCacheKey, | ||
assertCacheValue, | ||
parseCacheControlHeader, | ||
@@ -283,0 +348,0 @@ parseVaryHeader, |
{ | ||
"name": "undici", | ||
"version": "7.0.0-alpha.6", | ||
"version": "7.0.0-alpha.7", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -5,0 +5,0 @@ "homepage": "https://undici.nodejs.org", |
@@ -14,3 +14,3 @@ import { URL } from 'url' | ||
/** Dispatches a request. */ | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean | ||
} | ||
@@ -17,0 +17,0 @@ |
@@ -8,2 +8,6 @@ import { Readable, Writable } from 'node:stream' | ||
export interface CacheHandlerOptions { | ||
store: CacheStore | ||
} | ||
export interface CacheOptions { | ||
@@ -32,3 +36,3 @@ store?: CacheStore | ||
statusMessage: string | ||
rawHeaders: Buffer[] | ||
headers: Record<string, string | string[]> | ||
vary?: Record<string, string | string[]> | ||
@@ -50,3 +54,4 @@ etag?: string | ||
statusMessage: string | ||
rawHeaders: Buffer[] | ||
headers: Record<string, string | string[]> | ||
etag?: string | ||
body: null | Readable | Iterable<Buffer> | AsyncIterable<Buffer> | Buffer | Iterable<string> | AsyncIterable<string> | string | ||
@@ -76,9 +81,9 @@ cachedAt: number | ||
/** | ||
* @default Infinity | ||
*/ | ||
* @default Infinity | ||
*/ | ||
maxSize?: number | ||
/** | ||
* @default Infinity | ||
*/ | ||
* @default Infinity | ||
*/ | ||
maxEntrySize?: number | ||
@@ -98,2 +103,35 @@ | ||
} | ||
export interface SqliteCacheStoreOpts { | ||
/** | ||
* Location of the database | ||
* @default ':memory:' | ||
*/ | ||
location?: string | ||
/** | ||
* @default Infinity | ||
*/ | ||
maxCount?: number | ||
/** | ||
* @default Infinity | ||
*/ | ||
maxEntrySize?: number | ||
} | ||
export class SqliteCacheStore implements CacheStore { | ||
constructor (opts?: SqliteCacheStoreOpts) | ||
/** | ||
* Closes the connection to the database | ||
*/ | ||
close (): void | ||
get (key: CacheKey): GetResult | Promise<GetResult | undefined> | undefined | ||
createWriteStream (key: CacheKey, value: CacheValue): Writable | undefined | ||
delete (key: CacheKey): void | Promise<void> | ||
} | ||
} |
@@ -18,3 +18,3 @@ import { URL } from 'url' | ||
/** Dispatches a request. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this. */ | ||
dispatch (options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean | ||
dispatch (options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean | ||
/** Starts two-way communications with the requested resource. */ | ||
@@ -107,3 +107,3 @@ connect<TOpaque = null>(options: Dispatcher.ConnectOptions<TOpaque>): Promise<Dispatcher.ConnectData<TOpaque>> | ||
/** Default: `null` */ | ||
headers?: IncomingHttpHeaders | string[] | Iterable<[string, string | string[] | undefined]> | null; | ||
headers?: Record<string, string | string[]> | IncomingHttpHeaders | string[] | Iterable<[string, string | string[] | undefined]> | null; | ||
/** Query string params to be embedded in the request URL. Default: `null` */ | ||
@@ -218,18 +218,43 @@ query?: Record<string, any>; | ||
export type StreamFactory<TOpaque = null> = (data: StreamFactoryData<TOpaque>) => Writable | ||
export interface DispatchHandlers { | ||
export interface DispatchController { | ||
get aborted () : boolean | ||
get paused () : boolean | ||
get reason () : Error | null | ||
abort (reason: Error): void | ||
pause(): void | ||
resume(): void | ||
} | ||
export interface DispatchHandler { | ||
onRequestStart?(controller: DispatchController, context: any): void; | ||
onRequestUpgrade?(controller: DispatchController, statusCode: number, headers: IncomingHttpHeaders, socket: Duplex): void; | ||
onResponseStart?(controller: DispatchController, statusCode: number, statusMessage: string | null, headers: IncomingHttpHeaders): void; | ||
onResponseData?(controller: DispatchController, chunk: Buffer): void; | ||
onResponseEnd?(controller: DispatchController, trailers: IncomingHttpHeaders): void; | ||
onResponseError?(controller: DispatchController, error: Error): void; | ||
/** Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. */ | ||
/** @deprecated */ | ||
onConnect?(abort: (err?: Error) => void): void; | ||
/** Invoked when an error has occurred. */ | ||
/** @deprecated */ | ||
onError?(err: Error): void; | ||
/** Invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method. */ | ||
/** @deprecated */ | ||
onUpgrade?(statusCode: number, headers: Buffer[] | string[] | null, socket: Duplex): void; | ||
/** Invoked when response is received, before headers have been read. **/ | ||
/** @deprecated */ | ||
onResponseStarted?(): void; | ||
/** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */ | ||
/** @deprecated */ | ||
onHeaders?(statusCode: number, headers: Buffer[], resume: () => void, statusText: string): boolean; | ||
/** Invoked when response payload data is received. */ | ||
/** @deprecated */ | ||
onData?(chunk: Buffer): boolean; | ||
/** Invoked when response payload and trailers have been received and the request has completed. */ | ||
/** @deprecated */ | ||
onComplete?(trailers: string[] | null): void; | ||
/** Invoked when a body chunk is sent to the server. May be invoked multiple times for chunked requests */ | ||
/** @deprecated */ | ||
onBodySent?(chunkSize: number, totalBytesSent: number): void; | ||
@@ -236,0 +261,0 @@ } |
@@ -9,3 +9,3 @@ import Agent from './agent' | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean | ||
} | ||
@@ -12,0 +12,0 @@ |
import Dispatcher from './dispatcher' | ||
export declare class RedirectHandler implements Dispatcher.DispatchHandlers { | ||
export declare class RedirectHandler implements Dispatcher.DispatchHandler { | ||
constructor ( | ||
@@ -8,3 +8,3 @@ dispatch: Dispatcher, | ||
opts: Dispatcher.DispatchOptions, | ||
handler: Dispatcher.DispatchHandlers, | ||
handler: Dispatcher.DispatchHandler, | ||
redirectionLimitReached: boolean | ||
@@ -14,4 +14,4 @@ ) | ||
export declare class DecoratorHandler implements Dispatcher.DispatchHandlers { | ||
constructor (handler: Dispatcher.DispatchHandlers) | ||
export declare class DecoratorHandler implements Dispatcher.DispatchHandler { | ||
constructor (handler: Dispatcher.DispatchHandler) | ||
} |
@@ -20,3 +20,3 @@ import Agent from './agent' | ||
/** Dispatches a mocked request. */ | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean | ||
/** Closes the mock agent and waits for registered mock pools and clients to also close before resolving. */ | ||
@@ -23,0 +23,0 @@ close (): Promise<void> |
@@ -14,3 +14,3 @@ import Client from './client' | ||
/** Dispatches a mocked request. */ | ||
dispatch (options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean | ||
dispatch (options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandler): boolean | ||
/** Closes the mock client and gracefully waits for enqueued requests to complete. */ | ||
@@ -17,0 +17,0 @@ close (): Promise<void> |
@@ -14,3 +14,3 @@ import Pool from './pool' | ||
/** Dispatches a mocked request. */ | ||
dispatch (options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandlers): boolean | ||
dispatch (options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandler): boolean | ||
/** Closes the mock pool and gracefully waits for enqueued requests to complete. */ | ||
@@ -17,0 +17,0 @@ close (): Promise<void> |
@@ -11,3 +11,3 @@ import Agent from './agent' | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean | ||
dispatch (options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean | ||
close (): Promise<void> | ||
@@ -14,0 +14,0 @@ } |
@@ -5,3 +5,3 @@ import Dispatcher from './dispatcher' | ||
declare class RetryHandler implements Dispatcher.DispatchHandlers { | ||
declare class RetryHandler implements Dispatcher.DispatchHandler { | ||
constructor ( | ||
@@ -36,3 +36,3 @@ options: Dispatcher.DispatchOptions & { | ||
callback: OnRetryCallback | ||
) => number | null | ||
) => void | ||
@@ -116,4 +116,4 @@ export interface RetryOptions { | ||
dispatch: Dispatcher['dispatch']; | ||
handler: Dispatcher.DispatchHandlers; | ||
handler: Dispatcher.DispatchHandler; | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1281162
179
27625