@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.
- package/README.md +89 -46
- package/dist/core/auth/morojs-adapter.js +95 -45
- package/dist/core/auth/morojs-adapter.js.map +1 -1
- package/dist/core/config/config-manager.js.map +1 -1
- package/dist/core/config/config-sources.js +119 -48
- package/dist/core/config/config-sources.js.map +1 -1
- package/dist/core/config/config-validator.js +189 -8
- package/dist/core/config/config-validator.js.map +1 -1
- package/dist/core/config/file-loader.js.map +1 -1
- package/dist/core/config/index.js.map +1 -1
- package/dist/core/config/schema.js +35 -4
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/config/utils.js.map +1 -1
- package/dist/core/database/adapters/drizzle.js +2 -2
- package/dist/core/database/adapters/drizzle.js.map +1 -1
- package/dist/core/database/adapters/index.js.map +1 -1
- package/dist/core/database/adapters/mongodb.d.ts +1 -0
- package/dist/core/database/adapters/mongodb.js +16 -0
- package/dist/core/database/adapters/mongodb.js.map +1 -1
- package/dist/core/database/adapters/mysql.d.ts +1 -0
- package/dist/core/database/adapters/mysql.js +13 -2
- package/dist/core/database/adapters/mysql.js.map +1 -1
- package/dist/core/database/adapters/postgresql.js +2 -2
- package/dist/core/database/adapters/postgresql.js.map +1 -1
- package/dist/core/database/adapters/redis.js.map +1 -1
- package/dist/core/database/adapters/sql-safety.js +5 -4
- package/dist/core/database/adapters/sql-safety.js.map +1 -1
- package/dist/core/database/adapters/sqlite.js.map +1 -1
- package/dist/core/database/index.js.map +1 -1
- package/dist/core/docs/index.js +11 -7
- package/dist/core/docs/index.js.map +1 -1
- package/dist/core/docs/openapi-generator.js +8 -6
- package/dist/core/docs/openapi-generator.js.map +1 -1
- package/dist/core/docs/schema-to-openapi.js +3 -3
- package/dist/core/docs/schema-to-openapi.js.map +1 -1
- package/dist/core/docs/simple-docs.js.map +1 -1
- package/dist/core/docs/swagger-ui.js.map +1 -1
- package/dist/core/docs/zod-to-openapi.js +2 -2
- package/dist/core/docs/zod-to-openapi.js.map +1 -1
- package/dist/core/events/event-bus.js +14 -5
- package/dist/core/events/event-bus.js.map +1 -1
- package/dist/core/events/index.js.map +1 -1
- package/dist/core/framework.d.ts +41 -3
- package/dist/core/framework.js +342 -95
- package/dist/core/framework.js.map +1 -1
- package/dist/core/graphql/adapter.js.map +1 -1
- package/dist/core/graphql/adapters/graphql-js-adapter.js.map +1 -1
- package/dist/core/graphql/core.js.map +1 -1
- package/dist/core/graphql/index.js.map +1 -1
- package/dist/core/graphql/loader.js.map +1 -1
- package/dist/core/graphql/types.js.map +1 -1
- package/dist/core/grpc/adapters/grpc-js-adapter.js +4 -0
- package/dist/core/grpc/adapters/grpc-js-adapter.js.map +1 -1
- package/dist/core/grpc/adapters/index.js.map +1 -1
- package/dist/core/grpc/grpc-adapter.js.map +1 -1
- package/dist/core/grpc/grpc-manager.js.map +1 -1
- package/dist/core/grpc/index.js.map +1 -1
- package/dist/core/grpc/middleware/auth.js +1 -1
- package/dist/core/grpc/middleware/auth.js.map +1 -1
- package/dist/core/grpc/middleware/logging.js.map +1 -1
- package/dist/core/grpc/middleware/validation.js +1 -1
- package/dist/core/grpc/middleware/validation.js.map +1 -1
- package/dist/core/grpc/types.d.ts +7 -7
- package/dist/core/grpc/types.js.map +1 -1
- package/dist/core/http/http-server.d.ts +17 -23
- package/dist/core/http/http-server.js +109 -249
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/http2-server.d.ts +6 -16
- package/dist/core/http/http2-server.js +40 -83
- package/dist/core/http/http2-server.js.map +1 -1
- package/dist/core/http/index.d.ts +1 -0
- package/dist/core/http/index.js +3 -0
- package/dist/core/http/index.js.map +1 -1
- package/dist/core/http/moro-engine-server.d.ts +263 -0
- package/dist/core/http/moro-engine-server.js +1962 -0
- package/dist/core/http/moro-engine-server.js.map +1 -0
- package/dist/core/http/utils/compression.d.ts +47 -0
- package/dist/core/http/utils/compression.js +132 -0
- package/dist/core/http/utils/compression.js.map +1 -0
- package/dist/core/http/utils/lazy-event-emitter.d.ts +11 -0
- package/dist/core/http/utils/lazy-event-emitter.js +69 -0
- package/dist/core/http/utils/lazy-event-emitter.js.map +1 -0
- package/dist/core/http/utils/multipart-parser.d.ts +32 -0
- package/dist/core/http/utils/multipart-parser.js +215 -0
- package/dist/core/http/utils/multipart-parser.js.map +1 -0
- package/dist/core/http/utils/query-parser.d.ts +1 -0
- package/dist/core/http/utils/query-parser.js +44 -0
- package/dist/core/http/utils/query-parser.js.map +1 -0
- package/dist/core/http/utils/size.d.ts +33 -0
- package/dist/core/http/utils/size.js +37 -0
- package/dist/core/http/utils/size.js.map +1 -0
- package/dist/core/http/utils/ssl-config.d.ts +69 -0
- package/dist/core/http/utils/ssl-config.js +116 -0
- package/dist/core/http/utils/ssl-config.js.map +1 -0
- package/dist/core/http/utils/uws-worker-clustering.js +45 -32
- package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
- package/dist/core/http/uws-http-server.d.ts +25 -3
- package/dist/core/http/uws-http-server.js +465 -86
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/jobs/cron-parser.js +7 -5
- package/dist/core/jobs/cron-parser.js.map +1 -1
- package/dist/core/jobs/index.js.map +1 -1
- package/dist/core/jobs/job-executor.d.ts +1 -1
- package/dist/core/jobs/job-executor.js.map +1 -1
- package/dist/core/jobs/job-scheduler.d.ts +3 -3
- package/dist/core/jobs/job-scheduler.js.map +1 -1
- package/dist/core/jobs/job-state-manager.d.ts +3 -3
- package/dist/core/jobs/job-state-manager.js +25 -4
- package/dist/core/jobs/job-state-manager.js.map +1 -1
- package/dist/core/jobs/leader-election.d.ts +15 -0
- package/dist/core/jobs/leader-election.js +158 -69
- package/dist/core/jobs/leader-election.js.map +1 -1
- package/dist/core/jobs/types.d.ts +4 -4
- package/dist/core/jobs/types.js.map +1 -1
- package/dist/core/jobs/utils.d.ts +1 -1
- package/dist/core/jobs/utils.js +7 -3
- package/dist/core/jobs/utils.js.map +1 -1
- package/dist/core/logger/filters.js +3 -2
- package/dist/core/logger/filters.js.map +1 -1
- package/dist/core/logger/index.js.map +1 -1
- package/dist/core/logger/logger.js +56 -25
- package/dist/core/logger/logger.js.map +1 -1
- package/dist/core/logger/outputs.js.map +1 -1
- package/dist/core/mail/adapters/console-adapter.js.map +1 -1
- package/dist/core/mail/adapters/index.js.map +1 -1
- package/dist/core/mail/adapters/nodemailer-adapter.js.map +1 -1
- package/dist/core/mail/adapters/resend-adapter.js.map +1 -1
- package/dist/core/mail/adapters/sendgrid-adapter.js.map +1 -1
- package/dist/core/mail/adapters/ses-adapter.js.map +1 -1
- package/dist/core/mail/index.js.map +1 -1
- package/dist/core/mail/mail-adapter.js.map +1 -1
- package/dist/core/mail/mail-manager.d.ts +10 -0
- package/dist/core/mail/mail-manager.js +21 -5
- package/dist/core/mail/mail-manager.js.map +1 -1
- package/dist/core/mail/template-engine.js.map +1 -1
- package/dist/core/mail/types.js.map +1 -1
- package/dist/core/middleware/built-in/auth/core.js +3 -3
- package/dist/core/middleware/built-in/auth/core.js.map +1 -1
- package/dist/core/middleware/built-in/auth/helpers.js +4 -4
- package/dist/core/middleware/built-in/auth/helpers.js.map +1 -1
- package/dist/core/middleware/built-in/auth/hook.js.map +1 -1
- package/dist/core/middleware/built-in/auth/index.js.map +1 -1
- package/dist/core/middleware/built-in/auth/jwt-helpers.d.ts +4 -4
- package/dist/core/middleware/built-in/auth/jwt-helpers.js.map +1 -1
- package/dist/core/middleware/built-in/auth/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/auth/providers.js.map +1 -1
- package/dist/core/middleware/built-in/body-parsers/index.js.map +1 -1
- package/dist/core/middleware/built-in/body-size/core.js +2 -2
- package/dist/core/middleware/built-in/body-size/core.js.map +1 -1
- package/dist/core/middleware/built-in/body-size/hook.js.map +1 -1
- package/dist/core/middleware/built-in/body-size/index.js.map +1 -1
- package/dist/core/middleware/built-in/body-size/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/file.js +3 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/file.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/index.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/memory.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/cache/redis.js.map +1 -1
- package/dist/core/middleware/built-in/cache/adapters/index.js.map +1 -1
- package/dist/core/middleware/built-in/cache/core.js.map +1 -1
- package/dist/core/middleware/built-in/cache/hook.js +3 -3
- package/dist/core/middleware/built-in/cache/hook.js.map +1 -1
- package/dist/core/middleware/built-in/cache/index.js.map +1 -1
- package/dist/core/middleware/built-in/cache/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/azure.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudflare.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/cloudfront.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/cdn/index.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/adapters/index.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/core.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/hook.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/index.js.map +1 -1
- package/dist/core/middleware/built-in/cdn/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/compression/core.d.ts +3 -0
- package/dist/core/middleware/built-in/compression/core.js +28 -34
- package/dist/core/middleware/built-in/compression/core.js.map +1 -1
- package/dist/core/middleware/built-in/compression/hook.js.map +1 -1
- package/dist/core/middleware/built-in/compression/index.js.map +1 -1
- package/dist/core/middleware/built-in/compression/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/cookie/core.js.map +1 -1
- package/dist/core/middleware/built-in/cookie/hook.js.map +1 -1
- package/dist/core/middleware/built-in/cookie/index.js.map +1 -1
- package/dist/core/middleware/built-in/cookie/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/cors/core.js.map +1 -1
- package/dist/core/middleware/built-in/cors/hook.js.map +1 -1
- package/dist/core/middleware/built-in/cors/index.js.map +1 -1
- package/dist/core/middleware/built-in/cors/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/csp/core.js.map +1 -1
- package/dist/core/middleware/built-in/csp/hook.js.map +1 -1
- package/dist/core/middleware/built-in/csp/index.js.map +1 -1
- package/dist/core/middleware/built-in/csp/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/csrf/core.js +1 -1
- package/dist/core/middleware/built-in/csrf/core.js.map +1 -1
- package/dist/core/middleware/built-in/csrf/hook.js.map +1 -1
- package/dist/core/middleware/built-in/csrf/index.js.map +1 -1
- package/dist/core/middleware/built-in/csrf/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/error-tracker/index.js.map +1 -1
- package/dist/core/middleware/built-in/error-tracker/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/graphql/core.js.map +1 -1
- package/dist/core/middleware/built-in/graphql/helpers.js.map +1 -1
- package/dist/core/middleware/built-in/graphql/hook.js.map +1 -1
- package/dist/core/middleware/built-in/graphql/index.js.map +1 -1
- package/dist/core/middleware/built-in/graphql/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/graphql/subscriptions.js.map +1 -1
- package/dist/core/middleware/built-in/helmet/core.js.map +1 -1
- package/dist/core/middleware/built-in/helmet/hook.js.map +1 -1
- package/dist/core/middleware/built-in/helmet/index.js.map +1 -1
- package/dist/core/middleware/built-in/helmet/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/http2/core.js.map +1 -1
- package/dist/core/middleware/built-in/http2/hook.js.map +1 -1
- package/dist/core/middleware/built-in/http2/index.js.map +1 -1
- package/dist/core/middleware/built-in/http2/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/index.js.map +1 -1
- package/dist/core/middleware/built-in/performance-monitor/index.js.map +1 -1
- package/dist/core/middleware/built-in/performance-monitor/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/range/core.js +4 -1
- package/dist/core/middleware/built-in/range/core.js.map +1 -1
- package/dist/core/middleware/built-in/range/hook.js.map +1 -1
- package/dist/core/middleware/built-in/range/index.js.map +1 -1
- package/dist/core/middleware/built-in/range/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/rate-limit/core.js.map +1 -1
- package/dist/core/middleware/built-in/rate-limit/hook.js.map +1 -1
- package/dist/core/middleware/built-in/rate-limit/index.js.map +1 -1
- package/dist/core/middleware/built-in/rate-limit/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/request-logger/index.js.map +1 -1
- package/dist/core/middleware/built-in/request-logger/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/session/core.js +4 -4
- package/dist/core/middleware/built-in/session/core.js.map +1 -1
- package/dist/core/middleware/built-in/session/hook.js.map +1 -1
- package/dist/core/middleware/built-in/session/index.js.map +1 -1
- package/dist/core/middleware/built-in/session/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/sse/core.js.map +1 -1
- package/dist/core/middleware/built-in/sse/hook.js.map +1 -1
- package/dist/core/middleware/built-in/sse/index.js.map +1 -1
- package/dist/core/middleware/built-in/sse/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/static/core.js.map +1 -1
- package/dist/core/middleware/built-in/static/hook.js.map +1 -1
- package/dist/core/middleware/built-in/static/index.js.map +1 -1
- package/dist/core/middleware/built-in/static/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/template/core.js +1 -1
- package/dist/core/middleware/built-in/template/core.js.map +1 -1
- package/dist/core/middleware/built-in/template/hook.js.map +1 -1
- package/dist/core/middleware/built-in/template/index.js.map +1 -1
- package/dist/core/middleware/built-in/template/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/upload/core.js.map +1 -1
- package/dist/core/middleware/built-in/upload/hook.js.map +1 -1
- package/dist/core/middleware/built-in/upload/index.js.map +1 -1
- package/dist/core/middleware/built-in/upload/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/validation/core.js.map +1 -1
- package/dist/core/middleware/built-in/validation/hook.js.map +1 -1
- package/dist/core/middleware/built-in/validation/index.js.map +1 -1
- package/dist/core/middleware/built-in/validation/middleware.js.map +1 -1
- package/dist/core/middleware/index.js +2 -2
- package/dist/core/middleware/index.js.map +1 -1
- package/dist/core/modules/auto-discovery.js +11 -7
- package/dist/core/modules/auto-discovery.js.map +1 -1
- package/dist/core/modules/index.js.map +1 -1
- package/dist/core/modules/modules.js +18 -13
- package/dist/core/modules/modules.js.map +1 -1
- package/dist/core/networking/adapters/engine-adapter.d.ts +23 -0
- package/dist/core/networking/adapters/engine-adapter.js +365 -0
- package/dist/core/networking/adapters/engine-adapter.js.map +1 -0
- package/dist/core/networking/adapters/index.d.ts +1 -0
- package/dist/core/networking/adapters/index.js +1 -0
- package/dist/core/networking/adapters/index.js.map +1 -1
- package/dist/core/networking/adapters/socketio-adapter.d.ts +1 -0
- package/dist/core/networking/adapters/socketio-adapter.js +12 -1
- package/dist/core/networking/adapters/socketio-adapter.js.map +1 -1
- package/dist/core/networking/adapters/uws-adapter.js +27 -16
- package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
- package/dist/core/networking/adapters/ws-adapter.d.ts +15 -0
- package/dist/core/networking/adapters/ws-adapter.js +120 -6
- package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
- package/dist/core/networking/index.js.map +1 -1
- package/dist/core/networking/service-discovery.js +7 -5
- package/dist/core/networking/service-discovery.js.map +1 -1
- package/dist/core/networking/websocket-adapter.d.ts +22 -3
- package/dist/core/networking/websocket-adapter.js.map +1 -1
- package/dist/core/networking/websocket-manager.js +70 -67
- package/dist/core/networking/websocket-manager.js.map +1 -1
- package/dist/core/pooling/object-pool-manager.js +6 -0
- package/dist/core/pooling/object-pool-manager.js.map +1 -1
- package/dist/core/queue/adapters/bull-adapter.js.map +1 -1
- package/dist/core/queue/adapters/index.js.map +1 -1
- package/dist/core/queue/adapters/kafka-adapter.js +2 -2
- package/dist/core/queue/adapters/kafka-adapter.js.map +1 -1
- package/dist/core/queue/adapters/memory-adapter.js +6 -6
- package/dist/core/queue/adapters/memory-adapter.js.map +1 -1
- package/dist/core/queue/adapters/rabbitmq-adapter.d.ts +50 -0
- package/dist/core/queue/adapters/rabbitmq-adapter.js +176 -20
- package/dist/core/queue/adapters/rabbitmq-adapter.js.map +1 -1
- package/dist/core/queue/adapters/sqs-adapter.d.ts +12 -2
- package/dist/core/queue/adapters/sqs-adapter.js +75 -24
- package/dist/core/queue/adapters/sqs-adapter.js.map +1 -1
- package/dist/core/queue/index.js.map +1 -1
- package/dist/core/queue/middleware/index.js.map +1 -1
- package/dist/core/queue/middleware/monitoring.d.ts +1 -1
- package/dist/core/queue/middleware/monitoring.js.map +1 -1
- package/dist/core/queue/middleware/priority.js +2 -1
- package/dist/core/queue/middleware/priority.js.map +1 -1
- package/dist/core/queue/middleware/rate-limit.js.map +1 -1
- package/dist/core/queue/queue-adapter.js.map +1 -1
- package/dist/core/queue/queue-manager.js.map +1 -1
- package/dist/core/queue/types.d.ts +8 -8
- package/dist/core/queue/types.js.map +1 -1
- package/dist/core/routing/app-integration.js.map +1 -1
- package/dist/core/routing/create-router.js.map +1 -1
- package/dist/core/routing/index.js.map +1 -1
- package/dist/core/routing/path-matcher.js.map +1 -1
- package/dist/core/routing/radix-tree.js.map +1 -1
- package/dist/core/routing/router.js.map +1 -1
- package/dist/core/routing/unified-router.js +12 -4
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/runtime/aws-lambda-adapter.js.map +1 -1
- package/dist/core/runtime/base-adapter.js +2 -2
- package/dist/core/runtime/base-adapter.js.map +1 -1
- package/dist/core/runtime/cloudflare-workers-adapter.js +2 -0
- package/dist/core/runtime/cloudflare-workers-adapter.js.map +1 -1
- package/dist/core/runtime/index.js.map +1 -1
- package/dist/core/runtime/node-adapter.js +19 -17
- package/dist/core/runtime/node-adapter.js.map +1 -1
- package/dist/core/runtime/vercel-edge-adapter.js +2 -0
- package/dist/core/runtime/vercel-edge-adapter.js.map +1 -1
- package/dist/core/utilities/circuit-breaker.d.ts +1 -0
- package/dist/core/utilities/circuit-breaker.js +25 -2
- package/dist/core/utilities/circuit-breaker.js.map +1 -1
- package/dist/core/utilities/container.js +9 -9
- package/dist/core/utilities/container.js.map +1 -1
- package/dist/core/utilities/hooks.js +28 -6
- package/dist/core/utilities/hooks.js.map +1 -1
- package/dist/core/utilities/index.js.map +1 -1
- package/dist/core/utilities/module-path.js.map +1 -1
- package/dist/core/utilities/package-utils.d.ts +68 -0
- package/dist/core/utilities/package-utils.js +198 -1
- package/dist/core/utilities/package-utils.js.map +1 -1
- package/dist/core/utilities/response-helpers.js +1 -1
- package/dist/core/utilities/response-helpers.js.map +1 -1
- package/dist/core/validation/adapters.js.map +1 -1
- package/dist/core/validation/error-handler.js.map +1 -1
- package/dist/core/validation/index.js.map +1 -1
- package/dist/core/validation/schema-interface.js.map +1 -1
- package/dist/core/workers/facade.js.map +1 -1
- package/dist/core/workers/index.js.map +1 -1
- package/dist/core/workers/worker-entry.d.ts +1 -0
- package/dist/core/workers/worker-entry.js +12 -0
- package/dist/core/workers/worker-entry.js.map +1 -0
- package/dist/core/workers/worker-manager.d.ts +23 -2
- package/dist/core/workers/worker-manager.js +117 -17
- package/dist/core/workers/worker-manager.js.map +1 -1
- package/dist/core/workers/worker.js +8 -6
- package/dist/core/workers/worker.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +20 -1
- package/dist/moro.js +230 -70
- package/dist/moro.js.map +1 -1
- package/dist/types/auth.d.ts +4 -4
- package/dist/types/auth.js.map +1 -1
- package/dist/types/cache.js.map +1 -1
- package/dist/types/cdn.js.map +1 -1
- package/dist/types/config.d.ts +100 -15
- package/dist/types/config.js.map +1 -1
- package/dist/types/core.js.map +1 -1
- package/dist/types/database.js.map +1 -1
- package/dist/types/discovery.js.map +1 -1
- package/dist/types/events.js.map +1 -1
- package/dist/types/hooks.js.map +1 -1
- package/dist/types/http.js.map +1 -1
- package/dist/types/logger.js.map +1 -1
- package/dist/types/module.js.map +1 -1
- package/dist/types/runtime.js.map +1 -1
- package/dist/types/session.js.map +1 -1
- 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
|