| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent fcc881d commit 7e62a77
16 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -63,6 +63,7 @@ typings/ | |||
| 63 | 63 | # lock files | |
| 64 | 64 | package-lock.json | |
| 65 | 65 | yarn.lock | |
| 66 | + pnpm-lock.yaml | ||
| 66 | 67 | ||
| 67 | 68 | # IDE files | |
| 68 | 69 | .idea | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,8 +13,28 @@ The `MemoryCacheStore` stores the responses in-memory. | |||
| 13 | 13 | ||
| 14 | 14 | **Options** | |
| 15 | 15 | ||
| 16 | + - `maxSize` - The maximum total size in bytes of all stored responses. Default `Infinity`. | ||
| 16 | 17 | - `maxCount` - The maximum amount of responses to store. Default `Infinity`. | |
| 17 | - - `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. | ||
| 18 | + - `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. Default `Infinity`. | ||
| 19 | + | ||
| 20 | + ### Getters | ||
| 21 | + | ||
| 22 | + #### `MemoryCacheStore.size` | ||
| 23 | + | ||
| 24 | + Returns the current total size in bytes of all stored responses. | ||
| 25 | + | ||
| 26 | + ### Methods | ||
| 27 | + | ||
| 28 | + #### `MemoryCacheStore.isFull()` | ||
| 29 | + | ||
| 30 | + Returns a boolean indicating whether the cache has reached its maximum size or count. | ||
| 31 | + | ||
| 32 | + ### Events | ||
| 33 | + | ||
| 34 | + #### `'maxSizeExceeded'` | ||
| 35 | + | ||
| 36 | + Emitted when the cache exceeds its maximum size or count limits. The event payload contains `size`, `maxSize`, `count`, and `maxCount` properties. | ||
| 37 | + | ||
| 18 | 38 | ||
| 19 | 39 | ### `SqliteCacheStore` | |
| 20 | 40 | ||
@@ -26,7 +46,7 @@ The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present. | |||
| 26 | 46 | ||
| 27 | 47 | - `location` - The location of the SQLite database to use. Default `:memory:`. | |
| 28 | 48 | - `maxCount` - The maximum number of entries to store in the database. Default `Infinity`. | |
| 29 | - - `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`. | ||
| 49 | + - `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. Default `Infinity`. | ||
| 30 | 50 | ||
| 31 | 51 | ## Defining a Custom Cache Store | |
| 32 | 52 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,6 +19,7 @@ Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions) | |||
| 19 | 19 | ||
| 20 | 20 | * **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)` | |
| 21 | 21 | * **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances. | |
| 22 | + * **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `Pool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age. | ||
| 22 | 23 | ||
| 23 | 24 | ## Instance Properties | |
| 24 | 25 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,6 +25,7 @@ For detailed information on the parsing process and potential validation errors, | |||
| 25 | 25 | * **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)` | |
| 26 | 26 | * **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions). | |
| 27 | 27 | * **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions). | |
| 28 | + * **proxyTunnel** `boolean` (optional) - By default, ProxyAgent will request that the Proxy facilitate a tunnel between the endpoint and the agent. Setting `proxyTunnel` to false avoids issuing a CONNECT extension, and includes the endpoint domain and path in each request. | ||
| 28 | 29 | ||
| 29 | 30 | Examples: | |
| 30 | 31 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,6 +1,7 @@ | |||
| 1 | 1 | 'use strict' | |
| 2 | 2 | ||
| 3 | 3 | const { Writable } = require('node:stream') | |
| 4 | + const { EventEmitter } = require('node:events') | ||
| 4 | 5 | const { assertCacheKey, assertCacheValue } = require('../util/cache.js') | |
| 5 | 6 | ||
| 6 | 7 | /** | |
@@ -12,20 +13,23 @@ const { assertCacheKey, assertCacheValue } = require('../util/cache.js') | |||
| 12 | 13 | ||
| 13 | 14 | /** | |
| 14 | 15 | * @implements {CacheStore} | |
| 16 | + * @extends {EventEmitter} | ||
| 15 | 17 | */ | |
| 16 | - class MemoryCacheStore { | ||
| 18 | + class MemoryCacheStore extends EventEmitter { | ||
| 17 | 19 | #maxCount = Infinity | |
| 18 | 20 | #maxSize = Infinity | |
| 19 | 21 | #maxEntrySize = Infinity | |
| 20 | 22 | ||
| 21 | 23 | #size = 0 | |
| 22 | 24 | #count = 0 | |
| 23 | 25 | #entries = new Map() | |
| 26 | + #hasEmittedMaxSizeEvent = false | ||
| 24 | 27 | ||
| 25 | 28 | /** | |
| 26 | 29 | * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts] | |
| 27 | 30 | */ | |
| 28 | 31 | constructor (opts) { | |
| 32 | + super() | ||
| 29 | 33 | if (opts) { | |
| 30 | 34 | if (typeof opts !== 'object') { | |
| 31 | 35 | throw new TypeError('MemoryCacheStore options must be an object') | |
@@ -66,6 +70,22 @@ class MemoryCacheStore { | |||
| 66 | 70 | } | |
| 67 | 71 | } | |
| 68 | 72 | ||
| 73 | + /** | ||
| 74 | + * Get the current size of the cache in bytes | ||
| 75 | + * @returns {number} The current size of the cache in bytes | ||
| 76 | + */ | ||
| 77 | + get size () { | ||
| 78 | + return this.#size | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + /** | ||
| 82 | + * Check if the cache is full (either max size or max count reached) | ||
| 83 | + * @returns {boolean} True if the cache is full, false otherwise | ||
| 84 | + */ | ||
| 85 | + isFull () { | ||
| 86 | + return this.#size >= this.#maxSize || this.#count >= this.#maxCount | ||
| 87 | + } | ||
| 88 | + | ||
| 69 | 89 | /** | |
| 70 | 90 | * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req | |
| 71 | 91 | * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} | |
@@ -144,7 +164,20 @@ class MemoryCacheStore { | |||
| 144 | 164 | ||
| 145 | 165 | store.#size += entry.size | |
| 146 | 166 | ||
| 167 | + // Check if cache is full and emit event if needed | ||
| 147 | 168 | if (store.#size > store.#maxSize || store.#count > store.#maxCount) { | |
| 169 | + // Emit maxSizeExceeded event if we haven't already | ||
| 170 | + if (!store.#hasEmittedMaxSizeEvent) { | ||
| 171 | + store.emit('maxSizeExceeded', { | ||
| 172 | + size: store.#size, | ||
| 173 | + maxSize: store.#maxSize, | ||
| 174 | + count: store.#count, | ||
| 175 | + maxCount: store.#maxCount | ||
| 176 | + }) | ||
| 177 | + store.#hasEmittedMaxSizeEvent = true | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + // Perform eviction | ||
| 148 | 181 | for (const [key, entries] of store.#entries) { | |
| 149 | 182 | for (const entry of entries.splice(0, entries.length / 2)) { | |
| 150 | 183 | store.#size -= entry.size | |
@@ -154,6 +187,11 @@ class MemoryCacheStore { | |||
| 154 | 187 | store.#entries.delete(key) | |
| 155 | 188 | } | |
| 156 | 189 | } | |
| 190 | + | ||
| 191 | + // Reset the event flag after eviction | ||
| 192 | + if (store.#size < store.#maxSize && store.#count < store.#maxCount) { | ||
| 193 | + store.#hasEmittedMaxSizeEvent = false | ||
| 194 | + } | ||
| 157 | 195 | } | |
| 158 | 196 | ||
| 159 | 197 | callback(null) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -45,22 +45,35 @@ class Agent extends DispatcherBase { | |||
| 45 | 45 | } | |
| 46 | 46 | ||
| 47 | 47 | this[kOnConnect] = (origin, targets) => { | |
| 48 | + const result = this[kClients].get(origin) | ||
| 49 | + if (result) { | ||
| 50 | + result.count += 1 | ||
| 51 | + } | ||
| 48 | 52 | this.emit('connect', origin, [this, ...targets]) | |
| 49 | 53 | } | |
| 50 | 54 | ||
| 51 | 55 | this[kOnDisconnect] = (origin, targets, err) => { | |
| 56 | + const result = this[kClients].get(origin) | ||
| 57 | + if (result) { | ||
| 58 | + result.count -= 1 | ||
| 59 | + if (result.count <= 0) { | ||
| 60 | + this[kClients].delete(origin) | ||
| 61 | + result.dispatcher.destroy() | ||
| 62 | + } | ||
| 63 | + } | ||
| 52 | 64 | this.emit('disconnect', origin, [this, ...targets], err) | |
| 53 | 65 | } | |
| 54 | 66 | ||
| 55 | 67 | this[kOnConnectionError] = (origin, targets, err) => { | |
| 68 | + // TODO: should this decrement result.count here? | ||
| 56 | 69 | this.emit('connectionError', origin, [this, ...targets], err) | |
| 57 | 70 | } | |
| 58 | 71 | } | |
| 59 | 72 | ||
| 60 | 73 | get [kRunning] () { | |
| 61 | 74 | let ret = 0 | |
| 62 | - for (const client of this[kClients].values()) { | ||
| 63 | - ret += client[kRunning] | ||
| 75 | + for (const { dispatcher } of this[kClients].values()) { | ||
| 76 | + ret += dispatcher[kRunning] | ||
| 64 | 77 | } | |
| 65 | 78 | return ret | |
| 66 | 79 | } | |
@@ -73,28 +86,25 @@ class Agent extends DispatcherBase { | |||
| 73 | 86 | throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') | |
| 74 | 87 | } | |
| 75 | 88 | ||
| 76 | - let dispatcher = this[kClients].get(key) | ||
| 77 | - | ||
| 89 | + const result = this[kClients].get(key) | ||
| 90 | + let dispatcher = result && result.dispatcher | ||
| 78 | 91 | if (!dispatcher) { | |
| 79 | 92 | dispatcher = this[kFactory](opts.origin, this[kOptions]) | |
| 80 | 93 | .on('drain', this[kOnDrain]) | |
| 81 | 94 | .on('connect', this[kOnConnect]) | |
| 82 | 95 | .on('disconnect', this[kOnDisconnect]) | |
| 83 | 96 | .on('connectionError', this[kOnConnectionError]) | |
| 84 | 97 | ||
| 85 | - // This introduces a tiny memory leak, as dispatchers are never removed from the map. | ||
| 86 | - // TODO(mcollina): remove te timer when the client/pool do not have any more | ||
| 87 | - // active connections. | ||
| 88 | - this[kClients].set(key, dispatcher) | ||
| 98 | + this[kClients].set(key, { count: 0, dispatcher }) | ||
| 89 | 99 | } | |
| 90 | 100 | ||
| 91 | 101 | return dispatcher.dispatch(opts, handler) | |
| 92 | 102 | } | |
| 93 | 103 | ||
| 94 | 104 | async [kClose] () { | |
| 95 | 105 | const closePromises = [] | |
| 96 | - for (const client of this[kClients].values()) { | ||
| 97 | - closePromises.push(client.close()) | ||
| 106 | + for (const { dispatcher } of this[kClients].values()) { | ||
| 107 | + closePromises.push(dispatcher.close()) | ||
| 98 | 108 | } | |
| 99 | 109 | this[kClients].clear() | |
| 100 | 110 | ||
@@ -103,8 +113,8 @@ class Agent extends DispatcherBase { | |||
| 103 | 113 | ||
| 104 | 114 | async [kDestroy] (err) { | |
| 105 | 115 | const destroyPromises = [] | |
| 106 | - for (const client of this[kClients].values()) { | ||
| 107 | - destroyPromises.push(client.destroy(err)) | ||
| 116 | + for (const { dispatcher } of this[kClients].values()) { | ||
| 117 | + destroyPromises.push(dispatcher.destroy(err)) | ||
| 108 | 118 | } | |
| 109 | 119 | this[kClients].clear() | |
| 110 | 120 | ||
@@ -113,9 +123,9 @@ class Agent extends DispatcherBase { | |||
| 113 | 123 | ||
| 114 | 124 | get stats () { | |
| 115 | 125 | const allClientStats = {} | |
| 116 | - for (const client of this[kClients].values()) { | ||
| 117 | - if (client.stats) { | ||
| 118 | - allClientStats[client[kUrl].origin] = client.stats | ||
| 126 | + for (const { dispatcher } of this[kClients].values()) { | ||
| 127 | + if (dispatcher.stats) { | ||
| 128 | + allClientStats[dispatcher[kUrl].origin] = dispatcher.stats | ||
| 119 | 129 | } | |
| 120 | 130 | } | |
| 121 | 131 | return allClientStats | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,7 +5,8 @@ const { | |||
| 5 | 5 | kClients, | |
| 6 | 6 | kNeedDrain, | |
| 7 | 7 | kAddClient, | |
| 8 | - kGetDispatcher | ||
| 8 | + kGetDispatcher, | ||
| 9 | + kRemoveClient | ||
| 9 | 10 | } = require('./pool-base') | |
| 10 | 11 | const Client = require('./client') | |
| 11 | 12 | const { | |
@@ -35,6 +36,7 @@ class Pool extends PoolBase { | |||
| 35 | 36 | autoSelectFamily, | |
| 36 | 37 | autoSelectFamilyAttemptTimeout, | |
| 37 | 38 | allowH2, | |
| 39 | + clientTtl, | ||
| 38 | 40 | ...options | |
| 39 | 41 | } = {}) { | |
| 40 | 42 | if (connections != null && (!Number.isFinite(connections) || connections < 0)) { | |
@@ -65,12 +67,20 @@ class Pool extends PoolBase { | |||
| 65 | 67 | ||
| 66 | 68 | this[kConnections] = connections || null | |
| 67 | 69 | this[kUrl] = util.parseOrigin(origin) | |
| 68 | - this[kOptions] = { ...util.deepClone(options), connect, allowH2 } | ||
| 70 | + this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl } | ||
| 69 | 71 | this[kOptions].interceptors = options.interceptors | |
| 70 | 72 | ? { ...options.interceptors } | |
| 71 | 73 | : undefined | |
| 72 | 74 | this[kFactory] = factory | |
| 73 | 75 | ||
| 76 | + this.on('connect', (origin, targets) => { | ||
| 77 | + if (clientTtl != null && clientTtl > 0) { | ||
| 78 | + for (const target of targets) { | ||
| 79 | + Object.assign(target, { ttl: Date.now() }) | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + }) | ||
| 83 | + | ||
| 74 | 84 | this.on('connectionError', (origin, targets, error) => { | |
| 75 | 85 | // If a connection error occurs, we remove the client from the pool, | |
| 76 | 86 | // and emit a connectionError event. They will not be re-used. | |
@@ -87,8 +97,12 @@ class Pool extends PoolBase { | |||
| 87 | 97 | } | |
| 88 | 98 | ||
| 89 | 99 | [kGetDispatcher] () { | |
| 100 | + const clientTtlOption = this[kOptions].clientTtl | ||
| 90 | 101 | for (const client of this[kClients]) { | |
| 91 | - if (!client[kNeedDrain]) { | ||
| 102 | + // check ttl of client and if it's stale, remove it from the pool | ||
| 103 | + if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) { | ||
| 104 | + this[kRemoveClient](client) | ||
| 105 | + } else if (!client[kNeedDrain]) { | ||
| 92 | 106 | return client | |
| 93 | 107 | } | |
| 94 | 108 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments