@morojs/moro 1.7.30 → 1.8.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 (373) hide show
  1. package/README.md +89 -46
  2. package/dist/core/auth/morojs-adapter.js +95 -45
  3. package/dist/core/auth/morojs-adapter.js.map +1 -1
  4. package/dist/core/config/config-manager.js.map +1 -1
  5. package/dist/core/config/config-sources.js +119 -48
  6. package/dist/core/config/config-sources.js.map +1 -1
  7. package/dist/core/config/config-validator.js +189 -8
  8. package/dist/core/config/config-validator.js.map +1 -1
  9. package/dist/core/config/file-loader.js.map +1 -1
  10. package/dist/core/config/index.js.map +1 -1
  11. package/dist/core/config/schema.js +35 -4
  12. package/dist/core/config/schema.js.map +1 -1
  13. package/dist/core/config/utils.js.map +1 -1
  14. package/dist/core/database/adapters/drizzle.js +2 -2
  15. package/dist/core/database/adapters/drizzle.js.map +1 -1
  16. package/dist/core/database/adapters/index.js.map +1 -1
  17. package/dist/core/database/adapters/mongodb.d.ts +1 -0
  18. package/dist/core/database/adapters/mongodb.js +16 -0
  19. package/dist/core/database/adapters/mongodb.js.map +1 -1
  20. package/dist/core/database/adapters/mysql.d.ts +1 -0
  21. package/dist/core/database/adapters/mysql.js +13 -2
  22. package/dist/core/database/adapters/mysql.js.map +1 -1
  23. package/dist/core/database/adapters/postgresql.js +2 -2
  24. package/dist/core/database/adapters/postgresql.js.map +1 -1
  25. package/dist/core/database/adapters/redis.js.map +1 -1
  26. package/dist/core/database/adapters/sql-safety.js +5 -4
  27. package/dist/core/database/adapters/sql-safety.js.map +1 -1
  28. package/dist/core/database/adapters/sqlite.js.map +1 -1
  29. package/dist/core/database/index.js.map +1 -1
  30. package/dist/core/docs/index.js +11 -7
  31. package/dist/core/docs/index.js.map +1 -1
  32. package/dist/core/docs/openapi-generator.js +8 -6
  33. package/dist/core/docs/openapi-generator.js.map +1 -1
  34. package/dist/core/docs/schema-to-openapi.js +3 -3
  35. package/dist/core/docs/schema-to-openapi.js.map +1 -1
  36. package/dist/core/docs/simple-docs.js.map +1 -1
  37. package/dist/core/docs/swagger-ui.js.map +1 -1
  38. package/dist/core/docs/zod-to-openapi.js +2 -2
  39. package/dist/core/docs/zod-to-openapi.js.map +1 -1
  40. package/dist/core/events/event-bus.js +14 -5
  41. package/dist/core/events/event-bus.js.map +1 -1
  42. package/dist/core/events/index.js.map +1 -1
  43. package/dist/core/framework.d.ts +41 -3
  44. package/dist/core/framework.js +342 -95
  45. package/dist/core/framework.js.map +1 -1
  46. package/dist/core/graphql/adapter.js.map +1 -1
  47. package/dist/core/graphql/adapters/graphql-js-adapter.js.map +1 -1
  48. package/dist/core/graphql/core.js.map +1 -1
  49. package/dist/core/graphql/index.js.map +1 -1
  50. package/dist/core/graphql/loader.js.map +1 -1
  51. package/dist/core/graphql/types.js.map +1 -1
  52. package/dist/core/grpc/adapters/grpc-js-adapter.js +4 -0
  53. package/dist/core/grpc/adapters/grpc-js-adapter.js.map +1 -1
  54. package/dist/core/grpc/adapters/index.js.map +1 -1
  55. package/dist/core/grpc/grpc-adapter.js.map +1 -1
  56. package/dist/core/grpc/grpc-manager.js.map +1 -1
  57. package/dist/core/grpc/index.js.map +1 -1
  58. package/dist/core/grpc/middleware/auth.js +1 -1
  59. package/dist/core/grpc/middleware/auth.js.map +1 -1
  60. package/dist/core/grpc/middleware/logging.js.map +1 -1
  61. package/dist/core/grpc/middleware/validation.js +1 -1
  62. package/dist/core/grpc/middleware/validation.js.map +1 -1
  63. package/dist/core/grpc/types.d.ts +7 -7
  64. package/dist/core/grpc/types.js.map +1 -1
  65. package/dist/core/http/http-server.d.ts +17 -23
  66. package/dist/core/http/http-server.js +109 -249
  67. package/dist/core/http/http-server.js.map +1 -1
  68. package/dist/core/http/http2-server.d.ts +6 -16
  69. package/dist/core/http/http2-server.js +40 -83
  70. package/dist/core/http/http2-server.js.map +1 -1
  71. package/dist/core/http/index.d.ts +1 -0
  72. package/dist/core/http/index.js +3 -0
  73. package/dist/core/http/index.js.map +1 -1
  74. package/dist/core/http/moro-engine-server.d.ts +263 -0
  75. package/dist/core/http/moro-engine-server.js +1962 -0
  76. package/dist/core/http/moro-engine-server.js.map +1 -0
  77. package/dist/core/http/utils/compression.d.ts +47 -0
  78. package/dist/core/http/utils/compression.js +132 -0
  79. package/dist/core/http/utils/compression.js.map +1 -0
  80. package/dist/core/http/utils/lazy-event-emitter.d.ts +11 -0
  81. package/dist/core/http/utils/lazy-event-emitter.js +69 -0
  82. package/dist/core/http/utils/lazy-event-emitter.js.map +1 -0
  83. package/dist/core/http/utils/multipart-parser.d.ts +32 -0
  84. package/dist/core/http/utils/multipart-parser.js +215 -0
  85. package/dist/core/http/utils/multipart-parser.js.map +1 -0
  86. package/dist/core/http/utils/query-parser.d.ts +1 -0
  87. package/dist/core/http/utils/query-parser.js +44 -0
  88. package/dist/core/http/utils/query-parser.js.map +1 -0
  89. package/dist/core/http/utils/size.d.ts +33 -0
  90. package/dist/core/http/utils/size.js +37 -0
  91. package/dist/core/http/utils/size.js.map +1 -0
  92. package/dist/core/http/utils/ssl-config.d.ts +69 -0
  93. package/dist/core/http/utils/ssl-config.js +116 -0
  94. package/dist/core/http/utils/ssl-config.js.map +1 -0
  95. package/dist/core/http/utils/uws-worker-clustering.js +45 -32
  96. package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
  97. package/dist/core/http/uws-http-server.d.ts +25 -3
  98. package/dist/core/http/uws-http-server.js +465 -86
  99. package/dist/core/http/uws-http-server.js.map +1 -1
  100. package/dist/core/jobs/cron-parser.js +7 -5
  101. package/dist/core/jobs/cron-parser.js.map +1 -1
  102. package/dist/core/jobs/index.js.map +1 -1
  103. package/dist/core/jobs/job-executor.d.ts +1 -1
  104. package/dist/core/jobs/job-executor.js.map +1 -1
  105. package/dist/core/jobs/job-scheduler.d.ts +3 -3
  106. package/dist/core/jobs/job-scheduler.js.map +1 -1
  107. package/dist/core/jobs/job-state-manager.d.ts +3 -3
  108. package/dist/core/jobs/job-state-manager.js +25 -4
  109. package/dist/core/jobs/job-state-manager.js.map +1 -1
  110. package/dist/core/jobs/leader-election.d.ts +15 -0
  111. package/dist/core/jobs/leader-election.js +158 -69
  112. package/dist/core/jobs/leader-election.js.map +1 -1
  113. package/dist/core/jobs/types.d.ts +4 -4
  114. package/dist/core/jobs/types.js.map +1 -1
  115. package/dist/core/jobs/utils.d.ts +1 -1
  116. package/dist/core/jobs/utils.js +7 -3
  117. package/dist/core/jobs/utils.js.map +1 -1
  118. package/dist/core/logger/filters.js +3 -2
  119. package/dist/core/logger/filters.js.map +1 -1
  120. package/dist/core/logger/index.js.map +1 -1
  121. package/dist/core/logger/logger.js +56 -25
  122. package/dist/core/logger/logger.js.map +1 -1
  123. package/dist/core/logger/outputs.js.map +1 -1
  124. package/dist/core/mail/adapters/console-adapter.js.map +1 -1
  125. package/dist/core/mail/adapters/index.js.map +1 -1
  126. package/dist/core/mail/adapters/nodemailer-adapter.js.map +1 -1
  127. package/dist/core/mail/adapters/resend-adapter.js.map +1 -1
  128. package/dist/core/mail/adapters/sendgrid-adapter.js.map +1 -1
  129. package/dist/core/mail/adapters/ses-adapter.js.map +1 -1
  130. package/dist/core/mail/index.js.map +1 -1
  131. package/dist/core/mail/mail-adapter.js.map +1 -1
  132. package/dist/core/mail/mail-manager.d.ts +10 -0
  133. package/dist/core/mail/mail-manager.js +21 -5
  134. package/dist/core/mail/mail-manager.js.map +1 -1
  135. package/dist/core/mail/template-engine.js.map +1 -1
  136. package/dist/core/mail/types.js.map +1 -1
  137. package/dist/core/middleware/built-in/auth/core.js +3 -3
  138. package/dist/core/middleware/built-in/auth/core.js.map +1 -1
  139. package/dist/core/middleware/built-in/auth/helpers.js +4 -4
  140. package/dist/core/middleware/built-in/auth/helpers.js.map +1 -1
  141. package/dist/core/middleware/built-in/auth/hook.js.map +1 -1
  142. package/dist/core/middleware/built-in/auth/index.js.map +1 -1
  143. package/dist/core/middleware/built-in/auth/jwt-helpers.d.ts +4 -4
  144. package/dist/core/middleware/built-in/auth/jwt-helpers.js.map +1 -1
  145. package/dist/core/middleware/built-in/auth/middleware.js.map +1 -1
  146. package/dist/core/middleware/built-in/auth/providers.js.map +1 -1
  147. package/dist/core/middleware/built-in/body-parsers/index.js.map +1 -1
  148. package/dist/core/middleware/built-in/body-size/core.js +2 -2
  149. package/dist/core/middleware/built-in/body-size/core.js.map +1 -1
  150. package/dist/core/middleware/built-in/body-size/hook.js.map +1 -1
  151. package/dist/core/middleware/built-in/body-size/index.js.map +1 -1
  152. package/dist/core/middleware/built-in/body-size/middleware.js.map +1 -1
  153. package/dist/core/middleware/built-in/cache/adapters/cache/file.js +3 -1
  154. package/dist/core/middleware/built-in/cache/adapters/cache/file.js.map +1 -1
  155. package/dist/core/middleware/built-in/cache/adapters/cache/index.js.map +1 -1
  156. package/dist/core/middleware/built-in/cache/adapters/cache/memory.js.map +1 -1
  157. package/dist/core/middleware/built-in/cache/adapters/cache/redis.js.map +1 -1
  158. package/dist/core/middleware/built-in/cache/adapters/index.js.map +1 -1
  159. package/dist/core/middleware/built-in/cache/core.js.map +1 -1
  160. package/dist/core/middleware/built-in/cache/hook.js +3 -3
  161. package/dist/core/middleware/built-in/cache/hook.js.map +1 -1
  162. package/dist/core/middleware/built-in/cache/index.js.map +1 -1
  163. package/dist/core/middleware/built-in/cache/middleware.js.map +1 -1
  164. package/dist/core/middleware/built-in/cdn/adapters/cdn/azure.js.map +1 -1
  165. package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudflare.js.map +1 -1
  166. package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudfront.js.map +1 -1
  167. package/dist/core/middleware/built-in/cdn/adapters/cdn/index.js.map +1 -1
  168. package/dist/core/middleware/built-in/cdn/adapters/index.js.map +1 -1
  169. package/dist/core/middleware/built-in/cdn/core.js.map +1 -1
  170. package/dist/core/middleware/built-in/cdn/hook.js.map +1 -1
  171. package/dist/core/middleware/built-in/cdn/index.js.map +1 -1
  172. package/dist/core/middleware/built-in/cdn/middleware.js.map +1 -1
  173. package/dist/core/middleware/built-in/compression/core.d.ts +3 -0
  174. package/dist/core/middleware/built-in/compression/core.js +28 -34
  175. package/dist/core/middleware/built-in/compression/core.js.map +1 -1
  176. package/dist/core/middleware/built-in/compression/hook.js.map +1 -1
  177. package/dist/core/middleware/built-in/compression/index.js.map +1 -1
  178. package/dist/core/middleware/built-in/compression/middleware.js.map +1 -1
  179. package/dist/core/middleware/built-in/cookie/core.js.map +1 -1
  180. package/dist/core/middleware/built-in/cookie/hook.js.map +1 -1
  181. package/dist/core/middleware/built-in/cookie/index.js.map +1 -1
  182. package/dist/core/middleware/built-in/cookie/middleware.js.map +1 -1
  183. package/dist/core/middleware/built-in/cors/core.js.map +1 -1
  184. package/dist/core/middleware/built-in/cors/hook.js.map +1 -1
  185. package/dist/core/middleware/built-in/cors/index.js.map +1 -1
  186. package/dist/core/middleware/built-in/cors/middleware.js.map +1 -1
  187. package/dist/core/middleware/built-in/csp/core.js.map +1 -1
  188. package/dist/core/middleware/built-in/csp/hook.js.map +1 -1
  189. package/dist/core/middleware/built-in/csp/index.js.map +1 -1
  190. package/dist/core/middleware/built-in/csp/middleware.js.map +1 -1
  191. package/dist/core/middleware/built-in/csrf/core.js +1 -1
  192. package/dist/core/middleware/built-in/csrf/core.js.map +1 -1
  193. package/dist/core/middleware/built-in/csrf/hook.js.map +1 -1
  194. package/dist/core/middleware/built-in/csrf/index.js.map +1 -1
  195. package/dist/core/middleware/built-in/csrf/middleware.js.map +1 -1
  196. package/dist/core/middleware/built-in/error-tracker/index.js.map +1 -1
  197. package/dist/core/middleware/built-in/error-tracker/middleware.js.map +1 -1
  198. package/dist/core/middleware/built-in/graphql/core.js.map +1 -1
  199. package/dist/core/middleware/built-in/graphql/helpers.js.map +1 -1
  200. package/dist/core/middleware/built-in/graphql/hook.js.map +1 -1
  201. package/dist/core/middleware/built-in/graphql/index.js.map +1 -1
  202. package/dist/core/middleware/built-in/graphql/middleware.js.map +1 -1
  203. package/dist/core/middleware/built-in/graphql/subscriptions.js.map +1 -1
  204. package/dist/core/middleware/built-in/helmet/core.js.map +1 -1
  205. package/dist/core/middleware/built-in/helmet/hook.js.map +1 -1
  206. package/dist/core/middleware/built-in/helmet/index.js.map +1 -1
  207. package/dist/core/middleware/built-in/helmet/middleware.js.map +1 -1
  208. package/dist/core/middleware/built-in/http2/core.js.map +1 -1
  209. package/dist/core/middleware/built-in/http2/hook.js.map +1 -1
  210. package/dist/core/middleware/built-in/http2/index.js.map +1 -1
  211. package/dist/core/middleware/built-in/http2/middleware.js.map +1 -1
  212. package/dist/core/middleware/built-in/index.js.map +1 -1
  213. package/dist/core/middleware/built-in/performance-monitor/index.js.map +1 -1
  214. package/dist/core/middleware/built-in/performance-monitor/middleware.js.map +1 -1
  215. package/dist/core/middleware/built-in/range/core.js +4 -1
  216. package/dist/core/middleware/built-in/range/core.js.map +1 -1
  217. package/dist/core/middleware/built-in/range/hook.js.map +1 -1
  218. package/dist/core/middleware/built-in/range/index.js.map +1 -1
  219. package/dist/core/middleware/built-in/range/middleware.js.map +1 -1
  220. package/dist/core/middleware/built-in/rate-limit/core.js.map +1 -1
  221. package/dist/core/middleware/built-in/rate-limit/hook.js.map +1 -1
  222. package/dist/core/middleware/built-in/rate-limit/index.js.map +1 -1
  223. package/dist/core/middleware/built-in/rate-limit/middleware.js.map +1 -1
  224. package/dist/core/middleware/built-in/request-logger/index.js.map +1 -1
  225. package/dist/core/middleware/built-in/request-logger/middleware.js.map +1 -1
  226. package/dist/core/middleware/built-in/session/core.js +4 -4
  227. package/dist/core/middleware/built-in/session/core.js.map +1 -1
  228. package/dist/core/middleware/built-in/session/hook.js.map +1 -1
  229. package/dist/core/middleware/built-in/session/index.js.map +1 -1
  230. package/dist/core/middleware/built-in/session/middleware.js.map +1 -1
  231. package/dist/core/middleware/built-in/sse/core.js.map +1 -1
  232. package/dist/core/middleware/built-in/sse/hook.js.map +1 -1
  233. package/dist/core/middleware/built-in/sse/index.js.map +1 -1
  234. package/dist/core/middleware/built-in/sse/middleware.js.map +1 -1
  235. package/dist/core/middleware/built-in/static/core.js.map +1 -1
  236. package/dist/core/middleware/built-in/static/hook.js.map +1 -1
  237. package/dist/core/middleware/built-in/static/index.js.map +1 -1
  238. package/dist/core/middleware/built-in/static/middleware.js.map +1 -1
  239. package/dist/core/middleware/built-in/template/core.js +1 -1
  240. package/dist/core/middleware/built-in/template/core.js.map +1 -1
  241. package/dist/core/middleware/built-in/template/hook.js.map +1 -1
  242. package/dist/core/middleware/built-in/template/index.js.map +1 -1
  243. package/dist/core/middleware/built-in/template/middleware.js.map +1 -1
  244. package/dist/core/middleware/built-in/upload/core.js.map +1 -1
  245. package/dist/core/middleware/built-in/upload/hook.js.map +1 -1
  246. package/dist/core/middleware/built-in/upload/index.js.map +1 -1
  247. package/dist/core/middleware/built-in/upload/middleware.js.map +1 -1
  248. package/dist/core/middleware/built-in/validation/core.js.map +1 -1
  249. package/dist/core/middleware/built-in/validation/hook.js.map +1 -1
  250. package/dist/core/middleware/built-in/validation/index.js.map +1 -1
  251. package/dist/core/middleware/built-in/validation/middleware.js.map +1 -1
  252. package/dist/core/middleware/index.js +2 -2
  253. package/dist/core/middleware/index.js.map +1 -1
  254. package/dist/core/modules/auto-discovery.js +11 -7
  255. package/dist/core/modules/auto-discovery.js.map +1 -1
  256. package/dist/core/modules/index.js.map +1 -1
  257. package/dist/core/modules/modules.js +18 -13
  258. package/dist/core/modules/modules.js.map +1 -1
  259. package/dist/core/networking/adapters/engine-adapter.d.ts +23 -0
  260. package/dist/core/networking/adapters/engine-adapter.js +365 -0
  261. package/dist/core/networking/adapters/engine-adapter.js.map +1 -0
  262. package/dist/core/networking/adapters/index.d.ts +1 -0
  263. package/dist/core/networking/adapters/index.js +1 -0
  264. package/dist/core/networking/adapters/index.js.map +1 -1
  265. package/dist/core/networking/adapters/socketio-adapter.d.ts +1 -0
  266. package/dist/core/networking/adapters/socketio-adapter.js +12 -1
  267. package/dist/core/networking/adapters/socketio-adapter.js.map +1 -1
  268. package/dist/core/networking/adapters/uws-adapter.js +27 -16
  269. package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
  270. package/dist/core/networking/adapters/ws-adapter.d.ts +15 -0
  271. package/dist/core/networking/adapters/ws-adapter.js +120 -6
  272. package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
  273. package/dist/core/networking/index.js.map +1 -1
  274. package/dist/core/networking/service-discovery.js +7 -5
  275. package/dist/core/networking/service-discovery.js.map +1 -1
  276. package/dist/core/networking/websocket-adapter.d.ts +22 -3
  277. package/dist/core/networking/websocket-adapter.js.map +1 -1
  278. package/dist/core/networking/websocket-manager.js +70 -67
  279. package/dist/core/networking/websocket-manager.js.map +1 -1
  280. package/dist/core/pooling/object-pool-manager.js +6 -0
  281. package/dist/core/pooling/object-pool-manager.js.map +1 -1
  282. package/dist/core/queue/adapters/bull-adapter.js.map +1 -1
  283. package/dist/core/queue/adapters/index.js.map +1 -1
  284. package/dist/core/queue/adapters/kafka-adapter.js +2 -2
  285. package/dist/core/queue/adapters/kafka-adapter.js.map +1 -1
  286. package/dist/core/queue/adapters/memory-adapter.js +6 -6
  287. package/dist/core/queue/adapters/memory-adapter.js.map +1 -1
  288. package/dist/core/queue/adapters/rabbitmq-adapter.d.ts +50 -0
  289. package/dist/core/queue/adapters/rabbitmq-adapter.js +176 -20
  290. package/dist/core/queue/adapters/rabbitmq-adapter.js.map +1 -1
  291. package/dist/core/queue/adapters/sqs-adapter.d.ts +12 -2
  292. package/dist/core/queue/adapters/sqs-adapter.js +75 -24
  293. package/dist/core/queue/adapters/sqs-adapter.js.map +1 -1
  294. package/dist/core/queue/index.js.map +1 -1
  295. package/dist/core/queue/middleware/index.js.map +1 -1
  296. package/dist/core/queue/middleware/monitoring.d.ts +1 -1
  297. package/dist/core/queue/middleware/monitoring.js.map +1 -1
  298. package/dist/core/queue/middleware/priority.js +2 -1
  299. package/dist/core/queue/middleware/priority.js.map +1 -1
  300. package/dist/core/queue/middleware/rate-limit.js.map +1 -1
  301. package/dist/core/queue/queue-adapter.js.map +1 -1
  302. package/dist/core/queue/queue-manager.js.map +1 -1
  303. package/dist/core/queue/types.d.ts +8 -8
  304. package/dist/core/queue/types.js.map +1 -1
  305. package/dist/core/routing/app-integration.js.map +1 -1
  306. package/dist/core/routing/create-router.js.map +1 -1
  307. package/dist/core/routing/index.js.map +1 -1
  308. package/dist/core/routing/path-matcher.js.map +1 -1
  309. package/dist/core/routing/radix-tree.js.map +1 -1
  310. package/dist/core/routing/router.js.map +1 -1
  311. package/dist/core/routing/unified-router.js +12 -4
  312. package/dist/core/routing/unified-router.js.map +1 -1
  313. package/dist/core/runtime/aws-lambda-adapter.js.map +1 -1
  314. package/dist/core/runtime/base-adapter.js +2 -2
  315. package/dist/core/runtime/base-adapter.js.map +1 -1
  316. package/dist/core/runtime/cloudflare-workers-adapter.js +2 -0
  317. package/dist/core/runtime/cloudflare-workers-adapter.js.map +1 -1
  318. package/dist/core/runtime/index.js.map +1 -1
  319. package/dist/core/runtime/node-adapter.js +19 -17
  320. package/dist/core/runtime/node-adapter.js.map +1 -1
  321. package/dist/core/runtime/vercel-edge-adapter.js +2 -0
  322. package/dist/core/runtime/vercel-edge-adapter.js.map +1 -1
  323. package/dist/core/utilities/circuit-breaker.d.ts +1 -0
  324. package/dist/core/utilities/circuit-breaker.js +25 -2
  325. package/dist/core/utilities/circuit-breaker.js.map +1 -1
  326. package/dist/core/utilities/container.js +9 -9
  327. package/dist/core/utilities/container.js.map +1 -1
  328. package/dist/core/utilities/hooks.js +28 -6
  329. package/dist/core/utilities/hooks.js.map +1 -1
  330. package/dist/core/utilities/index.js.map +1 -1
  331. package/dist/core/utilities/module-path.js.map +1 -1
  332. package/dist/core/utilities/package-utils.d.ts +68 -0
  333. package/dist/core/utilities/package-utils.js +198 -1
  334. package/dist/core/utilities/package-utils.js.map +1 -1
  335. package/dist/core/utilities/response-helpers.js +1 -1
  336. package/dist/core/utilities/response-helpers.js.map +1 -1
  337. package/dist/core/validation/adapters.js.map +1 -1
  338. package/dist/core/validation/error-handler.js.map +1 -1
  339. package/dist/core/validation/index.js.map +1 -1
  340. package/dist/core/validation/schema-interface.js.map +1 -1
  341. package/dist/core/workers/facade.js.map +1 -1
  342. package/dist/core/workers/index.js.map +1 -1
  343. package/dist/core/workers/worker-entry.d.ts +1 -0
  344. package/dist/core/workers/worker-entry.js +12 -0
  345. package/dist/core/workers/worker-entry.js.map +1 -0
  346. package/dist/core/workers/worker-manager.d.ts +23 -2
  347. package/dist/core/workers/worker-manager.js +117 -17
  348. package/dist/core/workers/worker-manager.js.map +1 -1
  349. package/dist/core/workers/worker.js +8 -6
  350. package/dist/core/workers/worker.js.map +1 -1
  351. package/dist/index.d.ts +2 -2
  352. package/dist/index.js +5 -2
  353. package/dist/index.js.map +1 -1
  354. package/dist/moro.d.ts +20 -1
  355. package/dist/moro.js +230 -70
  356. package/dist/moro.js.map +1 -1
  357. package/dist/types/auth.d.ts +4 -4
  358. package/dist/types/auth.js.map +1 -1
  359. package/dist/types/cache.js.map +1 -1
  360. package/dist/types/cdn.js.map +1 -1
  361. package/dist/types/config.d.ts +100 -15
  362. package/dist/types/config.js.map +1 -1
  363. package/dist/types/core.js.map +1 -1
  364. package/dist/types/database.js.map +1 -1
  365. package/dist/types/discovery.js.map +1 -1
  366. package/dist/types/events.js.map +1 -1
  367. package/dist/types/hooks.js.map +1 -1
  368. package/dist/types/http.js.map +1 -1
  369. package/dist/types/logger.js.map +1 -1
  370. package/dist/types/module.js.map +1 -1
  371. package/dist/types/runtime.js.map +1 -1
  372. package/dist/types/session.js.map +1 -1
  373. package/package.json +32 -17
@@ -0,0 +1,1962 @@
1
+ // Moro Engine HTTP Server Implementation (@morojs/engine)
2
+ //
3
+ // One of the three HTTP backends, kept deliberately separate from the others:
4
+ // - MoroHttpServer (http-server.ts) -> Node's http module
5
+ // - UWebSocketsHttpServer (uws-http-server.ts) -> uWebSockets.js (opt-in)
6
+ // - MoroEngineServer (this file) -> @morojs/engine, Moro's own
7
+ // from-scratch native engine
8
+ //
9
+ // Bridges the Moro-shaped native engine API (one JS crossing per request in,
10
+ // one out - see the engine repo's docs/API.md) with Moro's HTTP abstractions.
11
+ // Unlike the uWS adapter there is no materialize() hazard: every per-request
12
+ // fetcher (getHeaders/getQuery/getBody/...) returns a stable snapshot, and the
13
+ // body is already complete when onRequest fires. Keep-alive, Content-Length,
14
+ // Date, chunked encoding, HEAD body suppression and native 413/400 are all
15
+ // engine concerns - this adapter never sees them. No dependency on the uWS
16
+ // adapter: shared helpers live in ./utils.
17
+ import { randomUUID } from 'crypto';
18
+ import { createFrameworkLogger } from '../logger/index.js';
19
+ import { loadNativeEngine, getNativeEngineLoadErrors, } from '../utilities/package-utils.js';
20
+ import { parseMultipart as parseMultipartBuffer, } from './utils/multipart-parser.js';
21
+ import { resolveCompressionSettings, compressBuffer, isCompressible, negotiateEncoding, } from './utils/compression.js';
22
+ import { LazyEventEmitter } from './utils/lazy-event-emitter.js';
23
+ import { parseRawQueryString as parseUwsQueryString } from './utils/query-parser.js';
24
+ import { PathMatcher } from '../routing/path-matcher.js';
25
+ /** Engine method table - onRequest's methodIdx indexes into this */
26
+ const METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'OTHER'];
27
+ // Pre-cached status strings for common codes (sendStatus bodies; the engine
28
+ // derives the wire reason phrase from the numeric status itself)
29
+ const STATUS_STRINGS = new Map([
30
+ [200, '200 OK'],
31
+ [201, '201 Created'],
32
+ [204, '204 No Content'],
33
+ [301, '301 Moved Permanently'],
34
+ [302, '302 Found'],
35
+ [304, '304 Not Modified'],
36
+ [400, '400 Bad Request'],
37
+ [401, '401 Unauthorized'],
38
+ [403, '403 Forbidden'],
39
+ [404, '404 Not Found'],
40
+ [500, '500 Internal Server Error'],
41
+ [502, '502 Bad Gateway'],
42
+ [503, '503 Service Unavailable'],
43
+ ]);
44
+ // Same table as MoroHttpServer.getMimeType - kept sync here so attachment()
45
+ // stays chainable without a fire-and-forget promise
46
+ const MIME_TYPES = {
47
+ '.html': 'text/html',
48
+ '.css': 'text/css',
49
+ '.js': 'application/javascript',
50
+ '.json': 'application/json',
51
+ '.png': 'image/png',
52
+ '.jpg': 'image/jpeg',
53
+ '.jpeg': 'image/jpeg',
54
+ '.gif': 'image/gif',
55
+ '.svg': 'image/svg+xml',
56
+ '.ico': 'image/x-icon',
57
+ '.pdf': 'application/pdf',
58
+ '.txt': 'text/plain',
59
+ '.xml': 'application/xml',
60
+ };
61
+ function mimeTypeFor(ext) {
62
+ return MIME_TYPES[ext.toLowerCase()] || 'application/octet-stream';
63
+ }
64
+ function addCharsetIfNeeded(mimeType) {
65
+ if (mimeType.startsWith('text/') ||
66
+ mimeType.startsWith('application/json') ||
67
+ mimeType.startsWith('application/javascript') ||
68
+ mimeType.startsWith('application/xml') ||
69
+ mimeType.startsWith('image/svg+xml')) {
70
+ if (!mimeType.includes('charset')) {
71
+ return `${mimeType}; charset=utf-8`;
72
+ }
73
+ }
74
+ return mimeType;
75
+ }
76
+ // Files up to this size are sent with a single respond(); larger ones stream
77
+ // through writeHead/write/end with backpressure respected via 'drain'
78
+ const SENDFILE_BUFFER_LIMIT = 1024 * 1024;
79
+ /**
80
+ * Prototype-based request object for the engine adapter.
81
+ *
82
+ * Headers/query/cookies/ip/requestId are lazy: a synchronous handler that
83
+ * never reads them pays nothing (no native crossing, no UUID). Every fetcher
84
+ * returns a stable snapshot and reqIds are safe no-ops after the response
85
+ * terminates, so - unlike the uWS adapter - there is no validity window.
86
+ */
87
+ export class EngineRequest extends LazyEventEmitter {
88
+ method;
89
+ path;
90
+ params = {};
91
+ body = null;
92
+ _server;
93
+ _reqId;
94
+ _url = undefined;
95
+ _queryString = undefined;
96
+ _query = undefined;
97
+ _headers = undefined;
98
+ _cookies = undefined;
99
+ _context = undefined;
100
+ _requestId = undefined;
101
+ _ip = undefined;
102
+ _socket = undefined;
103
+ constructor(server, reqId, method, path) {
104
+ super();
105
+ this._server = server;
106
+ this._reqId = reqId;
107
+ this.method = method;
108
+ this.path = path;
109
+ }
110
+ /** Engine snapshots are stable - nothing to detach. No-op kept for
111
+ * interface parity with UwsRequest so shared dispatch code can call it. */
112
+ materialize() { }
113
+ _rawQueryString() {
114
+ let qs = this._queryString;
115
+ if (qs === undefined) {
116
+ // Safe no-op (undefined) after the response terminates - coerce to ''
117
+ qs = this._server._engine.getQuery(this._reqId) ?? '';
118
+ this._queryString = qs;
119
+ }
120
+ return qs;
121
+ }
122
+ get url() {
123
+ let u = this._url;
124
+ if (u === undefined) {
125
+ const qs = this._rawQueryString();
126
+ u = qs ? `${this.path}?${qs}` : this.path;
127
+ this._url = u;
128
+ }
129
+ return u;
130
+ }
131
+ set url(value) {
132
+ this._url = value;
133
+ }
134
+ get query() {
135
+ let q = this._query;
136
+ if (q === undefined) {
137
+ const qs = this._rawQueryString();
138
+ q = qs ? parseUwsQueryString(qs) : {};
139
+ this._query = q;
140
+ }
141
+ return q;
142
+ }
143
+ set query(value) {
144
+ this._query = value;
145
+ }
146
+ get headers() {
147
+ let h = this._headers;
148
+ if (h === undefined) {
149
+ h = {};
150
+ // One crossing: flat [k1,v1,k2,v2,...] with keys already lowercased.
151
+ // Duplicate header lines are joined like Node's IncomingMessage ('; '
152
+ // for cookie, ', ' otherwise) - last-wins would let a client hide
153
+ // earlier X-Forwarded-For entries from IP-based middleware.
154
+ const flat = this._server._engine.getHeaders(this._reqId);
155
+ if (flat) {
156
+ for (let i = 0; i + 1 < flat.length; i += 2) {
157
+ const key = flat[i];
158
+ const val = flat[i + 1];
159
+ if (key === undefined || val === undefined)
160
+ continue;
161
+ const existing = h[key];
162
+ if (existing === undefined) {
163
+ h[key] = val;
164
+ }
165
+ else {
166
+ h[key] = existing + (key === 'cookie' ? '; ' : ', ') + val;
167
+ }
168
+ }
169
+ }
170
+ this._headers = h;
171
+ }
172
+ return h;
173
+ }
174
+ set headers(value) {
175
+ this._headers = value;
176
+ }
177
+ get cookies() {
178
+ let c = this._cookies;
179
+ if (c === undefined) {
180
+ c = {};
181
+ const header = this.headers.cookie;
182
+ if (header) {
183
+ const parts = header.split(';');
184
+ for (let i = 0; i < parts.length; i++) {
185
+ const part = parts[i];
186
+ if (part === undefined)
187
+ continue;
188
+ const eq = part.indexOf('=');
189
+ if (eq > 0) {
190
+ const name = part.substring(0, eq).trim();
191
+ const value = part.substring(eq + 1);
192
+ if (name && value) {
193
+ try {
194
+ c[name] = decodeURIComponent(value);
195
+ }
196
+ catch {
197
+ c[name] = value;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ this._cookies = c;
204
+ }
205
+ return c;
206
+ }
207
+ set cookies(value) {
208
+ this._cookies = value;
209
+ }
210
+ get context() {
211
+ let ctx = this._context;
212
+ if (ctx === undefined) {
213
+ ctx = {};
214
+ this._context = ctx;
215
+ }
216
+ return ctx;
217
+ }
218
+ set context(value) {
219
+ this._context = value;
220
+ }
221
+ get requestId() {
222
+ let id = this._requestId;
223
+ if (id === undefined) {
224
+ const server = this._server;
225
+ id = server && server.requestTrackingEnabled ? randomUUID() : '';
226
+ this._requestId = id;
227
+ }
228
+ return id;
229
+ }
230
+ set requestId(value) {
231
+ this._requestId = value;
232
+ }
233
+ get ip() {
234
+ let ip = this._ip;
235
+ if (ip === undefined) {
236
+ ip = this._server._engine.getRemoteAddress(this._reqId) ?? '';
237
+ this._ip = ip;
238
+ }
239
+ return ip;
240
+ }
241
+ set ip(value) {
242
+ this._ip = value;
243
+ }
244
+ // Minimal net.Socket-shaped shim. `encrypted` reflects whether the engine
245
+ // is terminating TLS for this server (engine >= 1.2.0); when it isn't and
246
+ // TLS is proxy-terminated, middleware should read `x-forwarded-proto`. This
247
+ // exists so code written against Node's IncomingMessage (req.socket.encrypted,
248
+ // the CSRF middleware; req.connection.remoteAddress, rate-limiters) doesn't
249
+ // throw on the engine transport. `connection` is the legacy alias for `socket`.
250
+ get socket() {
251
+ let s = this._socket;
252
+ if (s === undefined) {
253
+ const getIp = () => this.ip; // arrow captures this lexically (no this-alias)
254
+ const encrypted = Boolean(this._server?.isSsl);
255
+ s = {
256
+ get remoteAddress() {
257
+ return getIp() || undefined;
258
+ },
259
+ remotePort: undefined,
260
+ encrypted,
261
+ destroyed: false,
262
+ };
263
+ this._socket = s;
264
+ }
265
+ return s;
266
+ }
267
+ set socket(value) {
268
+ this._socket = value;
269
+ }
270
+ get connection() {
271
+ return this.socket;
272
+ }
273
+ set connection(value) {
274
+ this._socket = value;
275
+ }
276
+ // ==== Express-compatible request helpers (parity with the uWS adapter) ====
277
+ get hostname() {
278
+ const host = this.headers.host || '';
279
+ return host ? (host.split(':')[0] ?? '') : '';
280
+ }
281
+ get protocol() {
282
+ const forwardedProto = this.headers['x-forwarded-proto'];
283
+ if (forwardedProto)
284
+ return (forwardedProto.split(',')[0] ?? '').trim();
285
+ return this._server && this._server.isSsl ? 'https' : 'http';
286
+ }
287
+ get secure() {
288
+ return this.protocol === 'https';
289
+ }
290
+ get xhr() {
291
+ const xrw = this.headers['x-requested-with'];
292
+ return !!xrw && xrw.toLowerCase() === 'xmlhttprequest';
293
+ }
294
+ get originalUrl() {
295
+ return this.url;
296
+ }
297
+ get ips() {
298
+ const forwardedFor = this.headers['x-forwarded-for'];
299
+ return forwardedFor
300
+ ? forwardedFor
301
+ .split(',')
302
+ .map(s => s.trim())
303
+ .filter(Boolean)
304
+ : [];
305
+ }
306
+ get subdomains() {
307
+ const hostnameParts = this.hostname.split('.');
308
+ return hostnameParts.length > 2 ? hostnameParts.slice(0, -2).reverse() : [];
309
+ }
310
+ get(name) {
311
+ const lower = name.toLowerCase();
312
+ if (lower === 'referer' || lower === 'referrer') {
313
+ return this.headers.referer || this.headers.referrer;
314
+ }
315
+ return this.headers[lower];
316
+ }
317
+ header(name) {
318
+ return this.get(name);
319
+ }
320
+ is(type) {
321
+ const ct = this.headers['content-type'] || '';
322
+ if (!ct)
323
+ return false;
324
+ const mime = (ct.split(';')[0] ?? '').trim().toLowerCase();
325
+ const t = type.toLowerCase();
326
+ if (t.indexOf('/') === -1) {
327
+ return mime.endsWith(`/${t}`) || mime.endsWith(`+${t}`);
328
+ }
329
+ if (t.endsWith('/*')) {
330
+ return mime.startsWith(t.slice(0, -1));
331
+ }
332
+ return mime === t;
333
+ }
334
+ accepts(types) {
335
+ const accept = this.headers.accept || '*/*';
336
+ if (!types)
337
+ return accept;
338
+ const wanted = Array.isArray(types) ? types : [types];
339
+ if (accept === '*/*' || accept === '')
340
+ return wanted[0] || false;
341
+ const acceptTypes = accept.split(',').map(s => (s.split(';')[0] ?? '').trim().toLowerCase());
342
+ for (const w of wanted) {
343
+ const wl = w.toLowerCase();
344
+ const wMime = wl.indexOf('/') === -1 ? `application/${wl}` : wl;
345
+ for (const at of acceptTypes) {
346
+ if (at === '*/*')
347
+ return w;
348
+ if (at === wMime)
349
+ return w;
350
+ if (at.endsWith('/*') && wMime.startsWith(at.slice(0, -1)))
351
+ return w;
352
+ }
353
+ }
354
+ return false;
355
+ }
356
+ acceptsLanguages(langs) {
357
+ const acceptLang = this.headers['accept-language'] || '';
358
+ if (!langs)
359
+ return acceptLang || false;
360
+ const wanted = Array.isArray(langs) ? langs : [langs];
361
+ if (!acceptLang)
362
+ return wanted[0] || false;
363
+ const accepted = acceptLang
364
+ .split(',')
365
+ .map(s => {
366
+ const parts = s.trim().split(';');
367
+ const tag = (parts[0] ?? '').toLowerCase();
368
+ const qPart = parts.find(p => p.trim().startsWith('q='));
369
+ const q = qPart ? parseFloat(qPart.trim().slice(2)) : 1;
370
+ return { tag, q: isNaN(q) ? 0 : q };
371
+ })
372
+ .filter(a => a.q > 0)
373
+ .sort((a, b) => b.q - a.q);
374
+ for (const { tag } of accepted) {
375
+ for (const w of wanted) {
376
+ const wl = w.toLowerCase();
377
+ if (tag === '*' || tag === wl || tag.split('-')[0] === wl.split('-')[0])
378
+ return w;
379
+ }
380
+ }
381
+ return false;
382
+ }
383
+ }
384
+ /**
385
+ * Prototype-based response object for the engine adapter. Same
386
+ * MoroResponseMethods surface as the uWS adapter's ResponsePrototype.
387
+ *
388
+ * Terminal writes (json/send/end/redirect) go out as ONE respond() call -
389
+ * status + headers + body in a single native crossing. Streaming responses
390
+ * (writeHead/write/end) map to the engine's streaming API; the engine's
391
+ * onWritable backpressure signal surfaces as the 'drain' event.
392
+ */
393
+ export class EngineResponse extends LazyEventEmitter {
394
+ headersSent = false;
395
+ statusCode = 200;
396
+ responseHeaders = {};
397
+ locals = {};
398
+ // The EngineRequest wrapper, linked by the server so lifecycle events reach
399
+ // req.on('close') listeners (SSE cleanup, monitors)
400
+ _moroReq = null;
401
+ _server;
402
+ _engine;
403
+ _reqId;
404
+ _logger;
405
+ // Terminal-write latch: headersSent means "head flushed" (true mid-stream
406
+ // after writeHead/write), _ended means the body is complete
407
+ _ended = false;
408
+ _aborted = false;
409
+ constructor(server, reqId, logger) {
410
+ super();
411
+ this._server = server;
412
+ this._engine = server._engine;
413
+ this._reqId = reqId;
414
+ this._logger = logger;
415
+ }
416
+ get writableEnded() {
417
+ return this._ended;
418
+ }
419
+ get writableFinished() {
420
+ return this._ended;
421
+ }
422
+ get finished() {
423
+ return this._ended;
424
+ }
425
+ get aborted() {
426
+ return this._aborted;
427
+ }
428
+ // Node ServerResponse lifecycle: 'finish' then 'close' after the terminal
429
+ // write; the request wrapper's 'close' fires too (IncomingMessage parity).
430
+ // Also releases the reqId from the server's in-flight map.
431
+ _emitDone() {
432
+ this._ended = true;
433
+ this._server._complete(this._reqId);
434
+ if (this._events) {
435
+ this.emit('finish');
436
+ this.emit('close');
437
+ }
438
+ const req = this._moroReq;
439
+ if (req && req._events)
440
+ req.emit('close');
441
+ }
442
+ // Client aborted: 'close' without 'finish' (Node semantics), plus
443
+ // 'aborted'/'close' on the request wrapper. All later engine calls with
444
+ // this reqId are safe no-ops, but the guards below skip them anyway.
445
+ _handleAbort() {
446
+ if (this._ended)
447
+ return;
448
+ this._ended = true;
449
+ this._aborted = true;
450
+ if (this._events)
451
+ this.emit('close');
452
+ const req = this._moroReq;
453
+ if (req && req._events) {
454
+ req.emit('aborted');
455
+ req.emit('close');
456
+ }
457
+ }
458
+ // Backpressure: the engine's onWritable relayed as the 'drain' event that
459
+ // pipe() and manual streaming loops wait on
460
+ _handleWritable() {
461
+ this.emit('drain');
462
+ }
463
+ // Build the engine's flat [k1,v1,k2,v2,...] header array from headers
464
+ // actually set. Multi-value headers (set-cookie) become separate pairs so
465
+ // each goes out as its own header line.
466
+ _headersFlat() {
467
+ const headers = this.responseHeaders;
468
+ let flat = null;
469
+ // Performance: for...in is the fastest way to iterate response headers
470
+ for (const key in headers) {
471
+ const value = headers[key];
472
+ if (flat === null)
473
+ flat = [];
474
+ if (Array.isArray(value)) {
475
+ for (let i = 0; i < value.length; i++)
476
+ flat.push(key, String(value[i]));
477
+ }
478
+ else {
479
+ flat.push(key, String(value));
480
+ }
481
+ }
482
+ return flat;
483
+ }
484
+ status(code) {
485
+ this.statusCode = code;
486
+ return this;
487
+ }
488
+ setHeader(name, value) {
489
+ // Cache toLowerCase result to avoid repeated calls
490
+ const lowerName = name.toLowerCase();
491
+ this.responseHeaders[lowerName] = value;
492
+ return this;
493
+ }
494
+ getHeader(name) {
495
+ const lowerName = name.toLowerCase();
496
+ return this.responseHeaders[lowerName];
497
+ }
498
+ removeHeader(name) {
499
+ const lowerName = name.toLowerCase();
500
+ if (lowerName in this.responseHeaders) {
501
+ delete this.responseHeaders[lowerName];
502
+ }
503
+ return this;
504
+ }
505
+ json(data) {
506
+ if (this.headersSent || this._ended)
507
+ return;
508
+ // Fast-path JSON serialization for common API patterns
509
+ let body;
510
+ // The interpolated fast path is only valid when `success` is a real boolean
511
+ // (a string/number would produce invalid JSON like {"success":yes,...});
512
+ // otherwise fall through to JSON.stringify.
513
+ if (data && typeof data === 'object' && typeof data.success === 'boolean') {
514
+ let keyCount = 0;
515
+ let hasData = false;
516
+ let hasError = false;
517
+ let hasTotal = false;
518
+ for (const key in data) {
519
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
520
+ keyCount++;
521
+ if (key === 'data')
522
+ hasData = true;
523
+ else if (key === 'error')
524
+ hasError = true;
525
+ else if (key === 'total')
526
+ hasTotal = true;
527
+ }
528
+ }
529
+ // JSON.stringify(undefined) yields the literal token "undefined"; guard the
530
+ // interpolated branches so an explicit `data: undefined` doesn't corrupt
531
+ // the body (fall through to JSON.stringify, which omits the key instead).
532
+ if (keyCount === 3 &&
533
+ hasData &&
534
+ hasError &&
535
+ data.data !== undefined &&
536
+ data.error !== undefined) {
537
+ body = `{"success":${data.success},"data":${JSON.stringify(data.data)},"error":${JSON.stringify(data.error)}}`;
538
+ }
539
+ else if (keyCount === 3 &&
540
+ hasData &&
541
+ hasTotal &&
542
+ data.data !== undefined &&
543
+ typeof data.total === 'number') {
544
+ body = `{"success":${data.success},"data":${JSON.stringify(data.data)},"total":${data.total}}`;
545
+ }
546
+ else if (keyCount === 2 && hasData && data.data !== undefined) {
547
+ body = `{"success":${data.success},"data":${JSON.stringify(data.data)}}`;
548
+ }
549
+ else if (keyCount === 2 && hasError && data.error !== undefined) {
550
+ body = `{"success":${data.success},"error":${JSON.stringify(data.error)}}`;
551
+ }
552
+ else {
553
+ body = JSON.stringify(data);
554
+ }
555
+ }
556
+ else {
557
+ body = JSON.stringify(data);
558
+ }
559
+ if (!('content-type' in this.responseHeaders)) {
560
+ this.responseHeaders['content-type'] = 'application/json';
561
+ }
562
+ try {
563
+ this._respondMaybeCompressed(body, 'application/json');
564
+ }
565
+ catch (err) {
566
+ this._failSafe('Failed to send JSON response', err);
567
+ }
568
+ }
569
+ // Buffered-response compression parity with the Node path. Terminal helper:
570
+ // respond() with `body`, compressing first when enabled + compressible +
571
+ // over threshold + the client accepts an encoding. Keeps the synchronous
572
+ // fast path (zero extra work) whenever compression can't apply, so the
573
+ // default (compression off) pays nothing.
574
+ _respondMaybeCompressed(body, contentType) {
575
+ const s = this._server._compression;
576
+ // HOT PATH: compression is off by default - bail before ANY per-response
577
+ // work (byteLength scans the string; header reads allocate nothing but
578
+ // still cost). The default config must pay zero for this feature.
579
+ if (!s.enabled) {
580
+ this._engine.respond(this._reqId, this.statusCode, this._headersFlat(), body);
581
+ this.headersSent = true;
582
+ this._emitDone();
583
+ return;
584
+ }
585
+ const bytes = typeof body === 'string' ? Buffer.byteLength(body) : body.length;
586
+ const alreadyEncoded = 'content-encoding' in this.responseHeaders;
587
+ const ctHeader = this.responseHeaders['content-type'];
588
+ const ct = contentType ?? (Array.isArray(ctHeader) ? ctHeader[0] : ctHeader);
589
+ if (alreadyEncoded ||
590
+ bytes < s.threshold ||
591
+ !isCompressible(ct) ||
592
+ this.statusCode === 204 ||
593
+ this.statusCode === 304) {
594
+ this._engine.respond(this._reqId, this.statusCode, this._headersFlat(), body);
595
+ this.headersSent = true;
596
+ this._emitDone();
597
+ return;
598
+ }
599
+ const accept = this._engine.getHeader(this._reqId, 'accept-encoding');
600
+ const encoding = negotiateEncoding(accept, s.encodings);
601
+ if (!encoding) {
602
+ this._engine.respond(this._reqId, this.statusCode, this._headersFlat(), body);
603
+ this.headersSent = true;
604
+ this._emitDone();
605
+ return;
606
+ }
607
+ // Async path: compress, then respond (guarding against an abort that
608
+ // raced the await - the reqId would be invalid).
609
+ void compressBuffer(body, encoding, s.level)
610
+ .then(compressed => {
611
+ if (this._ended || this._engine.isAborted(this._reqId))
612
+ return;
613
+ this.responseHeaders['content-encoding'] = encoding;
614
+ const vary = this.responseHeaders['vary'];
615
+ this.responseHeaders['vary'] = vary ? `${vary}, Accept-Encoding` : 'Accept-Encoding';
616
+ this._engine.respond(this._reqId, this.statusCode, this._headersFlat(), compressed);
617
+ this.headersSent = true;
618
+ this._emitDone();
619
+ })
620
+ .catch(err => this._failSafe('Failed to compress response', err));
621
+ }
622
+ // A response-send failure must never leave the request hanging open (the
623
+ // sweep exempts active requests, so nothing else would ever close it):
624
+ // attempt a plain 500, then release the wrapper either way.
625
+ _failSafe(context, err) {
626
+ this._logger.error(`${context}: ${err instanceof Error ? err.message : String(err)}`, 'ResponseError');
627
+ if (!this._ended) {
628
+ try {
629
+ this._engine.respond(this._reqId, 500, null, '{"success":false,"error":"Internal server error"}');
630
+ this.headersSent = true;
631
+ }
632
+ catch {
633
+ // reqId already gone (aborted/finished) - nothing to send
634
+ }
635
+ this._emitDone();
636
+ }
637
+ }
638
+ send(data) {
639
+ if (this.headersSent || this._ended)
640
+ return;
641
+ // Default a Content-Type to match the Node server (parity for the now-
642
+ // default engine path): JSON for JSON-looking strings, octet-stream for
643
+ // Buffers, text/plain otherwise.
644
+ if (!('content-type' in this.responseHeaders)) {
645
+ if (typeof data === 'string') {
646
+ const t = data.trimStart();
647
+ this.responseHeaders['content-type'] =
648
+ t.startsWith('{') || t.startsWith('[')
649
+ ? 'application/json; charset=utf-8'
650
+ : 'text/plain; charset=utf-8';
651
+ }
652
+ else {
653
+ this.responseHeaders['content-type'] = 'application/octet-stream';
654
+ }
655
+ }
656
+ try {
657
+ // Buffers pass through binary-safe (the engine accepts Uint8Array).
658
+ // Compression applies only to compressible content types (isCompressible
659
+ // excludes octet-stream), so binary sends stay on the sync fast path.
660
+ this._respondMaybeCompressed(data);
661
+ // headersSent/_emitDone handled inside the helper.
662
+ }
663
+ catch (err) {
664
+ this._failSafe('Failed to send response', err);
665
+ }
666
+ }
667
+ // Flush status + headers without ending the response (Node streaming
668
+ // entry point - SSE and manual chunked responses start here). Supports
669
+ // both (code, headers) and (code, statusMessage, headers) signatures;
670
+ // the engine derives the reason phrase from the status code, so a custom
671
+ // message cannot be sent on the wire and is ignored.
672
+ writeHead(statusCode, reasonOrHeaders, maybeHeaders) {
673
+ if (this.headersSent || this._ended)
674
+ return this;
675
+ this.statusCode = statusCode;
676
+ const headers = reasonOrHeaders && typeof reasonOrHeaders === 'object' ? reasonOrHeaders : maybeHeaders;
677
+ if (headers) {
678
+ for (const key of Object.keys(headers)) {
679
+ this.setHeader(key, headers[key]);
680
+ }
681
+ }
682
+ try {
683
+ this._engine.writeHead(this._reqId, statusCode, this._headersFlat());
684
+ this.headersSent = true;
685
+ }
686
+ catch {
687
+ this._logger.error('Failed to write response head', 'ResponseError');
688
+ }
689
+ return this;
690
+ }
691
+ // Stream a body chunk, flushing the head first if needed. Returns the
692
+ // engine's backpressure signal like Node's Writable.write; 'drain' fires
693
+ // (via onWritable) when the socket can accept more.
694
+ write(chunk, encoding, callback) {
695
+ if (typeof encoding === 'function') {
696
+ callback = encoding;
697
+ encoding = undefined;
698
+ }
699
+ if (this._ended)
700
+ return false;
701
+ let ok = true;
702
+ try {
703
+ if (!this.headersSent) {
704
+ this._engine.writeHead(this._reqId, this.statusCode, this._headersFlat());
705
+ this.headersSent = true;
706
+ }
707
+ ok = this._engine.write(this._reqId, chunk) !== false;
708
+ }
709
+ catch {
710
+ this._logger.error('Failed to write response chunk', 'ResponseError');
711
+ return false;
712
+ }
713
+ if (typeof callback === 'function')
714
+ callback();
715
+ return ok;
716
+ }
717
+ end(data, encoding, callback) {
718
+ if (typeof encoding === 'function') {
719
+ callback = encoding;
720
+ encoding = undefined;
721
+ }
722
+ // Guard on _ended, not headersSent: in streaming mode (writeHead/write)
723
+ // the head is already flushed and end() must still complete the body
724
+ if (this._ended) {
725
+ if (typeof callback === 'function')
726
+ callback();
727
+ return this;
728
+ }
729
+ try {
730
+ if (!this.headersSent) {
731
+ // Terminal single-shot: status + headers + body in one native call
732
+ this._engine.respond(this._reqId, this.statusCode, this._headersFlat(), data !== undefined && data !== null ? data : null);
733
+ this.headersSent = true;
734
+ }
735
+ else {
736
+ this._engine.end(this._reqId, data !== undefined && data !== null ? data : undefined);
737
+ }
738
+ this._emitDone();
739
+ if (typeof callback === 'function')
740
+ callback();
741
+ }
742
+ catch (err) {
743
+ this._failSafe('Failed to end response', err);
744
+ if (typeof callback === 'function')
745
+ callback();
746
+ }
747
+ return this;
748
+ }
749
+ redirect(url, code) {
750
+ if (this.headersSent || this._ended)
751
+ return;
752
+ const redirectCode = code || 302;
753
+ this.statusCode = redirectCode;
754
+ this.setHeader('location', url.replace(/[\r\n]/g, ''));
755
+ try {
756
+ this._engine.respond(this._reqId, redirectCode, this._headersFlat(), null);
757
+ this.headersSent = true;
758
+ this._emitDone();
759
+ }
760
+ catch (err) {
761
+ this._failSafe('Failed to send redirect', err);
762
+ }
763
+ }
764
+ // Standardized response helpers
765
+ success(data, message) {
766
+ const response = {
767
+ success: true,
768
+ data,
769
+ };
770
+ if (message !== undefined) {
771
+ response.message = message;
772
+ }
773
+ this.json(response);
774
+ }
775
+ error(error, code, message) {
776
+ const response = {
777
+ success: false,
778
+ error,
779
+ };
780
+ if (code !== undefined) {
781
+ response.code = code;
782
+ }
783
+ if (message !== undefined) {
784
+ response.message = message;
785
+ }
786
+ this.json(response);
787
+ }
788
+ // Common HTTP error helpers (automatically set status code)
789
+ unauthorized(message = 'Authentication required') {
790
+ this.statusCode = 401;
791
+ this.json({
792
+ success: false,
793
+ error: 'Unauthorized',
794
+ code: 'UNAUTHORIZED',
795
+ message,
796
+ });
797
+ }
798
+ forbidden(message = 'Insufficient permissions') {
799
+ this.statusCode = 403;
800
+ this.json({
801
+ success: false,
802
+ error: 'Forbidden',
803
+ code: 'FORBIDDEN',
804
+ message,
805
+ });
806
+ }
807
+ notFound(resource = 'Resource') {
808
+ this.statusCode = 404;
809
+ this.json({
810
+ success: false,
811
+ error: 'Not Found',
812
+ code: 'NOT_FOUND',
813
+ message: `${resource} not found`,
814
+ });
815
+ }
816
+ badRequest(message = 'Invalid request') {
817
+ this.statusCode = 400;
818
+ this.json({
819
+ success: false,
820
+ error: 'Bad Request',
821
+ code: 'BAD_REQUEST',
822
+ message,
823
+ });
824
+ }
825
+ conflict(message) {
826
+ this.statusCode = 409;
827
+ this.json({
828
+ success: false,
829
+ error: 'Conflict',
830
+ code: 'CONFLICT',
831
+ message,
832
+ });
833
+ }
834
+ internalError(message = 'Internal server error') {
835
+ this.statusCode = 500;
836
+ this.json({
837
+ success: false,
838
+ error: 'Internal Server Error',
839
+ code: 'INTERNAL_ERROR',
840
+ message,
841
+ });
842
+ }
843
+ validationError(errors) {
844
+ this.statusCode = 422;
845
+ this.json({
846
+ success: false,
847
+ error: 'Validation Failed',
848
+ code: 'VALIDATION_ERROR',
849
+ errors,
850
+ });
851
+ }
852
+ rateLimited(retryAfter) {
853
+ this.statusCode = 429;
854
+ if (retryAfter) {
855
+ this.setHeader('Retry-After', retryAfter.toString());
856
+ }
857
+ this.json({
858
+ success: false,
859
+ error: 'Rate Limit Exceeded',
860
+ code: 'RATE_LIMITED',
861
+ message: retryAfter
862
+ ? `Too many requests. Retry after ${retryAfter} seconds.`
863
+ : 'Too many requests',
864
+ retryAfter,
865
+ });
866
+ }
867
+ // Common success patterns
868
+ created(data, location) {
869
+ this.statusCode = 201;
870
+ if (location) {
871
+ this.setHeader('Location', location);
872
+ }
873
+ this.json({
874
+ success: true,
875
+ data,
876
+ });
877
+ }
878
+ noContent() {
879
+ this.statusCode = 204;
880
+ this.end();
881
+ }
882
+ paginated(data, pagination) {
883
+ const totalPages = Math.ceil(pagination.total / pagination.limit);
884
+ this.json({
885
+ success: true,
886
+ data,
887
+ pagination: {
888
+ page: pagination.page,
889
+ limit: pagination.limit,
890
+ total: pagination.total,
891
+ totalPages,
892
+ hasNext: pagination.page < totalPages,
893
+ hasPrev: pagination.page > 1,
894
+ },
895
+ });
896
+ }
897
+ cookie(name, value, options) {
898
+ // Percent-encode the value (matches the Node path and Express): an
899
+ // unencoded value containing ';' or ',' would inject cookie attributes, and
900
+ // control chars would make the native header validator silently drop the
901
+ // whole Set-Cookie header.
902
+ const parts = [name, '=', encodeURIComponent(value)];
903
+ if (options) {
904
+ // maxAge: 0 is meaningful (immediate expiry - clearCookie relies on it)
905
+ if (options.maxAge !== undefined && options.maxAge !== null) {
906
+ parts.push('; Max-Age=', String(options.maxAge));
907
+ }
908
+ if (options.expires) {
909
+ const expires = options.expires instanceof Date ? options.expires.toUTCString() : String(options.expires);
910
+ parts.push('; Expires=', expires);
911
+ }
912
+ if (options.domain) {
913
+ parts.push('; Domain=', options.domain);
914
+ }
915
+ if (options.secure) {
916
+ parts.push('; Secure');
917
+ }
918
+ if (options.httpOnly) {
919
+ parts.push('; HttpOnly');
920
+ }
921
+ if (options.sameSite) {
922
+ parts.push('; SameSite=', options.sameSite);
923
+ }
924
+ }
925
+ // Path defaults to '/' (Express behavior): without it the browser scopes
926
+ // the cookie to the current URL's directory, so a clearCookie() from
927
+ // /auth/logout would miss a cookie originally set at Path=/.
928
+ parts.push('; Path=', options?.path ?? '/');
929
+ const cookie = parts.join('');
930
+ const lowerKey = 'set-cookie';
931
+ const existing = this.responseHeaders[lowerKey];
932
+ if (existing) {
933
+ if (Array.isArray(existing)) {
934
+ existing.push(cookie);
935
+ }
936
+ else {
937
+ this.responseHeaders[lowerKey] = [existing, cookie];
938
+ }
939
+ }
940
+ else {
941
+ this.responseHeaders[lowerKey] = cookie;
942
+ }
943
+ return this;
944
+ }
945
+ clearCookie(name, options = {}) {
946
+ const clearOptions = { expires: new Date(0), maxAge: 0 };
947
+ if (options.path !== undefined)
948
+ clearOptions.path = options.path;
949
+ if (options.domain !== undefined)
950
+ clearOptions.domain = options.domain;
951
+ if (options.httpOnly !== undefined)
952
+ clearOptions.httpOnly = options.httpOnly;
953
+ if (options.secure !== undefined)
954
+ clearOptions.secure = options.secure;
955
+ if (options.sameSite !== undefined)
956
+ clearOptions.sameSite = options.sameSite;
957
+ return this.cookie(name, '', clearOptions);
958
+ }
959
+ // ==== File responses ====
960
+ // Small files go out in one respond(); larger files stream with
961
+ // backpressure ('drain' via the engine's onWritable)
962
+ async sendFile(filePath) {
963
+ if (this.headersSent || this._ended)
964
+ return;
965
+ let streamRef = null; // read stream, so the catch can tear it down
966
+ try {
967
+ const fs = await import('fs');
968
+ const path = await import('path');
969
+ const stats = await fs.promises.stat(filePath);
970
+ const contentType = addCharsetIfNeeded(mimeTypeFor(path.extname(filePath)));
971
+ if (!('content-type' in this.responseHeaders)) {
972
+ this.setHeader('Content-Type', contentType);
973
+ }
974
+ this.setHeader('X-Content-Type-Options', 'nosniff');
975
+ this.setHeader('Last-Modified', stats.mtime.toUTCString());
976
+ this.setHeader('Cache-Control', 'public, max-age=31536000');
977
+ if (stats.size <= SENDFILE_BUFFER_LIMIT) {
978
+ const data = await fs.promises.readFile(filePath);
979
+ this.end(data);
980
+ return;
981
+ }
982
+ // Explicit length so the engine skips chunked encoding for the stream.
983
+ // (If the file shrinks mid-stream, the engine detects the short write at
984
+ // end() and forces Connection: close - the client sees truncation, never
985
+ // the next response's bytes as this body.)
986
+ this.setHeader('Content-Length', String(stats.size));
987
+ await new Promise((resolve, reject) => {
988
+ const stream = fs.createReadStream(filePath);
989
+ streamRef = stream;
990
+ let pendingResume = null;
991
+ // ONE close listener for the stream's whole lifetime (not one per
992
+ // backpressure pause), and it SETTLES the promise: a client abort
993
+ // while the stream is paused must not leave `await sendFile()`
994
+ // hanging forever.
995
+ const onClose = () => {
996
+ if (pendingResume)
997
+ this.removeListener('drain', pendingResume);
998
+ stream.destroy();
999
+ resolve();
1000
+ };
1001
+ this.once('close', onClose);
1002
+ const cleanup = () => {
1003
+ this.removeListener('close', onClose);
1004
+ if (pendingResume)
1005
+ this.removeListener('drain', pendingResume);
1006
+ };
1007
+ stream.on('data', chunk => {
1008
+ if (this._ended) {
1009
+ cleanup();
1010
+ stream.destroy();
1011
+ resolve();
1012
+ return;
1013
+ }
1014
+ if (!this.write(chunk)) {
1015
+ stream.pause();
1016
+ pendingResume = () => {
1017
+ pendingResume = null;
1018
+ stream.resume();
1019
+ };
1020
+ this.once('drain', pendingResume);
1021
+ }
1022
+ });
1023
+ stream.on('end', () => {
1024
+ cleanup();
1025
+ this.end();
1026
+ resolve();
1027
+ });
1028
+ stream.on('error', (err) => {
1029
+ cleanup();
1030
+ reject(err);
1031
+ });
1032
+ });
1033
+ }
1034
+ catch {
1035
+ // The head may already be on the wire (streaming branch). Terminate the
1036
+ // response so the client isn't left waiting on the declared
1037
+ // Content-Length, and tear down the read stream; only 404 if nothing was
1038
+ // sent yet.
1039
+ if (streamRef)
1040
+ streamRef.destroy();
1041
+ if (this.headersSent && !this._ended) {
1042
+ this.end();
1043
+ }
1044
+ else if (!this.headersSent && !this._ended) {
1045
+ this.status(404).json({ success: false, error: 'File not found' });
1046
+ }
1047
+ }
1048
+ }
1049
+ attachment(filename) {
1050
+ if (this.headersSent)
1051
+ return this;
1052
+ if (filename) {
1053
+ const safeName = filename.replace(/[\r\n"]/g, '');
1054
+ this.setHeader('Content-Disposition', `attachment; filename="${safeName}"`);
1055
+ if (!this.getHeader('Content-Type')) {
1056
+ const dot = filename.lastIndexOf('.');
1057
+ if (dot !== -1) {
1058
+ this.setHeader('Content-Type', mimeTypeFor(filename.substring(dot)));
1059
+ }
1060
+ }
1061
+ }
1062
+ else {
1063
+ this.setHeader('Content-Disposition', 'attachment');
1064
+ }
1065
+ return this;
1066
+ }
1067
+ async download(filePath, filename) {
1068
+ this.attachment(filename || filePath.split('/').pop());
1069
+ return this.sendFile(filePath);
1070
+ }
1071
+ format(handlers) {
1072
+ const types = Object.keys(handlers).filter(k => k !== 'default');
1073
+ const matched = this._moroReq ? this._moroReq.accepts(types) : false;
1074
+ const chosen = matched ? handlers[matched] : handlers.default;
1075
+ if (!chosen) {
1076
+ this.statusCode = 406;
1077
+ this.setHeader('Content-Type', 'text/plain; charset=utf-8');
1078
+ this.end('Not Acceptable');
1079
+ return;
1080
+ }
1081
+ this.vary('Accept');
1082
+ if (matched && typeof matched === 'string') {
1083
+ this.type(matched);
1084
+ }
1085
+ chosen();
1086
+ }
1087
+ // ==== Express-compatible response helpers (parity with the uWS adapter) ====
1088
+ set(field, value) {
1089
+ if (this.headersSent)
1090
+ return this;
1091
+ if (typeof field === 'string') {
1092
+ if (value !== undefined)
1093
+ this.setHeader(field, value);
1094
+ }
1095
+ else {
1096
+ for (const key in field) {
1097
+ this.setHeader(key, field[key]);
1098
+ }
1099
+ }
1100
+ return this;
1101
+ }
1102
+ get(field) {
1103
+ return this.getHeader(field);
1104
+ }
1105
+ hasHeader(name) {
1106
+ return this.getHeader(name) !== undefined;
1107
+ }
1108
+ setBulkHeaders(headers) {
1109
+ if (this.headersSent)
1110
+ return this;
1111
+ for (const key in headers) {
1112
+ this.setHeader(key, String(headers[key]));
1113
+ }
1114
+ return this;
1115
+ }
1116
+ appendHeader(name, value) {
1117
+ if (this.headersSent)
1118
+ return this;
1119
+ const lower = name.toLowerCase();
1120
+ const existing = this.responseHeaders[lower];
1121
+ if (existing) {
1122
+ const values = Array.isArray(existing) ? existing : [existing];
1123
+ const incoming = Array.isArray(value) ? value : [value];
1124
+ this.responseHeaders[lower] = values.concat(incoming);
1125
+ }
1126
+ else {
1127
+ this.responseHeaders[lower] = value;
1128
+ }
1129
+ return this;
1130
+ }
1131
+ append(field, value) {
1132
+ return this.appendHeader(field, value);
1133
+ }
1134
+ type(contentType) {
1135
+ if (this.headersSent)
1136
+ return this;
1137
+ let ct = contentType;
1138
+ if (ct.indexOf('/') === -1) {
1139
+ const shorthands = {
1140
+ json: 'application/json; charset=utf-8',
1141
+ html: 'text/html; charset=utf-8',
1142
+ text: 'text/plain; charset=utf-8',
1143
+ txt: 'text/plain; charset=utf-8',
1144
+ xml: 'application/xml',
1145
+ js: 'application/javascript',
1146
+ css: 'text/css; charset=utf-8',
1147
+ form: 'application/x-www-form-urlencoded',
1148
+ };
1149
+ ct = shorthands[ct.toLowerCase()] || `application/${ct}`;
1150
+ }
1151
+ this.setHeader('Content-Type', ct);
1152
+ return this;
1153
+ }
1154
+ sendStatus(code) {
1155
+ if (this.headersSent)
1156
+ return;
1157
+ this.statusCode = code;
1158
+ // Reason phrase when the code is known, otherwise the code itself
1159
+ // (Node behavior) - never a fabricated "OK" for a non-2xx status.
1160
+ const statusString = STATUS_STRINGS.get(code);
1161
+ const body = statusString ? statusString.slice(String(code).length + 1) : String(code);
1162
+ this.setHeader('Content-Type', 'text/plain; charset=utf-8');
1163
+ this.end(body);
1164
+ }
1165
+ location(url) {
1166
+ if (this.headersSent)
1167
+ return this;
1168
+ this.setHeader('Location', url.replace(/[\r\n]/g, ''));
1169
+ return this;
1170
+ }
1171
+ vary(field) {
1172
+ if (this.headersSent)
1173
+ return this;
1174
+ const existing = this.responseHeaders['vary'];
1175
+ const existingList = existing
1176
+ ? String(existing)
1177
+ .split(',')
1178
+ .map(s => s.trim())
1179
+ .filter(Boolean)
1180
+ : [];
1181
+ const incoming = Array.isArray(field) ? field : [field];
1182
+ for (const f of incoming) {
1183
+ if (!existingList.some(e => e.toLowerCase() === f.toLowerCase())) {
1184
+ existingList.push(f);
1185
+ }
1186
+ }
1187
+ this.responseHeaders['vary'] = existingList.join(', ');
1188
+ return this;
1189
+ }
1190
+ links(links) {
1191
+ if (this.headersSent)
1192
+ return this;
1193
+ const parts = [];
1194
+ for (const rel in links) {
1195
+ parts.push(`<${links[rel]}>; rel="${rel}"`);
1196
+ }
1197
+ if (parts.length > 0) {
1198
+ const existing = this.responseHeaders['link'];
1199
+ this.responseHeaders['link'] = existing
1200
+ ? `${existing}, ${parts.join(', ')}`
1201
+ : parts.join(', ');
1202
+ }
1203
+ return this;
1204
+ }
1205
+ canSetHeaders() {
1206
+ return !this.headersSent;
1207
+ }
1208
+ getResponseState() {
1209
+ return {
1210
+ headersSent: this.headersSent,
1211
+ statusCode: this.statusCode,
1212
+ headers: this.responseHeaders,
1213
+ finished: this._ended,
1214
+ writable: !this._ended,
1215
+ };
1216
+ }
1217
+ }
1218
+ /**
1219
+ * Moro Engine HTTP Server Adapter
1220
+ * Bridges @morojs/engine with Moro's HTTP abstractions. Duck-typed to the
1221
+ * same surface framework.ts/moro.ts use on UWebSocketsHttpServer.
1222
+ */
1223
+ export class MoroEngineServer {
1224
+ /** @internal read by EngineRequest/EngineResponse for lazy fetchers */
1225
+ _engine;
1226
+ /** @internal read by EngineRequest#protocol/socket.encrypted - true when the
1227
+ * engine is terminating TLS for this server (engine >= 1.2.0 + ssl passed) */
1228
+ isSsl = false;
1229
+ serverId;
1230
+ globalMiddleware = [];
1231
+ logger = createFrameworkLogger('EngineHttpServer');
1232
+ hookManager;
1233
+ requestCounter = 0;
1234
+ requestTrackingEnabled = true; // Generate request IDs (read lazily by EngineRequest)
1235
+ isListening = false;
1236
+ port;
1237
+ host;
1238
+ // In-flight responses by reqId so onAborted/onWritable reach the wrappers;
1239
+ // entries are dropped on terminal writes (_complete) and aborts
1240
+ inflight = new Map();
1241
+ // Small stable handle exposed via getServer()/getApp() (observability only)
1242
+ handle;
1243
+ // Set by the engine WebSocket adapter when WS support is enabled; when
1244
+ // present, upgrade requests are routed to the engine's native WS path.
1245
+ // onOpen receives the handshake request's remote address and headers -
1246
+ // captured before the upgrade invalidates the reqId - so IP-based WS
1247
+ // rate limiting and header/token connection auth see real values.
1248
+ _wsBridge;
1249
+ // Handshake info for the upgrade currently in flight: set just before
1250
+ // upgradeToWebSocket() (which fires onWsOpen synchronously), consumed there.
1251
+ _pendingWsInfo;
1252
+ // Body size limits (bytes) - configured from server.bodySizeLimit/maxUploadSize
1253
+ maxBodySize = 10 * 1024 * 1024;
1254
+ maxUploadSize = 100 * 1024 * 1024;
1255
+ reusePort = false;
1256
+ _capabilities;
1257
+ _limits;
1258
+ _ssl;
1259
+ _http2Settings;
1260
+ multipartLimits;
1261
+ /** @internal read by EngineResponse for buffered-response compression */
1262
+ _compression = {
1263
+ enabled: false,
1264
+ threshold: 1024,
1265
+ level: 6,
1266
+ encodings: ['br', 'gzip', 'deflate'],
1267
+ };
1268
+ // Set by close(); listen() then registers a fresh native server, because the
1269
+ // old serverId's uv handles are torn down and must never be reused.
1270
+ closed = false;
1271
+ // Direct router dispatch (set from Moro.listen via setRouterHandler)
1272
+ routerHandler;
1273
+ // Global error handler (Moro.setErrorHandler) - shapes errors thrown from
1274
+ // global middleware, hooks and body parsing (parity with MoroHttpServer)
1275
+ errorHandler;
1276
+ // Direct route table (parity with MoroHttpServer): routes registered via
1277
+ // get/post/... - the framework's registerDirectRoutes() calls these. They
1278
+ // live here, independent of the UnifiedRouter singleton, so app.post()-style
1279
+ // routes keep working even if the unified router is reset (as the test
1280
+ // harness does between cases). Checked before the unified router in dispatch.
1281
+ directRoutes = [];
1282
+ constructor(options = {}) {
1283
+ if (options.maxBodySize)
1284
+ this.maxBodySize = options.maxBodySize;
1285
+ if (options.maxUploadSize)
1286
+ this.maxUploadSize = options.maxUploadSize;
1287
+ this.reusePort = options.reusePort === true;
1288
+ if (options.capabilities)
1289
+ this._capabilities = options.capabilities;
1290
+ if (options.limits)
1291
+ this._limits = options.limits;
1292
+ if (options.ssl)
1293
+ this._ssl = options.ssl;
1294
+ if (options.http2Settings)
1295
+ this._http2Settings = options.http2Settings;
1296
+ if (options.limits?.multipart)
1297
+ this.multipartLimits = options.limits.multipart;
1298
+ // The framework preloads the engine synchronously (loadNativeEngine) and
1299
+ // injects it; loading here directly covers standalone construction.
1300
+ const engineModule = options.engineModule ?? loadNativeEngine()?.module;
1301
+ if (!engineModule) {
1302
+ throw new Error('No native HTTP engine available: ' +
1303
+ (getNativeEngineLoadErrors().join('; ') || 'not installed'));
1304
+ }
1305
+ const surface = engineModule.default || engineModule;
1306
+ if (typeof surface.serve !== 'function' || typeof surface.respond !== 'function') {
1307
+ throw new Error('Engine module does not provide the @morojs/engine API (serve/respond) - ' +
1308
+ 'use UWebSocketsHttpServer for uWS-style modules');
1309
+ }
1310
+ this._engine = surface;
1311
+ // TLS: pass it through when the engine supports it; otherwise keep the
1312
+ // (now capability-gated) warning so a proxy/node fallback is clear.
1313
+ if (this._ssl && this._capabilities?.tls) {
1314
+ this.isSsl = true; // req.secure/protocol now report https
1315
+ }
1316
+ else if (this._ssl && !this._capabilities?.tls) {
1317
+ this.logger.warn('This @morojs/engine build does not terminate TLS - terminate at a ' +
1318
+ "proxy, upgrade the engine, or set engine: 'node' for in-process HTTPS", 'Init');
1319
+ }
1320
+ // Register callbacks now - the engine opens sockets only at listen().
1321
+ this.serverId = this.registerWithEngine();
1322
+ // Framework observability handle exposed via getServer()/getApp(). It
1323
+ // mimics enough of a net.Server that code reaching for the raw server
1324
+ // (e.g. `app.core.httpServer.getServer().close(cb)`) works on the engine
1325
+ // path too - address() is null until listening, and close(cb) forwards to
1326
+ // the adapter's shutdown so callers never hang waiting on a missing method.
1327
+ this.handle = Object.defineProperties({
1328
+ engine: '@morojs/engine',
1329
+ address: () => this.isListening
1330
+ ? { address: this.host || '0.0.0.0', family: 'IPv4', port: this.port || 0 }
1331
+ : null,
1332
+ close: (cb) => {
1333
+ this.close(cb);
1334
+ return this.handle;
1335
+ },
1336
+ }, {
1337
+ serverId: { get: () => this.serverId, enumerable: true },
1338
+ listening: { get: () => this.isListening, enumerable: true },
1339
+ });
1340
+ this.logger.info('Moro engine HTTP server created', 'Init');
1341
+ }
1342
+ /** @internal terminal write/abort cleanup - drops the reqId mapping */
1343
+ _complete(reqId) {
1344
+ this.inflight.delete(reqId);
1345
+ }
1346
+ // Register callbacks + limits with the native engine, returning the new
1347
+ // serverId. Called from the constructor and again by listen() after a
1348
+ // close() (the old native server is gone; a restart needs a fresh one).
1349
+ // The engine buffers bodies natively and 413s over its limit; it cannot
1350
+ // see content types, so it gets the larger upload limit and parseBody
1351
+ // enforces the tighter non-multipart limit (mirrors the other servers).
1352
+ //
1353
+ // Every option beyond maxBodySize/reusePort is gated on engine capabilities
1354
+ // so this never hands a key an older engine build cannot parse (an unknown
1355
+ // option would just be ignored, but gating keeps the intent explicit).
1356
+ registerWithEngine() {
1357
+ const caps = this._capabilities;
1358
+ const limits = this._limits;
1359
+ const t = limits?.timeouts ?? {};
1360
+ const serveOptions = {
1361
+ maxBodySize: Math.max(this.maxBodySize, this.maxUploadSize),
1362
+ reusePort: this.reusePort,
1363
+ };
1364
+ // Timeouts + connection cap: understood since the engine's first release.
1365
+ if (t.idle !== undefined)
1366
+ serveOptions.idleTimeoutMs = t.idle;
1367
+ if (t.request !== undefined)
1368
+ serveOptions.requestTimeoutMs = t.request;
1369
+ if (limits?.maxConnections && limits.maxConnections > 0) {
1370
+ serveOptions.maxConnections = limits.maxConnections;
1371
+ }
1372
+ if (limits?.maxPendingBytes !== undefined) {
1373
+ serveOptions.maxPendingBytes = limits.maxPendingBytes;
1374
+ }
1375
+ // Full limit surface (engine >= 1.1.0).
1376
+ if (caps?.limits) {
1377
+ if (limits?.maxHeaderSize !== undefined)
1378
+ serveOptions.maxHeadSize = limits.maxHeaderSize;
1379
+ if (limits?.maxHeaders !== undefined)
1380
+ serveOptions.maxHeaders = limits.maxHeaders;
1381
+ if (limits?.wsMaxMessageSize !== undefined)
1382
+ serveOptions.wsMaxMessageSize = limits.wsMaxMessageSize;
1383
+ if (limits?.wsBackpressureLimit !== undefined)
1384
+ serveOptions.wsBackpressureLimit = limits.wsBackpressureLimit;
1385
+ if (limits?.writeHighWaterMark !== undefined)
1386
+ serveOptions.writeHighWaterMark = limits.writeHighWaterMark;
1387
+ if (limits?.backlog !== undefined)
1388
+ serveOptions.backlog = limits.backlog;
1389
+ }
1390
+ // TLS: gated on the engine's own capability flag (caps.tls), not a version
1391
+ // number, so a mismatched engine build is never handed an ssl option it
1392
+ // can't parse.
1393
+ if (caps?.tls && this._ssl) {
1394
+ serveOptions.ssl = this._ssl;
1395
+ }
1396
+ // HTTP/2: likewise gated on caps.http2.
1397
+ if (caps?.http2 && this._http2Settings) {
1398
+ serveOptions.http2 = {
1399
+ ...(this._http2Settings.settings ?? {}),
1400
+ };
1401
+ }
1402
+ return this._engine.serve({
1403
+ onRequest: (reqId, methodIdx, path) => this.onRequest(reqId, methodIdx, path),
1404
+ onAborted: (reqId) => this.onAborted(reqId),
1405
+ onWritable: (reqId) => this.onWritable(reqId),
1406
+ onWsOpen: (wsId, path) => this._wsBridge?.onOpen(wsId, path, this._pendingWsInfo),
1407
+ onWsMessage: (wsId, data, isBinary) => this._wsBridge?.onMessage(wsId, data, isBinary),
1408
+ onWsClose: (wsId, code) => this._wsBridge?.onClose(wsId, code),
1409
+ }, serveOptions);
1410
+ }
1411
+ // ---- WebSocket bridge (used by EngineWebSocketAdapter) ----
1412
+ /** Register the engine WebSocket bridge; upgrade requests then route natively. */
1413
+ enableWebSocket(bridge) {
1414
+ this._wsBridge = bridge;
1415
+ }
1416
+ /** Send a frame on a WebSocket. Returns false on backpressure. */
1417
+ wsSend(wsId, data, isBinary) {
1418
+ return this._engine.wsSend(wsId, data, isBinary) !== false;
1419
+ }
1420
+ /** Close a WebSocket (RFC 6455 close frame). */
1421
+ wsClose(wsId, code, reason) {
1422
+ this._engine.wsClose(wsId, code, reason);
1423
+ }
1424
+ // Direct router dispatch - called after global middleware without the
1425
+ // per-middleware promise machinery (mirrors MoroHttpServer.setRouterHandler)
1426
+ setRouterHandler(fn) {
1427
+ this.routerHandler = fn;
1428
+ }
1429
+ // Register a global error handler (called from Moro.setErrorHandler)
1430
+ setErrorHandler(fn) {
1431
+ this.errorHandler = fn;
1432
+ }
1433
+ // Request/response decorations (app.decorateRequest/decorateReply). Applied
1434
+ // per request/response since EngineRequest/EngineResponse are shared classes
1435
+ // (decorating their prototype would leak across apps in one process). Empty
1436
+ // by default, so the common path pays only a `> 0` length check.
1437
+ requestDecorationKeys = [];
1438
+ requestDecorations = {};
1439
+ responseDecorationKeys = [];
1440
+ responseDecorations = {};
1441
+ setRequestDecorations(decorations) {
1442
+ this.requestDecorations = decorations;
1443
+ this.requestDecorationKeys = Object.keys(decorations);
1444
+ }
1445
+ setResponseDecorations(decorations) {
1446
+ this.responseDecorations = decorations;
1447
+ this.responseDecorationKeys = Object.keys(decorations);
1448
+ }
1449
+ // Direct route registration (mirrors MoroHttpServer). The framework's
1450
+ // registerDirectRoutes() invokes these as httpServer[method](path, handler).
1451
+ addDirectRoute(method, path, handlers) {
1452
+ const compiled = PathMatcher.compile(path);
1453
+ const pattern = compiled.pattern || new RegExp(`^${(path.split('?')[0] ?? '').replace(/\//g, '\\/')}$`);
1454
+ this.directRoutes.push({ method, pattern, paramNames: compiled.paramNames, handlers });
1455
+ }
1456
+ get(path, ...handlers) {
1457
+ this.addDirectRoute('GET', path, handlers);
1458
+ }
1459
+ post(path, ...handlers) {
1460
+ this.addDirectRoute('POST', path, handlers);
1461
+ }
1462
+ put(path, ...handlers) {
1463
+ this.addDirectRoute('PUT', path, handlers);
1464
+ }
1465
+ delete(path, ...handlers) {
1466
+ this.addDirectRoute('DELETE', path, handlers);
1467
+ }
1468
+ patch(path, ...handlers) {
1469
+ this.addDirectRoute('PATCH', path, handlers);
1470
+ }
1471
+ options(path, ...handlers) {
1472
+ this.addDirectRoute('OPTIONS', path, handlers);
1473
+ }
1474
+ head(path, ...handlers) {
1475
+ this.addDirectRoute('HEAD', path, handlers);
1476
+ }
1477
+ // Match + run a direct route. Returns true if one handled the request.
1478
+ async dispatchDirectRoute(httpReq, httpRes) {
1479
+ if (this.directRoutes.length === 0)
1480
+ return false;
1481
+ const method = httpReq.method;
1482
+ const path = httpReq.path;
1483
+ for (const route of this.directRoutes) {
1484
+ if (route.method !== method)
1485
+ continue;
1486
+ const matches = path.match(route.pattern);
1487
+ if (!matches)
1488
+ continue;
1489
+ if (route.paramNames.length > 0) {
1490
+ const params = {};
1491
+ for (let i = 0; i < route.paramNames.length; i++) {
1492
+ const paramName = route.paramNames[i];
1493
+ if (paramName === undefined)
1494
+ continue;
1495
+ const raw = matches[i + 1];
1496
+ if (raw === undefined) {
1497
+ params[paramName] = '';
1498
+ }
1499
+ else {
1500
+ // Malformed escapes (e.g. /users/%zz) keep the raw text - a
1501
+ // URIError here would turn an odd URL into a 500
1502
+ try {
1503
+ params[paramName] = decodeURIComponent(raw);
1504
+ }
1505
+ catch {
1506
+ params[paramName] = raw;
1507
+ }
1508
+ }
1509
+ }
1510
+ httpReq.params = { ...httpReq.params, ...params };
1511
+ }
1512
+ // Run the handler chain: middleware-style (req,res,next) links run in
1513
+ // order; the final handler's return value (if any) is sent as JSON.
1514
+ for (let i = 0; i < route.handlers.length; i++) {
1515
+ if (httpRes.headersSent)
1516
+ break;
1517
+ const handler = route.handlers[i];
1518
+ const isLast = i === route.handlers.length - 1;
1519
+ if (isLast) {
1520
+ const result = handler(httpReq, httpRes);
1521
+ const value = result && typeof result.then === 'function' ? await result : result;
1522
+ if (value !== undefined && !httpRes.headersSent) {
1523
+ httpRes.json(value);
1524
+ }
1525
+ }
1526
+ else {
1527
+ await new Promise((resolve, reject) => {
1528
+ let settled = false;
1529
+ const done = (err) => {
1530
+ if (settled)
1531
+ return;
1532
+ settled = true;
1533
+ if (err) {
1534
+ reject(err);
1535
+ }
1536
+ else {
1537
+ resolve();
1538
+ }
1539
+ };
1540
+ try {
1541
+ const r = handler(httpReq, httpRes, done);
1542
+ if (r && typeof r.then === 'function') {
1543
+ r.then(() => done(), done);
1544
+ }
1545
+ else if (httpRes.headersSent) {
1546
+ // A middleware that ended the response without calling next()
1547
+ // would otherwise leave this await unresolved - unblock the chain.
1548
+ done();
1549
+ }
1550
+ }
1551
+ catch (e) {
1552
+ done(e);
1553
+ }
1554
+ });
1555
+ }
1556
+ }
1557
+ return true;
1558
+ }
1559
+ return false;
1560
+ }
1561
+ // The engine has no native route table - every request already reaches the
1562
+ // unified router through a single onRequest callback with zero middleware
1563
+ // machinery, so there is no fast path to register. Accepted for interface
1564
+ // parity with the uWS adapter.
1565
+ setNativeRouteProvider(_provider, _onError) {
1566
+ this.logger.debug('Native route provider noted (engine dispatch is already direct)', 'NativeRoutes');
1567
+ }
1568
+ onRequest(reqId, methodIdx, path) {
1569
+ // WebSocket upgrade: when a WS bridge is registered and this is an upgrade
1570
+ // request, hand the connection to the engine's native WS path. The engine
1571
+ // sends the 101 handshake and drives onWsOpen/Message/Close.
1572
+ if (this._wsBridge) {
1573
+ const upgrade = this._engine.getHeader(reqId, 'upgrade');
1574
+ if (upgrade && upgrade.toLowerCase() === 'websocket') {
1575
+ // Snapshot handshake data now - the upgrade invalidates the reqId,
1576
+ // after which these fetchers return empty values.
1577
+ const ip = this._engine.getRemoteAddress(reqId) || '';
1578
+ const headers = {};
1579
+ const flat = this._engine.getHeaders(reqId);
1580
+ if (flat) {
1581
+ // Join duplicates exactly like the EngineRequest.headers getter -
1582
+ // last-wins would let a client send two X-Forwarded-For lines and
1583
+ // hide the earlier one from IP-based WS auth / rate-limiting.
1584
+ for (let i = 0; i + 1 < flat.length; i += 2) {
1585
+ const key = flat[i];
1586
+ const val = flat[i + 1];
1587
+ if (key === undefined || val === undefined)
1588
+ continue;
1589
+ const existing = headers[key];
1590
+ headers[key] =
1591
+ existing === undefined ? val : existing + (key === 'cookie' ? '; ' : ', ') + val;
1592
+ }
1593
+ }
1594
+ this._pendingWsInfo = { ip, headers };
1595
+ const wsId = this._engine.upgradeToWebSocket(reqId);
1596
+ this._pendingWsInfo = undefined;
1597
+ if (wsId !== -1)
1598
+ return; // WS took over this connection
1599
+ // else: not a valid upgrade - fall through to normal HTTP handling
1600
+ }
1601
+ }
1602
+ // methodIdx 7 (OTHER) needs the extra crossing; the seven common methods
1603
+ // resolve from the static table
1604
+ const method = methodIdx === 7 ? this._engine.getMethod(reqId) || 'OTHER' : METHODS[methodIdx];
1605
+ const httpReq = new EngineRequest(this, reqId, method, path);
1606
+ const httpRes = new EngineResponse(this, reqId, this.logger);
1607
+ httpRes._moroReq = httpReq;
1608
+ // Apply app.decorateRequest/decorateReply values (rare; opt-in)
1609
+ if (this.requestDecorationKeys.length > 0) {
1610
+ for (const key of this.requestDecorationKeys) {
1611
+ httpReq[key] = this.requestDecorations[key];
1612
+ }
1613
+ }
1614
+ if (this.responseDecorationKeys.length > 0) {
1615
+ for (const key of this.responseDecorationKeys) {
1616
+ httpRes[key] = this.responseDecorations[key];
1617
+ }
1618
+ }
1619
+ void this.handleRequest(httpReq, httpRes);
1620
+ // Register for onAborted/onWritable routing only when the response is
1621
+ // still in flight after the synchronous dispatch window. Sync handlers
1622
+ // have already ended by here, and the engine never fires those callbacks
1623
+ // for a completed reqId - registering every request just churns this
1624
+ // long-lived Map's old-space backing store, and entries alive at a
1625
+ // scavenge promote the whole request/response graph (at ~570k pipelined
1626
+ // req/s that meant full GCs every ~400ms and a ~150 MB heap vs ~30 MB).
1627
+ if (!httpRes.writableEnded)
1628
+ this.inflight.set(reqId, httpRes);
1629
+ }
1630
+ onAborted(reqId) {
1631
+ const httpRes = this.inflight.get(reqId);
1632
+ if (httpRes) {
1633
+ this.inflight.delete(reqId);
1634
+ httpRes._handleAbort();
1635
+ }
1636
+ }
1637
+ onWritable(reqId) {
1638
+ const httpRes = this.inflight.get(reqId);
1639
+ if (httpRes)
1640
+ httpRes._handleWritable();
1641
+ }
1642
+ async handleRequest(httpReq, httpRes) {
1643
+ this.requestCounter++;
1644
+ try {
1645
+ const method = httpReq.method;
1646
+ // Parse body only for body methods (all start with 'P'). The engine
1647
+ // buffered the body before onRequest fired, so this is synchronous.
1648
+ if (method.charCodeAt(0) === 80 && // 'P' char code
1649
+ (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
1650
+ this.parseBody(httpReq);
1651
+ }
1652
+ // Execute hooks before request processing - skipped entirely when none registered
1653
+ const hookManager = this.hookManager;
1654
+ if (hookManager && (hookManager.hasHooks === undefined || hookManager.hasHooks('request'))) {
1655
+ await hookManager.execute('request', {
1656
+ request: httpReq,
1657
+ response: httpRes,
1658
+ });
1659
+ }
1660
+ // Execute global middleware chain
1661
+ if (this.globalMiddleware.length > 0) {
1662
+ await this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
1663
+ if (httpRes.headersSent)
1664
+ return;
1665
+ }
1666
+ // Unified router first - it carries the full route feature set
1667
+ // (per-route middleware, validation, auth, ...). The direct route table
1668
+ // below is only a fallback for when the unified router singleton was
1669
+ // reset out from under a listening app (as the test harness does), so it
1670
+ // must never shadow the richer unified-router path.
1671
+ const routerHandler = this.routerHandler;
1672
+ if (routerHandler) {
1673
+ const handled = routerHandler(httpReq, httpRes);
1674
+ if (handled) {
1675
+ if (typeof handled.then === 'function') {
1676
+ if (await handled)
1677
+ return;
1678
+ }
1679
+ else {
1680
+ return;
1681
+ }
1682
+ }
1683
+ }
1684
+ // Direct route table fallback (parity with node's dual registration -
1685
+ // survives a UnifiedRouter reset)
1686
+ if (await this.dispatchDirectRoute(httpReq, httpRes))
1687
+ return;
1688
+ // No route matched
1689
+ if (!httpRes.headersSent && !httpRes.writableEnded) {
1690
+ httpRes.statusCode = 404;
1691
+ httpRes.setHeader('Content-Type', 'application/json');
1692
+ httpRes.end('{"success":false,"error":"Not found"}');
1693
+ }
1694
+ }
1695
+ catch (error) {
1696
+ // Payload-too-large: respond 413 rather than a generic 500
1697
+ if (error?.statusCode === 413 && !httpRes.writableEnded && !httpRes.headersSent) {
1698
+ httpRes.statusCode = 413;
1699
+ httpRes.setHeader('Content-Type', 'application/json');
1700
+ httpRes.end('{"success":false,"error":"Request entity too large"}');
1701
+ return;
1702
+ }
1703
+ // Malformed body (parseBody): a client error, not a server error
1704
+ if (error?.statusCode === 400 && !httpRes.writableEnded && !httpRes.headersSent) {
1705
+ httpRes.statusCode = 400;
1706
+ httpRes.setHeader('Content-Type', 'application/json');
1707
+ httpRes.end('{"success":false,"error":"Invalid request body"}');
1708
+ return;
1709
+ }
1710
+ this.logger.error(`Request handling error: ${error instanceof Error ? error.message : String(error)}`, 'RequestError');
1711
+ // The app's global error handler (Moro.setErrorHandler) gets first shot
1712
+ // at shaping the response - same contract as the Node server.
1713
+ if (this.errorHandler && !httpRes.writableEnded && !httpRes.headersSent) {
1714
+ try {
1715
+ await this.errorHandler(error, httpReq, httpRes);
1716
+ if (httpRes.headersSent || httpRes.writableEnded)
1717
+ return;
1718
+ }
1719
+ catch (handlerError) {
1720
+ this.logger.error(`Error handler threw: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`, 'ErrorHandler');
1721
+ }
1722
+ }
1723
+ // Send error response if not already sent
1724
+ if (!httpRes.writableEnded && !httpRes.headersSent) {
1725
+ try {
1726
+ httpRes.statusCode = 500;
1727
+ httpRes.setHeader('Content-Type', 'application/json');
1728
+ httpRes.end('{"success":false,"error":"Internal server error"}');
1729
+ }
1730
+ catch {
1731
+ this.logger.error('Failed to send error response', 'ResponseError');
1732
+ }
1733
+ }
1734
+ }
1735
+ }
1736
+ // Parse the engine-buffered body (JSON/urlencoded/multipart/text - identical
1737
+ // semantics to the Node and uWS servers' readBody). getBody returns a STABLE
1738
+ // ArrayBuffer copy, so Buffer.from() views it with no aliasing hazard.
1739
+ parseBody(httpReq) {
1740
+ const raw = this._engine.getBody(httpReq._reqId);
1741
+ if (!raw || raw.byteLength === 0)
1742
+ return;
1743
+ const contentType = httpReq.headers['content-type'] || '';
1744
+ const isMultipart = contentType.includes('multipart/form-data');
1745
+ const maxSize = isMultipart ? this.maxUploadSize : this.maxBodySize;
1746
+ // The engine natively enforces the larger of the two limits; the tighter
1747
+ // non-multipart limit is enforced here (responds 413 via handleRequest)
1748
+ if (raw.byteLength > maxSize) {
1749
+ const error = new Error(isMultipart ? 'File upload too large' : 'Request body too large');
1750
+ error.statusCode = 413;
1751
+ throw error;
1752
+ }
1753
+ const buffer = Buffer.from(raw);
1754
+ try {
1755
+ if (contentType.includes('application/json')) {
1756
+ httpReq.body = JSON.parse(buffer.toString('utf-8'));
1757
+ }
1758
+ else if (contentType.includes('application/x-www-form-urlencoded')) {
1759
+ const params = new URLSearchParams(buffer.toString('utf-8'));
1760
+ const body = {};
1761
+ params.forEach((value, key) => {
1762
+ body[key] = value;
1763
+ });
1764
+ httpReq.body = body;
1765
+ }
1766
+ else if (isMultipart) {
1767
+ // Shared parser (same as the Node/uWS servers) - stringifying
1768
+ // multipart would corrupt binary uploads and lose fields/files
1769
+ httpReq.body = parseMultipartBuffer(buffer, contentType, this.multipartLimits);
1770
+ }
1771
+ else {
1772
+ httpReq.body = buffer.toString('utf-8');
1773
+ }
1774
+ }
1775
+ catch (parseError) {
1776
+ // A limit error (multipart maxParts/maxFiles/maxFileSize) carries its own
1777
+ // 413 - preserve it rather than masking it as a generic 400 malformed.
1778
+ if (parseError && typeof parseError.statusCode === 'number') {
1779
+ throw parseError;
1780
+ }
1781
+ // A malformed body must fail the request (Node-path parity), not slip a
1782
+ // silent `body = null` past handlers that then treat it as "no input".
1783
+ const error = new Error(`Invalid request body: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
1784
+ error.statusCode = 400;
1785
+ throw error;
1786
+ }
1787
+ }
1788
+ async executeMiddleware(middleware, req, res) {
1789
+ // Express error semantics: a middleware that calls next(err) (or throws)
1790
+ // routes to the NEXT 4-arg error handler (err, req, res, next), skipping
1791
+ // the normal 3-arg middleware in between; normal middleware is skipped
1792
+ // while an error is pending, and error handlers are skipped when there is
1793
+ // no error. An uncaught error propagates to handleRequest's 500 path.
1794
+ let error = null;
1795
+ for (const mw of middleware) {
1796
+ if (res.headersSent)
1797
+ break;
1798
+ const isErrorHandler = mw.length >= 4;
1799
+ if (error && !isErrorHandler)
1800
+ continue;
1801
+ if (!error && isErrorHandler)
1802
+ continue;
1803
+ try {
1804
+ await new Promise((resolve, reject) => {
1805
+ const next = (err) => (err ? reject(err) : resolve());
1806
+ try {
1807
+ const result = error ? mw(error, req, res, next) : mw(req, res, next);
1808
+ if (result && typeof result.then === 'function') {
1809
+ result.then(() => resolve(), reject);
1810
+ }
1811
+ }
1812
+ catch (e) {
1813
+ reject(e);
1814
+ }
1815
+ });
1816
+ error = null; // handler (error or normal) completed cleanly
1817
+ }
1818
+ catch (e) {
1819
+ error = e; // route to the next error handler
1820
+ }
1821
+ }
1822
+ if (error)
1823
+ throw error;
1824
+ }
1825
+ // Public API - matches MoroHttpServer/UWebSocketsHttpServer interface
1826
+ use(middleware) {
1827
+ // Defensive check: Don't add MiddlewareInterface objects to globalMiddleware
1828
+ // MiddlewareInterface objects should only be handled by MiddlewareManager
1829
+ if (middleware &&
1830
+ typeof middleware === 'object' &&
1831
+ middleware.install &&
1832
+ middleware.metadata) {
1833
+ this.logger?.warn?.(`Attempted to add MiddlewareInterface "${middleware.metadata?.name}" to HTTP server globalMiddleware. ` +
1834
+ `This should be handled by MiddlewareManager instead. Skipping.`, 'Middleware');
1835
+ return;
1836
+ }
1837
+ this.globalMiddleware.push(middleware);
1838
+ }
1839
+ // Configure request tracking (ID generation)
1840
+ setRequestTracking(enabled) {
1841
+ this.requestTrackingEnabled = enabled;
1842
+ }
1843
+ // Note: Route registration methods (get, post, etc.) are not used by the
1844
+ // engine adapter - all routing is handled by UnifiedRouter via setRouterHandler
1845
+ // (same as the uWS adapter)
1846
+ setHookManager(hookManager) {
1847
+ this.hookManager = hookManager;
1848
+ }
1849
+ configurePerformance(config) {
1850
+ // The engine's request hot path is batched natively; the one thing it
1851
+ // does NOT do itself is response compression, so wire that here (parity
1852
+ // with the Node path). Buffered responses only - streaming stays raw.
1853
+ this._compression = resolveCompressionSettings(config);
1854
+ this.logger.debug(`Performance configured (compression ${this._compression.enabled ? 'on' : 'off'})`, 'Config');
1855
+ }
1856
+ listen(port, hostOrCallback, callback) {
1857
+ if (this.isListening) {
1858
+ this.logger.warn('Server is already listening', 'Listen');
1859
+ return;
1860
+ }
1861
+ const host = typeof hostOrCallback === 'string' ? hostOrCallback : '0.0.0.0';
1862
+ const cb = typeof hostOrCallback === 'function' ? hostOrCallback : callback;
1863
+ try {
1864
+ // listen() after close(): the old native server's handles are torn
1865
+ // down (its serverId is dead) - register a fresh one, like a Node
1866
+ // http.Server supports close() -> listen() cycles.
1867
+ if (this.closed) {
1868
+ this.serverId = this.registerWithEngine();
1869
+ this.closed = false;
1870
+ }
1871
+ // Synchronous bind; throws on bind errors, returns the actual port
1872
+ // (meaningful when port 0 asks for an ephemeral one)
1873
+ const actualPort = this._engine.listen(this.serverId, host, port);
1874
+ this.port = typeof actualPort === 'number' ? actualPort : port;
1875
+ this.host = host;
1876
+ this.isListening = true;
1877
+ this.logger.info(`Moro engine HTTP server listening on ${host}:${this.port}`, 'Listen');
1878
+ if (cb)
1879
+ cb();
1880
+ }
1881
+ catch (error) {
1882
+ this.logger.error(`Failed to listen on port ${port}: ${error instanceof Error ? error.message : String(error)}`, 'Listen');
1883
+ throw error;
1884
+ }
1885
+ }
1886
+ /** Bounded drain window for in-flight responses during close() */
1887
+ static CLOSE_DRAIN_MS = 2000;
1888
+ close(callback) {
1889
+ if (!this.isListening) {
1890
+ if (callback)
1891
+ callback();
1892
+ return;
1893
+ }
1894
+ try {
1895
+ this.isListening = false;
1896
+ this.closed = true;
1897
+ // Graceful shutdown: stop accepting first, give in-flight responses a
1898
+ // bounded window to finish, then tear down. Requests still open at
1899
+ // teardown get onAborted -> 'close' events, so SSE intervals/monitors
1900
+ // registered on req.on('close') are released, not leaked.
1901
+ // Middleware is intentionally kept: close() -> listen() must serve with
1902
+ // the same cors/auth/compression stack (Node http.Server parity).
1903
+ if (typeof this._engine.stopListening === 'function') {
1904
+ this._engine.stopListening(this.serverId);
1905
+ }
1906
+ const finish = () => {
1907
+ try {
1908
+ this._engine.close(this.serverId);
1909
+ }
1910
+ catch (error) {
1911
+ this.logger.error('Error closing server', 'Close', {
1912
+ error: error instanceof Error ? error.message : String(error),
1913
+ });
1914
+ if (callback)
1915
+ callback(error instanceof Error ? error : new Error(String(error)));
1916
+ return;
1917
+ }
1918
+ this.inflight.clear();
1919
+ this.logger.info('Moro engine HTTP server closed', 'Close');
1920
+ if (callback) {
1921
+ // Give the event loop time to process the engine's socket teardown
1922
+ // before invoking the callback (matches the uWS adapter)
1923
+ setTimeout(() => {
1924
+ callback();
1925
+ }, 50);
1926
+ }
1927
+ };
1928
+ if (this.inflight.size === 0) {
1929
+ finish();
1930
+ return;
1931
+ }
1932
+ const deadline = Date.now() + MoroEngineServer.CLOSE_DRAIN_MS;
1933
+ const poll = setInterval(() => {
1934
+ if (this.inflight.size === 0 || Date.now() >= deadline) {
1935
+ clearInterval(poll);
1936
+ finish();
1937
+ }
1938
+ }, 25);
1939
+ // The drain poll must never keep the process alive by itself
1940
+ poll.unref?.();
1941
+ }
1942
+ catch (error) {
1943
+ this.logger.error('Error closing server', 'Close', {
1944
+ error: error instanceof Error ? error.message : String(error),
1945
+ });
1946
+ if (callback)
1947
+ callback(error instanceof Error ? error : new Error(String(error)));
1948
+ }
1949
+ }
1950
+ getServer() {
1951
+ // Stable observability handle (listening/address) - there is no app object
1952
+ return this.handle;
1953
+ }
1954
+ getApp() {
1955
+ return this.handle;
1956
+ }
1957
+ forceCleanup() {
1958
+ // Cleanup method for compatibility
1959
+ this.logger.debug('Force cleanup called', 'Cleanup');
1960
+ }
1961
+ }
1962
+ //# sourceMappingURL=moro-engine-server.js.map