undici 7.14.0 → 7.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/docs/docs/api/Agent.md +1 -0
  3. package/docs/docs/api/Dispatcher.md +59 -0
  4. package/docs/docs/api/Errors.md +0 -1
  5. package/index-fetch.js +2 -2
  6. package/index.js +6 -9
  7. package/lib/api/api-request.js +22 -8
  8. package/lib/api/readable.js +7 -5
  9. package/lib/core/errors.js +217 -13
  10. package/lib/core/request.js +5 -1
  11. package/lib/core/util.js +45 -11
  12. package/lib/dispatcher/agent.js +44 -23
  13. package/lib/dispatcher/client-h1.js +20 -9
  14. package/lib/dispatcher/client-h2.js +13 -3
  15. package/lib/dispatcher/client.js +57 -57
  16. package/lib/dispatcher/dispatcher-base.js +12 -7
  17. package/lib/dispatcher/env-http-proxy-agent.js +12 -16
  18. package/lib/dispatcher/fixed-queue.js +15 -39
  19. package/lib/dispatcher/h2c-client.js +6 -6
  20. package/lib/dispatcher/pool-base.js +60 -43
  21. package/lib/dispatcher/pool.js +2 -2
  22. package/lib/dispatcher/proxy-agent.js +14 -9
  23. package/lib/global.js +19 -1
  24. package/lib/interceptor/cache.js +61 -0
  25. package/lib/interceptor/decompress.js +253 -0
  26. package/lib/llhttp/constants.d.ts +99 -1
  27. package/lib/llhttp/constants.js +34 -1
  28. package/lib/llhttp/llhttp-wasm.js +1 -1
  29. package/lib/llhttp/llhttp_simd-wasm.js +1 -1
  30. package/lib/llhttp/utils.d.ts +2 -2
  31. package/lib/llhttp/utils.js +3 -6
  32. package/lib/mock/mock-agent.js +4 -4
  33. package/lib/mock/mock-errors.js +10 -0
  34. package/lib/mock/mock-utils.js +12 -10
  35. package/lib/util/cache.js +6 -7
  36. package/lib/util/date.js +534 -140
  37. package/lib/web/cookies/index.js +1 -1
  38. package/lib/web/cookies/parse.js +2 -2
  39. package/lib/web/eventsource/eventsource-stream.js +2 -2
  40. package/lib/web/eventsource/eventsource.js +34 -29
  41. package/lib/web/eventsource/util.js +1 -9
  42. package/lib/web/fetch/body.js +20 -26
  43. package/lib/web/fetch/index.js +15 -16
  44. package/lib/web/fetch/response.js +2 -4
  45. package/lib/web/fetch/util.js +8 -230
  46. package/lib/web/subresource-integrity/Readme.md +9 -0
  47. package/lib/web/subresource-integrity/subresource-integrity.js +306 -0
  48. package/lib/web/webidl/index.js +203 -42
  49. package/lib/web/websocket/connection.js +4 -3
  50. package/lib/web/websocket/events.js +1 -1
  51. package/lib/web/websocket/stream/websocketerror.js +22 -1
  52. package/lib/web/websocket/stream/websocketstream.js +16 -7
  53. package/lib/web/websocket/websocket.js +32 -42
  54. package/package.json +9 -7
  55. package/types/agent.d.ts +1 -0
  56. package/types/diagnostics-channel.d.ts +0 -1
  57. package/types/errors.d.ts +5 -15
  58. package/types/interceptors.d.ts +5 -0
  59. package/types/snapshot-agent.d.ts +5 -3
  60. package/types/webidl.d.ts +82 -21
  61. package/lib/api/util.js +0 -95
  62. package/lib/llhttp/constants.js.map +0 -1
  63. package/lib/llhttp/utils.js.map +0 -1
@@ -12,8 +12,6 @@ class H2CClient extends DispatcherBase {
12
12
  #client = null
13
13
 
14
14
  constructor (origin, clientOpts) {
15
- super()
16
-
17
15
  if (typeof origin === 'string') {
18
16
  origin = new URL(origin)
19
17
  }
@@ -47,6 +45,8 @@ class H2CClient extends DispatcherBase {
47
45
  )
48
46
  }
49
47
 
48
+ super()
49
+
50
50
  this.#client = new Client(origin, {
51
51
  ...opts,
52
52
  connect: this.#buildConnector(connect),
@@ -110,12 +110,12 @@ class H2CClient extends DispatcherBase {
110
110
  return this.#client.dispatch(opts, handler)
111
111
  }
112
112
 
113
- async [kClose] () {
114
- await this.#client.close()
113
+ [kClose] () {
114
+ return this.#client.close()
115
115
  }
116
116
 
117
- async [kDestroy] () {
118
- await this.#client.destroy()
117
+ [kDestroy] () {
118
+ return this.#client.destroy()
119
119
  }
120
120
  }
121
121
 
@@ -18,54 +18,55 @@ const kAddClient = Symbol('add client')
18
18
  const kRemoveClient = Symbol('remove client')
19
19
 
20
20
  class PoolBase extends DispatcherBase {
21
- constructor () {
22
- super()
21
+ [kQueue] = new FixedQueue();
23
22
 
24
- this[kQueue] = new FixedQueue()
25
- this[kClients] = []
26
- this[kQueued] = 0
23
+ [kQueued] = 0;
27
24
 
28
- const pool = this
25
+ [kClients] = [];
29
26
 
30
- this[kOnDrain] = function onDrain (origin, targets) {
31
- const queue = pool[kQueue]
27
+ [kNeedDrain] = false;
32
28
 
33
- let needDrain = false
29
+ [kOnDrain] (client, origin, targets) {
30
+ const queue = this[kQueue]
34
31
 
35
- while (!needDrain) {
36
- const item = queue.shift()
37
- if (!item) {
38
- break
39
- }
40
- pool[kQueued]--
41
- needDrain = !this.dispatch(item.opts, item.handler)
32
+ let needDrain = false
33
+
34
+ while (!needDrain) {
35
+ const item = queue.shift()
36
+ if (!item) {
37
+ break
42
38
  }
39
+ this[kQueued]--
40
+ needDrain = !client.dispatch(item.opts, item.handler)
41
+ }
43
42
 
44
- this[kNeedDrain] = needDrain
43
+ client[kNeedDrain] = needDrain
45
44
 
46
- if (!this[kNeedDrain] && pool[kNeedDrain]) {
47
- pool[kNeedDrain] = false
48
- pool.emit('drain', origin, [pool, ...targets])
49
- }
45
+ if (!needDrain && this[kNeedDrain]) {
46
+ this[kNeedDrain] = false
47
+ this.emit('drain', origin, [this, ...targets])
48
+ }
50
49
 
51
- if (pool[kClosedResolve] && queue.isEmpty()) {
52
- Promise
53
- .all(pool[kClients].map(c => c.close()))
54
- .then(pool[kClosedResolve])
50
+ if (this[kClosedResolve] && queue.isEmpty()) {
51
+ const closeAll = new Array(this[kClients].length)
52
+ for (let i = 0; i < this[kClients].length; i++) {
53
+ closeAll[i] = this[kClients][i].close()
55
54
  }
55
+ Promise.all(closeAll)
56
+ .then(this[kClosedResolve])
56
57
  }
58
+ }
57
59
 
58
- this[kOnConnect] = (origin, targets) => {
59
- pool.emit('connect', origin, [pool, ...targets])
60
- }
60
+ [kOnConnect] = (origin, targets) => {
61
+ this.emit('connect', origin, [this, ...targets])
62
+ };
61
63
 
62
- this[kOnDisconnect] = (origin, targets, err) => {
63
- pool.emit('disconnect', origin, [pool, ...targets], err)
64
- }
64
+ [kOnDisconnect] = (origin, targets, err) => {
65
+ this.emit('disconnect', origin, [this, ...targets], err)
66
+ };
65
67
 
66
- this[kOnConnectionError] = (origin, targets, err) => {
67
- pool.emit('connectionError', origin, [pool, ...targets], err)
68
- }
68
+ [kOnConnectionError] = (origin, targets, err) => {
69
+ this.emit('connectionError', origin, [this, ...targets], err)
69
70
  }
70
71
 
71
72
  get [kBusy] () {
@@ -73,11 +74,19 @@ class PoolBase extends DispatcherBase {
73
74
  }
74
75
 
75
76
  get [kConnected] () {
76
- return this[kClients].filter(client => client[kConnected]).length
77
+ let ret = 0
78
+ for (const { [kConnected]: connected } of this[kClients]) {
79
+ ret += connected
80
+ }
81
+ return ret
77
82
  }
78
83
 
79
84
  get [kFree] () {
80
- return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length
85
+ let ret = 0
86
+ for (const { [kConnected]: connected, [kNeedDrain]: needDrain } of this[kClients]) {
87
+ ret += connected && !needDrain
88
+ }
89
+ return ret
81
90
  }
82
91
 
83
92
  get [kPending] () {
@@ -108,17 +117,21 @@ class PoolBase extends DispatcherBase {
108
117
  return new PoolStats(this)
109
118
  }
110
119
 
111
- async [kClose] () {
120
+ [kClose] () {
112
121
  if (this[kQueue].isEmpty()) {
113
- await Promise.all(this[kClients].map(c => c.close()))
122
+ const closeAll = new Array(this[kClients].length)
123
+ for (let i = 0; i < this[kClients].length; i++) {
124
+ closeAll[i] = this[kClients][i].close()
125
+ }
126
+ return Promise.all(closeAll)
114
127
  } else {
115
- await new Promise((resolve) => {
128
+ return new Promise((resolve) => {
116
129
  this[kClosedResolve] = resolve
117
130
  })
118
131
  }
119
132
  }
120
133
 
121
- async [kDestroy] (err) {
134
+ [kDestroy] (err) {
122
135
  while (true) {
123
136
  const item = this[kQueue].shift()
124
137
  if (!item) {
@@ -127,7 +140,11 @@ class PoolBase extends DispatcherBase {
127
140
  item.handler.onError(err)
128
141
  }
129
142
 
130
- await Promise.all(this[kClients].map(c => c.destroy(err)))
143
+ const destroyAll = new Array(this[kClients].length)
144
+ for (let i = 0; i < this[kClients].length; i++) {
145
+ destroyAll[i] = this[kClients][i].destroy(err)
146
+ }
147
+ return Promise.all(destroyAll)
131
148
  }
132
149
 
133
150
  [kDispatch] (opts, handler) {
@@ -147,7 +164,7 @@ class PoolBase extends DispatcherBase {
147
164
 
148
165
  [kAddClient] (client) {
149
166
  client
150
- .on('drain', this[kOnDrain])
167
+ .on('drain', this[kOnDrain].bind(this, client))
151
168
  .on('connect', this[kOnConnect])
152
169
  .on('disconnect', this[kOnDisconnect])
153
170
  .on('connectionError', this[kOnConnectionError])
@@ -157,7 +174,7 @@ class PoolBase extends DispatcherBase {
157
174
  if (this[kNeedDrain]) {
158
175
  queueMicrotask(() => {
159
176
  if (this[kNeedDrain]) {
160
- this[kOnDrain](client[kUrl], [this, client])
177
+ this[kOnDrain](client, client[kUrl], [client, this])
161
178
  }
162
179
  })
163
180
  }
@@ -51,8 +51,6 @@ class Pool extends PoolBase {
51
51
  throw new InvalidArgumentError('connect must be a function or an object')
52
52
  }
53
53
 
54
- super()
55
-
56
54
  if (typeof connect !== 'function') {
57
55
  connect = buildConnector({
58
56
  ...tls,
@@ -65,6 +63,8 @@ class Pool extends PoolBase {
65
63
  })
66
64
  }
67
65
 
66
+ super()
67
+
68
68
  this[kConnections] = connections || null
69
69
  this[kUrl] = util.parseOrigin(origin)
70
70
  this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
@@ -37,11 +37,12 @@ class Http1ProxyWrapper extends DispatcherBase {
37
37
  #client
38
38
 
39
39
  constructor (proxyUrl, { headers = {}, connect, factory }) {
40
- super()
41
40
  if (!proxyUrl) {
42
41
  throw new InvalidArgumentError('Proxy URL is mandatory')
43
42
  }
44
43
 
44
+ super()
45
+
45
46
  this[kProxyHeaders] = headers
46
47
  if (factory) {
47
48
  this.#client = factory(proxyUrl, { connect })
@@ -80,11 +81,11 @@ class Http1ProxyWrapper extends DispatcherBase {
80
81
  return this.#client[kDispatch](opts, handler)
81
82
  }
82
83
 
83
- async [kClose] () {
84
+ [kClose] () {
84
85
  return this.#client.close()
85
86
  }
86
87
 
87
- async [kDestroy] (err) {
88
+ [kDestroy] (err) {
88
89
  return this.#client.destroy(err)
89
90
  }
90
91
  }
@@ -220,14 +221,18 @@ class ProxyAgent extends DispatcherBase {
220
221
  }
221
222
  }
222
223
 
223
- async [kClose] () {
224
- await this[kAgent].close()
225
- await this[kClient].close()
224
+ [kClose] () {
225
+ return Promise.all([
226
+ this[kAgent].close(),
227
+ this[kClient].close()
228
+ ])
226
229
  }
227
230
 
228
- async [kDestroy] () {
229
- await this[kAgent].destroy()
230
- await this[kClient].destroy()
231
+ [kDestroy] () {
232
+ return Promise.all([
233
+ this[kAgent].destroy(),
234
+ this[kClient].destroy()
235
+ ])
231
236
  }
232
237
  }
233
238
 
package/lib/global.js CHANGED
@@ -26,7 +26,25 @@ function getGlobalDispatcher () {
26
26
  return globalThis[globalDispatcher]
27
27
  }
28
28
 
29
+ // These are the globals that can be installed by undici.install().
30
+ // Not exported by index.js to avoid use outside of this module.
31
+ const installedExports = /** @type {const} */ (
32
+ [
33
+ 'fetch',
34
+ 'Headers',
35
+ 'Response',
36
+ 'Request',
37
+ 'FormData',
38
+ 'WebSocket',
39
+ 'CloseEvent',
40
+ 'ErrorEvent',
41
+ 'MessageEvent',
42
+ 'EventSource'
43
+ ]
44
+ )
45
+
29
46
  module.exports = {
30
47
  setGlobalDispatcher,
31
- getGlobalDispatcher
48
+ getGlobalDispatcher,
49
+ installedExports
32
50
  }
@@ -56,6 +56,22 @@ function needsRevalidation (result, cacheControlDirectives) {
56
56
  return false
57
57
  }
58
58
 
59
+ /**
60
+ * Check if we're within the stale-while-revalidate window for a stale response
61
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
62
+ * @returns {boolean}
63
+ */
64
+ function withinStaleWhileRevalidateWindow (result) {
65
+ const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
66
+ if (!staleWhileRevalidate) {
67
+ return false
68
+ }
69
+
70
+ const now = Date.now()
71
+ const staleWhileRevalidateExpiry = result.staleAt + (staleWhileRevalidate * 1000)
72
+ return now <= staleWhileRevalidateExpiry
73
+ }
74
+
59
75
  /**
60
76
  * @param {DispatchFn} dispatch
61
77
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
@@ -231,6 +247,51 @@ function handleResult (
231
247
  return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
232
248
  }
233
249
 
250
+ // RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
251
+ // and revalidate in background
252
+ if (withinStaleWhileRevalidateWindow(result)) {
253
+ // Serve stale response immediately
254
+ sendCachedValue(handler, opts, result, age, null, true)
255
+
256
+ // Start background revalidation (fire-and-forget)
257
+ queueMicrotask(() => {
258
+ let headers = {
259
+ ...opts.headers,
260
+ 'if-modified-since': new Date(result.cachedAt).toUTCString()
261
+ }
262
+
263
+ if (result.etag) {
264
+ headers['if-none-match'] = result.etag
265
+ }
266
+
267
+ if (result.vary) {
268
+ headers = {
269
+ ...headers,
270
+ ...result.vary
271
+ }
272
+ }
273
+
274
+ // Background revalidation - update cache if we get new data
275
+ dispatch(
276
+ {
277
+ ...opts,
278
+ headers
279
+ },
280
+ new CacheHandler(globalOpts, cacheKey, {
281
+ // Silent handler that just updates the cache
282
+ onRequestStart () {},
283
+ onRequestUpgrade () {},
284
+ onResponseStart () {},
285
+ onResponseData () {},
286
+ onResponseEnd () {},
287
+ onResponseError () {}
288
+ })
289
+ )
290
+ })
291
+
292
+ return true
293
+ }
294
+
234
295
  let withinStaleIfErrorThreshold = false
235
296
  const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
236
297
  if (staleIfErrorExpiry) {
@@ -0,0 +1,253 @@
1
+ 'use strict'
2
+
3
+ const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = require('node:zlib')
4
+ const { pipeline } = require('node:stream')
5
+ const DecoratorHandler = require('../handler/decorator-handler')
6
+
7
+ /** @typedef {import('node:stream').Transform} Transform */
8
+ /** @typedef {import('node:stream').Transform} Controller */
9
+ /** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
10
+
11
+ /** @type {Record<string, () => DecompressorStream>} */
12
+ const supportedEncodings = {
13
+ gzip: createGunzip,
14
+ 'x-gzip': createGunzip,
15
+ br: createBrotliDecompress,
16
+ deflate: createInflate,
17
+ compress: createInflate,
18
+ 'x-compress': createInflate,
19
+ ...(createZstdDecompress ? { zstd: createZstdDecompress } : {})
20
+ }
21
+
22
+ const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
23
+
24
+ let warningEmitted = /** @type {boolean} */ (false)
25
+
26
+ /**
27
+ * @typedef {Object} DecompressHandlerOptions
28
+ * @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
29
+ * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
30
+ */
31
+
32
+ class DecompressHandler extends DecoratorHandler {
33
+ /** @type {Transform[]} */
34
+ #decompressors = []
35
+ /** @type {NodeJS.WritableStream&NodeJS.ReadableStream|null} */
36
+ #pipelineStream
37
+ /** @type {Readonly<number[]>} */
38
+ #skipStatusCodes
39
+ /** @type {boolean} */
40
+ #skipErrorResponses
41
+
42
+ constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true } = {}) {
43
+ super(handler)
44
+ this.#skipStatusCodes = skipStatusCodes
45
+ this.#skipErrorResponses = skipErrorResponses
46
+ }
47
+
48
+ /**
49
+ * Determines if decompression should be skipped based on encoding and status code
50
+ * @param {string} contentEncoding - Content-Encoding header value
51
+ * @param {number} statusCode - HTTP status code of the response
52
+ * @returns {boolean} - True if decompression should be skipped
53
+ */
54
+ #shouldSkipDecompression (contentEncoding, statusCode) {
55
+ if (!contentEncoding || statusCode < 200) return true
56
+ if (this.#skipStatusCodes.includes(statusCode)) return true
57
+ if (this.#skipErrorResponses && statusCode >= 400) return true
58
+ return false
59
+ }
60
+
61
+ /**
62
+ * Creates a chain of decompressors for multiple content encodings
63
+ *
64
+ * @param {string} encodings - Comma-separated list of content encodings
65
+ * @returns {Array<DecompressorStream>} - Array of decompressor streams
66
+ */
67
+ #createDecompressionChain (encodings) {
68
+ const parts = encodings.split(',')
69
+
70
+ /** @type {DecompressorStream[]} */
71
+ const decompressors = []
72
+
73
+ for (let i = parts.length - 1; i >= 0; i--) {
74
+ const encoding = parts[i].trim()
75
+ if (!encoding) continue
76
+
77
+ if (!supportedEncodings[encoding]) {
78
+ decompressors.length = 0 // Clear if unsupported encoding
79
+ return decompressors // Unsupported encoding
80
+ }
81
+
82
+ decompressors.push(supportedEncodings[encoding]())
83
+ }
84
+
85
+ return decompressors
86
+ }
87
+
88
+ /**
89
+ * Sets up event handlers for a decompressor stream using readable events
90
+ * @param {DecompressorStream} decompressor - The decompressor stream
91
+ * @param {Controller} controller - The controller to coordinate with
92
+ * @returns {void}
93
+ */
94
+ #setupDecompressorEvents (decompressor, controller) {
95
+ decompressor.on('readable', () => {
96
+ let chunk
97
+ while ((chunk = decompressor.read()) !== null) {
98
+ const result = super.onResponseData(controller, chunk)
99
+ if (result === false) {
100
+ break
101
+ }
102
+ }
103
+ })
104
+
105
+ decompressor.on('error', (error) => {
106
+ super.onResponseError(controller, error)
107
+ })
108
+ }
109
+
110
+ /**
111
+ * Sets up event handling for a single decompressor
112
+ * @param {Controller} controller - The controller to handle events
113
+ * @returns {void}
114
+ */
115
+ #setupSingleDecompressor (controller) {
116
+ const decompressor = this.#decompressors[0]
117
+ this.#setupDecompressorEvents(decompressor, controller)
118
+
119
+ decompressor.on('end', () => {
120
+ super.onResponseEnd(controller, {})
121
+ })
122
+ }
123
+
124
+ /**
125
+ * Sets up event handling for multiple chained decompressors using pipeline
126
+ * @param {Controller} controller - The controller to handle events
127
+ * @returns {void}
128
+ */
129
+ #setupMultipleDecompressors (controller) {
130
+ const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
131
+ this.#setupDecompressorEvents(lastDecompressor, controller)
132
+
133
+ this.#pipelineStream = pipeline(this.#decompressors, (err) => {
134
+ if (err) {
135
+ super.onResponseError(controller, err)
136
+ return
137
+ }
138
+ super.onResponseEnd(controller, {})
139
+ })
140
+ }
141
+
142
+ /**
143
+ * Cleans up decompressor references to prevent memory leaks
144
+ * @returns {void}
145
+ */
146
+ #cleanupDecompressors () {
147
+ this.#decompressors.length = 0
148
+ this.#pipelineStream = null
149
+ }
150
+
151
+ /**
152
+ * @param {Controller} controller
153
+ * @param {number} statusCode
154
+ * @param {Record<string, string | string[] | undefined>} headers
155
+ * @param {string} statusMessage
156
+ * @returns {void}
157
+ */
158
+ onResponseStart (controller, statusCode, headers, statusMessage) {
159
+ const contentEncoding = headers['content-encoding']
160
+
161
+ // If content encoding is not supported or status code is in skip list
162
+ if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
163
+ return super.onResponseStart(controller, statusCode, headers, statusMessage)
164
+ }
165
+
166
+ const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
167
+
168
+ if (decompressors.length === 0) {
169
+ this.#cleanupDecompressors()
170
+ return super.onResponseStart(controller, statusCode, headers, statusMessage)
171
+ }
172
+
173
+ this.#decompressors = decompressors
174
+
175
+ // Remove compression headers since we're decompressing
176
+ const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
177
+
178
+ if (this.#decompressors.length === 1) {
179
+ this.#setupSingleDecompressor(controller)
180
+ } else {
181
+ this.#setupMultipleDecompressors(controller)
182
+ }
183
+
184
+ super.onResponseStart(controller, statusCode, newHeaders, statusMessage)
185
+ }
186
+
187
+ /**
188
+ * @param {Controller} controller
189
+ * @param {Buffer} chunk
190
+ * @returns {void}
191
+ */
192
+ onResponseData (controller, chunk) {
193
+ if (this.#decompressors.length > 0) {
194
+ this.#decompressors[0].write(chunk)
195
+ return
196
+ }
197
+ super.onResponseData(controller, chunk)
198
+ }
199
+
200
+ /**
201
+ * @param {Controller} controller
202
+ * @param {Record<string, string | string[]> | undefined} trailers
203
+ * @returns {void}
204
+ */
205
+ onResponseEnd (controller, trailers) {
206
+ if (this.#decompressors.length > 0) {
207
+ this.#decompressors[0].end()
208
+ this.#cleanupDecompressors()
209
+ return
210
+ }
211
+ super.onResponseEnd(controller, trailers)
212
+ }
213
+
214
+ /**
215
+ * @param {Controller} controller
216
+ * @param {Error} err
217
+ * @returns {void}
218
+ */
219
+ onResponseError (controller, err) {
220
+ if (this.#decompressors.length > 0) {
221
+ for (const decompressor of this.#decompressors) {
222
+ decompressor.destroy(err)
223
+ }
224
+ this.#cleanupDecompressors()
225
+ }
226
+ super.onResponseError(controller, err)
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Creates a decompression interceptor for HTTP responses
232
+ * @param {DecompressHandlerOptions} [options] - Options for the interceptor
233
+ * @returns {Function} - Interceptor function
234
+ */
235
+ function createDecompressInterceptor (options = {}) {
236
+ // Emit experimental warning only once
237
+ if (!warningEmitted) {
238
+ process.emitWarning(
239
+ 'DecompressInterceptor is experimental and subject to change',
240
+ 'ExperimentalWarning'
241
+ )
242
+ warningEmitted = true
243
+ }
244
+
245
+ return (dispatch) => {
246
+ return (opts, handler) => {
247
+ const decompressHandler = new DecompressHandler(handler, options)
248
+ return dispatch(opts, decompressHandler)
249
+ }
250
+ }
251
+ }
252
+
253
+ module.exports = createDecompressInterceptor