@harperfast/harper 5.3.0-alpha.1 → 5.3.0-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 (291) hide show
  1. package/bin/cliOperations.ts +12 -0
  2. package/bin/restart.ts +66 -6
  3. package/components/Application.ts +1134 -112
  4. package/components/OptionsWatcher.ts +368 -102
  5. package/components/Scope.ts +20 -1
  6. package/components/componentLoader.ts +43 -4
  7. package/components/deploymentOperations.ts +4 -1
  8. package/components/deploymentRecorder.ts +9 -2
  9. package/components/operations.js +284 -52
  10. package/components/operationsValidation.js +49 -2
  11. package/components/packageComponent.ts +25 -1
  12. package/components/requestRestart.ts +11 -0
  13. package/config/RootConfigWatcher.ts +191 -37
  14. package/config/configReadRetry.ts +62 -0
  15. package/config/configUtils.ts +78 -26
  16. package/config/parseConfigFile.ts +34 -0
  17. package/config/readConfigFileSync.ts +44 -0
  18. package/config/watcherArming.ts +59 -0
  19. package/config-root.schema.json +4 -0
  20. package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
  21. package/dist/bin/cliOperations.js +13 -0
  22. package/dist/bin/cliOperations.js.map +1 -1
  23. package/dist/bin/restart.js +42 -6
  24. package/dist/bin/restart.js.map +1 -1
  25. package/dist/components/Application.d.ts +104 -9
  26. package/dist/components/Application.js +954 -102
  27. package/dist/components/Application.js.map +1 -1
  28. package/dist/components/OptionsWatcher.d.ts +4 -1
  29. package/dist/components/OptionsWatcher.js +378 -104
  30. package/dist/components/OptionsWatcher.js.map +1 -1
  31. package/dist/components/Scope.js +15 -1
  32. package/dist/components/Scope.js.map +1 -1
  33. package/dist/components/componentLoader.js +35 -3
  34. package/dist/components/componentLoader.js.map +1 -1
  35. package/dist/components/deploymentOperations.js +4 -1
  36. package/dist/components/deploymentOperations.js.map +1 -1
  37. package/dist/components/deploymentRecorder.d.ts +4 -2
  38. package/dist/components/deploymentRecorder.js +1 -0
  39. package/dist/components/deploymentRecorder.js.map +1 -1
  40. package/dist/components/operations.d.ts +28 -0
  41. package/dist/components/operations.js +263 -54
  42. package/dist/components/operations.js.map +1 -1
  43. package/dist/components/operationsValidation.js +48 -2
  44. package/dist/components/operationsValidation.js.map +1 -1
  45. package/dist/components/packageComponent.js +24 -0
  46. package/dist/components/packageComponent.js.map +1 -1
  47. package/dist/components/requestRestart.d.ts +1 -0
  48. package/dist/components/requestRestart.js +7 -0
  49. package/dist/components/requestRestart.js.map +1 -1
  50. package/dist/config/RootConfigWatcher.d.ts +2 -0
  51. package/dist/config/RootConfigWatcher.js +189 -35
  52. package/dist/config/RootConfigWatcher.js.map +1 -1
  53. package/dist/config/configReadRetry.d.ts +8 -0
  54. package/dist/config/configReadRetry.js +62 -0
  55. package/dist/config/configReadRetry.js.map +1 -0
  56. package/dist/config/configUtils.d.ts +10 -9
  57. package/dist/config/configUtils.js +63 -27
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/parseConfigFile.d.ts +4 -0
  60. package/dist/config/parseConfigFile.js +35 -0
  61. package/dist/config/parseConfigFile.js.map +1 -0
  62. package/dist/config/readConfigFileSync.d.ts +1 -0
  63. package/dist/config/readConfigFileSync.js +47 -0
  64. package/dist/config/readConfigFileSync.js.map +1 -0
  65. package/dist/config/watcherArming.d.ts +15 -0
  66. package/dist/config/watcherArming.js +59 -0
  67. package/dist/config/watcherArming.js.map +1 -0
  68. package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
  69. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.js +4 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/json/systemSchema.json +3 -0
  74. package/dist/resources/DatabaseTransaction.d.ts +25 -0
  75. package/dist/resources/DatabaseTransaction.js +224 -18
  76. package/dist/resources/DatabaseTransaction.js.map +1 -1
  77. package/dist/resources/LMDBTransaction.d.ts +2 -1
  78. package/dist/resources/LMDBTransaction.js +22 -3
  79. package/dist/resources/LMDBTransaction.js.map +1 -1
  80. package/dist/resources/PrimaryRocksDatabase.js +22 -6
  81. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  82. package/dist/resources/RecordEncoder.d.ts +1 -1
  83. package/dist/resources/RecordEncoder.js +21 -5
  84. package/dist/resources/RecordEncoder.js.map +1 -1
  85. package/dist/resources/Resource.js +97 -13
  86. package/dist/resources/Resource.js.map +1 -1
  87. package/dist/resources/ResourceInterface.d.ts +8 -0
  88. package/dist/resources/RocksIndexStore.js +2 -1
  89. package/dist/resources/RocksIndexStore.js.map +1 -1
  90. package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
  91. package/dist/resources/RocksTransactionLogStore.js +104 -33
  92. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  93. package/dist/resources/Table.d.ts +58 -7
  94. package/dist/resources/Table.js +1095 -346
  95. package/dist/resources/Table.js.map +1 -1
  96. package/dist/resources/analytics/write.js +10 -3
  97. package/dist/resources/analytics/write.js.map +1 -1
  98. package/dist/resources/auditStore.d.ts +170 -0
  99. package/dist/resources/auditStore.js +457 -11
  100. package/dist/resources/auditStore.js.map +1 -1
  101. package/dist/resources/dataLoader.js +3 -4
  102. package/dist/resources/dataLoader.js.map +1 -1
  103. package/dist/resources/databases.d.ts +16 -13
  104. package/dist/resources/databases.js +624 -176
  105. package/dist/resources/databases.js.map +1 -1
  106. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  107. package/dist/resources/derivedIndexRegistry.js +68 -0
  108. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  109. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  110. package/dist/resources/derivedIndexRuntime.js +2027 -0
  111. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  112. package/dist/resources/graphql.js +3 -2
  113. package/dist/resources/graphql.js.map +1 -1
  114. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
  115. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
  116. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  117. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  118. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  119. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  120. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  121. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  122. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  123. package/dist/resources/nodeIdMapping.d.ts +5 -0
  124. package/dist/resources/nodeIdMapping.js +49 -0
  125. package/dist/resources/nodeIdMapping.js.map +1 -1
  126. package/dist/resources/recordLock.d.ts +47 -4
  127. package/dist/resources/recordLock.js +138 -7
  128. package/dist/resources/recordLock.js.map +1 -1
  129. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  130. package/dist/resources/recordLockCoordinator.js +2565 -0
  131. package/dist/resources/recordLockCoordinator.js.map +1 -0
  132. package/dist/resources/replayLogs.js +5 -0
  133. package/dist/resources/replayLogs.js.map +1 -1
  134. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  135. package/dist/resources/replicatedApplyFailure.js +63 -0
  136. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  137. package/dist/resources/scheduler/scheduler.js +3 -3
  138. package/dist/resources/scheduler/scheduler.js.map +1 -1
  139. package/dist/resources/search.d.ts +10 -4
  140. package/dist/resources/search.js +160 -40
  141. package/dist/resources/search.js.map +1 -1
  142. package/dist/resources/tracked.d.ts +5 -1
  143. package/dist/resources/tracked.js +74 -23
  144. package/dist/resources/tracked.js.map +1 -1
  145. package/dist/security/jsLoader.js +6 -4
  146. package/dist/security/jsLoader.js.map +1 -1
  147. package/dist/server/REST.js +33 -2
  148. package/dist/server/REST.js.map +1 -1
  149. package/dist/server/http.d.ts +5 -1
  150. package/dist/server/http.js +34 -2
  151. package/dist/server/http.js.map +1 -1
  152. package/dist/server/serverHelpers/Headers.d.ts +2 -0
  153. package/dist/server/serverHelpers/Headers.js +6 -0
  154. package/dist/server/serverHelpers/Headers.js.map +1 -1
  155. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  156. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  157. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  158. package/dist/server/serverHelpers/Request.d.ts +5 -10
  159. package/dist/server/serverHelpers/Request.js +38 -136
  160. package/dist/server/serverHelpers/Request.js.map +1 -1
  161. package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
  162. package/dist/server/serverHelpers/contentTypes.js +189 -15
  163. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  164. package/dist/server/serverHelpers/serverUtilities.js +96 -17
  165. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  166. package/dist/server/storageReclamation.js +1 -1
  167. package/dist/server/storageReclamation.js.map +1 -1
  168. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  169. package/dist/server/threads/isolatedApplications.js +171 -0
  170. package/dist/server/threads/isolatedApplications.js.map +1 -0
  171. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  172. package/dist/server/threads/logRotationTransport.js +33 -0
  173. package/dist/server/threads/logRotationTransport.js.map +1 -0
  174. package/dist/server/threads/manageThreads.d.ts +64 -6
  175. package/dist/server/threads/manageThreads.js +261 -12
  176. package/dist/server/threads/manageThreads.js.map +1 -1
  177. package/dist/server/threads/socketRouter.d.ts +1 -0
  178. package/dist/server/threads/socketRouter.js +196 -13
  179. package/dist/server/threads/socketRouter.js.map +1 -1
  180. package/dist/server/threads/threadServer.js +30 -7
  181. package/dist/server/threads/threadServer.js.map +1 -1
  182. package/dist/utility/errors/hdbError.d.ts +24 -0
  183. package/dist/utility/errors/hdbError.js +58 -1
  184. package/dist/utility/errors/hdbError.js.map +1 -1
  185. package/dist/utility/hdbTerms.d.ts +2 -0
  186. package/dist/utility/hdbTerms.js +2 -0
  187. package/dist/utility/hdbTerms.js.map +1 -1
  188. package/dist/utility/logging/harper_logger.js +217 -38
  189. package/dist/utility/logging/harper_logger.js.map +1 -1
  190. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  191. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  192. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  193. package/dist/utility/logging/logRotation.d.ts +46 -0
  194. package/dist/utility/logging/logRotation.js +365 -0
  195. package/dist/utility/logging/logRotation.js.map +1 -0
  196. package/dist/utility/logging/logRotator.d.ts +1 -1
  197. package/dist/utility/logging/logRotator.js +172 -92
  198. package/dist/utility/logging/logRotator.js.map +1 -1
  199. package/dist/utility/npmUtilities.js +6 -4
  200. package/dist/utility/npmUtilities.js.map +1 -1
  201. package/dist/utility/watcherFallback.d.ts +0 -45
  202. package/dist/utility/watcherFallback.js +1 -125
  203. package/dist/utility/watcherFallback.js.map +1 -1
  204. package/dist/validation/configValidator.js +6 -3
  205. package/dist/validation/configValidator.js.map +1 -1
  206. package/index.ts +6 -0
  207. package/json/systemSchema.json +3 -0
  208. package/npm-shrinkwrap.json +131 -41
  209. package/package.json +10 -3
  210. package/resources/DESIGN.md +124 -19
  211. package/resources/DatabaseTransaction.ts +230 -17
  212. package/resources/LMDBTransaction.ts +21 -3
  213. package/resources/PrimaryRocksDatabase.ts +20 -7
  214. package/resources/RecordEncoder.ts +25 -5
  215. package/resources/Resource.ts +97 -13
  216. package/resources/ResourceInterface.ts +8 -0
  217. package/resources/RocksIndexStore.ts +2 -1
  218. package/resources/RocksTransactionLogStore.ts +111 -31
  219. package/resources/Table.ts +1224 -393
  220. package/resources/analytics/write.ts +10 -3
  221. package/resources/auditStore.ts +460 -11
  222. package/resources/dataLoader.ts +3 -4
  223. package/resources/databases.ts +610 -146
  224. package/resources/derivedIndexRegistry.ts +56 -0
  225. package/resources/derivedIndexRuntime.ts +2292 -0
  226. package/resources/graphql.ts +3 -2
  227. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
  228. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  229. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  230. package/resources/nodeIdMapping.ts +50 -0
  231. package/resources/recordLock.ts +173 -7
  232. package/resources/recordLockCoordinator.ts +3043 -0
  233. package/resources/replayLogs.ts +5 -0
  234. package/resources/replicatedApplyFailure.ts +77 -0
  235. package/resources/scheduler/scheduler.ts +4 -4
  236. package/resources/search.ts +169 -49
  237. package/resources/tracked.ts +73 -22
  238. package/security/jsLoader.ts +6 -4
  239. package/server/DESIGN.md +11 -0
  240. package/server/REST.ts +36 -3
  241. package/server/http.ts +34 -2
  242. package/server/serverHelpers/Headers.ts +5 -1
  243. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  244. package/server/serverHelpers/Request.ts +33 -131
  245. package/server/serverHelpers/contentTypes.ts +188 -15
  246. package/server/serverHelpers/serverUtilities.ts +143 -24
  247. package/server/storageReclamation.ts +2 -2
  248. package/server/threads/isolatedApplications.ts +157 -0
  249. package/server/threads/logRotationTransport.ts +40 -0
  250. package/server/threads/manageThreads.js +254 -12
  251. package/server/threads/socketRouter.ts +217 -11
  252. package/server/threads/threadServer.js +30 -7
  253. package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
  254. package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
  255. package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
  256. package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
  257. package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
  258. package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
  259. package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
  260. package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
  261. package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
  262. package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
  263. package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
  264. package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
  265. package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
  266. package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  267. package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
  268. package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
  269. package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
  270. package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
  271. package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
  272. package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
  273. package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
  274. package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
  275. package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
  276. package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
  277. package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
  278. package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  279. package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
  280. package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
  281. package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
  282. package/studio/web/index.html +1 -1
  283. package/utility/errors/hdbError.ts +54 -0
  284. package/utility/hdbTerms.ts +2 -0
  285. package/utility/logging/harper_logger.ts +209 -30
  286. package/utility/logging/logGenerationCoordinator.ts +196 -0
  287. package/utility/logging/logRotation.ts +367 -0
  288. package/utility/logging/logRotator.ts +196 -91
  289. package/utility/npmUtilities.ts +6 -4
  290. package/utility/watcherFallback.ts +0 -122
  291. package/validation/configValidator.ts +6 -3
package/server/DESIGN.md CHANGED
@@ -208,6 +208,17 @@ mirror because it only widens what an allowlist may _name_; enforcement stays on
208
208
 
209
209
  `REST.ts → http(request, nextHandler)` is the chief integration point: it takes a `Request`, asks the `Resources` registry for a match, builds a `RequestTarget`, and dispatches into the Resource class's static method. Cache headers are translated to `request.expiresAt` / `onlyIfCached` / `noCache` flags within the same function.
210
210
 
211
+ ### Streaming startup errors
212
+
213
+ SSE and NDJSON serializers eagerly take and hold their first iterator step for GET requests. `REST.ts` waits through the next event-loop turn for that step: an immediate rejection remains an HTTP error rendered as Problem Details, while a first item or the cutoff commits the stream. Mutating requests do not use the startup-status gate because their transaction has already committed by this point; all of their stream failures use the in-band form. Later GET failures are terminal, format-valid records (`event: harper-error` for SSE and a reserved control record for NDJSON). Keep the decision in the serializer/REST boundary so Node, uWS, Bun, compression, and injection share one contract; transports must not independently prefetch the iterator.
214
+
215
+ SSE terminal event data uses `{ error: <code-or-class>, message: <message>, status?: <status> }`; NDJSON wraps the same object as `{ "$harperStreamError": { ... } }` so it cannot be mistaken for an ordinary row with an `error` field.
216
+ Sources with `mapError` remain unmapped for these two formats so their failures reach this single contract instead of becoming legacy data records.
217
+ generic JSON-array streaming retains its older `{ error: "<name>: <message>" }` element shape.
218
+ The `error` value is the stable programmatic discriminator; `message` is diagnostic and follows the same thrown-message exposure policy as pre-commit Problem Details.
219
+
220
+ Clean stream completion does not prove completeness; clients must inspect SSE for the `harper-error` event and NDJSON for the `$harperStreamError` control record.
221
+
211
222
  ### Deferred credential rejection (#2418)
212
223
 
213
224
  `authentication` runs before route matching, so when it meets an `Authorization` header it cannot
package/server/REST.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { serialize, serializeMessage, getDeserializer } from '../server/serverHelpers/contentTypes.ts';
3
+ import {
4
+ serialize,
5
+ serializeMessage,
6
+ getDeserializer,
7
+ waitForStreamStartup,
8
+ discardSerializedStream,
9
+ } from '../server/serverHelpers/contentTypes.ts';
4
10
  import { addAnalyticsListener, recordAction, recordActionBinary } from '../resources/analytics/write.ts';
5
11
  import * as harperLogger from '../utility/logging/harper_logger.ts';
6
12
  import { ServerError, ClientError } from '../utility/errors/hdbError.ts';
@@ -348,7 +354,15 @@ async function http(request: Request, nextHandler, resources: Resources, httpOpt
348
354
  headers.setIfNone('Last-Modified', new Date(lastModification).toUTCString());
349
355
  } else if (responseData.headers) {
350
356
  // if response is a Response object (or response-like envelope with headers), use it as the response
351
- return finalizeResponse(responseData, headers, status, request);
357
+ const response = finalizeResponse(responseData, headers, status, request);
358
+ if (request.method === 'HEAD') {
359
+ discardSerializedStream(response.body);
360
+ if (!(response instanceof Response)) response.body = undefined;
361
+ } else if (request.method === 'GET') {
362
+ const startup = waitForStreamStartup(response.body);
363
+ if (startup) await startup;
364
+ }
365
+ return response;
352
366
  } else if (isFinite(lastModification)) {
353
367
  etagFloat[0] = lastModification;
354
368
  // base64 encoding of the 64-bit float encoding of the date in ms (with quotes)
@@ -422,7 +436,13 @@ async function http(request: Request, nextHandler, resources: Resources, httpOpt
422
436
  setCountHeaders(headers, (target as any).offset || 0, (target as any).count, responseData);
423
437
  }
424
438
  responseObject.body = serialize(responseData, request, responseObject);
425
- if (method === 'HEAD') responseObject.body = undefined; // we want everything else to be the same as GET, but then omit the body
439
+ if (request.method === 'HEAD') {
440
+ discardSerializedStream(responseObject.body);
441
+ responseObject.body = undefined; // we want everything else to be the same as GET, but then omit the body
442
+ } else if (request.method === 'GET') {
443
+ const startup = waitForStreamStartup(responseObject.body);
444
+ if (startup) await startup;
445
+ }
426
446
  }
427
447
  // A collection read's count headers vary by the request's `Prefer` value; serialize() just reset
428
448
  // `Vary`, so declare it here (after serialization) — otherwise a shared cache could serve count
@@ -466,6 +486,19 @@ async function http(request: Request, nextHandler, resources: Resources, httpOpt
466
486
  headers,
467
487
  body: undefined,
468
488
  };
489
+ // The error body is a different representation, so success representation and caching headers
490
+ // accumulated before the startup failure must not survive its serialization.
491
+ for (const header of [
492
+ 'Content-Encoding',
493
+ 'Cache-Control',
494
+ 'ETag',
495
+ 'Last-Modified',
496
+ 'Content-Range',
497
+ 'Range-Unit',
498
+ 'Preference-Applied',
499
+ 'Age',
500
+ ])
501
+ headers.delete(header);
469
502
  responseObject.body = serialize(problemDetail, request, responseObject);
470
503
  return responseObject;
471
504
  }
package/server/http.ts CHANGED
@@ -12,6 +12,11 @@ import * as env from '../utility/environment/environmentManager.ts';
12
12
  import * as terms from '../utility/hdbTerms.ts';
13
13
  import { getConfigPath } from '../config/configUtils.ts';
14
14
  import { getTicketKeys, getWorkerIndex } from './threads/manageThreads.js';
15
+ import {
16
+ applicationSocketName,
17
+ isolatedApplicationRoute,
18
+ thisThreadsIsolatedApplication,
19
+ } from './threads/isolatedApplications.ts';
15
20
  import { createTLSSelector, getEffectiveTlsCiphers } from '../security/keys.ts';
16
21
  import { createSecureServer, createServer as createH2CServer } from 'node:http2';
17
22
  import { createServer as createSecureServerHttp1 } from 'node:https';
@@ -196,10 +201,21 @@ export function writeUdsMetadata(
196
201
  port: number | string,
197
202
  secureServer: any,
198
203
  protocol?: string,
199
- mtlsForwarding = true
204
+ mtlsForwarding = true,
205
+ route: { application: string; hosts: string[] } | undefined = isolatedApplicationRoute()
200
206
  ) {
201
207
  const contexts = secureServer.secureContexts;
202
208
  let yaml = `pid: ${process.pid}\ntid: ${currentThreadId()}\nport: ${port}\n`;
209
+ // Routing identity, separate from certificate coverage: a proxy sends this application's hosts to
210
+ // this socket alone, whatever hostnames the (shared, possibly wildcard) certificates carry.
211
+ if (route) {
212
+ yaml += `application: ${JSON.stringify(route.application)}\n`;
213
+ if (route.hosts.length === 0) yaml += `applicationHosts: []\n`;
214
+ else {
215
+ yaml += `applicationHosts:\n`;
216
+ for (const host of route.hosts) yaml += ` - ${JSON.stringify(host)}\n`;
217
+ }
218
+ }
203
219
  // Which application protocol this socket speaks (absent = http/1.1, the historical
204
220
  // default) — lets a fronting proxy route by negotiated ALPN.
205
221
  if (protocol) yaml += `protocol: ${protocol}\n`;
@@ -267,6 +283,19 @@ export function cleanupSocketsDirectory() {
267
283
  } catch {}
268
284
  }
269
285
 
286
+ export function cleanupApplicationSockets(application: string) {
287
+ const socketsDir = join(env.getHdbBasePath(), 'sockets');
288
+ const prefix = applicationSocketName(application, '');
289
+ try {
290
+ for (const file of readdirSync(socketsDir)) {
291
+ if (!file.startsWith(prefix) || (!file.endsWith('.sock') && !file.endsWith('.yaml'))) continue;
292
+ try {
293
+ unlinkSync(join(socketsDir, file));
294
+ } catch {}
295
+ }
296
+ } catch {}
297
+ }
298
+
270
299
  // Entries in `universalHeaders` that were pushed by `applySecurityHeaders`, so a config
271
300
  // hot-reload can remove exactly the entries it owns without clobbering entries pushed by
272
301
  // other components.
@@ -875,7 +904,10 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
875
904
  if (secure && env.get(terms.CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) {
876
905
  const socketsDir = join(env.getHdbBasePath(), 'sockets');
877
906
  mkdirSync(socketsDir, { recursive: true });
878
- const socketName = `${getWorkerIndex()}-${port}`;
907
+ const isolatedApplication = thisThreadsIsolatedApplication();
908
+ const socketName = isolatedApplication
909
+ ? applicationSocketName(isolatedApplication, port)
910
+ : `${getWorkerIndex()}-${port}`;
879
911
  const udsPath = join(socketsDir, `${socketName}.sock`);
880
912
  const yamlPath = join(socketsDir, `${socketName}.yaml`);
881
913
 
@@ -31,6 +31,10 @@ export class Headers extends Map<string, [string, string | string[]]> {
31
31
  if (typeof name !== 'string') name = '' + name;
32
32
  return super.has(name.toLowerCase());
33
33
  }
34
+ delete(name) {
35
+ if (typeof name !== 'string') name = '' + name;
36
+ return super.delete(name.toLowerCase());
37
+ }
34
38
  setIfNone(name, value) {
35
39
  if (typeof name !== 'string') name = '' + name;
36
40
  if (typeof value !== 'string') value = '' + value;
@@ -239,7 +243,7 @@ function nodeResponseHeaders(nodeResponse: any) {
239
243
  };
240
244
  }
241
245
 
242
- function applyWriteHeadHeaders(nodeResponse: any, headers: any): void {
246
+ export function applyWriteHeadHeaders(nodeResponse: any, headers: any): void {
243
247
  const suppliedHeaders = new Map<string, { name: string; value: any }>();
244
248
  const addHeader = (name: string, value: any) => {
245
249
  const key = String(name).toLowerCase();
@@ -0,0 +1,221 @@
1
+ import { STATUS_CODES } from 'node:http';
2
+ import type {
3
+ IncomingMessage as NodeIncomingMessage,
4
+ OutgoingHttpHeader,
5
+ OutgoingHttpHeaders,
6
+ ServerResponse as NodeServerResponse,
7
+ } from 'node:http';
8
+ import type { Socket } from 'node:net';
9
+ import { PassThrough } from 'node:stream';
10
+ import { Headers as ResponseHeaders, applyWriteHeadHeaders } from './Headers.ts';
11
+
12
+ export interface AdaptedResponse {
13
+ status: number;
14
+ headers: ResponseHeaders;
15
+ body: PassThrough;
16
+ }
17
+
18
+ class HeadersSentError extends Error {
19
+ code = 'ERR_HTTP_HEADERS_SENT';
20
+ constructor(action: string) {
21
+ super(`Cannot ${action} headers after they are sent to the client`);
22
+ }
23
+ }
24
+
25
+ class UnsupportedResponseMethodError extends Error {
26
+ constructor(method: string) {
27
+ super(`${method}() is not supported on a withNodeAdapter() response`);
28
+ }
29
+ }
30
+
31
+ const ignoreError = () => {};
32
+
33
+ export class NodeAdapterResponse extends PassThrough implements NodeServerResponse {
34
+ statusCode = 200;
35
+ statusMessage = '';
36
+ strictContentLength = false;
37
+ chunkedEncoding = false;
38
+ shouldKeepAlive = true;
39
+ useChunkedEncodingByDefault = true;
40
+ sendDate = true;
41
+ readonly req: NodeIncomingMessage;
42
+ readonly socket: Socket | null;
43
+ #headers = new ResponseHeaders();
44
+ #committedStatus: number | undefined;
45
+ #headerText: string | undefined;
46
+ #nodeResponse: NodeServerResponse | undefined;
47
+ #resolve: (response: AdaptedResponse) => void;
48
+ #reject: (reason: unknown) => void;
49
+
50
+ constructor(
51
+ req: NodeIncomingMessage,
52
+ nodeResponse: NodeServerResponse | undefined,
53
+ resolve: (response: AdaptedResponse) => void,
54
+ reject: (reason: unknown) => void
55
+ ) {
56
+ super();
57
+ this.req = req;
58
+ this.socket = req.socket ?? null;
59
+ this.#nodeResponse = nodeResponse;
60
+ this.#resolve = resolve;
61
+ this.#reject = reject;
62
+ this.on('error', ignoreError);
63
+ if (typeof nodeResponse?.on === 'function') {
64
+ const forward = (...args: any[]) => this.emit('timeout', ...args);
65
+ let forwarding = false;
66
+ const syncForwarding = (wanted: boolean) => {
67
+ if (wanted === forwarding) return;
68
+ forwarding = wanted;
69
+ if (wanted) nodeResponse.on('timeout', forward);
70
+ else nodeResponse.removeListener('timeout', forward);
71
+ };
72
+ const onNewListener = (event: string | symbol) => event === 'timeout' && syncForwarding(true);
73
+ const onRemoveListener = (event: string | symbol) =>
74
+ event === 'timeout' && syncForwarding(this.listenerCount('timeout') > 0);
75
+ this.on('newListener', onNewListener);
76
+ this.on('removeListener', onRemoveListener);
77
+ this.once('close', () => {
78
+ syncForwarding(false);
79
+ this.removeListener('newListener', onNewListener);
80
+ this.removeListener('removeListener', onRemoveListener);
81
+ });
82
+ }
83
+ }
84
+
85
+ get headersSent() {
86
+ return this.#committedStatus !== undefined;
87
+ }
88
+ get finished() {
89
+ return this.writableEnded;
90
+ }
91
+ get connection() {
92
+ return this.socket;
93
+ }
94
+ get _header(): string | null {
95
+ if (this.#committedStatus === undefined) return null;
96
+ if (this.#headerText === undefined) {
97
+ let text = `HTTP/1.1 ${this.#committedStatus} ${this.statusMessage}\r\n`;
98
+ for (const [name, value] of this.#headers) {
99
+ if (Array.isArray(value)) for (const entry of value) text += `${name}: ${entry}\r\n`;
100
+ else text += `${name}: ${value}\r\n`;
101
+ }
102
+ this.#headerText = text + '\r\n';
103
+ }
104
+ return this.#headerText;
105
+ }
106
+
107
+ setHeader(name: string, value: number | string | readonly string[]) {
108
+ if (this.headersSent) throw new HeadersSentError('set');
109
+ this.#headers.set(name, value);
110
+ return this;
111
+ }
112
+ setHeaders(headers: Headers | Map<string, number | string | readonly string[]> | ResponseHeaders) {
113
+ let cookies: string[] | undefined;
114
+ for (const [name, value] of headers) {
115
+ if (name.toLowerCase() !== 'set-cookie') this.setHeader(name, value);
116
+ else if (Array.isArray(value)) (cookies ??= []).push(...value);
117
+ else (cookies ??= []).push(String(value));
118
+ }
119
+ if (cookies) this.setHeader('set-cookie', cookies);
120
+ return this;
121
+ }
122
+ appendHeader(name: string, value: string | readonly string[]) {
123
+ if (this.headersSent) throw new HeadersSentError('append');
124
+ if (Array.isArray(value)) for (const entry of value) this.#headers.append(name, entry);
125
+ else this.#headers.append(name, value);
126
+ return this;
127
+ }
128
+ getHeader(name: string) {
129
+ return this.#headers.get(name);
130
+ }
131
+ getHeaders() {
132
+ const headers: OutgoingHttpHeaders = Object.create(null);
133
+ for (const [name, value] of this.#headers) headers[name.toLowerCase()] = value;
134
+ return headers;
135
+ }
136
+ getHeaderNames() {
137
+ return [...this.#headers.keys()];
138
+ }
139
+ hasHeader(name: string) {
140
+ return this.#headers.has(name);
141
+ }
142
+ removeHeader(name: string) {
143
+ if (this.headersSent) throw new HeadersSentError('remove');
144
+ this.#headers.delete(name);
145
+ }
146
+
147
+ writeHead(statusCode: number, statusMessage?: string, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]): this;
148
+ writeHead(statusCode: number, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]): this;
149
+ writeHead(
150
+ statusCode: number,
151
+ statusMessageOrHeaders?: string | OutgoingHttpHeaders | OutgoingHttpHeader[],
152
+ headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]
153
+ ) {
154
+ if (this.headersSent) throw new HeadersSentError('write');
155
+ this.statusCode = statusCode;
156
+ if (typeof statusMessageOrHeaders === 'string') this.statusMessage = statusMessageOrHeaders;
157
+ else {
158
+ this.statusMessage ||= STATUS_CODES[statusCode] || 'unknown';
159
+ // writeHead(302, undefined, headers) is the three-argument form with the status message
160
+ // omitted; the hole must not erase the headers the third argument carries.
161
+ headers ??= statusMessageOrHeaders;
162
+ }
163
+ if (headers) applyWriteHeadHeaders(this, headers);
164
+ this.#committedStatus = statusCode;
165
+ this.#resolve({ status: statusCode, headers: this.#headers, body: this });
166
+ return this;
167
+ }
168
+ _implicitHeader() {
169
+ this.writeHead(this.statusCode);
170
+ }
171
+ flushHeaders() {
172
+ if (!this.headersSent) this._implicitHeader();
173
+ }
174
+
175
+ write(
176
+ chunk: unknown,
177
+ encoding?: BufferEncoding | ((error?: Error | null) => void),
178
+ callback?: (error?: Error | null) => void
179
+ ) {
180
+ if (!this.headersSent) this._implicitHeader();
181
+ return super.write(chunk, encoding as BufferEncoding, callback);
182
+ }
183
+ end(chunk?: unknown, encoding?: BufferEncoding | (() => void), callback?: () => void) {
184
+ if (!this.headersSent) this._implicitHeader();
185
+ return super.end(chunk, encoding as BufferEncoding, callback);
186
+ }
187
+ _destroy(error: Error | null, callback: (error?: Error | null) => void) {
188
+ if (!this.headersSent) this.#reject(error ?? new Error('Response destroyed before headers were sent'));
189
+ callback(error);
190
+ }
191
+
192
+ setTimeout(msecs: number, callback?: () => void) {
193
+ if (callback) this.on('timeout', callback);
194
+ const nodeResponse = this.#nodeResponse;
195
+ if (typeof nodeResponse?.setTimeout !== 'function') return this;
196
+ nodeResponse.setTimeout(msecs);
197
+ return this;
198
+ }
199
+ writeContinue(callback?: () => void) {
200
+ if (typeof this.#nodeResponse?.writeContinue === 'function') this.#nodeResponse.writeContinue(callback);
201
+ else callback?.();
202
+ }
203
+ writeProcessing(callback?: () => void) {
204
+ if (typeof this.#nodeResponse?.writeProcessing === 'function') this.#nodeResponse.writeProcessing(callback);
205
+ else callback?.();
206
+ }
207
+ writeEarlyHints(hints: Record<string, string | string[]>, callback?: () => void) {
208
+ if (typeof this.#nodeResponse?.writeEarlyHints === 'function') this.#nodeResponse.writeEarlyHints(hints, callback);
209
+ else callback?.();
210
+ }
211
+ // Trailers need chunked encoding on the wire, which Harper's response layer owns; dropping one silently (a Digest, say) is worse than failing.
212
+ addTrailers(): never {
213
+ throw new UnsupportedResponseMethodError('addTrailers');
214
+ }
215
+ assignSocket(): never {
216
+ throw new UnsupportedResponseMethodError('assignSocket');
217
+ }
218
+ detachSocket(): never {
219
+ throw new UnsupportedResponseMethodError('detachSocket');
220
+ }
221
+ }
@@ -1,10 +1,11 @@
1
1
  import type { IncomingMessage as NodeIncomingMessage, ServerResponse as NodeServerResponse } from 'node:http';
2
2
  import type { Socket } from 'node:net';
3
3
  import { TLSSocket } from 'node:tls';
4
- import { EventEmitter } from 'node:events';
5
- import { Readable, PassThrough } from 'node:stream';
4
+ import { Readable } from 'node:stream';
6
5
  import { Headers as ResponseHeaders } from './Headers.ts';
6
+ import { NodeAdapterResponse, type AdaptedResponse } from './NodeAdapterResponse.ts';
7
7
  import type { ConnectionInfo } from './proxyProtocol.ts';
8
+ import harperLogger from '../../utility/logging/harper_logger.ts';
8
9
 
9
10
  export const isBun = typeof globalThis.Bun !== 'undefined';
10
11
 
@@ -178,10 +179,8 @@ export class Request {
178
179
  * status, headers, and body, resolving the returned promise as soon as headers are available with a
179
180
  * streaming body that can be piped back through the Harper middleware chain.
180
181
  *
181
- * **Important:** The resolved `body` PassThrough must have an `error` listener attached (or be piped
182
- * to a destination that handles errors) before it is consumed. If the underlying connection is reset
183
- * after headers are sent, the body stream is destroyed with an error — without a listener, Node.js
184
- * will throw an uncaught exception.
182
+ * Consume the returned body with `pipeline()`, `finished()` or async iteration so stored stream errors
183
+ * and premature closes are reported.
185
184
  *
186
185
  * Example:
187
186
  * server.http((request, next) =>
@@ -190,11 +189,13 @@ export class Request {
190
189
  */
191
190
  withNodeAdapter(
192
191
  handler: (request: NodeIncomingMessage, response: NodeServerResponse) => void | Promise<void>
193
- ): Promise<{ status: number; headers: ResponseHeaders; body: PassThrough }> {
194
- // Flat headers object matching IncomingMessage.headers format (lowercase keys)
195
- const reqHeaders: Record<string, string | string[]> = Object.create(null);
192
+ ): Promise<AdaptedResponse> {
193
+ const reqHeaders: Record<string, string | string[]> = {};
196
194
  for (const [key, value] of this.headers) {
197
- reqHeaders[key.toLowerCase()] = value;
195
+ const lowerKey = key.toLowerCase();
196
+ if (lowerKey === '__proto__')
197
+ Object.defineProperty(reqHeaders, lowerKey, { value, configurable: true, enumerable: true, writable: true });
198
+ else reqHeaders[lowerKey] = value;
198
199
  }
199
200
 
200
201
  // Proxy the underlying IncomingMessage so body streaming works, but expose
@@ -209,131 +210,32 @@ export class Request {
209
210
  },
210
211
  }) as NodeIncomingMessage;
211
212
 
212
- let resolveResponse!: (value: { status: number; headers: ResponseHeaders; body: PassThrough }) => void;
213
- let rejectResponse!: (reason: unknown) => void;
214
- const response = new Promise<{ status: number; headers: ResponseHeaders; body: PassThrough }>((resolve, reject) => {
215
- resolveResponse = resolve;
216
- rejectResponse = reject;
213
+ let nodeRes!: NodeAdapterResponse;
214
+ const response = new Promise<AdaptedResponse>((resolve, reject) => {
215
+ nodeRes = new NodeAdapterResponse(nodeReq, this._nodeResponse, resolve, reject);
217
216
  });
218
217
 
219
- const responseBody = new PassThrough();
220
- const capturedHeaders = new ResponseHeaders();
221
- let headersFlushed = false;
222
- let nodeRes: ReturnType<typeof Object.assign> & NodeServerResponse;
218
+ const signal = this.signal;
219
+ const onAbort = () => nodeRes.destroy(nodeRes.headersSent ? undefined : signal.reason);
220
+ if (signal.aborted) onAbort();
221
+ else {
222
+ signal.addEventListener('abort', onAbort, { once: true });
223
+ nodeRes.once('close', () => signal.removeEventListener('abort', onAbort));
224
+ }
223
225
 
224
- const flushHeaders = () => {
225
- if (!headersFlushed) {
226
- headersFlushed = true;
227
- nodeRes.headersSent = true;
228
- resolveResponse({ status: nodeRes.statusCode as number, headers: capturedHeaders, body: responseBody });
229
- }
226
+ const onHandlerFailure = (error: Error) => {
227
+ if (!nodeRes.writableEnded) nodeRes.destroy(error);
228
+ else harperLogger.warn('withNodeAdapter handler failed after ending its response', error);
230
229
  };
231
-
232
- const applyHeaders = (hdrs: object | unknown[]) => {
233
- if (Array.isArray(hdrs)) {
234
- if (hdrs.length > 0 && Array.isArray(hdrs[0])) {
235
- for (const [name, value] of hdrs as [string, string][]) capturedHeaders.set(name, value);
236
- } else {
237
- for (let i = 0; i < hdrs.length; i += 2) capturedHeaders.set(hdrs[i] as string, hdrs[i + 1] as string);
238
- }
239
- } else {
240
- for (const [k, v] of Object.entries(hdrs)) capturedHeaders.set(k, v as string);
241
- }
242
- };
243
-
244
- nodeRes = Object.assign(new EventEmitter(), {
245
- statusCode: 200 as number,
246
- statusMessage: '',
247
- headersSent: false,
248
- writable: true,
249
- writableEnded: false,
250
- writableFinished: false,
251
- socket: this._nodeRequest.socket,
252
-
253
- setHeader(name: string, value: string | number | string[]) {
254
- if (Array.isArray(value)) {
255
- // Use set() for first value (overwrites any existing entry) then append() for
256
- // the rest so multiple values — critical for Set-Cookie — are preserved as an
257
- // array rather than collapsed into a single comma-joined string.
258
- if (value.length > 0) {
259
- capturedHeaders.set(name, String(value[0]));
260
- for (let i = 1; i < value.length; i++) capturedHeaders.append(name, String(value[i]));
261
- }
262
- } else {
263
- capturedHeaders.set(name, String(value));
264
- }
265
- return nodeRes;
266
- },
267
- getHeader(name: string) {
268
- return capturedHeaders.get(name);
269
- },
270
- getHeaders() {
271
- return Object.fromEntries(capturedHeaders);
272
- },
273
- hasHeader(name: string) {
274
- return capturedHeaders.has(name);
275
- },
276
- removeHeader(name: string) {
277
- capturedHeaders.delete(name);
278
- },
279
- flushHeaders() {
280
- flushHeaders();
281
- },
282
- writeHead(statusCode: number, statusMessageOrHeaders?: string | object, maybeHeaders?: object) {
283
- if (headersFlushed) return nodeRes;
284
- nodeRes.statusCode = statusCode;
285
- const hdrs = typeof statusMessageOrHeaders === 'string' ? maybeHeaders : statusMessageOrHeaders;
286
- if (hdrs) applyHeaders(hdrs as object | unknown[]);
287
- flushHeaders();
288
- return nodeRes;
289
- },
290
- write(
291
- chunk: unknown,
292
- encoding?: BufferEncoding | ((error?: Error | null) => void),
293
- callback?: (error?: Error | null) => void
294
- ) {
295
- flushHeaders();
296
- if (typeof encoding === 'function') return responseBody.write(chunk as any, encoding);
297
- return responseBody.write(chunk as any, encoding, callback);
298
- },
299
- end(chunk?: unknown, encoding?: BufferEncoding | (() => void), callback?: () => void) {
300
- flushHeaders();
301
- nodeRes.writableEnded = true;
302
- if (typeof chunk === 'function') responseBody.end(chunk as () => void);
303
- else if (typeof encoding === 'function') responseBody.end(chunk as any, encoding);
304
- else responseBody.end(chunk as any, encoding, callback);
305
- return nodeRes;
306
- },
307
- destroy(error?: Error) {
308
- if (!headersFlushed) {
309
- if (error) rejectResponse(error);
310
- else rejectResponse(new Error('Response destroyed before headers were sent'));
311
- // The error has been forwarded to the response promise; suppress the
312
- // PassThrough 'error' event for this one destroy call so Node doesn't
313
- // throw due to having no other listeners.
314
- responseBody.once('error', () => {});
315
- }
316
- responseBody.destroy(error);
317
- return nodeRes;
318
- },
319
- }) as unknown as NodeServerResponse;
320
-
321
- responseBody.on('finish', () => {
322
- nodeRes.writableFinished = true;
323
- (nodeRes as unknown as EventEmitter).emit('finish');
324
- });
325
- responseBody.on('drain', () => {
326
- (nodeRes as unknown as EventEmitter).emit('drain');
327
- });
328
- responseBody.on('close', () => {
329
- (nodeRes as unknown as EventEmitter).emit('close');
330
- });
331
-
332
- const handlerResult = handler(nodeReq, nodeRes);
333
- if (handlerResult != null && typeof (handlerResult as unknown as Promise<void>).then === 'function') {
334
- (handlerResult as unknown as Promise<void>).catch((err: unknown) => {
335
- if (!headersFlushed) rejectResponse(err);
336
- });
230
+ let handlerResult: void | Promise<void>;
231
+ try {
232
+ handlerResult = handler(nodeReq, nodeRes);
233
+ } catch (error) {
234
+ onHandlerFailure(error as Error);
235
+ return response;
236
+ }
237
+ if (typeof (handlerResult as Promise<void>)?.then === 'function') {
238
+ Promise.resolve(handlerResult).catch(onHandlerFailure);
337
239
  }
338
240
  return response;
339
241
  }