@openclaw/discord 2026.9.8 → 2026.10.1-beta.1

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 (147) hide show
  1. package/README.md +7 -0
  2. package/dist/.setup/{account-inspect-De8hOUxG.mjs → account-inspect-CjKqkDnH.mjs} +2 -2
  3. package/dist/.setup/{accounts-ChaMcixG.mjs → accounts-DXHCL8Zm.mjs} +9 -32
  4. package/dist/.setup/{active-turn-thread-route-fmHl1HNd.mjs → active-turn-thread-route-C4tp9noA.mjs} +5 -9
  5. package/dist/.setup/{activities-api-tvbsU2e3.mjs → activities-api-BL_hJ0JQ.mjs} +15 -18
  6. package/dist/.setup/{api-DVbJwZVt.mjs → api-_oznjffB.mjs} +1 -1
  7. package/dist/.setup/{approval-handler.runtime-DxCK2Rux.mjs → approval-handler.runtime-G-tMI_uD.mjs} +22 -61
  8. package/dist/.setup/{approval-message-updates-BKFqavHZ.mjs → approval-message-updates-CgRwc2_Q.mjs} +2 -2
  9. package/dist/.setup/{audit-CLg9I71N.mjs → audit-DtvzxONM.mjs} +4 -4
  10. package/dist/.setup/{channel-DxlUVU9t.mjs → channel-Ct3fRxet.mjs} +79 -75
  11. package/dist/.setup/{channel-access-C12aDZ0p.mjs → channel-access-BRp3Y-rF.mjs} +4 -17
  12. package/dist/.setup/{channel-type-DRCrEMss.mjs → channel-type-D1_b5UhU.mjs} +12 -16
  13. package/dist/.setup/{channel.setup-CRo1GjV7.mjs → channel.setup-DBJtugJO.mjs} +14 -34
  14. package/dist/.setup/{components-Cq1PvIpN.mjs → components-CzXLR3QH.mjs} +56 -72
  15. package/dist/.setup/{config-schema-oxELUSDx.mjs → config-schema-CsemBDpS.mjs} +6 -17
  16. package/dist/.setup/{conversation-binding-route-Caz_XuFH.mjs → conversation-binding-route-Bi62ApSt.mjs} +1 -1
  17. package/dist/.setup/{directory-config-esqNn0Eo.mjs → directory-config-BH1LA-gi.mjs} +1 -1
  18. package/dist/.setup/{directory-live-DtKRHuGV.mjs → directory-live-C9xz7IPm.mjs} +10 -13
  19. package/dist/.setup/{discord-0xJYfdDD.mjs → discord-J7aRzq2Q.mjs} +101 -381
  20. package/dist/.setup/{doctor-BCl1H5tl.mjs → doctor-DxSAngJq.mjs} +31 -70
  21. package/dist/.setup/doctor-contract-Bg3NPAMw.mjs +137 -0
  22. package/dist/.setup/{handle-action-CUvQWnE7.mjs → handle-action-DlmFX-_G.mjs} +10 -11
  23. package/dist/.setup/{handle-action.guild-admin-Df-8gVle.mjs → handle-action.guild-admin-BnyqTczR.mjs} +67 -129
  24. package/dist/.setup/{live-policy-config-8Pv20CHc.mjs → live-policy-config-emmPNyh4.mjs} +1 -1
  25. package/dist/.setup/{message-handler-B-Jk4oDk.mjs → message-handler-CDWDjWF0.mjs} +6 -6
  26. package/dist/.setup/{message-handler.preflight-v-__NSDq.mjs → message-handler.preflight-Cop4atEb.mjs} +13 -23
  27. package/dist/.setup/{message-handler.preflight-pluralkit-CWLUBVcx.mjs → message-handler.preflight-pluralkit-D1g3eq0B.mjs} +5 -8
  28. package/dist/.setup/{message-handler.process-BNFFuChn.mjs → message-handler.process-lX7pDqsd.mjs} +57 -96
  29. package/dist/.setup/{outbound-adapter-BobT-kse.mjs → outbound-adapter-BPq0c2wY.mjs} +11 -17
  30. package/dist/.setup/{outbound-session-route-zVGxxceg.mjs → outbound-session-route-DGPPKA0z.mjs} +21 -46
  31. package/dist/.setup/{presence-cache-Di3H3DIU.mjs → presence-cache-Dg3_dADn.mjs} +2 -4
  32. package/dist/.setup/{probe-LYrIHlBF.mjs → probe-DIRY8vh7.mjs} +17 -27
  33. package/dist/.setup/{provider-B_G5hASF.mjs → provider-DIexE7gK.mjs} +273 -446
  34. package/dist/.setup/{provider-session.runtime-BYlTBFD2.mjs → provider-session.runtime-C2UCZz8d.mjs} +3 -3
  35. package/dist/.setup/provider.runtime-Dkzrmmcm.mjs +2 -0
  36. package/dist/.setup/{realtime-session.runtime-oUd9DOcz.mjs → realtime-session.runtime-DvjyeY87.mjs} +6 -8
  37. package/dist/.setup/{receive-recovery-CHPOsnLc.mjs → receive-recovery-GqF2PUN_.mjs} +1 -4
  38. package/dist/.setup/{resolve-allowlist-common-DPdRggu5.mjs → resolve-allowlist-common-DUYRrrVB.mjs} +5 -9
  39. package/dist/.setup/{resolve-channels-BFPNRmVv.mjs → resolve-channels--MUatisx.mjs} +3 -3
  40. package/dist/.setup/{resolve-users-0-aBuCy3.mjs → resolve-users-CxLcDmM0.mjs} +6 -9
  41. package/dist/.setup/{retry-pz_cSha2.mjs → retry-BKzO25k4.mjs} +27 -51
  42. package/dist/.setup/{runtime-sUksZ--Q.mjs → runtime-CerRBfaa.mjs} +2 -2
  43. package/dist/.setup/{runtime-FN000lxA.mjs → runtime-CwUA0RLr.mjs} +228 -255
  44. package/dist/.setup/{runtime-doctor-migrations-CshUGl36.mjs → runtime-doctor-migrations-BPV-R4G9.mjs} +6 -0
  45. package/dist/.setup/{security-audit-Cdaegq7C.mjs → security-audit-D42cp5N7.mjs} +21 -48
  46. package/dist/.setup/{send-CrfEI6Bs.mjs → send-2X7m2Zmw.mjs} +45 -58
  47. package/dist/.setup/{send.components-DxQAMfES.mjs → send.components-RqvgmCs3.mjs} +44 -90
  48. package/dist/.setup/{send.outbound-BISKnb-a.mjs → send.outbound-B4QHSEzm.mjs} +60 -98
  49. package/dist/.setup/{send.shared-DGFkbkQR.mjs → send.shared-BQIUh6Vn.mjs} +13 -20
  50. package/dist/.setup/{sender-identity-B9X5Vb9R.mjs → sender-identity-CooeYfAn.mjs} +1 -1
  51. package/dist/.setup/{session-key-normalization-wJgsKPNF.mjs → session-key-normalization-CCHGNSQw.mjs} +3 -9
  52. package/dist/.setup/setup-plugin-api-nn_giXvZ.mjs +3 -0
  53. package/dist/.setup/{shared-B3wZ4hN-.mjs → shared-CIJn6AEC.mjs} +7 -7
  54. package/dist/.setup/{subagent-hooks-nlUE9ow7.mjs → subagent-hooks-BYlVnxzy.mjs} +4 -9
  55. package/dist/.setup/{system-events-CdeZnvPV.mjs → system-events-CQ3if_pS.mjs} +2 -2
  56. package/dist/.setup/{target-resolver-BuLapkhy.mjs → target-resolver-Dzxf80Y3.mjs} +5 -13
  57. package/dist/.setup/targets-Lf8D55w-.mjs +3 -0
  58. package/dist/.setup/{thread-bindings-D7TfgjR_.mjs → thread-bindings-yNNlcUWE.mjs} +5 -6
  59. package/dist/.setup/{thread-bindings.manager-DN-NissX.mjs → thread-bindings.manager-plk0zJAW.mjs} +14 -20
  60. package/dist/.setup/{thread-bindings.session-updates-CshfKghV.mjs → thread-bindings.session-updates-C9aKtVs6.mjs} +295 -5
  61. package/dist/.setup/{timeouts-D86uWLt_.mjs → timeouts-pun7VJ02.mjs} +2 -15
  62. package/dist/.setup/{transcripts-source-p8EV179h.mjs → transcripts-source-CNWswry-.mjs} +31 -79
  63. package/dist/.setup/typing-CNlSKY5S.mjs +11 -0
  64. package/dist/.setup/{voice-runtime-B46rcwrG.mjs → voice-runtime-CeS6zfY3.mjs} +62 -110
  65. package/dist/account-inspect-api.js +1 -1
  66. package/dist/action-runtime-api.js +1 -1
  67. package/dist/activities-api.js +1 -1
  68. package/dist/api.js +18 -18
  69. package/dist/channel-config-api.js +1 -1
  70. package/dist/channel-plugin-api.js +1 -1
  71. package/dist/control-ui/2afbdab1d3aa65a789d8a3eba666f4b1ec5df07073538fe30d6cf32093a1fb2d/index.js +1 -0
  72. package/dist/directory-contract-api.js +1 -1
  73. package/dist/doctor-contract-api.js +19 -128
  74. package/dist/index.js +1 -1
  75. package/dist/runtime-api.js +20 -21
  76. package/dist/security-audit-contract-api.js +1 -1
  77. package/dist/session-key-api.js +1 -1
  78. package/dist/setup-entry.js +1 -1
  79. package/dist/setup-plugin-api.js +1 -1
  80. package/dist/src/voice/audio-worker.runtime.js +6 -8
  81. package/dist/subagent-hooks-api.js +1 -1
  82. package/dist/transcripts-source-api.js +1 -1
  83. package/node_modules/libopus-wasm/CHANGELOG.md +8 -0
  84. package/node_modules/libopus-wasm/dist/discordjs.d.ts.map +1 -1
  85. package/node_modules/libopus-wasm/dist/discordjs.js +14 -2
  86. package/node_modules/libopus-wasm/dist/generated/libopus.generated.mjs +0 -0
  87. package/node_modules/libopus-wasm/dist/index.d.ts.map +1 -1
  88. package/node_modules/libopus-wasm/dist/index.js +55 -100
  89. package/node_modules/libopus-wasm/package.json +5 -5
  90. package/node_modules/undici/README.md +10 -1
  91. package/node_modules/undici/docs/docs/api/Agent.md +8 -8
  92. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +9 -6
  93. package/node_modules/undici/docs/docs/api/Dispatcher.md +9 -0
  94. package/node_modules/undici/docs/docs/api/Errors.md +25 -0
  95. package/node_modules/undici/docs/docs/api/Fetch.md +10 -0
  96. package/node_modules/undici/docs/docs/api/Interceptors.md +4 -3
  97. package/node_modules/undici/docs/docs/api/Pool.md +5 -6
  98. package/node_modules/undici/docs/docs/index.md +10 -1
  99. package/node_modules/undici/lib/api/readable.js +1 -2
  100. package/node_modules/undici/lib/core/request.js +23 -2
  101. package/node_modules/undici/lib/core/util.js +0 -39
  102. package/node_modules/undici/lib/dispatcher/client-h1.js +11 -4
  103. package/node_modules/undici/lib/dispatcher/client-h2.js +52 -8
  104. package/node_modules/undici/lib/dispatcher/client.js +6 -1
  105. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +21 -6
  106. package/node_modules/undici/lib/dispatcher/pool-base.js +63 -2
  107. package/node_modules/undici/lib/dispatcher/pool.js +90 -7
  108. package/node_modules/undici/lib/dispatcher/proxy-agent.js +16 -3
  109. package/node_modules/undici/lib/handler/deduplication-handler.js +24 -11
  110. package/node_modules/undici/lib/handler/redirect-handler.js +5 -0
  111. package/node_modules/undici/lib/handler/retry-handler.js +72 -13
  112. package/node_modules/undici/lib/interceptor/cache.js +4 -1
  113. package/node_modules/undici/lib/interceptor/decompress.js +236 -69
  114. package/node_modules/undici/lib/web/cache/cache.js +17 -8
  115. package/node_modules/undici/lib/web/cache/cachestorage.js +11 -5
  116. package/node_modules/undici/lib/web/cookies/index.js +19 -7
  117. package/node_modules/undici/lib/web/cookies/util.js +3 -1
  118. package/node_modules/undici/lib/web/eventsource/eventsource.js +25 -1
  119. package/node_modules/undici/lib/web/fetch/body.js +22 -15
  120. package/node_modules/undici/lib/web/fetch/data-url.js +6 -6
  121. package/node_modules/undici/lib/web/fetch/formdata.js +30 -36
  122. package/node_modules/undici/lib/web/fetch/headers.js +34 -31
  123. package/node_modules/undici/lib/web/fetch/index.js +46 -25
  124. package/node_modules/undici/lib/web/fetch/request.js +74 -79
  125. package/node_modules/undici/lib/web/fetch/response.js +43 -44
  126. package/node_modules/undici/lib/web/fetch/util.js +7 -7
  127. package/node_modules/undici/lib/web/infra/index.js +16 -1
  128. package/node_modules/undici/lib/web/webidl/index.js +2 -14
  129. package/node_modules/undici/lib/web/websocket/connection.js +33 -12
  130. package/node_modules/undici/lib/web/websocket/events.js +43 -26
  131. package/node_modules/undici/lib/web/websocket/receiver.js +2 -0
  132. package/node_modules/undici/lib/web/websocket/stream/websocketerror.js +13 -10
  133. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +19 -4
  134. package/node_modules/undici/lib/web/websocket/websocket.js +44 -41
  135. package/node_modules/undici/package.json +1 -1
  136. package/node_modules/undici/types/diagnostics-channel.d.ts +2 -1
  137. package/node_modules/undici/types/interceptors.d.ts +1 -1
  138. package/node_modules/undici/types/webidl.d.ts +13 -7
  139. package/openclaw.plugin.json +6 -0
  140. package/package.json +12 -7
  141. package/dist/.setup/doctor-contract-CvvgwrrG.mjs +0 -409
  142. package/dist/.setup/model-picker-preference-primitives-BHT1LkR_.mjs +0 -37
  143. package/dist/.setup/provider.runtime-CUE3UxER.mjs +0 -2
  144. package/dist/.setup/setup-plugin-api-BQUBxbhe.mjs +0 -3
  145. package/dist/.setup/targets-0KUcEIYx.mjs +0 -3
  146. package/dist/.setup/thread-bindings.state-g6v9zAym.mjs +0 -331
  147. package/dist/.setup/typing-BiHCzBRu.mjs +0 -15
@@ -47,34 +47,51 @@ function validatePartialResponseContentLength (headers, range, statusCode, retry
47
47
  // so nothing outside the handler can trigger it.
48
48
  class RetryController {
49
49
  #onAbort
50
+ #paused = false
51
+ #target = null
50
52
 
51
53
  constructor (onAbort) {
52
54
  this.#onAbort = onAbort
53
- this.target = null
54
55
  }
55
56
 
56
- pause () { this.target?.pause() }
57
- resume () { this.target?.resume() }
57
+ set target (target) {
58
+ this.#target = target
59
+ if (this.#paused) {
60
+ target?.pause()
61
+ }
62
+ }
63
+
64
+ get target () { return this.#target }
65
+
66
+ pause () {
67
+ this.#paused = true
68
+ this.#target?.pause()
69
+ }
70
+
71
+ resume () {
72
+ this.#paused = false
73
+ this.#target?.resume()
74
+ }
58
75
 
59
76
  abort (reason) {
60
- this.target?.abort(reason)
77
+ this.#target?.abort(reason)
61
78
  this.#onAbort(reason)
62
79
  }
63
80
 
64
- get paused () { return this.target?.paused ?? false }
65
- get aborted () { return this.target?.aborted ?? false }
66
- get reason () { return this.target?.reason ?? null }
67
- get rawHeaders () { return this.target?.rawHeaders ?? null }
81
+ get paused () { return this.#paused || (this.#target?.paused ?? false) }
82
+ get aborted () { return this.#target?.aborted ?? false }
83
+ get reason () { return this.#target?.reason ?? null }
84
+ get rawHeaders () { return this.#target?.rawHeaders ?? null }
68
85
  set rawHeaders (value) {
69
- if (this.target) {
70
- this.target.rawHeaders = value
86
+ if (this.#target) {
87
+ this.#target.rawHeaders = value
71
88
  }
72
89
  }
73
90
 
74
- get rawTrailers () { return this.target?.rawTrailers ?? null }
91
+ get rawTrailers () { return this.#target?.rawTrailers ?? null }
75
92
  set rawTrailers (value) {
76
- if (this.target) {
77
- this.target.rawTrailers = value
93
+ if (this.#target) {
94
+ this.#target.rawTrailers = value
78
95
  }
79
96
  }
80
97
  }
@@ -142,6 +159,12 @@ class RetryHandler {
142
159
  // Backoff timer returned by the retry policy, so #onAbort can cancel it.
143
160
  // Null for custom policies that do not return their timer.
144
161
  this.retryTimer = null
162
+ // A response can complete while its controller is paused if the peer closes
163
+ // the connection. Hold its body until the retry policy decides whether to
164
+ // discard it for a retry or forward it as the final response.
165
+ this.pendingResponseData = null
166
+ this.pendingResponseTrailers = null
167
+ this.pendingResponseEnded = false
145
168
  // Set once an abort during the backoff delivered the terminal error
146
169
  // downstream; late policy callbacks and connection errors are then moot.
147
170
  this.aborted = false
@@ -183,6 +206,13 @@ class RetryHandler {
183
206
  this.retryPending = false
184
207
  this.retryTimer = null
185
208
 
209
+ const pendingData = this.pendingResponseData
210
+ const pendingTrailers = this.pendingResponseTrailers
211
+ const pendingEnd = this.pendingResponseEnded
212
+ this.pendingResponseData = null
213
+ this.pendingResponseTrailers = null
214
+ this.pendingResponseEnded = false
215
+
186
216
  if (passedErr) {
187
217
  if (this.headersSent) {
188
218
  // The downstream handler already received the response from an
@@ -193,6 +223,15 @@ class RetryHandler {
193
223
  this.headersSent = true
194
224
  this.checkpointResponseEnd(headers)
195
225
  this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
226
+ controller.resume()
227
+
228
+ if (pendingEnd) {
229
+ for (const chunk of pendingData) {
230
+ this.onResponseData(controller, chunk)
231
+ }
232
+ this.onResponseEnd(controller, pendingTrailers)
233
+ }
234
+ return
196
235
  }
197
236
  controller.resume()
198
237
  return
@@ -200,6 +239,9 @@ class RetryHandler {
200
239
 
201
240
  this.error = err
202
241
  controller.resume()
242
+ if (pendingEnd) {
243
+ this.onResponseEnd(controller, pendingTrailers)
244
+ }
203
245
  }
204
246
 
205
247
  // The pause()/resume() pair (here and in shouldRetry) acts on THIS
@@ -213,6 +255,9 @@ class RetryHandler {
213
255
  // The default policy returns its backoff timer so an abort can cancel it;
214
256
  // a custom policy may return anything (or nothing), which is ignored.
215
257
  this.retryPending = true
258
+ this.pendingResponseData = []
259
+ this.pendingResponseTrailers = null
260
+ this.pendingResponseEnded = false
216
261
  this.retryTimer = this.retryOpts.retry(
217
262
  err,
218
263
  {
@@ -464,6 +509,11 @@ class RetryHandler {
464
509
  }
465
510
 
466
511
  onResponseData (_controller, chunk) {
512
+ if (this.pendingResponseData !== null) {
513
+ this.pendingResponseData.push(chunk)
514
+ return
515
+ }
516
+
467
517
  if (this.error) {
468
518
  return
469
519
  }
@@ -474,6 +524,12 @@ class RetryHandler {
474
524
  }
475
525
 
476
526
  onResponseEnd (_controller, trailers) {
527
+ if (this.pendingResponseData !== null) {
528
+ this.pendingResponseTrailers = trailers
529
+ this.pendingResponseEnded = true
530
+ return
531
+ }
532
+
477
533
  if (this.error && this.retryOpts.throwOnError) {
478
534
  throw this.error
479
535
  }
@@ -587,6 +643,9 @@ class RetryHandler {
587
643
  this.retryPending = false
588
644
  clearTimeout(this.retryTimer)
589
645
  this.retryTimer = null
646
+ this.pendingResponseData = null
647
+ this.pendingResponseTrailers = null
648
+ this.pendingResponseEnded = false
590
649
  this.handler.onResponseError?.(this.controllerProxy, reason ?? new RequestAbortedError())
591
650
  }
592
651
  }
@@ -330,6 +330,8 @@ function sendCachedValue (handler, opts, result, age, context, isStale) {
330
330
  assert(!stream.destroyed, 'stream should not be destroyed')
331
331
  assert(!stream.readableDidRead, 'stream should not be readableDidRead')
332
332
 
333
+ let aborted = false
334
+
333
335
  const controller = {
334
336
  rawHeaders: [],
335
337
  rawTrailers: [],
@@ -343,12 +345,13 @@ function sendCachedValue (handler, opts, result, age, context, isStale) {
343
345
  return stream.isPaused()
344
346
  },
345
347
  get aborted () {
346
- return stream.destroyed
348
+ return aborted
347
349
  },
348
350
  get reason () {
349
351
  return stream.errored
350
352
  },
351
353
  abort (reason) {
354
+ aborted = true
352
355
  stream.destroy(reason ?? new AbortError())
353
356
  }
354
357
  }
@@ -9,8 +9,63 @@ const DecoratorHandler = require('../handler/decorator-handler')
9
9
  /** @typedef {import('node:stream').Transform} Controller */
10
10
  /** @typedef {Transform&import('node:zlib').Zlib} DecompressorStream */
11
11
 
12
+ class DecompressController {
13
+ #onPause
14
+ #onResume
15
+ #onAbort
16
+ #paused = false
17
+
18
+ constructor (onPause, onResume, onAbort) {
19
+ this.#onPause = onPause
20
+ this.#onResume = onResume
21
+ this.#onAbort = onAbort
22
+ this.target = null
23
+ }
24
+
25
+ pause () {
26
+ if (this.#paused) {
27
+ return
28
+ }
29
+
30
+ this.#paused = true
31
+ this.#onPause()
32
+ }
33
+
34
+ resume () {
35
+ if (!this.#paused) {
36
+ return
37
+ }
38
+
39
+ this.#paused = false
40
+ this.#onResume()
41
+ }
42
+
43
+ abort (reason) {
44
+ this.target?.abort(reason)
45
+ this.#onAbort(reason)
46
+ }
47
+
48
+ get paused () { return this.#paused }
49
+ get aborted () { return this.target?.aborted ?? false }
50
+ get reason () { return this.target?.reason ?? null }
51
+ get rawHeaders () { return this.target?.rawHeaders ?? null }
52
+ set rawHeaders (value) {
53
+ if (this.target) {
54
+ this.target.rawHeaders = value
55
+ }
56
+ }
57
+
58
+ get rawTrailers () { return this.target?.rawTrailers ?? null }
59
+ set rawTrailers (value) {
60
+ if (this.target) {
61
+ this.target.rawTrailers = value
62
+ }
63
+ }
64
+ }
65
+
12
66
  /** @type {Record<string, () => DecompressorStream>} */
13
67
  const supportedEncodings = {
68
+ __proto__: null,
14
69
  gzip: createGunzip,
15
70
  'x-gzip': createGunzip,
16
71
  br: createBrotliDecompress,
@@ -21,7 +76,7 @@ const supportedEncodings = {
21
76
  }
22
77
 
23
78
  const defaultSkipStatusCodes = /** @type {const} */ ([204, 304])
24
- const defaultMaxSize = 64 * 1024 * 1024
79
+ const defaultMaxSize = 0
25
80
 
26
81
  /**
27
82
  * Limits the output of one stage in a decompression chain.
@@ -53,7 +108,7 @@ let warningEmitted = /** @type {boolean} */ (false)
53
108
  * @typedef {Object} DecompressHandlerOptions
54
109
  * @property {number[]|Readonly<number[]>} [skipStatusCodes=[204, 304]] - List of status codes to skip decompression for
55
110
  * @property {boolean} [skipErrorResponses] - Whether to skip decompression for error responses (status codes >= 400)
56
- * @property {number} [maxSize=67108864] - Maximum decompressed response size in bytes
111
+ * @property {number} [maxSize=0] - Maximum decompressed response size in bytes. 0 disables the limit
57
112
  */
58
113
 
59
114
  class DecompressHandler extends DecoratorHandler {
@@ -73,16 +128,130 @@ class DecompressHandler extends DecoratorHandler {
73
128
  #terminated = false
74
129
  /** @type {boolean} */
75
130
  #inputEnded = false
131
+ /** @type {boolean} */
132
+ #inputBackpressured = false
133
+ /** @type {boolean} */
134
+ #upstreamPaused = false
135
+ /** @type {boolean} */
136
+ #draining = false
137
+ /** @type {boolean} */
138
+ #drainRequested = false
139
+ /** @type {boolean} */
140
+ #completionPending = false
141
+ /** @type {DecompressorStream | undefined} */
142
+ #finalDecompressor
143
+ /** @type {DecompressController} */
144
+ #controller
76
145
 
77
146
  constructor (handler, { skipStatusCodes = defaultSkipStatusCodes, skipErrorResponses = true, maxSize = defaultMaxSize } = {}) {
78
- if (!Number.isSafeInteger(maxSize) || maxSize < 1) {
79
- throw new InvalidArgumentError('maxSize must be a positive integer')
147
+ if (!Number.isSafeInteger(maxSize) || maxSize < 0) {
148
+ throw new InvalidArgumentError('maxSize must be a non-negative integer')
80
149
  }
81
150
 
82
151
  super(handler)
83
152
  this.#skipStatusCodes = skipStatusCodes
84
153
  this.#skipErrorResponses = skipErrorResponses
85
154
  this.#maxSize = maxSize
155
+ this.#controller = new DecompressController(
156
+ () => this.#onDownstreamPause(),
157
+ () => this.#onDownstreamResume(),
158
+ reason => {
159
+ if (this.#inputEnded && !this.#terminated) {
160
+ this.onResponseError(this.#controller, reason)
161
+ }
162
+ }
163
+ )
164
+ }
165
+
166
+ #onDownstreamPause () {
167
+ this.#pauseUpstream()
168
+ }
169
+
170
+ #onDownstreamResume () {
171
+ const drainWasDeferred = this.#draining
172
+ this.#drainOutput()
173
+ if (!drainWasDeferred) {
174
+ this.#resumeUpstreamIfNeeded()
175
+ this.#finishIfReady()
176
+ }
177
+ }
178
+
179
+ #pauseUpstream () {
180
+ if (!this.#upstreamPaused && !this.#terminated) {
181
+ this.#upstreamPaused = true
182
+ this.#controller.target?.pause()
183
+ }
184
+ }
185
+
186
+ #resumeUpstreamIfNeeded () {
187
+ if (this.#upstreamPaused && !this.#controller.paused && !this.#inputBackpressured) {
188
+ this.#upstreamPaused = false
189
+ if (!this.#inputEnded) {
190
+ this.#controller.target?.resume()
191
+ }
192
+ }
193
+ }
194
+
195
+ #drainOutput () {
196
+ if (this.#terminated || this.#controller.paused || !this.#finalDecompressor) {
197
+ return
198
+ }
199
+
200
+ if (this.#draining) {
201
+ this.#drainRequested = true
202
+ return
203
+ }
204
+
205
+ this.#draining = true
206
+ try {
207
+ do {
208
+ this.#drainRequested = false
209
+ let chunk
210
+ while (!this.#terminated && !this.#controller.paused && (chunk = this.#finalDecompressor.read()) !== null) {
211
+ if (this.#maxSize > 0) {
212
+ const decompressedSize = this.#decompressedSize + chunk.length
213
+ if (decompressedSize > this.#maxSize) {
214
+ this.#fail(new ResponseExceededMaxSizeError(
215
+ `Decompressed response size (${decompressedSize}) exceeded maxSize (${this.#maxSize})`
216
+ ))
217
+ return
218
+ }
219
+
220
+ this.#decompressedSize = decompressedSize
221
+ }
222
+
223
+ const result = super.onResponseData(this.#controller, chunk)
224
+ if (result === false && !this.#controller.paused) {
225
+ this.#controller.pause()
226
+ }
227
+ }
228
+ } while (this.#drainRequested && !this.#terminated && !this.#controller.paused)
229
+ } finally {
230
+ this.#draining = false
231
+ }
232
+
233
+ this.#resumeUpstreamIfNeeded()
234
+ this.#finishIfReady()
235
+ }
236
+
237
+ #finishIfReady () {
238
+ if (this.#terminated || !this.#completionPending || this.#controller.paused || this.#draining) {
239
+ return
240
+ }
241
+
242
+ this.#terminated = true
243
+ this.#cleanupDecompressors()
244
+ super.onResponseEnd(this.#controller, this.#trailers)
245
+ }
246
+
247
+ #onDecompressionEnd () {
248
+ if (this.#terminated) {
249
+ return
250
+ }
251
+
252
+ this.#completionPending = true
253
+ this.#drainOutput()
254
+ this.#finishIfReady()
86
255
  }
87
256
 
88
257
  /**
@@ -138,7 +307,7 @@ class DecompressHandler extends DecoratorHandler {
138
307
  const streams = []
139
308
  for (let i = 0; i < decompressors.length; i++) {
140
309
  streams.push(decompressors[i])
141
- if (i < decompressors.length - 1) {
310
+ if (i < decompressors.length - 1 && this.#maxSize > 0) {
142
311
  streams.push(createMaxSizeLimiter(this.#maxSize))
143
312
  }
144
313
  }
@@ -148,11 +317,10 @@ class DecompressHandler extends DecoratorHandler {
148
317
 
149
318
  /**
150
319
  * Stops decompression and reports an error.
151
- * @param {Controller} controller - The controller to coordinate with
152
320
  * @param {Error} error - The decompression error
153
321
  * @returns {void}
154
322
  */
155
- #fail (controller, error) {
323
+ #fail (error) {
156
324
  if (this.#terminated) {
157
325
  return
158
326
  }
@@ -160,75 +328,41 @@ class DecompressHandler extends DecoratorHandler {
160
328
  if (this.#inputEnded) {
161
329
  // The request is already marked complete once the compressed input ends,
162
330
  // so controller.abort() can no longer propagate decoder flush errors.
163
- this.onResponseError(controller, error)
331
+ this.onResponseError(this.#controller, error)
164
332
  } else {
165
- controller.abort(error)
333
+ this.#controller.abort(error)
166
334
  }
167
335
  }
168
336
 
169
337
  /**
170
- * Sets up event handlers for a decompressor stream using readable events
338
+ * Sets up event handlers for the final decompressor stream.
171
339
  * @param {DecompressorStream} decompressor - The decompressor stream
172
- * @param {Controller} controller - The controller to coordinate with
173
340
  * @returns {void}
174
341
  */
175
- #setupDecompressorEvents (decompressor, controller) {
176
- decompressor.on('readable', () => {
177
- if (this.#terminated) {
178
- return
179
- }
180
-
181
- let chunk
182
- while ((chunk = decompressor.read()) !== null) {
183
- const decompressedSize = this.#decompressedSize + chunk.length
184
- if (decompressedSize > this.#maxSize) {
185
- this.#fail(controller, new ResponseExceededMaxSizeError(
186
- `Decompressed response size (${decompressedSize}) exceeded maxSize (${this.#maxSize})`
187
- ))
188
- return
189
- }
190
-
191
- this.#decompressedSize = decompressedSize
192
- const result = super.onResponseData(controller, chunk)
193
- if (result === false) {
194
- break
195
- }
196
- }
197
- })
198
-
199
- decompressor.on('error', (error) => {
200
- this.#fail(controller, error)
201
- })
342
+ #setupDecompressorEvents (decompressor) {
343
+ this.#finalDecompressor = decompressor
344
+ decompressor.on('readable', () => this.#drainOutput())
345
+ decompressor.on('error', (error) => this.#fail(error))
202
346
  }
203
347
 
204
348
  /**
205
349
  * Sets up event handling for a single decompressor
206
- * @param {Controller} controller - The controller to handle events
207
350
  * @returns {void}
208
351
  */
209
- #setupSingleDecompressor (controller) {
352
+ #setupSingleDecompressor () {
210
353
  const decompressor = this.#decompressors[0]
211
- this.#setupDecompressorEvents(decompressor, controller)
354
+ this.#setupDecompressorEvents(decompressor)
212
355
 
213
- decompressor.on('end', () => {
214
- if (this.#terminated) {
215
- return
216
- }
217
-
218
- this.#terminated = true
219
- this.#cleanupDecompressors()
220
- super.onResponseEnd(controller, this.#trailers)
221
- })
356
+ decompressor.on('end', () => this.#onDecompressionEnd())
222
357
  }
223
358
 
224
359
  /**
225
360
  * Sets up event handling for multiple chained decompressors using pipeline
226
- * @param {Controller} controller - The controller to handle events
227
361
  * @returns {void}
228
362
  */
229
- #setupMultipleDecompressors (controller) {
363
+ #setupMultipleDecompressors () {
230
364
  const lastDecompressor = this.#decompressors[this.#decompressors.length - 1]
231
- this.#setupDecompressorEvents(lastDecompressor, controller)
365
+ this.#setupDecompressorEvents(lastDecompressor)
232
366
 
233
367
  pipeline(this.#decompressors, (err) => {
234
368
  if (this.#terminated) {
@@ -236,13 +370,26 @@ class DecompressHandler extends DecoratorHandler {
236
370
  }
237
371
 
238
372
  if (err) {
239
- this.#fail(controller, err)
373
+ this.#fail(err)
240
374
  return
241
375
  }
242
376
 
243
- this.#terminated = true
244
- this.#cleanupDecompressors()
245
- super.onResponseEnd(controller, this.#trailers)
377
+ this.#onDecompressionEnd()
378
+ })
379
+ }
380
+
381
+ #setupInputBackpressure () {
382
+ const decompressor = this.#decompressors[0]
383
+ decompressor.on('drain', () => {
384
+ if (this.#terminated) {
385
+ return
386
+ }
387
+
388
+ this.#inputBackpressured = false
389
+ if (!this.#controller.paused) {
390
+ this.#drainOutput()
391
+ this.#resumeUpstreamIfNeeded()
392
+ }
246
393
  })
247
394
  }
248
395
 
@@ -252,6 +399,16 @@ class DecompressHandler extends DecoratorHandler {
252
399
  */
253
400
  #cleanupDecompressors () {
254
401
  this.#decompressors.length = 0
402
+ this.#finalDecompressor = undefined
403
+ }
404
+
405
+ onRequestStart (controller, context) {
406
+ this.#controller.target = controller
407
+ return super.onRequestStart(this.#controller, context)
408
+ }
409
+
410
+ onRequestUpgrade (controller, statusCode, headers, socket) {
411
+ return super.onRequestUpgrade(this.#controller, statusCode, headers, socket)
255
412
  }
256
413
 
257
414
  /**
@@ -262,18 +419,24 @@ class DecompressHandler extends DecoratorHandler {
262
419
  * @returns {void}
263
420
  */
264
421
  onResponseStart (controller, statusCode, headers, statusMessage) {
265
- const contentEncoding = headers['content-encoding']
422
+ // Repeated field lines reach us as an array. RFC 9110 section 5.3 lets a
423
+ // recipient join them with commas, which yields the single-line form the
424
+ // decompression chain already handles.
425
+ const rawContentEncoding = headers['content-encoding']
426
+ const contentEncoding = Array.isArray(rawContentEncoding)
427
+ ? rawContentEncoding.join(',')
428
+ : rawContentEncoding
266
429
 
267
430
  // If content encoding is not supported or status code is in skip list
268
431
  if (this.#shouldSkipDecompression(contentEncoding, statusCode)) {
269
- return super.onResponseStart(controller, statusCode, headers, statusMessage)
432
+ return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
270
433
  }
271
434
 
272
435
  const decompressors = this.#createDecompressionChain(contentEncoding.toLowerCase())
273
436
 
274
437
  if (decompressors.length === 0) {
275
438
  this.#cleanupDecompressors()
276
- return super.onResponseStart(controller, statusCode, headers, statusMessage)
439
+ return super.onResponseStart(this.#controller, statusCode, headers, statusMessage)
277
440
  }
278
441
 
279
442
  this.#decompressors = decompressors
@@ -281,8 +444,8 @@ class DecompressHandler extends DecoratorHandler {
281
444
  // Remove compression headers since we're decompressing
282
445
  const { 'content-encoding': _, 'content-length': __, ...newHeaders } = headers
283
446
 
284
- if (controller?.rawHeaders) {
285
- const rawHeaders = controller.rawHeaders
447
+ if (this.#controller.rawHeaders) {
448
+ const rawHeaders = this.#controller.rawHeaders
286
449
 
287
450
  if (Array.isArray(rawHeaders)) {
288
451
  const filteredHeaders = []
@@ -308,13 +471,14 @@ class DecompressHandler extends DecoratorHandler {
308
471
  }
309
472
  }
310
473
 
474
+ this.#setupInputBackpressure()
311
475
  if (this.#decompressors.length === 1) {
312
- this.#setupSingleDecompressor(controller)
476
+ this.#setupSingleDecompressor()
313
477
  } else {
314
- this.#setupMultipleDecompressors(controller)
478
+ this.#setupMultipleDecompressors()
315
479
  }
316
480
 
317
- return super.onResponseStart(controller, statusCode, newHeaders, statusMessage)
481
+ return super.onResponseStart(this.#controller, statusCode, newHeaders, statusMessage)
318
482
  }
319
483
 
320
484
  /**
@@ -324,10 +488,13 @@ class DecompressHandler extends DecoratorHandler {
324
488
  */
325
489
  onResponseData (controller, chunk) {
326
490
  if (this.#decompressors.length > 0) {
327
- this.#decompressors[0].write(chunk)
491
+ if (!this.#decompressors[0].write(chunk)) {
492
+ this.#inputBackpressured = true
493
+ this.#pauseUpstream()
494
+ }
328
495
  return
329
496
  }
330
- super.onResponseData(controller, chunk)
497
+ return super.onResponseData(this.#controller, chunk)
331
498
  }
332
499
 
333
500
  /**
@@ -342,7 +509,7 @@ class DecompressHandler extends DecoratorHandler {
342
509
  this.#decompressors[0].end()
343
510
  return
344
511
  }
345
- super.onResponseEnd(controller, trailers)
512
+ return super.onResponseEnd(this.#controller, trailers)
346
513
  }
347
514
 
348
515
  /**
@@ -360,7 +527,7 @@ class DecompressHandler extends DecoratorHandler {
360
527
  decompressor.destroy()
361
528
  }
362
529
  this.#cleanupDecompressors()
363
- super.onResponseError(controller, err)
530
+ super.onResponseError(this.#controller, err)
364
531
  }
365
532
  }
366
533