Comparing version 7.0.0-alpha.4 to 7.0.0-alpha.5
@@ -19,3 +19,3 @@ # Agent | ||
Extends: [`PoolOptions`](Pool.md#parameter-pooloptions) | ||
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions) | ||
@@ -28,7 +28,7 @@ * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)` | ||
Implements [Client.closed](Client.md#clientclosed) | ||
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed) | ||
### `Agent.destroyed` | ||
Implements [Client.destroyed](Client.md#clientdestroyed) | ||
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) | ||
@@ -39,42 +39,42 @@ ## Instance Methods | ||
Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise). | ||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise). | ||
### `Agent.destroy([error, callback])` | ||
Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
### `Agent.dispatch(options, handler: AgentDispatchOptions)` | ||
Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
#### Parameter: `AgentDispatchOptions` | ||
Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions) | ||
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions) | ||
* **origin** `string | URL` | ||
Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
### `Agent.connect(options[, callback])` | ||
See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). | ||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback). | ||
### `Agent.dispatch(options, handler)` | ||
Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
### `Agent.pipeline(options, handler)` | ||
See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). | ||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler). | ||
### `Agent.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
### `Agent.stream(options, factory[, callback])` | ||
See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
### `Agent.upgrade(options[, callback])` | ||
See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). | ||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback). |
@@ -61,29 +61,29 @@ # Client Lifecycle | ||
The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](Client.md#clientconnectoptions-callback), [`Client.pipeline()`](Client.md#clientpipelineoptions-handler), [`Client.request()`](Client.md#clientrequestoptions-callback), [`Client.stream()`](Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](#pending) and then most likely directly to [**processing**](#processing). | ||
The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](Client.md#clientconnectoptions-callback), [`Client.pipeline()`](Client.md#clientpipelineoptions-handler), [`Client.request()`](Client.md#clientrequestoptions-callback), [`Client.stream()`](Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](/docs/docs/api/Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](/docs/docs/api/Client.md#pending) and then most likely directly to [**processing**](/docs/docs/api/Client.md#processing). | ||
Calling [`Client.close()`](Client.md#clientclosecallback) or [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](#destroyed) state since the `Client` instance will have no queued requests in this state. | ||
Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) or [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state since the `Client` instance will have no queued requests in this state. | ||
### pending | ||
The **pending** state signifies a non-processing `Client`. Upon entering this state, the `Client` establishes a socket connection and emits the [`'connect'`](Client.md#event-connect) event signalling a connection was successfully established with the `origin` provided during `Client` instantiation. The internal queue is initially empty, and requests can start queueing. | ||
The **pending** state signifies a non-processing `Client`. Upon entering this state, the `Client` establishes a socket connection and emits the [`'connect'`](/docs/docs/api/Client.md#event-connect) event signalling a connection was successfully established with the `origin` provided during `Client` instantiation. The internal queue is initially empty, and requests can start queueing. | ||
Calling [`Client.close()`](Client.md#clientclosecallback) with queued requests, transitions the `Client` to the [**processing**](#processing) state. Without queued requests, it transitions to the [**destroyed**](#destroyed) state. | ||
Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) with queued requests, transitions the `Client` to the [**processing**](/docs/docs/api/Client.md#processing) state. Without queued requests, it transitions to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state. | ||
Calling [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](#destroyed) state regardless of existing requests. | ||
Calling [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state regardless of existing requests. | ||
### processing | ||
The **processing** state is a state machine within itself. It initializes to the [**processing.running**](#running) state. The [`Client.dispatch()`](Client.md#clientdispatchoptions-handlers), [`Client.close()`](Client.md#clientclosecallback), and [`Client.destroy()`](Client.md#clientdestroyerror-callback) can be called at any time while the `Client` is in this state. `Client.dispatch()` will add more requests to the queue while existing requests continue to be processed. `Client.close()` will transition to the [**processing.closing**](#closing) state. And `Client.destroy()` will transition to [**destroyed**](#destroyed). | ||
The **processing** state is a state machine within itself. It initializes to the [**processing.running**](/docs/docs/api/Client.md#running) state. The [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers), [`Client.close()`](Client.md#clientclosecallback), and [`Client.destroy()`](Client.md#clientdestroyerror-callback) can be called at any time while the `Client` is in this state. `Client.dispatch()` will add more requests to the queue while existing requests continue to be processed. `Client.close()` will transition to the [**processing.closing**](/docs/docs/api/Client.md#closing) state. And `Client.destroy()` will transition to [**destroyed**](/docs/docs/api/Client.md#destroyed). | ||
#### running | ||
In the **processing.running** sub-state, queued requests are being processed in a FIFO order. If a request body requires draining, the *needDrain* event transitions to the [**processing.busy**](#busy) sub-state. The *close* event transitions the Client to the [**process.closing**](#closing) sub-state. If all queued requests are processed and neither [`Client.close()`](Client.md#clientclosecallback) nor [`Client.destroy()`](Client.md#clientdestroyerror-callback) are called, then the [**processing**](#processing) machine will trigger a *keepalive* event transitioning the `Client` back to the [**pending**](#pending) state. During this time, the `Client` is waiting for the socket connection to timeout, and once it does, it triggers the *timeout* event and transitions to the [**idle**](#idle) state. | ||
In the **processing.running** sub-state, queued requests are being processed in a FIFO order. If a request body requires draining, the *needDrain* event transitions to the [**processing.busy**](/docs/docs/api/Client.md#busy) sub-state. The *close* event transitions the Client to the [**process.closing**](/docs/docs/api/Client.md#closing) sub-state. If all queued requests are processed and neither [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) nor [`Client.destroy()`](Client.md#clientdestroyerror-callback) are called, then the [**processing**](/docs/docs/api/Client.md#processing) machine will trigger a *keepalive* event transitioning the `Client` back to the [**pending**](/docs/docs/api/Client.md#pending) state. During this time, the `Client` is waiting for the socket connection to timeout, and once it does, it triggers the *timeout* event and transitions to the [**idle**](/docs/docs/api/Client.md#idle) state. | ||
#### busy | ||
This sub-state is only entered when a request body is an instance of [Stream](https://nodejs.org/api/stream.html) and requires draining. The `Client` cannot process additional requests while in this state and must wait until the currently processing request body is completely drained before transitioning back to [**processing.running**](#running). | ||
This sub-state is only entered when a request body is an instance of [Stream](https://nodejs.org/api/stream.html) and requires draining. The `Client` cannot process additional requests while in this state and must wait until the currently processing request body is completely drained before transitioning back to [**processing.running**](/docs/docs/api/Client.md#running). | ||
#### closing | ||
This sub-state is only entered when a `Client` instance has queued requests and the [`Client.close()`](Client.md#clientclosecallback) method is called. In this state, the `Client` instance continues to process requests as usual, with the one exception that no additional requests can be queued. Once all of the queued requests are processed, the `Client` will trigger the *done* event gracefully entering the [**destroyed**](#destroyed) state without an error. | ||
This sub-state is only entered when a `Client` instance has queued requests and the [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) method is called. In this state, the `Client` instance continues to process requests as usual, with the one exception that no additional requests can be queued. Once all of the queued requests are processed, the `Client` will trigger the *done* event gracefully entering the [**destroyed**](/docs/docs/api/Client.md#destroyed) state without an error. | ||
@@ -90,0 +90,0 @@ ### destroyed |
@@ -18,3 +18,3 @@ # Class: BalancedPool | ||
Extends: [`PoolOptions`](Pool.md#parameter-pooloptions) | ||
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions) | ||
@@ -32,11 +32,11 @@ * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)` | ||
Implements [Client.closed](Client.md#clientclosed) | ||
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed) | ||
### `BalancedPool.destroyed` | ||
Implements [Client.destroyed](Client.md#clientdestroyed) | ||
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) | ||
### `Pool.stats` | ||
Returns [`PoolStats`](PoolStats.md) instance for this pool. | ||
Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool. | ||
@@ -59,31 +59,31 @@ ## Instance Methods | ||
Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise). | ||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise). | ||
### `BalancedPool.destroy([error, callback])` | ||
Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
### `BalancedPool.connect(options[, callback])` | ||
See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). | ||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback). | ||
### `BalancedPool.dispatch(options, handlers)` | ||
Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
### `BalancedPool.pipeline(options, handler)` | ||
See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). | ||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler). | ||
### `BalancedPool.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
### `BalancedPool.stream(options, factory[, callback])` | ||
See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
### `BalancedPool.upgrade(options[, callback])` | ||
See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). | ||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback). | ||
@@ -94,10 +94,10 @@ ## Instance Events | ||
See [Dispatcher Event: `'connect'`](Dispatcher.md#event-connect). | ||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect). | ||
### Event: `'disconnect'` | ||
See [Dispatcher Event: `'disconnect'`](Dispatcher.md#event-disconnect). | ||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect). | ||
### Event: `'drain'` | ||
See [Dispatcher Event: `'drain'`](Dispatcher.md#event-drain). | ||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain). |
@@ -39,3 +39,3 @@ # Cache Store | ||
* **response** `CachedResponse` - The cached response data. | ||
* **response** `CacheValue` - The cached response data. | ||
* **body** `Readable | undefined` - The response's body. | ||
@@ -48,7 +48,7 @@ | ||
* **req** `Dispatcher.RequestOptions` - Incoming request | ||
* **value** `CachedResponse` - Response to store | ||
* **value** `CacheValue` - Response to store | ||
Returns: `Writable | undefined` - If the store is full, return `undefined`. Otherwise, return a writable so that the cache interceptor can stream the body and trailers to the store. | ||
## `CachedResponse` | ||
## `CacheValue` | ||
@@ -103,1 +103,20 @@ This is an interface containing the majority of a response's data (minus the body). | ||
The store must not return a response after the time defined in this property. | ||
## `CacheStoreReadable` | ||
This extends Node's [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable) | ||
and defines extra properties relevant to the cache interceptor. | ||
### Getter: `value` | ||
The response's [`CacheStoreValue`](/docs/docs/api/CacheStore.md#cachestorevalue) | ||
## `CacheStoreWriteable` | ||
This extends Node's [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable) | ||
and defines extra properties relevant to the cache interceptor. | ||
### Setter: `rawTrailers` | ||
If the response has trailers, the cache interceptor will pass them to the cache | ||
interceptor through this method. |
@@ -89,7 +89,7 @@ # Class: Client | ||
Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise). | ||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise). | ||
### `Client.destroy([error, callback])` | ||
Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
@@ -100,23 +100,23 @@ Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided). | ||
See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). | ||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback). | ||
### `Client.dispatch(options, handlers)` | ||
Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
### `Client.pipeline(options, handler)` | ||
See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). | ||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler). | ||
### `Client.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
### `Client.stream(options, factory[, callback])` | ||
See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
### `Client.upgrade(options[, callback])` | ||
See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). | ||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback). | ||
@@ -147,3 +147,3 @@ ## Instance Properties | ||
See [Dispatcher Event: `'connect'`](Dispatcher.md#event-connect). | ||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect). | ||
@@ -194,3 +194,3 @@ Parameters: | ||
See [Dispatcher Event: `'disconnect'`](Dispatcher.md#event-disconnect). | ||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect). | ||
@@ -240,3 +240,3 @@ Parameters: | ||
See [Dispatcher Event: `'drain'`](Dispatcher.md#event-drain). | ||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain). | ||
@@ -243,0 +243,0 @@ #### Example - Client drain event |
@@ -380,3 +380,3 @@ # Dispatcher | ||
Extends: [`RequestOptions`](#parameter-requestoptions) | ||
Extends: [`RequestOptions`](/docs/docs/api/Dispatcher.md#parameter-requestoptions) | ||
@@ -471,3 +471,3 @@ * **objectMode** `boolean` (optional) - Default: `false` - Set to `true` if the `handler` will return an object stream. | ||
Extends: [`DispatchOptions`](#parameter-dispatchoptions) | ||
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions) | ||
@@ -659,3 +659,3 @@ * **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`. | ||
As demonstrated in [Example 1 - Basic GET stream request](#example-1---basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](#example-2---stream-to-fastify-response) for more details. | ||
As demonstrated in [Example 1 - Basic GET stream request](/docs/docs/api/Dispatcher.md#example-1---basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](/docs/docs/api/Dispatch.md#example-2---stream-to-fastify-response) for more details. | ||
@@ -942,3 +942,3 @@ Arguments: | ||
It accepts the same arguments as the [`RedirectHandler` constructor](./RedirectHandler.md). | ||
It accepts the same arguments as the [`RedirectHandler` constructor](/docs/docs/api/RedirectHandler.md). | ||
@@ -961,3 +961,3 @@ **Example - Basic Redirect Interceptor** | ||
It accepts the same arguments as the [`RetryHandler` constructor](./RetryHandler.md). | ||
It accepts the same arguments as the [`RetryHandler` constructor](/docs/docs/api/RetryHandler.md). | ||
@@ -1268,3 +1268,3 @@ **Example - Basic Redirect Interceptor** | ||
- `store` - The [`CacheStore`](./CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](./CacheStore.md#memorycachestore). | ||
- `store` - The [`CacheStore`](/docs/docs/api/CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](/docs/docs/api/CacheStore.md#memorycachestore). | ||
- `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of. | ||
@@ -1318,3 +1318,3 @@ | ||
Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in three forms: | ||
Header arguments such as `options.headers` in [`Client.dispatch`](/docs/docs/api/Client.md#clientdispatchoptions-handlers) can be specified in three forms: | ||
* As an object specified by the `Record<string, string | string[] | undefined>` (`IncomingHttpHeaders`) type. | ||
@@ -1325,3 +1325,3 @@ * As an array of strings. An array representation of a header list must have an even length, or an `InvalidArgumentError` will be thrown. | ||
Response headers will derive a `host` from the `url` of the [Client](Client.md#class-client) instance if no `host` header was previously specified. | ||
Response headers will derive a `host` from the `url` of the [Client](/docs/docs/api/Client.md#class-client) instance if no `host` header was previously specified. | ||
@@ -1328,0 +1328,0 @@ ### Example 1 - Object |
@@ -23,3 +23,3 @@ # Class: EnvHttpProxyAgent | ||
Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) | ||
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions) | ||
@@ -122,42 +122,42 @@ * **httpProxy** `string` (optional) - When set, it will override the `HTTP_PROXY` environment variable. | ||
Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise). | ||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise). | ||
### `EnvHttpProxyAgent.destroy([error, callback])` | ||
Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
### `EnvHttpProxyAgent.dispatch(options, handler: AgentDispatchOptions)` | ||
Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
#### Parameter: `AgentDispatchOptions` | ||
Extends: [`DispatchOptions`](Dispatcher.md#parameter-dispatchoptions) | ||
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions) | ||
* **origin** `string | URL` | ||
Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
### `EnvHttpProxyAgent.connect(options[, callback])` | ||
See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). | ||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback). | ||
### `EnvHttpProxyAgent.dispatch(options, handler)` | ||
Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
### `EnvHttpProxyAgent.pipeline(options, handler)` | ||
See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). | ||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler). | ||
### `EnvHttpProxyAgent.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
### `EnvHttpProxyAgent.stream(options, factory[, callback])` | ||
See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
### `EnvHttpProxyAgent.upgrade(options[, callback])` | ||
See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). | ||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback). |
@@ -17,3 +17,3 @@ # Class: MockAgent | ||
Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) | ||
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions) | ||
@@ -305,7 +305,7 @@ * **agent** `Agent` (optional) - Default: `new Agent([options])` - a custom agent encapsulated by the MockAgent. | ||
Implements [`Agent.dispatch(options, handlers)`](Agent.md#parameter-agentdispatchoptions). | ||
Implements [`Agent.dispatch(options, handlers)`](/docs/docs/api/Agent.md#parameter-agentdispatchoptions). | ||
### `MockAgent.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
@@ -312,0 +312,0 @@ #### Example - MockAgent request |
@@ -39,15 +39,15 @@ # Class: MockClient | ||
Implements: [`MockPool.intercept(options)`](MockPool.md#mockpoolinterceptoptions) | ||
Implements: [`MockPool.intercept(options)`](/docs/docs/api/MockPool.md#mockpoolinterceptoptions) | ||
### `MockClient.close()` | ||
Implements: [`MockPool.close()`](MockPool.md#mockpoolclose) | ||
Implements: [`MockPool.close()`](/docs/docs/api/MockPool.md#mockpoolclose) | ||
### `MockClient.dispatch(options, handlers)` | ||
Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
### `MockClient.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
@@ -54,0 +54,0 @@ #### Example - MockClient request |
@@ -515,7 +515,7 @@ # Class: MockPool | ||
Implements [`Dispatcher.dispatch(options, handlers)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
### `MockPool.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
@@ -522,0 +522,0 @@ #### Example - MockPool request |
@@ -18,3 +18,3 @@ # Class: Pool | ||
Extends: [`ClientOptions`](Client.md#parameter-clientoptions) | ||
Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions) | ||
@@ -28,7 +28,7 @@ * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)` | ||
Implements [Client.closed](Client.md#clientclosed) | ||
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed) | ||
### `Pool.destroyed` | ||
Implements [Client.destroyed](Client.md#clientdestroyed) | ||
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) | ||
@@ -43,31 +43,31 @@ ### `Pool.stats` | ||
Implements [`Dispatcher.close([callback])`](Dispatcher.md#dispatcherclosecallback-promise). | ||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise). | ||
### `Pool.destroy([error, callback])` | ||
Implements [`Dispatcher.destroy([error, callback])`](Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise). | ||
### `Pool.connect(options[, callback])` | ||
See [`Dispatcher.connect(options[, callback])`](Dispatcher.md#dispatcherconnectoptions-callback). | ||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback). | ||
### `Pool.dispatch(options, handler)` | ||
Implements [`Dispatcher.dispatch(options, handler)`](Dispatcher.md#dispatcherdispatchoptions-handler). | ||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler). | ||
### `Pool.pipeline(options, handler)` | ||
See [`Dispatcher.pipeline(options, handler)`](Dispatcher.md#dispatcherpipelineoptions-handler). | ||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler). | ||
### `Pool.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). | ||
### `Pool.stream(options, factory[, callback])` | ||
See [`Dispatcher.stream(options, factory[, callback])`](Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback). | ||
### `Pool.upgrade(options[, callback])` | ||
See [`Dispatcher.upgrade(options[, callback])`](Dispatcher.md#dispatcherupgradeoptions-callback). | ||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback). | ||
@@ -78,10 +78,10 @@ ## Instance Events | ||
See [Dispatcher Event: `'connect'`](Dispatcher.md#event-connect). | ||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect). | ||
### Event: `'disconnect'` | ||
See [Dispatcher Event: `'disconnect'`](Dispatcher.md#event-disconnect). | ||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect). | ||
### Event: `'drain'` | ||
See [Dispatcher Event: `'drain'`](Dispatcher.md#event-drain). | ||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain). |
# Class: PoolStats | ||
Aggregate stats for a [Pool](Pool.md) or [BalancedPool](BalancedPool.md). | ||
Aggregate stats for a [Pool](/docs/docs/api/Pool.md) or [BalancedPool](/docs/docs/api/BalancedPool.md). | ||
@@ -5,0 +5,0 @@ ## `new PoolStats(pool)` |
@@ -17,3 +17,3 @@ # Class: ProxyAgent | ||
Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) | ||
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions) | ||
@@ -127,6 +127,6 @@ * **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string. | ||
Implements [`Agent.dispatch(options, handlers)`](Agent.md#parameter-agentdispatchoptions). | ||
Implements [`Agent.dispatch(options, handlers)`](/docs/docs/api/Agent.md#parameter-agentdispatchoptions). | ||
### `ProxyAgent.request(options[, callback])` | ||
See [`Dispatcher.request(options [, callback])`](Dispatcher.md#dispatcherrequestoptions-callback). | ||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback). |
@@ -18,3 +18,3 @@ # Class: RetryHandler | ||
Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions). | ||
Extends: [`Dispatch.DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions). | ||
@@ -48,3 +48,3 @@ #### `RetryOptions` | ||
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry. | ||
- **handler** Extends [`Dispatch.DispatchHandlers`](Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted. | ||
- **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. | ||
@@ -51,0 +51,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 |
@@ -12,3 +12,3 @@ # Class: WebSocket | ||
* **url** `URL | string` | ||
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md). | ||
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](/docs/docs/api/Dispatcher.md). | ||
@@ -15,0 +15,0 @@ ### Example: |
'use strict' | ||
const { Writable } = require('node:stream') | ||
const { nowAbsolute } = require('../util/timers.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} | ||
* | ||
* @typedef {{ | ||
* locked: boolean | ||
* opts: import('../../types/cache-interceptor.d.ts').default.CachedResponse | ||
* body?: Buffer[] | ||
* }} MemoryStoreValue | ||
*/ | ||
class MemoryCacheStore { | ||
#maxCount = Infinity | ||
#maxSize = Infinity | ||
#maxEntrySize = Infinity | ||
#entryCount = 0 | ||
#size = 0 | ||
#count = 0 | ||
#entries = new Map() | ||
/** | ||
* @type {Map<string, Map<string, MemoryStoreValue[]>>} | ||
*/ | ||
#data = new Map() | ||
/** | ||
* @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts] | ||
@@ -47,2 +45,13 @@ */ | ||
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) { | ||
@@ -61,8 +70,4 @@ if ( | ||
get isFull () { | ||
return this.#entryCount >= this.#maxCount | ||
} | ||
/** | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req | ||
* @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} | ||
@@ -75,14 +80,23 @@ */ | ||
const values = this.#getValuesForRequest(key, false) | ||
if (!values) { | ||
return undefined | ||
} | ||
const topLevelKey = `${key.origin}:${key.path}` | ||
const value = this.#findValue(key, values) | ||
const now = nowAbsolute() | ||
const entry = this.#entries.get(topLevelKey)?.find((entry) => ( | ||
entry.deleteAt > now && | ||
entry.method === key.method && | ||
(entry.vary == null || Object.keys(entry.vary).every(headerName => entry.vary[headerName] === key.headers?.[headerName])) | ||
)) | ||
if (!value || value.locked) { | ||
return undefined | ||
} | ||
return { ...value.opts, body: value.body } | ||
return entry == null | ||
? undefined | ||
: { | ||
statusMessage: entry.statusMessage, | ||
statusCode: entry.statusCode, | ||
rawHeaders: entry.rawHeaders, | ||
body: entry.body, | ||
etag: entry.etag, | ||
cachedAt: entry.cachedAt, | ||
staleAt: entry.staleAt, | ||
deleteAt: entry.deleteAt | ||
} | ||
} | ||
@@ -92,109 +106,20 @@ | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CachedResponse} opts | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} val | ||
* @returns {Writable | undefined} | ||
*/ | ||
createWriteStream (key, opts) { | ||
createWriteStream (key, val) { | ||
if (typeof key !== 'object') { | ||
throw new TypeError(`expected key to be object, got ${typeof key}`) | ||
} | ||
if (typeof opts !== 'object') { | ||
throw new TypeError(`expected value to be object, got ${typeof opts}`) | ||
if (typeof val !== 'object') { | ||
throw new TypeError(`expected value to be object, got ${typeof val}`) | ||
} | ||
if (this.isFull) { | ||
return undefined | ||
} | ||
const topLevelKey = `${key.origin}:${key.path}` | ||
const values = this.#getValuesForRequest(key, true) | ||
const store = this | ||
const entry = { ...key, ...val, body: [], size: 0 } | ||
/** | ||
* @type {(MemoryStoreValue & { index: number }) | undefined} | ||
*/ | ||
let value = this.#findValue(key, values) | ||
let valueIndex = value?.index | ||
if (!value) { | ||
// The value doesn't already exist, meaning we haven't cached this | ||
// response before. Let's assign it a value and insert it into our data | ||
// property. | ||
if (this.isFull) { | ||
// Or not, we don't have space to add another response | ||
return undefined | ||
} | ||
this.#entryCount++ | ||
value = { | ||
locked: true, | ||
opts | ||
} | ||
// We want to sort our responses in decending order by their deleteAt | ||
// timestamps so that deleting expired responses is faster | ||
if ( | ||
values.length === 0 || | ||
opts.deleteAt < values[values.length - 1].deleteAt | ||
) { | ||
// Our value is either the only response for this path or our deleteAt | ||
// time is sooner than all the other responses | ||
values.push(value) | ||
valueIndex = values.length - 1 | ||
} else if (opts.deleteAt >= values[0].deleteAt) { | ||
// Our deleteAt is later than everyone elses | ||
values.unshift(value) | ||
valueIndex = 0 | ||
} else { | ||
// We're neither in the front or the end, let's just binary search to | ||
// find our stop we need to be in | ||
let startIndex = 0 | ||
let endIndex = values.length | ||
while (true) { | ||
if (startIndex === endIndex) { | ||
values.splice(startIndex, 0, value) | ||
break | ||
} | ||
const middleIndex = Math.floor((startIndex + endIndex) / 2) | ||
const middleValue = values[middleIndex] | ||
if (opts.deleteAt === middleIndex) { | ||
values.splice(middleIndex, 0, value) | ||
valueIndex = middleIndex | ||
break | ||
} else if (opts.deleteAt > middleValue.opts.deleteAt) { | ||
endIndex = middleIndex | ||
continue | ||
} else { | ||
startIndex = middleIndex | ||
continue | ||
} | ||
} | ||
} | ||
} else { | ||
// Check if there's already another request writing to the value or | ||
// a request reading from it | ||
if (value.locked) { | ||
return undefined | ||
} | ||
// Empty it so we can overwrite it | ||
value.body = [] | ||
} | ||
let currentSize = 0 | ||
/** | ||
* @type {Buffer[] | null} | ||
*/ | ||
let body = key.method !== 'HEAD' ? [] : null | ||
const maxEntrySize = this.#maxEntrySize | ||
const writable = new Writable({ | ||
return new Writable({ | ||
write (chunk, encoding, callback) { | ||
if (key.method === 'HEAD') { | ||
throw new Error('HEAD request shouldn\'t have a body') | ||
} | ||
if (!body) { | ||
return callback() | ||
} | ||
if (typeof chunk === 'string') { | ||
@@ -204,127 +129,58 @@ chunk = Buffer.from(chunk, encoding) | ||
currentSize += chunk.byteLength | ||
entry.size += chunk.byteLength | ||
if (currentSize >= maxEntrySize) { | ||
body = null | ||
this.end() | ||
shiftAtIndex(values, valueIndex) | ||
return callback() | ||
if (entry.size >= store.#maxEntrySize) { | ||
this.destroy() | ||
} else { | ||
entry.body.push(chunk) | ||
} | ||
body.push(chunk) | ||
callback() | ||
callback(null) | ||
}, | ||
final (callback) { | ||
value.locked = false | ||
if (body !== null) { | ||
value.body = body | ||
let entries = store.#entries.get(topLevelKey) | ||
if (!entries) { | ||
entries = [] | ||
store.#entries.set(topLevelKey, entries) | ||
} | ||
entries.push(entry) | ||
callback() | ||
store.#size += entry.size | ||
store.#count += 1 | ||
if (store.#size > store.#maxSize || store.#count > store.#maxCount) { | ||
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) | ||
} | ||
} | ||
} | ||
callback(null) | ||
} | ||
}) | ||
return writable | ||
} | ||
/** | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key | ||
* @param {CacheKey} key | ||
*/ | ||
delete (key) { | ||
this.#data.delete(`${key.origin}:${key.path}`) | ||
} | ||
if (typeof key !== 'object') { | ||
throw new TypeError(`expected key to be object, got ${typeof key}`) | ||
} | ||
/** | ||
* Gets all of the requests of the same origin, path, and method. Does not | ||
* take the `vary` property into account. | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key | ||
* @param {boolean} [makeIfDoesntExist=false] | ||
* @returns {MemoryStoreValue[] | undefined} | ||
*/ | ||
#getValuesForRequest (key, makeIfDoesntExist) { | ||
// https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3 | ||
const topLevelKey = `${key.origin}:${key.path}` | ||
let cachedPaths = this.#data.get(topLevelKey) | ||
if (!cachedPaths) { | ||
if (!makeIfDoesntExist) { | ||
return undefined | ||
} | ||
cachedPaths = new Map() | ||
this.#data.set(topLevelKey, cachedPaths) | ||
for (const entry of this.#entries.get(topLevelKey) ?? []) { | ||
this.#size -= entry.size | ||
this.#count -= 1 | ||
} | ||
let value = cachedPaths.get(key.method) | ||
if (!value && makeIfDoesntExist) { | ||
value = [] | ||
cachedPaths.set(key.method, value) | ||
} | ||
return value | ||
this.#entries.delete(topLevelKey) | ||
} | ||
/** | ||
* Given a list of values of a certain request, this decides the best value | ||
* to respond with. | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req | ||
* @param {MemoryStoreValue[]} values | ||
* @returns {(MemoryStoreValue & { index: number }) | undefined} | ||
*/ | ||
#findValue (req, values) { | ||
/** | ||
* @type {MemoryStoreValue | undefined} | ||
*/ | ||
let value | ||
const now = Date.now() | ||
for (let i = values.length - 1; i >= 0; i--) { | ||
const current = values[i] | ||
const currentCacheValue = current.opts | ||
if (now >= currentCacheValue.deleteAt) { | ||
// We've reached expired values, let's delete them | ||
this.#entryCount -= values.length - i | ||
values.length = i | ||
break | ||
} | ||
let matches = true | ||
if (currentCacheValue.vary) { | ||
if (!req.headers) { | ||
matches = false | ||
break | ||
} | ||
for (const key in currentCacheValue.vary) { | ||
if (currentCacheValue.vary[key] !== req.headers[key]) { | ||
matches = false | ||
break | ||
} | ||
} | ||
} | ||
if (matches) { | ||
value = { | ||
...current, | ||
index: i | ||
} | ||
break | ||
} | ||
} | ||
return value | ||
} | ||
} | ||
/** | ||
* @param {any[]} array Array to modify | ||
* @param {number} idx Index to delete | ||
*/ | ||
function shiftAtIndex (array, idx) { | ||
for (let i = idx + 1; idx < array.length; i++) { | ||
array[i - 1] = array[i] | ||
} | ||
array.length-- | ||
} | ||
module.exports = MemoryCacheStore |
@@ -7,4 +7,6 @@ 'use strict' | ||
parseCacheControlHeader, | ||
parseVaryHeader | ||
parseVaryHeader, | ||
isEtagUsable | ||
} = require('../util/cache') | ||
const { nowAbsolute } = require('../util/timers.js') | ||
@@ -125,3 +127,3 @@ function noop () {} | ||
const now = Date.now() | ||
const now = nowAbsolute() | ||
const staleAt = determineStaleAt(now, headers, cacheControlDirectives) | ||
@@ -140,3 +142,6 @@ if (staleAt) { | ||
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, { | ||
/** | ||
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue} | ||
*/ | ||
const value = { | ||
statusCode, | ||
@@ -149,4 +154,10 @@ statusMessage, | ||
deleteAt | ||
}) | ||
} | ||
if (typeof headers.etag === 'string' && isEtagUsable(headers.etag)) { | ||
value.etag = headers.etag | ||
} | ||
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value) | ||
if (this.#writeStream) { | ||
@@ -307,3 +318,3 @@ const handler = this | ||
if (expiresDate instanceof Date && !isNaN(expiresDate)) { | ||
return now + (Date.now() - expiresDate.getTime()) | ||
return now + (nowAbsolute() - expiresDate.getTime()) | ||
} | ||
@@ -310,0 +321,0 @@ } |
@@ -9,3 +9,4 @@ 'use strict' | ||
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler') | ||
const { assertCacheStore, assertCacheMethods, makeCacheKey } = require('../util/cache.js') | ||
const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js') | ||
const { nowAbsolute } = require('../util/timers.js') | ||
@@ -15,6 +16,74 @@ const AGE_HEADER = Buffer.from('age') | ||
/** | ||
* @typedef {import('../../types/cache-interceptor.d.ts').default.CachedResponse} CachedResponse | ||
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler | ||
*/ | ||
function sendGatewayTimeout (handler) { | ||
let aborted = false | ||
try { | ||
if (typeof handler.onConnect === 'function') { | ||
handler.onConnect(() => { | ||
aborted = true | ||
}) | ||
if (aborted) { | ||
return | ||
} | ||
} | ||
if (typeof handler.onHeaders === 'function') { | ||
handler.onHeaders(504, [], () => {}, 'Gateway Timeout') | ||
if (aborted) { | ||
return | ||
} | ||
} | ||
if (typeof handler.onComplete === 'function') { | ||
handler.onComplete([]) | ||
} | ||
} catch (err) { | ||
if (typeof handler.onError === 'function') { | ||
handler.onError(err) | ||
} | ||
} | ||
} | ||
/** | ||
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result | ||
* @param {number} age | ||
* @param {import('../util/cache.js').CacheControlDirectives | undefined} cacheControlDirectives | ||
* @returns {boolean} | ||
*/ | ||
function needsRevalidation (result, age, cacheControlDirectives) { | ||
if (cacheControlDirectives?.['no-cache']) { | ||
// Always revalidate requests with the no-cache directive | ||
return true | ||
} | ||
const now = nowAbsolute() | ||
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 | ||
} | ||
/** | ||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts] | ||
@@ -53,2 +122,10 @@ * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor} | ||
const requestCacheControl = opts.headers?.['cache-control'] | ||
? parseCacheControlHeader(opts.headers['cache-control']) | ||
: undefined | ||
if (requestCacheControl?.['no-store']) { | ||
return dispatch(opts, handler) | ||
} | ||
/** | ||
@@ -64,2 +141,9 @@ * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey} | ||
if (!result) { | ||
if (requestCacheControl?.['only-if-cached']) { | ||
// We only want cached responses | ||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached | ||
sendGatewayTimeout(handler) | ||
return true | ||
} | ||
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) | ||
@@ -70,4 +154,5 @@ } | ||
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result | ||
* @param {number} age | ||
*/ | ||
const respondWithCachedValue = ({ cachedAt, rawHeaders, statusCode, statusMessage, body }) => { | ||
const respondWithCachedValue = ({ rawHeaders, statusCode, statusMessage, body }, age) => { | ||
const stream = util.isStream(body) | ||
@@ -109,3 +194,2 @@ ? body | ||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age | ||
const age = Math.round((Date.now() - cachedAt) / 1000) | ||
@@ -141,17 +225,19 @@ // TODO (fix): What if rawHeaders already contains age header? | ||
const age = Math.round((nowAbsolute() - result.cachedAt) / 1000) | ||
if (requestCacheControl?.['max-age'] && age >= requestCacheControl['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) | ||
} | ||
// Check if the response is stale | ||
const now = Date.now() | ||
if (now < result.staleAt) { | ||
// Dump request body. | ||
if (util.isStream(opts.body)) { | ||
opts.body.on('error', () => {}).destroy() | ||
if (needsRevalidation(result, age, requestCacheControl)) { | ||
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { | ||
// If body is is stream we can't revalidate... | ||
// TODO (fix): This could be less strict... | ||
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) | ||
} | ||
respondWithCachedValue(result) | ||
} else if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) { | ||
// If body is is stream we can't revalidate... | ||
// TODO (fix): This could be less strict... | ||
dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) | ||
} else { | ||
// Need to revalidate the response | ||
dispatch( | ||
// We need to revalidate the response | ||
return dispatch( | ||
{ | ||
@@ -161,3 +247,4 @@ ...opts, | ||
...opts.headers, | ||
'if-modified-since': new Date(result.cachedAt).toUTCString() | ||
'if-modified-since': new Date(result.cachedAt).toUTCString(), | ||
etag: result.etag | ||
} | ||
@@ -168,3 +255,3 @@ }, | ||
if (success) { | ||
respondWithCachedValue(result) | ||
respondWithCachedValue(result, age) | ||
} else if (util.isStream(result.body)) { | ||
@@ -178,2 +265,8 @@ result.body.on('error', () => {}).destroy() | ||
} | ||
// Dump request body. | ||
if (util.isStream(opts.body)) { | ||
opts.body.on('error', () => {}).destroy() | ||
} | ||
respondWithCachedValue(result, age) | ||
} | ||
@@ -184,2 +277,9 @@ | ||
if (!result) { | ||
if (requestCacheControl?.['only-if-cached']) { | ||
// We only want cached responses | ||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached | ||
sendGatewayTimeout(handler) | ||
return true | ||
} | ||
dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) | ||
@@ -186,0 +286,0 @@ } else { |
@@ -205,2 +205,36 @@ 'use strict' | ||
/** | ||
* 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 | ||
@@ -248,4 +282,5 @@ * @returns {asserts store is import('../../types/cache-interceptor.d.ts').default.CacheStore} | ||
parseVaryHeader, | ||
isEtagUsable, | ||
assertCacheMethods, | ||
assertCacheStore | ||
} |
@@ -25,2 +25,9 @@ 'use strict' | ||
/** | ||
* The fastNowAbsolute variable contains the rough result of Date.now() | ||
* | ||
* @type {number} | ||
*/ | ||
let fastNowAbsolute = Date.now() | ||
/** | ||
* RESOLUTION_MS represents the target resolution time in milliseconds. | ||
@@ -126,2 +133,4 @@ * | ||
fastNowAbsolute = Date.now() | ||
/** | ||
@@ -395,2 +404,5 @@ * The `idx` variable is used to iterate over the `fastTimers` array. | ||
}, | ||
nowAbsolute () { | ||
return fastNowAbsolute | ||
}, | ||
/** | ||
@@ -425,3 +437,9 @@ * Trigger the onTick function to process the fastTimers array. | ||
*/ | ||
kFastTimer | ||
kFastTimer, | ||
/** | ||
* Exporting for testing purposes only. | ||
* Marking as deprecated to discourage any use outside of testing. | ||
* @deprecated | ||
*/ | ||
RESOLUTION_MS | ||
} |
{ | ||
"name": "undici", | ||
"version": "7.0.0-alpha.4", | ||
"version": "7.0.0-alpha.5", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -5,0 +5,0 @@ "homepage": "https://undici.nodejs.org", |
@@ -28,2 +28,13 @@ import { Readable, Writable } from 'node:stream' | ||
export interface CacheValue { | ||
statusCode: number | ||
statusMessage: string | ||
rawHeaders: Buffer[] | ||
vary?: Record<string, string | string[]> | ||
etag?: string | ||
cachedAt: number | ||
staleAt: number | ||
deleteAt: number | ||
} | ||
export interface DeleteByUri { | ||
@@ -35,3 +46,11 @@ origin: string | ||
type GetResult = CachedResponse & { body: null | Readable | Iterable<Buffer> | Buffer | Iterable<string> | string } | ||
type GetResult = { | ||
statusCode: number | ||
statusMessage: string | ||
rawHeaders: Buffer[] | ||
body: null | Readable | Iterable<Buffer> | AsyncIterable<Buffer> | Buffer | Iterable<string> | AsyncIterable<string> | string | ||
cachedAt: number | ||
staleAt: number | ||
deleteAt: number | ||
} | ||
@@ -42,10 +61,5 @@ /** | ||
export interface CacheStore { | ||
/** | ||
* Whether or not the cache is full and can not store any more responses | ||
*/ | ||
get isFull(): boolean | undefined | ||
get(key: CacheKey): GetResult | Promise<GetResult | undefined> | undefined | ||
createWriteStream(key: CacheKey, value: CachedResponse): Writable | undefined | ||
createWriteStream(key: CacheKey, val: CacheValue): Writable | undefined | ||
@@ -55,26 +69,2 @@ delete(key: CacheKey): void | Promise<void> | ||
export interface CachedResponse { | ||
statusCode: number; | ||
statusMessage: string; | ||
rawHeaders: Buffer[]; | ||
/** | ||
* Headers defined by the Vary header and their respective values for | ||
* later comparison | ||
*/ | ||
vary?: Record<string, string | string[]> | ||
/** | ||
* Time in millis that this value was cached | ||
*/ | ||
cachedAt: number | ||
/** | ||
* Time in millis that this value is considered stale | ||
*/ | ||
staleAt: number | ||
/** | ||
* Time in millis that this value is to be deleted from the cache. This is | ||
* either the same as staleAt or the `max-stale` caching directive. | ||
*/ | ||
deleteAt: number | ||
} | ||
export interface MemoryCacheStoreOpts { | ||
@@ -89,2 +79,7 @@ /** | ||
*/ | ||
maxSize?: number | ||
/** | ||
* @default Infinity | ||
*/ | ||
maxEntrySize?: number | ||
@@ -98,7 +93,5 @@ | ||
get isFull (): boolean | undefined | ||
get (key: CacheKey): GetResult | Promise<GetResult | undefined> | undefined | ||
createWriteStream (key: CacheKey, value: CachedResponse): Writable | undefined | ||
createWriteStream (key: CacheKey, value: CacheValue): Writable | undefined | ||
@@ -105,0 +98,0 @@ delete (key: CacheKey): void | Promise<void> |
1264218
27072