@fluojs/platform-express 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -21,7 +21,7 @@ fluo 런타임을 위한 Express 기반 HTTP 어댑터 패키지입니다.
21
21
  npm install @fluojs/platform-express express
22
22
  ```
23
23
 
24
- `@fluojs/platform-express`는 Node.js 20 이상이 필요합니다. Package manifest는 `engines.node >=20.0.0`을 선언하므로 deployment host가 Bun, Deno, Cloudflare Workers라면 fetch-style adapter를 선택하세요.
24
+ `@fluojs/platform-express`는 Node.js `>=24.0.0 <27`이 필요합니다. Listener-level RFC `QUERY` 요청이 Express와 fluo dispatch에 도달하도록 package manifest는 `engines.node >=24.0.0 <27`을 선언합니다. Node 24 미만과 Node 27 이상은 제외됩니다. Deployment host가 Bun, Deno, Cloudflare Workers라면 fetch-style adapter를 선택하세요.
25
25
 
26
26
  ## 사용 시점
27
27
 
@@ -49,6 +49,10 @@ await app.listen();
49
49
 
50
50
  ## 주요 패턴
51
51
 
52
+ ### Early Hints
53
+
54
+ Express response는 underlying Node `ServerResponse`를 사용하는 optional `context.response.earlyHints` capability를 노출합니다. `103` 하나마다 `write(...)`를 await하면 final response 전에 여러 write를 관찰할 수 있습니다. Early field는 Express final header, status, body, commit state와 분리됩니다. Capability가 없으면 unsupported이며 late write와 disconnect는 no-op이 아니라 결정적으로 reject됩니다.
55
+
52
56
  ### 스트리밍 응답 처리 (SSE)
53
57
  Express 어댑터는 공유 `SseResponse` 유틸리티를 통해 Server-Sent Events(SSE)를 지원하며, Express 전용 스트림 처리를 추상화합니다.
54
58
 
@@ -83,6 +87,8 @@ const adapter = createExpressAdapter(
83
87
  ### Express/Connect Middleware 경계
84
88
  Express adapter는 Express를 host HTTP engine으로 보존하지만 request pipeline middleware는 dispatcher가 소유합니다. Portable middleware는 fluo `Middleware` 계약으로 등록하세요.
85
89
 
90
+ Adapter는 Express application을 직접 생성하고 소유하므로 기존 Express application을 채택하거나 재사용하는 방식은 지원하지 않습니다. Native Express handler는 construction-time `nativeMiddleware`로 제공해야 하며, bootstrap 이후 `use(...)`로 native stack에 middleware를 추가하는 방식은 지원하지 않습니다. 이식 가능한 동작은 fluo `Middleware`로 재작성하는 방식을 우선하세요.
91
+
86
92
  ```typescript
87
93
  import type { Middleware } from '@fluojs/http';
88
94
 
@@ -131,9 +137,12 @@ Native stack은 adapter 생성 시 고정됩니다. Adapter는 Node HTTP/S liste
131
137
  ### Startup Retry와 Shutdown
132
138
  `listen()`은 adapter가 열린 상태인 동안에만 `retryDelayMs` 및 `retryLimit`에 따라 `EADDRINUSE`를 재시도합니다. 동시에 호출된 `listen()`은 서로 겹치는 retry loop를 시작하지 않고 첫 호출자의 in-flight startup lifecycle과 dispatcher를 공유합니다. Startup이 retry loop에서 대기 중일 때 `close()`가 호출되면, underlying Node server가 아직 listening 상태에 도달하지 않았더라도 adapter는 공유 listen attempt를 abort하고 그 작업이 settle될 때까지 기다린 뒤 `close()`를 완료합니다. `close()` 진행 중 호출된 `listen()`은 reject되며 `close()`가 resolve된 뒤 다시 시도할 수 있습니다. `close()`가 resolve된 뒤 막혀 있던 port를 해제해도 adapter가 나중에 bind되지 않으며, 다시 시작하려면 호출자가 명시적으로 `listen()`을 다시 호출해야 합니다.
133
139
 
140
+ 같은 adapter instance를 close 후 다시 listen하면 traffic을 받기 전에 native route descriptor registry를 새 dispatcher에서 갱신합니다. 유지된 Express Router layer는 현재 descriptor만 resolve하고 그 밖의 경우 full dispatcher matching으로 fallback하므로 native handoff metadata가 이전 application graph를 가리키지 않습니다.
141
+
134
142
  ## 어댑터 계약
135
143
 
136
144
  - **공유 dispatcher 소유권 유지**: Native Express Router 매치 이후에도 실제 요청은 공유 fluo dispatcher가 처리하므로 middleware, guards, interceptors, observers, params, error envelope 계약은 그대로 유지됩니다.
145
+ - **Express application 소유권**: Adapter가 Express application을 생성하고 소유합니다. 기존 Express application을 채택하지 않고 post-bootstrap `use(...)` mutation도 노출하지 않습니다. 불가피한 native handler는 construction-time `nativeMiddleware`로 제공하고, 이식 가능한 동작은 fluo `Middleware`로 재작성하세요.
137
146
  - **Host engine 경계**: Express는 host/platform HTTP engine이지만 fluo는 native Express/Connect middleware를 fluo middleware로 재해석하지 않습니다. Application-level middleware는 공유 `Middleware` 계약을 구현하고, platform-specific `nativeMiddleware` 옵션은 routing 전에 native handler를 mount합니다.
138
147
  - **Native middleware 소유권**: Native handler는 선언 순서대로 실행되며 Express continuation, response termination, error-chain semantics를 유지합니다. Adapter shutdown은 listener와 connection을 닫지만 handler가 소유한 resource는 dispose하지 않습니다.
139
148
  - **안전한 fallback 범위**: `@All(...)` 핸들러와 shape가 겹치는 파라미터 라우트는 Express Router에 강제 등록하지 않고 의도적으로 catch-all fallback 경로에 둡니다.
@@ -142,7 +151,7 @@ Native stack은 adapter 생성 시 고정됩니다. Adapter는 Node HTTP/S liste
142
151
  - **버저닝 parity**: Express Router가 최초 path match를 하더라도 header/media-type/custom version 선택은 계속 dispatcher가 최종 결정합니다.
143
152
  - **Middleware rewrite parity**: App middleware가 method/path를 rewrite하면 native handoff는 무효화되고 rewrite된 요청을 기준으로 다시 매칭합니다.
144
153
  - **응답 serialization parity**: String response는 기본적으로 `text/plain`, object/array는 JSON, binary payload는 `application/octet-stream`으로 serialize되며 `set-cookie` 값은 병합됩니다.
145
- - **Startup과 shutdown**: 어댑터는 HTTP/HTTPS startup, adapter가 열린 상태에서 `retryLimit` 소진 전까지 retry option에 따른 `EADDRINUSE` 재시도, 동시 startup 호출자의 단일 in-flight listen lifecycle 및 dispatcher 재사용, `close()` 중 해당 공유 retry loop를 abort 및 join한 뒤 shutdown 완료 보고, close 진행 중 `listen()` reject, 이미 시작된 adapter에 대한 중복 `listen()` 호출의 idempotent 처리와 live dispatcher 보존, 정상 close 시 idle keep-alive socket drain, 동시에 들어온 `close()` 호출의 단일 in-flight close lifecycle 재사용, shutdown timeout 이후 force-close를 지원하며, `shutdownTimeoutMs`가 `0`이면 즉시 force-close합니다.
154
+ - **Startup과 shutdown**: 어댑터는 HTTP/HTTPS startup, adapter가 열린 상태에서 `retryLimit` 소진 전까지 retry option에 따른 `EADDRINUSE` 재시도, 동시 startup 호출자의 단일 in-flight listen lifecycle 및 dispatcher 재사용, `close()` 중 해당 공유 retry loop를 abort 및 join한 뒤 shutdown 완료 보고, close 진행 중 `listen()` reject, 이미 시작된 adapter에 대한 중복 `listen()` 호출의 idempotent 처리와 live dispatcher 보존, close 후 adapter를 다시 listen할 때 native route descriptor 갱신, 정상 close 시 idle keep-alive socket drain, 동시에 들어온 `close()` 호출의 단일 in-flight close lifecycle 재사용, shutdown timeout 이후 force-close를 지원하며, `shutdownTimeoutMs`가 `0`이면 즉시 force-close합니다.
146
155
 
147
156
  ## 공개 API 개요
148
157
 
@@ -150,10 +159,22 @@ Native stack은 adapter 생성 시 고정됩니다. Adapter는 Node HTTP/S liste
150
159
  - `bootstrapExpressApplication(module, options)`: 수동 제어를 위한 고급 부트스트랩 헬퍼입니다.
151
160
  - `runExpressApplication(module, options)`: 시그널 연결을 포함한 빠른 시작을 위한 호환 헬퍼입니다. timeout/실패 시에는 해당 상태를 로그와 `process.exitCode`로 보고하고, 최종 프로세스 종료는 주변 호스트에 맡깁니다.
152
161
  - `isExpressMultipartTooLargeError(error)`: adapter error shape 전반에서 multipart limit 감지를 정규화합니다.
153
- - `ExpressHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다. `getServer()`는 좁은 platform integration을 위해 underlying Node HTTP/HTTPS server를 노출하고, `getListenTarget()`은 startup 이후 resolved bind target과 public URL을 보고하며, `getRealtimeCapability()`는 realtime package가 사용하는 server-backed capability를 반환합니다. 이러한 helper는 모두 일반 애플리케이션 코드에 native server object를 퍼뜨리기보다 infrastructure boundary에만 두세요.
162
+ - `ExpressServer`: `ExpressHttpApplicationAdapter.getServer()`가 infrastructure boundary에서 반환하는 adapter-owned `node:http` `Server` 또는 `node:https` `Server` union의 공개 type alias입니다.
163
+ - `ExpressHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다. `getServer()`는 좁은 platform integration을 위해 underlying Node HTTP/HTTPS server를 노출하며 `ExpressServer`를 반환하고, `getListenTarget()`은 startup 이후 resolved bind target과 public URL을 보고하며, `getRealtimeCapability()`는 realtime package가 사용하는 server-backed capability를 반환합니다. 이러한 helper는 모두 일반 애플리케이션 코드에 native server object를 퍼뜨리기보다 infrastructure boundary에만 두세요.
154
164
  - Option type: `ExpressAdapterOptions`, `BootstrapExpressApplicationOptions`, `RunExpressApplicationOptions`, `ExpressNativeMiddleware`, `CorsInput`, `ExpressApplicationSignal`.
155
165
 
156
- `createExpressAdapter(options, multipartOptions?)`는 `host`, `https`, `maxBodySize`, `nativeMiddleware`, `port`, `rawBody`, `retryDelayMs`, `retryLimit`, `shutdownTimeoutMs`를 지원합니다. `ExpressHttpApplicationAdapter`를 직접 생성하는 경우에도 factory와 같은 numeric validation이 적용됩니다. `bootstrapExpressApplication(...)`과 `runExpressApplication(...)`은 `cors`, `globalPrefix`, `globalPrefixExclude`, `middleware`, `multipart`, `nativeMiddleware`, `securityHeaders`, `forceExitTimeoutMs`, `shutdownSignals`, `logger`도 받습니다. startup/shutdown diagnostics에는 framework console logger를 기본으로 사용하며, `logger`가 제공되면 주입된 `ApplicationLogger`를 따릅니다.
166
+ `createExpressAdapter(options, multipartOptions?)`는 `host`, `https`, `maxBodySize`, `nativeMiddleware`, `port`, `rawBody`, `retryDelayMs`, `retryLimit`, `shutdownTimeoutMs`를 지원합니다. `ExpressHttpApplicationAdapter`를 직접 생성하는 경우에도 factory와 같은 numeric validation이 적용됩니다.
167
+
168
+ - `BootstrapExpressApplicationOptions`와 `RunExpressApplicationOptions`는 위의 adapter option과 함께 `cors`, `globalPrefix`, `globalPrefixExclude`, `middleware`, `multipart`, `nativeMiddleware`, `securityHeaders`, `logger`를 공통으로 받습니다.
169
+ - `RunExpressApplicationOptions`만 signal-driven shutdown을 위한 `forceExitTimeoutMs`와 `shutdownSignals`를 추가로 받습니다.
170
+
171
+ 두 helper는 startup/shutdown diagnostics에 framework console logger를 기본으로 사용하며, `logger`가 제공되면 주입된 `ApplicationLogger`를 따릅니다.
172
+
173
+ ## Multipart 스트리밍
174
+
175
+ Express를 bootstrap할 때 `multipart: { strategy: 'stream' }`을 설정하면 multipart part가 `RequestContext.request.body`의 `AsyncIterable`로 노출됩니다. Express는 iterator를 미리 읽거나 버퍼링하지 않으며, file part를 소비할 때만 바이트를 가져옵니다. 버퍼링 multipart parsing은 기본값이며 fields와 `request.files`를 노출하고, 하나의 request body에서 stream 소비와 함께 사용할 수 없습니다.
176
+
177
+ Runtime route dispatch는 route를 위해 만든 iterator를 소유하며 handler가 끝난 뒤 자동으로 `return()`을 호출해 active source를 cancel하고 release합니다. Standalone `parseMultipartStream(...)` consumer는 이 책임을 직접 집니다. iterator를 끝까지 소비하거나 일찍 끝낼 때 `return()`을 호출하세요.
157
178
 
158
179
  ## 관련 패키지
159
180
 
@@ -164,4 +185,4 @@ Native stack은 adapter 생성 시 고정됩니다. Adapter는 Node HTTP/S liste
164
185
  ## 예제 소스
165
186
 
166
187
  - `packages/platform-express/src/adapter.test.ts`
167
- - 이 패키지는 아직 전용 `examples/platform-express` 앱을 제공하지 않습니다. Express bootstrap 형태는 이 README의 빠른 시작 및 native middleware scenario를 사용하고, native middleware ordering/termination/error propagation, SSE framing, native-route fallback parity, duplicate listen idempotency, retry exhaustion, shutdown 중 startup retry cancellation, idle keep-alive drain, forced shutdown을 포함한 실행 가능한 Express adapter coverage는 `packages/platform-express/src/adapter.test.ts`를 사용하세요. `examples/minimal/src/main.ts`는 Fastify 기반이므로 Express 예제 소스로 취급하지 않아야 합니다.
188
+ - 이 패키지는 아직 전용 `examples/platform-express` 앱을 제공하지 않습니다. Express bootstrap 형태는 이 README의 빠른 시작 및 native middleware scenario를 사용하고, native middleware ordering/termination/error propagation, SSE framing, native-route fallback parity, close/relisten 후 native descriptor 갱신, duplicate listen idempotency, retry exhaustion, shutdown 중 startup retry cancellation, idle keep-alive drain, forced shutdown을 포함한 실행 가능한 Express adapter coverage는 `packages/platform-express/src/adapter.test.ts`를 사용하세요. `examples/minimal/src/main.ts`는 Fastify 기반이므로 Express 예제 소스로 취급하지 않아야 합니다.
package/README.md CHANGED
@@ -21,7 +21,7 @@ Express-backed HTTP adapter for the fluo runtime.
21
21
  npm install @fluojs/platform-express express
22
22
  ```
23
23
 
24
- `@fluojs/platform-express` requires Node.js 20 or newer. Its package manifest declares `engines.node >=20.0.0`; choose a fetch-style adapter instead when the deployment host is Bun, Deno, or Cloudflare Workers.
24
+ `@fluojs/platform-express` requires Node.js `>=24.0.0 <27`. Its package manifest declares `engines.node >=24.0.0 <27` so listener-level RFC `QUERY` requests reach Express and fluo dispatch; Node versions below 24 and Node 27+ are excluded. Choose a fetch-style adapter instead when the deployment host is Bun, Deno, or Cloudflare Workers.
25
25
 
26
26
  ## When to Use
27
27
 
@@ -49,6 +49,10 @@ await app.listen();
49
49
 
50
50
  ## Common Patterns
51
51
 
52
+ ### Early Hints
53
+
54
+ Express responses expose the optional `context.response.earlyHints` capability backed by the underlying Node `ServerResponse`. Await one `write(...)` per `103`; multiple writes are observable before the final response. Early fields stay separate from Express final headers, status, body, and commit state. Missing capability means unsupported, while late writes and disconnects reject deterministically instead of becoming no-ops.
55
+
52
56
  ### Handling Streaming Responses (SSE)
53
57
  The Express adapter supports Server-Sent Events (SSE) via the shared `SseResponse` utility, abstracting away the Express-specific stream handling.
54
58
 
@@ -83,6 +87,8 @@ const adapter = createExpressAdapter(
83
87
  ### Express/Connect Middleware Boundary
84
88
  The Express adapter preserves Express as the host HTTP engine, but request pipeline middleware remains dispatcher-owned. Register portable middleware through the fluo `Middleware` contract:
85
89
 
90
+ The adapter constructs and owns its Express application. Adopting or reusing an existing Express application is unsupported. Native Express handlers must be supplied through construction-time `nativeMiddleware`; after bootstrap, calling `use(...)` to append to the native stack is not a supported surface. Prefer rewriting portable behavior as fluo `Middleware`.
91
+
86
92
  ```typescript
87
93
  import type { Middleware } from '@fluojs/http';
88
94
 
@@ -131,9 +137,12 @@ To avoid changing documented fluo semantics, overlapping same-shape param routes
131
137
  ### Startup Retry and Shutdown
132
138
  `listen()` retries `EADDRINUSE` according to `retryDelayMs` and `retryLimit` only while the adapter remains open. Concurrent `listen()` calls share the first caller's in-flight startup lifecycle and dispatcher instead of starting overlapping retry loops. If `close()` is called while startup is waiting in that retry loop, the adapter aborts the shared listen attempt and waits for it to settle before `close()` resolves, even when the underlying Node server has not reached the listening state yet. A `listen()` call made while `close()` is in progress rejects and can be retried after `close()` resolves. Releasing the blocked port after `close()` resolves cannot make the adapter bind later; callers must invoke `listen()` again explicitly to start it.
133
139
 
140
+ If the same adapter instance is listened again after close, its native route descriptor registry refreshes from the new dispatcher before traffic is served. Retained Express Router layers resolve only current descriptors and otherwise fall back to full dispatcher matching, so native handoff metadata never points at a previous application graph.
141
+
134
142
  ## Adapter Contract
135
143
 
136
144
  - **Shared dispatcher ownership**: Native Express Router matches still hand off to the shared fluo dispatcher, so middleware, guards, interceptors, observers, params, and error envelopes remain framework-defined.
145
+ - **Express application ownership**: The adapter constructs and owns its Express application. It does not adopt an existing Express application, and it does not expose post-bootstrap `use(...)` mutation. Supply unavoidable native handlers through construction-time `nativeMiddleware`; prefer rewriting portable behavior as fluo `Middleware`.
137
146
  - **Host engine boundary**: Express is the host/platform HTTP engine, but fluo does not reinterpret native Express/Connect middleware as fluo middleware; application-level middleware must implement the shared `Middleware` contract, while the platform-specific `nativeMiddleware` option mounts native handlers before routing.
138
147
  - **Native middleware ownership**: Native handlers run in declared order and retain Express continuation, response termination, and error-chain semantics. Adapter shutdown closes the listener and connections but does not dispose resources owned by those handlers.
139
148
  - **Safe fallback scope**: `@All(...)` handlers and overlapping same-shape param routes intentionally stay on the catch-all fallback path instead of being force-registered through Express Router.
@@ -142,7 +151,7 @@ To avoid changing documented fluo semantics, overlapping same-shape param routes
142
151
  - **Versioning parity**: Header/media-type/custom version selection remains dispatcher-owned even when Express Router handles the initial path match.
143
152
  - **Middleware rewrite parity**: App middleware that rewrites method or path invalidates native handoff and rematches the rewritten request.
144
153
  - **Response serialization parity**: String responses default to `text/plain`, objects/arrays serialize as JSON, binary payloads default to `application/octet-stream`, and `set-cookie` values are merged.
145
- - **Startup and shutdown**: The adapter supports HTTP/HTTPS startup, retries `EADDRINUSE` according to retry options until `retryLimit` is exhausted while the adapter is open, reuses one in-flight listen lifecycle and its dispatcher for concurrent startup callers, aborts and joins that shared retry loop during `close()` before shutdown completion is reported, rejects `listen()` while close is in progress, treats duplicate `listen()` calls on an already-started adapter as idempotent without replacing the live dispatcher, drains idle keep-alive sockets on normal close, reuses one in-flight close lifecycle for concurrent `close()` calls, and can force-close connections after shutdown timeout, including immediate force-close when `shutdownTimeoutMs` is `0`.
154
+ - **Startup and shutdown**: The adapter supports HTTP/HTTPS startup, retries `EADDRINUSE` according to retry options until `retryLimit` is exhausted while the adapter is open, reuses one in-flight listen lifecycle and its dispatcher for concurrent startup callers, aborts and joins that shared retry loop during `close()` before shutdown completion is reported, rejects `listen()` while close is in progress, treats duplicate `listen()` calls on an already-started adapter as idempotent without replacing the live dispatcher, refreshes native route descriptors when the adapter is listened again after close, drains idle keep-alive sockets on normal close, reuses one in-flight close lifecycle for concurrent `close()` calls, and can force-close connections after shutdown timeout, including immediate force-close when `shutdownTimeoutMs` is `0`.
146
155
 
147
156
  ## Public API Overview
148
157
 
@@ -150,10 +159,22 @@ To avoid changing documented fluo semantics, overlapping same-shape param routes
150
159
  - `bootstrapExpressApplication(module, options)`: Advanced bootstrap helper for manual control.
151
160
  - `runExpressApplication(module, options)`: Compatibility helper for quick startup with signal wiring. On timeout/failure it reports the condition through logging and `process.exitCode`, while leaving final process termination to the surrounding host.
152
161
  - `isExpressMultipartTooLargeError(error)`: Normalizes multipart limit detection across adapter error shapes.
153
- - `ExpressHttpApplicationAdapter`: The core adapter implementation class. `getServer()` exposes the underlying Node HTTP/HTTPS server for narrow platform integrations, `getListenTarget()` reports the resolved bind target and public URL after startup, and `getRealtimeCapability()` returns the server-backed capability used by realtime packages. Keep these helpers at infrastructure boundaries instead of threading native server objects through ordinary application code.
162
+ - `ExpressServer`: Public type alias for the adapter-owned `node:http` `Server` or `node:https` `Server` union returned by `ExpressHttpApplicationAdapter.getServer()` at infrastructure boundaries.
163
+ - `ExpressHttpApplicationAdapter`: The core adapter implementation class. `getServer()` exposes the underlying Node HTTP/HTTPS server for narrow platform integrations and returns `ExpressServer`, `getListenTarget()` reports the resolved bind target and public URL after startup, and `getRealtimeCapability()` returns the server-backed capability used by realtime packages. Keep these helpers at infrastructure boundaries instead of threading native server objects through ordinary application code.
154
164
  - Option types: `ExpressAdapterOptions`, `BootstrapExpressApplicationOptions`, `RunExpressApplicationOptions`, `ExpressNativeMiddleware`, `CorsInput`, `ExpressApplicationSignal`.
155
165
 
156
- `createExpressAdapter(options, multipartOptions?)` supports `host`, `https`, `maxBodySize`, `nativeMiddleware`, `port`, `rawBody`, `retryDelayMs`, `retryLimit`, and `shutdownTimeoutMs`. Direct `ExpressHttpApplicationAdapter` construction applies the same numeric validation as the factory. `bootstrapExpressApplication(...)` and `runExpressApplication(...)` also accept `cors`, `globalPrefix`, `globalPrefixExclude`, `middleware`, `multipart`, `nativeMiddleware`, `securityHeaders`, `forceExitTimeoutMs`, `shutdownSignals`, and `logger`; they use the framework console logger by default for startup and shutdown diagnostics and honor an injected `ApplicationLogger` when provided.
166
+ `createExpressAdapter(options, multipartOptions?)` supports `host`, `https`, `maxBodySize`, `nativeMiddleware`, `port`, `rawBody`, `retryDelayMs`, `retryLimit`, and `shutdownTimeoutMs`. Direct `ExpressHttpApplicationAdapter` construction applies the same numeric validation as the factory.
167
+
168
+ - `BootstrapExpressApplicationOptions` and `RunExpressApplicationOptions` share `cors`, `globalPrefix`, `globalPrefixExclude`, `middleware`, `multipart`, `nativeMiddleware`, `securityHeaders`, and `logger` in addition to the adapter options above.
169
+ - `RunExpressApplicationOptions` alone adds `forceExitTimeoutMs` and `shutdownSignals` for signal-driven shutdown.
170
+
171
+ Both helpers use the framework console logger by default for startup and shutdown diagnostics and honor an injected `ApplicationLogger` when `logger` is provided.
172
+
173
+ ## Multipart streaming
174
+
175
+ Set `multipart: { strategy: 'stream' }` when bootstrapping Express to expose multipart parts through `RequestContext.request.body` as an `AsyncIterable`. Express creates the iterator without pre-reading or buffering it; consuming a file part pulls its bytes on demand. Buffered multipart parsing remains the default, exposes fields and `request.files`, and cannot be combined with stream consumption for the same request body.
176
+
177
+ Runtime route dispatch owns an iterator created for a route and automatically calls `return()` after the handler finishes, cancelling and releasing an active source. Standalone `parseMultipartStream(...)` consumers own that responsibility: consume the iterator to completion or call `return()` when ending early.
157
178
 
158
179
  ## Related Packages
159
180
 
@@ -164,4 +185,4 @@ To avoid changing documented fluo semantics, overlapping same-shape param routes
164
185
  ## Example Sources
165
186
 
166
187
  - `packages/platform-express/src/adapter.test.ts`
167
- - This package does not currently ship a dedicated `examples/platform-express` app. Use the Quick Start and native middleware scenario in this README for Express bootstrap shape and `packages/platform-express/src/adapter.test.ts` for executable Express adapter coverage, including native middleware ordering/termination/error propagation, SSE framing, native-route fallback parity, duplicate listen idempotency, retry exhaustion, startup retry cancellation during shutdown, idle keep-alive drain, and forced shutdown. `examples/minimal/src/main.ts` is Fastify-based and should not be treated as an Express example source.
188
+ - This package does not currently ship a dedicated `examples/platform-express` app. Use the Quick Start and native middleware scenario in this README for Express bootstrap shape and `packages/platform-express/src/adapter.test.ts` for executable Express adapter coverage, including native middleware ordering/termination/error propagation, SSE framing, native-route fallback parity, native descriptor refresh after close/relisten, duplicate listen idempotency, retry exhaustion, startup retry cancellation during shutdown, idle keep-alive drain, and forced shutdown. `examples/minimal/src/main.ts` is Fastify-based and should not be treated as an Express example source.
package/dist/adapter.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type ServerOptions as HttpsServerOptions } from 'node:https';
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import { type Server as HttpsServer, type ServerOptions as HttpsServerOptions } from 'node:https';
2
3
  import { type CorsOptions, type Dispatcher, type HttpApplicationAdapter, type MiddlewareLike, type SecurityHeadersOptions } from '@fluojs/http';
3
4
  import type { Application, ApplicationLogger, CreateApplicationOptions, ModuleType, MultipartOptions } from '@fluojs/runtime';
4
5
  import { type ErrorRequestHandler, type RequestHandler } from 'express';
@@ -62,6 +63,10 @@ interface ExpressListenTarget {
62
63
  bindTarget: string;
63
64
  url: string;
64
65
  }
66
+ /**
67
+ * The Node.js HTTP or HTTPS server owned by the Express adapter.
68
+ */
69
+ export type ExpressServer = HttpServer | HttpsServer;
65
70
  /**
66
71
  * Represents the express http application adapter.
67
72
  */
@@ -81,13 +86,19 @@ export declare class ExpressHttpApplicationAdapter implements HttpApplicationAda
81
86
  private listenAbortController?;
82
87
  private listenInFlight?;
83
88
  private readonly app;
89
+ private readonly nativeRouteDescriptors;
84
90
  private nativeRoutesReady;
85
91
  private readonly requestResponseFactory;
86
92
  private readonly router;
87
93
  private readonly server;
88
94
  private readonly sockets;
89
95
  constructor(port: number, host: string | undefined, retryDelayMs: number | undefined, retryLimit: number | undefined, httpsOptions: HttpsServerOptions | undefined, multipartOptions?: MultipartOptions | undefined, maxBodySize?: number, preserveRawBody?: boolean, shutdownTimeoutMs?: number, nativeMiddleware?: readonly ExpressNativeMiddleware[]);
90
- getServer(): unknown;
96
+ /**
97
+ * Returns the Node.js HTTP or HTTPS server owned by this adapter.
98
+ *
99
+ * @returns The platform-owned Node.js HTTP or HTTPS server.
100
+ */
101
+ getServer(): ExpressServer;
91
102
  getRealtimeCapability(): import("@fluojs/http").ServerBackedHttpAdapterRealtimeCapability;
92
103
  getListenTarget(): ExpressListenTarget;
93
104
  listen(dispatcher: Dispatcher): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { createServer as createHttpServer } from 'node:http';
2
2
  import { createServer as createHttpsServer } from 'node:https';
3
- import { Readable } from 'node:stream';
4
3
  import { BadRequestException, createErrorResponse, createServerBackedHttpAdapterRealtimeCapability, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
5
4
  import { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive } from '@fluojs/http/internal';
6
5
  import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
7
- import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
8
- import { cloneRequestHeaders, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createRequestSignal, normalizePrimaryContentType, parseQueryParamsFromSearch, resolveAbsoluteRequestUrl, resolveRequestIdFromHeaders, snapshotSimpleQueryRecord, splitRawRequestUrl } from '@fluojs/runtime/internal-node';
9
- import { createConsoleApplicationLogger, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
10
- import { parseMultipart } from '@fluojs/runtime/web';
6
+ import { dispatchWithRequestResponseFactory, finalizeRouteOwnedMultipartBody } from '@fluojs/runtime/internal/request-response-factory';
7
+ import { cloneRequestHeaders, createNodeEarlyHintsCapability, createDeferredFrameworkRequestShell, createMemoizedAsyncValue, createRequestSignal, normalizePrimaryContentType, parseQueryParamsFromSearch, resolveRequestIdFromHeaders, snapshotSimpleQueryRecord, splitRawRequestUrl } from '@fluojs/platform-nodejs/internal';
8
+ import { createConsoleApplicationLogger, createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/platform-nodejs';
9
+ import { parseMultipart, parseMultipartStream } from '@fluojs/runtime/web';
11
10
  import express from 'express';
12
11
 
13
12
  /**
@@ -40,6 +39,14 @@ const EXPRESS_NATIVE_ROUTE_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '
40
39
  * Describes the run express application options contract.
41
40
  */
42
41
 
42
+ /**
43
+ * The Node.js HTTP or HTTPS server owned by the Express adapter.
44
+ */
45
+
46
+ function isExpressResponseTerminated(response) {
47
+ return response.writableEnded || response.destroyed;
48
+ }
49
+
43
50
  /**
44
51
  * Represents the express http application adapter.
45
52
  */
@@ -50,6 +57,7 @@ export class ExpressHttpApplicationAdapter {
50
57
  listenAbortController;
51
58
  listenInFlight;
52
59
  app;
60
+ nativeRouteDescriptors = new Map();
53
61
  nativeRoutesReady = false;
54
62
  requestResponseFactory;
55
63
  router = express.Router();
@@ -78,6 +86,9 @@ export class ExpressHttpApplicationAdapter {
78
86
  this.server = createExpressServer(this.httpsOptions, this.app);
79
87
  this.app.use(this.router);
80
88
  this.app.use((request, response) => {
89
+ if (isExpressResponseTerminated(response)) {
90
+ return;
91
+ }
81
92
  void this.handleRequest(request, response);
82
93
  });
83
94
  this.server.on('connection', socket => {
@@ -87,6 +98,12 @@ export class ExpressHttpApplicationAdapter {
87
98
  });
88
99
  });
89
100
  }
101
+
102
+ /**
103
+ * Returns the Node.js HTTP or HTTPS server owned by this adapter.
104
+ *
105
+ * @returns The platform-owned Node.js HTTP or HTTPS server.
106
+ */
90
107
  getServer() {
91
108
  return this.server;
92
109
  }
@@ -141,6 +158,7 @@ export class ExpressHttpApplicationAdapter {
141
158
  this.closing = false;
142
159
  this.closeInFlight = undefined;
143
160
  this.dispatcher = undefined;
161
+ this.nativeRouteDescriptors.clear();
144
162
  });
145
163
  this.closeInFlight = closeInFlight;
146
164
  void closeInFlight.catch(() => {});
@@ -171,20 +189,32 @@ export class ExpressHttpApplicationAdapter {
171
189
  }
172
190
  }
173
191
  registerNativeRoutes(dispatcher) {
192
+ const nativeRoutes = createExpressNativeRoutes(resolveDispatcherRouteDescriptors(dispatcher));
193
+ this.nativeRouteDescriptors.clear();
194
+ for (const route of nativeRoutes) {
195
+ for (const method of route.methods) {
196
+ const descriptor = route.descriptorsByMethod[method];
197
+ if (descriptor) {
198
+ this.nativeRouteDescriptors.set(`${method}:${route.path}`, descriptor);
199
+ }
200
+ }
201
+ }
174
202
  if (this.nativeRoutesReady) {
175
203
  return;
176
204
  }
177
- const nativeRoutes = createExpressNativeRoutes(resolveDispatcherRouteDescriptors(dispatcher));
178
205
  Reflect.set(this.router, '__fluoNativeRoutes', nativeRoutes);
179
206
  for (const route of nativeRoutes) {
180
207
  this.router.all(route.path, (request, response, next) => {
208
+ if (isExpressResponseTerminated(response)) {
209
+ return;
210
+ }
181
211
  if (!route.methods.includes(request.method.toUpperCase())) {
182
212
  next();
183
213
  return;
184
214
  }
185
215
  const nativeMethod = request.method.toUpperCase();
186
216
  const requestPath = splitRawRequestUrl(request.originalUrl || request.url || '/').path;
187
- const descriptor = route.descriptorsByMethod[nativeMethod];
217
+ const descriptor = this.nativeRouteDescriptors.get(`${nativeMethod}:${route.path}`);
188
218
  const params = normalizeNativeRouteParams(request.params);
189
219
  if (descriptor && !isRoutePathNormalizationSensitive(requestPath) && !hasNativeRouteParamSeparators(params)) {
190
220
  void this.handleNativeRouteRequest(descriptor, params, request, response);
@@ -217,8 +247,9 @@ export class ExpressHttpApplicationAdapter {
217
247
  const factory = this.requestResponseFactory;
218
248
  const frameworkResponse = factory.createResponse(response, request);
219
249
  const signal = factory.createRequestSignal(response);
250
+ let frameworkRequest;
220
251
  try {
221
- const frameworkRequest = attachFrameworkRequestNativeRouteHandoff(await factory.createRequest(request, signal), {
252
+ frameworkRequest = attachFrameworkRequestNativeRouteHandoff(await factory.createRequest(request, signal), {
222
253
  descriptor,
223
254
  params
224
255
  });
@@ -238,6 +269,8 @@ export class ExpressHttpApplicationAdapter {
238
269
  return;
239
270
  }
240
271
  await factory.writeErrorResponse(error, frameworkResponse, factory.resolveRequestId(request));
272
+ } finally {
273
+ await finalizeRouteOwnedMultipartBody(frameworkRequest);
241
274
  }
242
275
  }
243
276
  }
@@ -367,9 +400,12 @@ export async function runExpressApplication(rootModule, options) {
367
400
  }, adapter, logger);
368
401
  }
369
402
  function createFrameworkResponse(response) {
370
- return {
403
+ const headers = Object.fromEntries(Object.entries(response.getHeaders()).filter(entry => entry[1] !== undefined).map(([name, value]) => [name, typeof value === 'number' ? String(value) : value]));
404
+ let frameworkResponse;
405
+ frameworkResponse = {
371
406
  committed: response.headersSent || response.writableEnded,
372
- headers: {},
407
+ earlyHints: createNodeEarlyHintsCapability(response, () => frameworkResponse.committed),
408
+ headers,
373
409
  raw: response,
374
410
  stream: createFrameworkResponseStream(response),
375
411
  redirect(status, location) {
@@ -378,11 +414,19 @@ function createFrameworkResponse(response) {
378
414
  this.committed = true;
379
415
  response.redirect(status, location);
380
416
  },
381
- async send(body) {
417
+ async send(body, options) {
382
418
  if (response.writableEnded) {
383
419
  this.committed = true;
384
420
  return;
385
421
  }
422
+ if (options?.compression === false) {
423
+ disableNativeCompression(response);
424
+ }
425
+ if (body === undefined && response.req.method.toUpperCase() === 'HEAD') {
426
+ this.committed = true;
427
+ response.end();
428
+ return;
429
+ }
386
430
  const existingContentType = response.getHeader('content-type');
387
431
  const serialized = serializeResponseBody(body, typeof existingContentType === 'string' ? existingContentType : undefined);
388
432
  if (!response.hasHeader('content-type') && serialized.defaultContentType) {
@@ -422,6 +466,14 @@ function createFrameworkResponse(response) {
422
466
  statusCode: undefined,
423
467
  statusSet: false
424
468
  };
469
+ return frameworkResponse;
470
+ }
471
+ function disableNativeCompression(response) {
472
+ const cacheControl = response.getHeader('cache-control');
473
+ const value = Array.isArray(cacheControl) ? cacheControl.join(', ') : String(cacheControl ?? '');
474
+ if (!/\bno-transform\b/i.test(value)) {
475
+ response.setHeader('Cache-Control', value ? `${value}, no-transform` : 'no-transform');
476
+ }
425
477
  }
426
478
  function createFrameworkResponseStream(response) {
427
479
  return {
@@ -433,6 +485,9 @@ function createFrameworkResponseStream(response) {
433
485
  get closed() {
434
486
  return response.writableEnded;
435
487
  },
488
+ disableCompression() {
489
+ disableNativeCompression(response);
490
+ },
436
491
  flush() {
437
492
  response.flushHeaders?.();
438
493
  },
@@ -442,20 +497,33 @@ function createFrameworkResponseStream(response) {
442
497
  response.removeListener('close', listener);
443
498
  };
444
499
  },
500
+ onError(listener) {
501
+ response.on('error', listener);
502
+ return () => {
503
+ response.removeListener('error', listener);
504
+ };
505
+ },
445
506
  waitForDrain() {
446
507
  if (response.writableEnded || response.destroyed) {
447
508
  return Promise.resolve();
448
509
  }
449
- return new Promise(resolve => {
450
- const settle = () => {
451
- response.removeListener('drain', settle);
452
- response.removeListener('close', settle);
453
- response.removeListener('error', settle);
510
+ return new Promise((resolve, reject) => {
511
+ const cleanup = () => {
512
+ response.removeListener('drain', resolveDrain);
513
+ response.removeListener('close', resolveDrain);
514
+ response.removeListener('error', rejectError);
515
+ };
516
+ const rejectError = error => {
517
+ cleanup();
518
+ reject(error);
519
+ };
520
+ const resolveDrain = () => {
521
+ cleanup();
454
522
  resolve();
455
523
  };
456
- response.once('drain', settle);
457
- response.once('close', settle);
458
- response.once('error', settle);
524
+ response.once('drain', resolveDrain);
525
+ response.once('close', resolveDrain);
526
+ response.once('error', rejectError);
459
527
  });
460
528
  },
461
529
  write(chunk) {
@@ -473,10 +541,21 @@ async function createFrameworkRequest(request, signal, multipartOptions, maxBody
473
541
  let frameworkRequest;
474
542
  const materializeBody = createMemoizedAsyncValue(async () => {
475
543
  if (isMultipart) {
476
- const parsed = await parseMultipartRequest(request, {
544
+ const resolvedMultipartOptions = {
477
545
  ...multipartOptions,
478
546
  maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
479
- });
547
+ };
548
+ if (multipartOptions?.strategy === 'stream') {
549
+ frameworkRequest.body = parseMultipartStream({
550
+ body: request,
551
+ headers,
552
+ method: request.method,
553
+ signal,
554
+ url: rawUrl
555
+ }, resolvedMultipartOptions);
556
+ return;
557
+ }
558
+ const parsed = await parseMultipartRequest(request, resolvedMultipartOptions);
480
559
  frameworkRequest.body = parsed.fields;
481
560
  frameworkRequest.files = parsed.files;
482
561
  return;
@@ -530,12 +609,7 @@ function collectVersionSensitiveRouteKeys(descriptors) {
530
609
  }
531
610
  async function parseMultipartRequest(request, options = {}) {
532
611
  try {
533
- const result = await parseMultipart({
534
- body: Readable.toWeb(request),
535
- headers: normalizeHeaders(request.headers),
536
- method: request.method,
537
- url: resolveAbsoluteRequestUrl(request.url)
538
- }, options);
612
+ const result = await parseMultipart(request, options);
539
613
  return {
540
614
  fields: result.fields,
541
615
  files: result.files.map(file => ({
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "platform",
9
9
  "server"
10
10
  ],
11
- "version": "1.1.0",
11
+ "version": "2.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/platform-express"
18
18
  },
19
19
  "engines": {
20
- "node": ">=20.0.0"
20
+ "node": ">=24.0.0 <27"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
@@ -36,13 +36,14 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "express": "^5.1.0",
39
- "@fluojs/http": "^2.0.1",
40
- "@fluojs/runtime": "^2.0.1"
39
+ "@fluojs/http": "^3.0.0",
40
+ "@fluojs/platform-nodejs": "^2.0.0",
41
+ "@fluojs/runtime": "^3.0.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/express": "^5.0.3",
44
- "vitest": "^3.2.4",
45
- "@fluojs/di": "^2.0.0"
45
+ "vitest": "^4.1.11",
46
+ "@fluojs/di": "^3.0.0"
46
47
  },
47
48
  "scripts": {
48
49
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",
File without changes