@noodleseed/one 0.22.0 → 0.23.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 (197) hide show
  1. package/dist/commands/author-loop.d.ts.map +1 -1
  2. package/dist/commands/author-loop.js +2 -2
  3. package/dist/commands/author-loop.js.map +1 -1
  4. package/dist/commands/catalog-dispatch.d.ts.map +1 -1
  5. package/dist/commands/catalog-dispatch.js +21 -2
  6. package/dist/commands/catalog-dispatch.js.map +1 -1
  7. package/dist/commands/deploy-ops.d.ts.map +1 -1
  8. package/dist/commands/deploy-ops.js +28 -11
  9. package/dist/commands/deploy-ops.js.map +1 -1
  10. package/dist/first-run.d.ts +11 -0
  11. package/dist/first-run.d.ts.map +1 -1
  12. package/dist/first-run.js +19 -4
  13. package/dist/first-run.js.map +1 -1
  14. package/dist/project-scaffold-templates.d.ts +11 -0
  15. package/dist/project-scaffold-templates.d.ts.map +1 -0
  16. package/dist/project-scaffold-templates.js +359 -0
  17. package/dist/project-scaffold-templates.js.map +1 -0
  18. package/dist/project.d.ts.map +1 -1
  19. package/dist/project.js +1 -323
  20. package/dist/project.js.map +1 -1
  21. package/dist/react.d.ts +2 -2
  22. package/dist/react.d.ts.map +1 -1
  23. package/dist/react.js +1 -1
  24. package/dist/react.js.map +1 -1
  25. package/dist/validate.d.ts +12 -0
  26. package/dist/validate.d.ts.map +1 -1
  27. package/dist/validate.js +11 -0
  28. package/dist/validate.js.map +1 -1
  29. package/node_modules/@noodle-borg/agent-kit/dist/skill-content.d.ts.map +1 -1
  30. package/node_modules/@noodle-borg/agent-kit/dist/skill-content.js +6 -9
  31. package/node_modules/@noodle-borg/agent-kit/dist/skill-content.js.map +1 -1
  32. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  33. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +3 -3
  34. package/node_modules/@noodle-borg/authoring/dist/index.d.ts.map +1 -1
  35. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
  36. package/node_modules/@noodle-borg/authoring/dist/index.js.map +1 -1
  37. package/node_modules/@noodle-borg/authoring/dist/react/components.d.ts +302 -7
  38. package/node_modules/@noodle-borg/authoring/dist/react/components.d.ts.map +1 -1
  39. package/node_modules/@noodle-borg/authoring/dist/react/components.js +39 -7
  40. package/node_modules/@noodle-borg/authoring/dist/react/components.js.map +1 -1
  41. package/node_modules/@noodle-borg/authoring/dist/react/index.d.ts +4 -2
  42. package/node_modules/@noodle-borg/authoring/dist/react/index.d.ts.map +1 -1
  43. package/node_modules/@noodle-borg/authoring/dist/react/index.js +2 -1
  44. package/node_modules/@noodle-borg/authoring/dist/react/index.js.map +1 -1
  45. package/node_modules/@noodle-borg/authoring/dist/react/semantic-components.d.ts +2618 -0
  46. package/node_modules/@noodle-borg/authoring/dist/react/semantic-components.d.ts.map +1 -0
  47. package/node_modules/@noodle-borg/authoring/dist/react/semantic-components.js +110 -0
  48. package/node_modules/@noodle-borg/authoring/dist/react/semantic-components.js.map +1 -0
  49. package/node_modules/@noodle-borg/authoring/dist/react.d.ts +2 -2
  50. package/node_modules/@noodle-borg/authoring/dist/react.d.ts.map +1 -1
  51. package/node_modules/@noodle-borg/authoring/dist/react.js +1 -1
  52. package/node_modules/@noodle-borg/authoring/dist/react.js.map +1 -1
  53. package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +1 -1
  54. package/node_modules/@noodle-borg/authoring/dist/recording.d.ts.map +1 -1
  55. package/node_modules/@noodle-borg/authoring/dist/recording.js +14 -1
  56. package/node_modules/@noodle-borg/authoring/dist/recording.js.map +1 -1
  57. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +25 -2
  58. package/node_modules/@noodle-borg/authoring/dist/server.d.ts.map +1 -1
  59. package/node_modules/@noodle-borg/authoring/dist/server.js +7 -3
  60. package/node_modules/@noodle-borg/authoring/dist/server.js.map +1 -1
  61. package/node_modules/@noodle-borg/compiler/dist/compile.d.ts.map +1 -1
  62. package/node_modules/@noodle-borg/compiler/dist/compile.js +20 -8
  63. package/node_modules/@noodle-borg/compiler/dist/compile.js.map +1 -1
  64. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +2 -0
  65. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts.map +1 -1
  66. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +6 -0
  67. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js.map +1 -1
  68. package/node_modules/@noodle-borg/connector-http/package.json +1 -1
  69. package/node_modules/@noodle-borg/protocol/dist/mapping.d.ts.map +1 -1
  70. package/node_modules/@noodle-borg/protocol/dist/mapping.js +8 -0
  71. package/node_modules/@noodle-borg/protocol/dist/mapping.js.map +1 -1
  72. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +15 -21
  73. package/node_modules/hono/dist/adapter/bun/websocket.js +3 -1
  74. package/node_modules/hono/dist/adapter/lambda-edge/handler.js +8 -2
  75. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +15 -21
  76. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +3 -1
  77. package/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +8 -2
  78. package/node_modules/hono/dist/cjs/helper/css/common.js +3 -1
  79. package/node_modules/hono/dist/cjs/helper/css/index.js +9 -1
  80. package/node_modules/hono/dist/cjs/jsx/base.js +8 -14
  81. package/node_modules/hono/dist/cjs/jsx/components.js +41 -21
  82. package/node_modules/hono/dist/cjs/jsx/context.js +131 -5
  83. package/node_modules/hono/dist/cjs/jsx/streaming.js +9 -7
  84. package/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -5
  85. package/node_modules/hono/dist/cjs/middleware/serve-static/index.js +2 -2
  86. package/node_modules/hono/dist/cjs/utils/body.js +12 -4
  87. package/node_modules/hono/dist/cjs/utils/buffer.js +2 -1
  88. package/node_modules/hono/dist/cjs/validator/validator.js +3 -3
  89. package/node_modules/hono/dist/helper/css/common.js +3 -1
  90. package/node_modules/hono/dist/helper/css/index.js +9 -1
  91. package/node_modules/hono/dist/jsx/base.js +15 -15
  92. package/node_modules/hono/dist/jsx/components.js +42 -22
  93. package/node_modules/hono/dist/jsx/context.js +129 -5
  94. package/node_modules/hono/dist/jsx/streaming.js +10 -8
  95. package/node_modules/hono/dist/middleware/cors/index.js +2 -5
  96. package/node_modules/hono/dist/middleware/serve-static/index.js +2 -2
  97. package/node_modules/hono/dist/types/adapter/aws-lambda/handler.d.ts +1 -1
  98. package/node_modules/hono/dist/types/jsx/base.d.ts +1 -3
  99. package/node_modules/hono/dist/types/jsx/context.d.ts +39 -0
  100. package/node_modules/hono/dist/types/middleware/context-storage/index.d.ts +2 -2
  101. package/node_modules/hono/dist/types/utils/body.d.ts +1 -1
  102. package/node_modules/hono/dist/utils/body.js +12 -4
  103. package/node_modules/hono/dist/utils/buffer.js +2 -1
  104. package/node_modules/hono/dist/validator/validator.js +3 -3
  105. package/node_modules/hono/package.json +9 -10
  106. package/node_modules/tailwindcss/dist/chunk-HMCCH6MG.mjs +1 -0
  107. package/node_modules/tailwindcss/dist/default-theme.mjs +1 -1
  108. package/node_modules/tailwindcss/dist/lib.js +13 -13
  109. package/node_modules/tailwindcss/dist/lib.mjs +18 -18
  110. package/node_modules/tailwindcss/package.json +2 -2
  111. package/node_modules/undici/README.md +3 -2
  112. package/node_modules/undici/docs/docs/api/Agent.md +248 -44
  113. package/node_modules/undici/docs/docs/api/BalancedPool.md +246 -48
  114. package/node_modules/undici/docs/docs/api/CacheStorage.md +171 -13
  115. package/node_modules/undici/docs/docs/api/CacheStore.md +294 -98
  116. package/node_modules/undici/docs/docs/api/Client.md +365 -178
  117. package/node_modules/undici/docs/docs/api/ClientStats.md +80 -14
  118. package/node_modules/undici/docs/docs/api/Connector.md +118 -36
  119. package/node_modules/undici/docs/docs/api/ContentType.md +65 -25
  120. package/node_modules/undici/docs/docs/api/Cookies.md +125 -47
  121. package/node_modules/undici/docs/docs/api/Debug.md +34 -18
  122. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +364 -164
  123. package/node_modules/undici/docs/docs/api/Dispatcher.md +479 -1093
  124. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +101 -87
  125. package/node_modules/undici/docs/docs/api/Errors.md +503 -40
  126. package/node_modules/undici/docs/docs/api/EventSource.md +294 -32
  127. package/node_modules/undici/docs/docs/api/Fetch.md +680 -31
  128. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +78 -98
  129. package/node_modules/undici/docs/docs/api/H2CClient.md +298 -176
  130. package/node_modules/undici/docs/docs/api/Interceptors.md +355 -0
  131. package/node_modules/undici/docs/docs/api/MockAgent.md +314 -322
  132. package/node_modules/undici/docs/docs/api/MockCallHistory.md +272 -98
  133. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +189 -26
  134. package/node_modules/undici/docs/docs/api/MockClient.md +151 -33
  135. package/node_modules/undici/docs/docs/api/MockErrors.md +56 -5
  136. package/node_modules/undici/docs/docs/api/MockPool.md +299 -263
  137. package/node_modules/undici/docs/docs/api/Pool.md +235 -43
  138. package/node_modules/undici/docs/docs/api/PoolStats.md +119 -13
  139. package/node_modules/undici/docs/docs/api/ProxyAgent.md +179 -160
  140. package/node_modules/undici/docs/docs/api/RedirectHandler.md +238 -63
  141. package/node_modules/undici/docs/docs/api/RetryAgent.md +113 -26
  142. package/node_modules/undici/docs/docs/api/RetryHandler.md +161 -77
  143. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +166 -72
  144. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +264 -542
  145. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +162 -186
  146. package/node_modules/undici/docs/docs/api/Util.md +68 -11
  147. package/node_modules/undici/docs/docs/api/WebSocket.md +475 -80
  148. package/node_modules/undici/docs/docs/api/api-lifecycle.md +100 -32
  149. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +2 -0
  150. package/node_modules/undici/docs/docs/best-practices/crawling.md +3 -1
  151. package/node_modules/undici/docs/docs/best-practices/migrating-from-v7-to-v8.md +6 -4
  152. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +9 -7
  153. package/node_modules/undici/docs/docs/best-practices/proxy.md +3 -1
  154. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +10 -8
  155. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +2 -0
  156. package/node_modules/undici/docs/docs/{GettingStarted.md → getting-started.md} +22 -19
  157. package/node_modules/undici/docs/docs/index.md +779 -0
  158. package/node_modules/undici/docs/docs/site.json +125 -0
  159. package/node_modules/undici/docs/docs/type-map.json +79 -0
  160. package/node_modules/undici/lib/api/api-request.js +7 -1
  161. package/node_modules/undici/lib/api/readable.js +47 -2
  162. package/node_modules/undici/lib/core/errors.js +20 -0
  163. package/node_modules/undici/lib/core/request.js +1 -1
  164. package/node_modules/undici/lib/core/util.js +24 -1
  165. package/node_modules/undici/lib/dispatcher/client-h1.js +92 -1
  166. package/node_modules/undici/lib/dispatcher/client-h2.js +322 -118
  167. package/node_modules/undici/lib/dispatcher/client.js +6 -2
  168. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +1 -0
  169. package/node_modules/undici/lib/dispatcher/proxy-agent.js +42 -7
  170. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +4 -2
  171. package/node_modules/undici/lib/handler/redirect-handler.js +1 -0
  172. package/node_modules/undici/lib/handler/retry-handler.js +57 -19
  173. package/node_modules/undici/lib/util/cache.js +8 -2
  174. package/node_modules/undici/lib/web/cookies/parse.js +20 -27
  175. package/node_modules/undici/lib/web/cookies/util.js +1 -1
  176. package/node_modules/undici/lib/web/eventsource/eventsource.js +7 -18
  177. package/node_modules/undici/lib/web/eventsource/util.js +32 -1
  178. package/node_modules/undici/lib/web/fetch/body.js +43 -0
  179. package/node_modules/undici/lib/web/fetch/constants.js +1 -1
  180. package/node_modules/undici/lib/web/fetch/index.js +11 -2
  181. package/node_modules/undici/lib/web/fetch/request.js +1 -0
  182. package/node_modules/undici/lib/web/fetch/util.js +4 -1
  183. package/node_modules/undici/lib/web/websocket/receiver.js +20 -3
  184. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +8 -1
  185. package/node_modules/undici/lib/web/websocket/websocket.js +3 -1
  186. package/node_modules/undici/package.json +1 -1
  187. package/node_modules/undici/types/client.d.ts +6 -1
  188. package/node_modules/undici/types/cookies.d.ts +1 -1
  189. package/node_modules/undici/types/errors.d.ts +10 -0
  190. package/node_modules/undici/types/fetch.d.ts +1 -0
  191. package/node_modules/undici/types/handlers.d.ts +2 -0
  192. package/node_modules/undici/types/proxy-agent.d.ts +7 -0
  193. package/package.json +4 -4
  194. package/react/styles.css +510 -5
  195. package/react/styles.source.css +850 -0
  196. package/node_modules/hono/dist/tsconfig.build.tsbuildinfo +0 -1
  197. package/node_modules/tailwindcss/dist/chunk-XVOTX5KS.mjs +0 -1
@@ -35,6 +35,7 @@ const {
35
35
  kSize,
36
36
  kHTTPContext,
37
37
  kClosed,
38
+ kKeepAliveDefaultTimeout,
38
39
  kHeadersTimeout,
39
40
  kBodyTimeout,
40
41
  kEnableConnectProtocol,
@@ -152,17 +153,38 @@ function requeueUnsentRequest (client, request) {
152
153
  client[kQueue].splice(client[kPendingIdx] + 1, 0, request)
153
154
  }
154
155
 
156
+ function completeRequest (client, request, resetPendingIdx = false) {
157
+ const queue = client[kQueue]
158
+ const runningIdx = client[kRunningIdx]
159
+
160
+ // In-order completion: advance the running index instead of splicing.
161
+ // The client's resume loop compacts the queue once the index grows.
162
+ if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
163
+ client[kRunningIdx] = runningIdx + 1
164
+ return
165
+ }
166
+
167
+ const index = queue.indexOf(request, runningIdx)
168
+
169
+ if (index === -1 || index >= client[kPendingIdx]) {
170
+ return
171
+ }
172
+
173
+ queue.splice(index, 1)
174
+ client[kPendingIdx]--
175
+
176
+ if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
177
+ client[kPendingIdx] = client[kRunningIdx]
178
+ }
179
+ }
180
+
155
181
  function canRetryRequestAfterGoAway (request) {
156
182
  const { body } = request
157
183
 
158
184
  return body == null || util.isBuffer(body) || util.isBlobLike(body)
159
185
  }
160
186
 
161
- function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
162
- const stream = request[kRequestStream]
163
-
164
- clearRequestStream(request)
165
-
187
+ function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) {
166
188
  if (stream != null && !stream.destroyed && !stream.closed) {
167
189
  try {
168
190
  stream.close(code)
@@ -170,6 +192,14 @@ function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
170
192
  }
171
193
  }
172
194
 
195
+ function detachRequestStreamForClose (request) {
196
+ const stream = request[kRequestStream]
197
+
198
+ clearRequestStream(request)
199
+
200
+ return stream
201
+ }
202
+
173
203
  function connectH2 (client, socket) {
174
204
  client[kSocket] = socket
175
205
 
@@ -191,6 +221,12 @@ function connectH2 (client, socket) {
191
221
  session[kClient] = client
192
222
  session[kSocket] = socket
193
223
  session[kHTTP2SessionState] = {
224
+ idleTimeout: null,
225
+ // Sockets start out ref'd. Session ref/unref proxies to the socket, so a
226
+ // single cached flag lets us skip redundant uv ref/unref calls, provided
227
+ // every ref/unref of the session or its socket goes through
228
+ // refH2Session/unrefH2Session.
229
+ refed: true,
194
230
  ping: {
195
231
  interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
196
232
  }
@@ -217,7 +253,7 @@ function connectH2 (client, socket) {
217
253
  // TODO (@metcoder95): implement SETTINGS support
218
254
  // util.addListener(session, 'localSettings', onHttp2RemoteSettings)
219
255
 
220
- session.unref()
256
+ unrefH2Session(session)
221
257
 
222
258
  client[kHTTP2Session] = session
223
259
  socket[kHTTP2Session] = session
@@ -279,10 +315,10 @@ function connectH2 (client, socket) {
279
315
  if (client[kRunning] > 0) {
280
316
  // We are already processing requests
281
317
 
282
- // Non-idempotent request cannot be retried.
283
- // Ensure that no other requests are inflight and
284
- // could cause failure.
285
- if (request.idempotent === false) return true
318
+ // Unlike HTTP/1.1 pipelining, HTTP/2 multiplexes requests on
319
+ // independent streams, so non-idempotent requests can be dispatched
320
+ // concurrently. Retry eligibility is handled by stream/session error
321
+ // handling instead of by serializing all non-idempotent requests.
286
322
  // Don't dispatch an upgrade until all preceding requests have completed.
287
323
  // Possibly, we do not have remote settings confirmed yet.
288
324
  if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
@@ -306,20 +342,89 @@ function connectH2 (client, socket) {
306
342
  }
307
343
  }
308
344
 
345
+ // Session ref/unref proxies to the underlying socket, so refH2Session and
346
+ // unrefH2Session cover both and can skip the call when the cached ref state
347
+ // already matches.
348
+ function refH2Session (session) {
349
+ const state = session[kHTTP2SessionState]
350
+
351
+ if (state.refed === false) {
352
+ state.refed = true
353
+ session.ref()
354
+ }
355
+ }
356
+
357
+ function unrefH2Session (session) {
358
+ const state = session[kHTTP2SessionState]
359
+
360
+ if (state.refed === true) {
361
+ state.refed = false
362
+ session.unref()
363
+ }
364
+ }
365
+
309
366
  function resumeH2 (client) {
310
367
  const socket = client[kSocket]
368
+ const session = client[kHTTP2Session]
311
369
 
312
370
  if (socket?.destroyed === false) {
313
371
  if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
314
- socket.unref()
315
- client[kHTTP2Session].unref()
372
+ unrefH2Session(session)
316
373
  } else {
317
- socket.ref()
318
- client[kHTTP2Session].ref()
374
+ refH2Session(session)
319
375
  }
376
+
377
+ if (client[kSize] === 0 && session[kOpenStreams] === 0) {
378
+ setHttp2IdleTimeout(session)
379
+ } else {
380
+ clearHttp2IdleTimeout(session)
381
+ }
382
+ }
383
+ }
384
+
385
+ function clearHttp2IdleTimeout (session) {
386
+ const state = session[kHTTP2SessionState]
387
+
388
+ if (state?.idleTimeout != null) {
389
+ clearTimeout(state.idleTimeout)
390
+ state.idleTimeout = null
391
+ }
392
+ }
393
+
394
+ function setHttp2IdleTimeout (session) {
395
+ const client = session[kClient]
396
+
397
+ if (client[kHTTP2Session] !== session || session.closed || session.destroyed) {
398
+ return
399
+ }
400
+
401
+ if (session[kOpenStreams] !== 0 || client[kSize] !== 0) {
402
+ clearHttp2IdleTimeout(session)
403
+ return
404
+ }
405
+
406
+ const state = session[kHTTP2SessionState]
407
+ if (state.idleTimeout == null) {
408
+ state.idleTimeout = setTimeout(onHttp2SessionIdleTimeout, client[kKeepAliveDefaultTimeout], session).unref()
320
409
  }
321
410
  }
322
411
 
412
+ function onHttp2SessionIdleTimeout (session) {
413
+ const client = session[kClient]
414
+ const socket = session[kSocket]
415
+ const state = session[kHTTP2SessionState]
416
+
417
+ state.idleTimeout = null
418
+
419
+ if (client[kHTTP2Session] !== session || session[kOpenStreams] !== 0 || client[kSize] !== 0 || session.closed || session.destroyed) {
420
+ return
421
+ }
422
+
423
+ const err = new InformationalError('socket idle timeout')
424
+ socket[kError] = err
425
+ util.destroy(socket, err)
426
+ }
427
+
323
428
  function applyConnectionWindowSize (connectionWindowSize) {
324
429
  try {
325
430
  if (typeof this.setLocalWindowSize === 'function') {
@@ -380,11 +485,20 @@ function onHttp2SessionError (err) {
380
485
  assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
381
486
 
382
487
  this[kSocket][kError] = err
488
+
489
+ if (this[kReceivedGoAway]) {
490
+ return
491
+ }
492
+
383
493
  this[kClient][kOnError](err)
384
494
  }
385
495
 
386
496
  function onHttp2FrameError (type, code, id) {
387
497
  if (id === 0) {
498
+ if (this[kReceivedGoAway]) {
499
+ return
500
+ }
501
+
388
502
  const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
389
503
  this[kSocket][kError] = err
390
504
  this[kClient][kOnError](err)
@@ -418,12 +532,16 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
418
532
  const previousPendingIdx = client[kPendingIdx]
419
533
  const pendingIdx = getGoAwayPendingIdx(client, lastStreamID)
420
534
  const retriableRequests = []
535
+ const streamsToClose = []
421
536
 
537
+ // Closing one stream after GOAWAY can synchronously emit frameError on
538
+ // sibling streams. Detach all affected requests first so those errors do
539
+ // not fail requests that are about to be requeued.
422
540
  for (let i = pendingIdx; i < previousPendingIdx; i++) {
423
541
  const request = client[kQueue][i]
424
542
 
425
543
  if (request != null) {
426
- closeRequestStream(request)
544
+ streamsToClose.push(detachRequestStreamForClose(request))
427
545
 
428
546
  if (canRetryRequestAfterGoAway(request)) {
429
547
  retriableRequests.push(request)
@@ -433,6 +551,10 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
433
551
  }
434
552
  }
435
553
 
554
+ for (let i = 0; i < streamsToClose.length; i++) {
555
+ closeStream(streamsToClose[i])
556
+ }
557
+
436
558
  if (pendingIdx !== previousPendingIdx) {
437
559
  const remainingPendingRequests = client[kQueue].slice(previousPendingIdx)
438
560
  client[kQueue].length = pendingIdx
@@ -445,6 +567,8 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
445
567
  client[kHTTP2Session] = null
446
568
  }
447
569
 
570
+ clearHttp2IdleTimeout(this)
571
+
448
572
  if (!this.closed && !this.destroyed) {
449
573
  this.close()
450
574
  }
@@ -467,6 +591,8 @@ function onHttp2SessionClose () {
467
591
  client[kHTTP2Session] = null
468
592
  }
469
593
 
594
+ clearHttp2IdleTimeout(this)
595
+
470
596
  if (state.ping.interval != null) {
471
597
  clearInterval(state.ping.interval)
472
598
  state.ping.interval = null
@@ -479,7 +605,9 @@ function onHttp2SessionClose () {
479
605
  const requests = client[kQueue].splice(client[kRunningIdx])
480
606
  for (let i = 0; i < requests.length; i++) {
481
607
  const request = requests[i]
482
- util.errorRequest(client, request, err)
608
+ if (request != null) {
609
+ util.errorRequest(client, request, err)
610
+ }
483
611
  }
484
612
  }
485
613
  }
@@ -522,6 +650,10 @@ function onHttp2SocketError (err) {
522
650
 
523
651
  this[kError] = err
524
652
 
653
+ if (this[kHTTP2Session]?.[kReceivedGoAway]) {
654
+ return
655
+ }
656
+
525
657
  this[kClient][kOnError](err)
526
658
  }
527
659
 
@@ -541,7 +673,8 @@ function closeStreamSession (stream) {
541
673
  stream[kHTTP2Session] = null
542
674
  session[kOpenStreams] -= 1
543
675
  if (session[kOpenStreams] === 0) {
544
- session.unref()
676
+ unrefH2Session(session)
677
+ setHttp2IdleTimeout(session)
545
678
  }
546
679
  }
547
680
 
@@ -565,12 +698,10 @@ function onRequestStreamClose () {
565
698
 
566
699
  if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
567
700
  state.request.onResponseEnd(state.trailers || {})
568
- state.finalizeRequest()
701
+ finalizeRequest(state)
569
702
  }
570
703
  }
571
704
 
572
- this.off('data', onData)
573
- this.off('error', noop)
574
705
  closeStreamSession(this)
575
706
  this[kRequestStreamState] = null
576
707
  }
@@ -693,7 +824,7 @@ function onUpgradeResponse (headers, _flags) {
693
824
 
694
825
  removeUpgradeStreamListeners(stream)
695
826
  detachRequestFromStream(request)
696
- state.finalizeRequest()
827
+ finalizeRequest(state)
697
828
  }
698
829
 
699
830
  function setupUpgradeStream (stream, state) {
@@ -711,16 +842,56 @@ function setupUpgradeStream (stream, state) {
711
842
  stream.on('timeout', onUpgradeStreamTimeout)
712
843
  stream.once('close', onUpgradeStreamClose)
713
844
 
845
+ clearHttp2IdleTimeout(session)
714
846
  ++session[kOpenStreams]
715
847
  stream.setTimeout(headersTimeout)
716
848
  }
717
849
 
850
+ function finalizeRequest (state, resetPendingIdx = false) {
851
+ if (state.requestFinalized) {
852
+ return
853
+ }
854
+
855
+ state.requestFinalized = true
856
+ completeRequest(state.client, state.request, resetPendingIdx)
857
+
858
+ state.client[kResume]()
859
+ }
860
+
861
+ function openStream (client, request, session, abort, headers, options) {
862
+ try {
863
+ return session.request(headers, options)
864
+ } catch (err) {
865
+ // A GOAWAY'd session rejects new streams, same as an invalid session:
866
+ // reset and requeue on a fresh connection rather than the destroy + abort
867
+ // below, whose destroy(socket, err) can crash via an unhandled 'error'.
868
+ if (err?.code === 'ERR_HTTP2_INVALID_SESSION' || err?.code === 'ERR_HTTP2_GOAWAY_SESSION') {
869
+ const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
870
+ wrappedErr.cause = err
871
+ session[kError] = wrappedErr
872
+ resetHttp2Session(session, wrappedErr)
873
+ requeueUnsentRequest(client, request)
874
+
875
+ return null
876
+ }
877
+
878
+ const wrappedErr = new InformationalError(err.message, { cause: err })
879
+ session[kError] = wrappedErr
880
+ session[kSocket][kError] = wrappedErr
881
+
882
+ session.destroy(wrappedErr)
883
+ util.destroy(session[kSocket], wrappedErr)
884
+ abort(wrappedErr)
885
+
886
+ return null
887
+ }
888
+ }
889
+
718
890
  function writeH2 (client, request) {
719
891
  const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
720
892
  const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
721
893
  const session = client[kHTTP2Session]
722
894
  const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
723
- let { body } = request
724
895
 
725
896
  if (upgrade != null && upgrade !== 'websocket') {
726
897
  util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
@@ -729,26 +900,28 @@ function writeH2 (client, request) {
729
900
 
730
901
  const headers = buildRequestHeaders(reqHeaders)
731
902
 
732
- /** @type {import('node:http2').ClientHttp2Stream} */
733
- let stream = null
734
-
735
903
  headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority]
736
904
  headers[HTTP2_HEADER_METHOD] = method
737
905
 
738
- let requestFinalized = false
739
- const finalizeRequest = (resetPendingIdx = false) => {
740
- if (requestFinalized) {
741
- return
742
- }
743
-
744
- requestFinalized = true
745
- client[kQueue][client[kRunningIdx]++] = null
746
-
747
- if (resetPendingIdx) {
748
- client[kPendingIdx] = client[kRunningIdx]
749
- }
750
-
751
- client[kResume]()
906
+ // Single pre-shaped state object shared by all stream event handlers.
907
+ // All fields are declared up-front so the object keeps a stable hidden
908
+ // class for the whole request lifetime.
909
+ const state = {
910
+ abort: null,
911
+ body: request.body,
912
+ client,
913
+ contentLength: null,
914
+ expectsPayload: false,
915
+ request,
916
+ headersTimeout,
917
+ bodyTimeout,
918
+ requestFinalized: false,
919
+ responseReceived: false,
920
+ bodySent: false,
921
+ pendingEnd: false,
922
+ trailers: null,
923
+ session,
924
+ stream: null
752
925
  }
753
926
 
754
927
  const abort = (err, resetPendingIdx = false) => {
@@ -760,47 +933,39 @@ function writeH2 (client, request) {
760
933
 
761
934
  util.errorRequest(client, request, err)
762
935
 
763
- if (stream != null) {
936
+ if (state.stream != null) {
764
937
  clearRequestStream(request)
765
938
 
766
- // On Abort, we close the stream to send RST_STREAM frame
939
+ // On Abort, we close the stream to send RST_STREAM frame.
940
+ const stream = state.stream
767
941
  stream.close()
768
942
 
943
+ // close() alone leaves cleanup waiting on the 'close' event; on a busy,
944
+ // long-lived multiplexed session that event can fail to fire, leaving the
945
+ // native Http2Stream (and the whole request graph it pins) alive for the
946
+ // session's life. Destroy the stream to release the handle
947
+ // deterministically, but defer it by a setImmediate so the RST_STREAM
948
+ // frame queued by close() gets a chance to be written first.
949
+ setImmediate(() => {
950
+ if (!stream.destroyed) {
951
+ util.destroy(stream)
952
+ }
953
+ })
954
+
769
955
  // We move the running index to the next request
770
956
  client[kOnError](err)
771
- finalizeRequest(resetPendingIdx)
957
+ finalizeRequest(state, resetPendingIdx)
772
958
  }
773
959
 
774
960
  // We do not destroy the socket as we can continue using the session
775
961
  // the stream gets destroyed and the session remains to create new streams
776
- util.destroy(body, err)
962
+ util.destroy(state.body, err)
777
963
  }
778
964
 
779
- const requestStream = (headers, options) => {
780
- try {
781
- return session.request(headers, options)
782
- } catch (err) {
783
- if (err?.code === 'ERR_HTTP2_INVALID_SESSION') {
784
- const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
785
- wrappedErr.cause = err
786
- session[kError] = wrappedErr
787
- resetHttp2Session(session, wrappedErr)
788
- requeueUnsentRequest(client, request)
789
-
790
- return null
791
- }
792
-
793
- const wrappedErr = new InformationalError(err.message, { cause: err })
794
- session[kError] = wrappedErr
795
- session[kSocket][kError] = wrappedErr
796
-
797
- session.destroy(wrappedErr)
798
- util.destroy(session[kSocket], wrappedErr)
799
- abort(wrappedErr)
965
+ state.abort = abort
800
966
 
801
- return null
802
- }
803
- }
967
+ /** @type {import('node:http2').ClientHttp2Stream} */
968
+ let stream = null
804
969
 
805
970
  try {
806
971
  // We are already connected, streams are pending.
@@ -815,24 +980,13 @@ function writeH2 (client, request) {
815
980
  }
816
981
 
817
982
  if (upgrade || method === 'CONNECT') {
818
- session.ref()
819
-
820
- const upgradeState = {
821
- abort,
822
- finalizeRequest,
823
- request,
824
- headersTimeout,
825
- bodyTimeout,
826
- responseReceived: false,
827
- session,
828
- stream: null
829
- }
983
+ refH2Session(session)
830
984
 
831
985
  if (upgrade === 'websocket') {
832
986
  // We cannot upgrade to websocket if extended CONNECT protocol is not supported
833
987
  if (session[kEnableConnectProtocol] === false) {
834
988
  util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
835
- session.unref()
989
+ unrefH2Session(session)
836
990
  return false
837
991
  }
838
992
 
@@ -850,12 +1004,12 @@ function writeH2 (client, request) {
850
1004
  headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
851
1005
  }
852
1006
 
853
- stream = requestStream(headers, { endStream: false, signal })
1007
+ stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
854
1008
  if (stream == null) {
855
- session.unref()
1009
+ unrefH2Session(session)
856
1010
  return false
857
1011
  }
858
- setupUpgradeStream(stream, upgradeState)
1012
+ setupUpgradeStream(stream, state)
859
1013
  return true
860
1014
  }
861
1015
 
@@ -864,12 +1018,12 @@ function writeH2 (client, request) {
864
1018
  // will create a new stream. We trigger a request to create the stream and wait until
865
1019
  // `ready` event is triggered
866
1020
  // We disabled endStream to allow the user to write to the stream
867
- stream = requestStream(headers, { endStream: false, signal })
1021
+ stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
868
1022
  if (stream == null) {
869
- session.unref()
1023
+ unrefH2Session(session)
870
1024
  return false
871
1025
  }
872
- setupUpgradeStream(stream, upgradeState)
1026
+ setupUpgradeStream(stream, state)
873
1027
 
874
1028
  return true
875
1029
  }
@@ -897,6 +1051,8 @@ function writeH2 (client, request) {
897
1051
  method === 'PROPPATCH'
898
1052
  )
899
1053
 
1054
+ let body = state.body
1055
+
900
1056
  if (body && typeof body.read === 'function') {
901
1057
  // Try to read EOF in order to get length.
902
1058
  body.read(0)
@@ -943,7 +1099,7 @@ function writeH2 (client, request) {
943
1099
  headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
944
1100
  }
945
1101
 
946
- session.ref()
1102
+ refH2Session(session)
947
1103
 
948
1104
  if (channels.sendHeaders.hasSubscribers) {
949
1105
  let header = ''
@@ -955,26 +1111,16 @@ function writeH2 (client, request) {
955
1111
 
956
1112
  // TODO(metcoder95): add support for sending trailers
957
1113
  const shouldEndStream = body === null || contentLength === 0
958
- const state = {
959
- abort,
960
- body,
961
- client,
962
- contentLength,
963
- expectsPayload,
964
- finalizeRequest,
965
- request,
966
- headersTimeout,
967
- bodyTimeout,
968
- responseReceived: false,
969
- session,
970
- stream: null
971
- }
1114
+
1115
+ state.body = body
1116
+ state.contentLength = contentLength
1117
+ state.expectsPayload = expectsPayload
972
1118
 
973
1119
  if (expectContinue) {
974
1120
  headers[HTTP2_HEADER_EXPECT] = '100-continue'
975
1121
  }
976
1122
 
977
- stream = requestStream(headers, { endStream: shouldEndStream, signal })
1123
+ stream = openStream(client, request, session, abort, headers, { endStream: shouldEndStream, signal })
978
1124
  if (stream == null) {
979
1125
  return false
980
1126
  }
@@ -983,23 +1129,32 @@ function writeH2 (client, request) {
983
1129
  state.stream = stream
984
1130
 
985
1131
  // Increment counter as we have new streams open
1132
+ clearHttp2IdleTimeout(session)
986
1133
  ++session[kOpenStreams]
987
- stream.setTimeout(headersTimeout)
1134
+
1135
+ if (headersTimeout) {
1136
+ stream.setTimeout(headersTimeout)
1137
+ }
988
1138
 
989
1139
  stream[kHTTP2Session] = session
990
- stream.once('close', onRequestStreamClose)
1140
+ stream.on('close', onRequestStreamClose)
991
1141
 
992
1142
  bindRequestToStream(request, stream, releaseRequestStream)
993
1143
  if (expectContinue) {
994
1144
  stream.once('continue', writeBodyH2)
995
1145
  }
996
- stream.once('response', onResponse)
997
- stream.once('end', onEnd)
998
- stream.once('error', onError)
999
- stream.once('frameError', onFrameError)
1146
+ // The handlers below either remove themselves on first invocation or
1147
+ // become unreachable once the stream closes, so plain `on` avoids the
1148
+ // per-listener `once` wrapper allocation.
1149
+ stream.on('response', onResponse)
1150
+ stream.on('end', onEnd)
1151
+ stream.on('error', onError)
1152
+ stream.on('frameError', onFrameError)
1000
1153
  stream.on('aborted', onAborted)
1001
- stream.on('timeout', onTimeout)
1002
- stream.once('trailers', onTrailers)
1154
+ if (headersTimeout || bodyTimeout) {
1155
+ stream.on('timeout', onTimeout)
1156
+ }
1157
+ stream.on('trailers', onTrailers)
1003
1158
 
1004
1159
  if (!expectContinue) {
1005
1160
  writeBodyH2.call(stream)
@@ -1037,16 +1192,24 @@ function releaseRequestStream (stream) {
1037
1192
  detachRequestFromStream(request)
1038
1193
  }
1039
1194
 
1040
- removeRequestStreamListeners(stream)
1041
-
1195
+ // A closed or destroyed stream cannot emit further events; leaving the
1196
+ // listeners in place saves the removal scans (they are collected with
1197
+ // the stream). All handlers bail out when the stream state is gone.
1042
1198
  if (!stream.destroyed && !stream.closed) {
1199
+ removeRequestStreamListeners(stream)
1043
1200
  stream.once('error', noop)
1044
1201
  }
1045
1202
  }
1046
1203
 
1047
1204
  function onData (chunk) {
1048
1205
  const stream = this
1049
- const { request } = stream[kRequestStreamState]
1206
+ const state = stream[kRequestStreamState]
1207
+
1208
+ if (state == null) {
1209
+ return
1210
+ }
1211
+
1212
+ const { request } = state
1050
1213
 
1051
1214
  if (request.aborted || request.completed) {
1052
1215
  return
@@ -1060,22 +1223,40 @@ function onData (chunk) {
1060
1223
  function onResponse (headers) {
1061
1224
  const stream = this
1062
1225
  const state = stream[kRequestStreamState]
1226
+
1227
+ if (state == null) {
1228
+ return
1229
+ }
1230
+
1063
1231
  const { request } = state
1064
1232
 
1065
1233
  stream.off('response', onResponse)
1066
1234
 
1235
+ // Final response received while still awaiting 100 (Continue): the body won't
1236
+ // be sent, so close our half or the stream stays open and never completes.
1237
+ if (state.body != null && !state.bodySent && !stream.writableEnded) {
1238
+ stream.removeListener('continue', writeBodyH2)
1239
+ stream.end()
1240
+ }
1241
+
1067
1242
  const statusCode = headers[HTTP2_HEADER_STATUS]
1068
1243
  delete headers[HTTP2_HEADER_STATUS]
1069
1244
  request.onResponseStarted()
1070
1245
  state.responseReceived = true
1071
- stream.setTimeout(state.bodyTimeout)
1246
+
1247
+ if (state.headersTimeout || state.bodyTimeout) {
1248
+ stream.setTimeout(state.bodyTimeout)
1249
+ }
1072
1250
 
1073
1251
  // Due to the stream nature, it is possible we face a race condition
1074
1252
  // where the stream has been assigned, but the request has been aborted
1075
- // the request remains in-flight and headers hasn't been received yet
1076
- // for those scenarios, best effort is to destroy the stream immediately
1077
- // as there's no value to keep it open.
1078
- if (request.aborted) {
1253
+ // or already completed and headers hasn't been received yet. A late
1254
+ // 'response' delivered after completion would call request.onResponseStart
1255
+ // post-completion, tripping its `assert(!this.completed)` (an uncatchable
1256
+ // throw on the http2 event tick). Guard `completed` here as onEnd/onTrailers
1257
+ // already do; best effort is to release the stream immediately as there's
1258
+ // no value to keep it open.
1259
+ if (request.aborted || request.completed) {
1079
1260
  releaseRequestStream(stream)
1080
1261
  return
1081
1262
  }
@@ -1090,6 +1271,11 @@ function onResponse (headers) {
1090
1271
  function onEnd () {
1091
1272
  const stream = this
1092
1273
  const state = stream[kRequestStreamState]
1274
+
1275
+ if (state == null) {
1276
+ return
1277
+ }
1278
+
1093
1279
  const { request } = state
1094
1280
 
1095
1281
  stream.off('end', onEnd)
@@ -1113,6 +1299,10 @@ function onError (err) {
1113
1299
  const stream = this
1114
1300
  const state = stream[kRequestStreamState]
1115
1301
 
1302
+ if (state == null) {
1303
+ return
1304
+ }
1305
+
1116
1306
  stream.off('error', onError)
1117
1307
  state.abort(err)
1118
1308
  }
@@ -1121,6 +1311,10 @@ function onFrameError (type, code) {
1121
1311
  const stream = this
1122
1312
  const state = stream[kRequestStreamState]
1123
1313
 
1314
+ if (state == null) {
1315
+ return
1316
+ }
1317
+
1124
1318
  stream.off('frameError', onFrameError)
1125
1319
  state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
1126
1320
  }
@@ -1133,6 +1327,10 @@ function onTimeout () {
1133
1327
  const stream = this
1134
1328
  const state = stream[kRequestStreamState]
1135
1329
 
1330
+ if (state == null) {
1331
+ return
1332
+ }
1333
+
1136
1334
  // Remove self so timeout doesn't fire again after we handle it
1137
1335
  stream.off('timeout', onTimeout)
1138
1336
 
@@ -1145,6 +1343,11 @@ function onTimeout () {
1145
1343
  function onTrailers (trailers) {
1146
1344
  const stream = this
1147
1345
  const state = stream[kRequestStreamState]
1346
+
1347
+ if (state == null) {
1348
+ return
1349
+ }
1350
+
1148
1351
  const { request } = state
1149
1352
 
1150
1353
  stream.off('trailers', onTrailers)
@@ -1161,6 +1364,7 @@ function onTrailers (trailers) {
1161
1364
  function writeBodyH2 () {
1162
1365
  const stream = this
1163
1366
  const state = stream[kRequestStreamState]
1367
+ state.bodySent = true
1164
1368
  const { abort, body, client, contentLength, expectsPayload, request } = state
1165
1369
 
1166
1370
  if (!body || contentLength === 0) {