@fluojs/runtime 2.0.1 → 3.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.
Files changed (127) hide show
  1. package/README.ko.md +278 -20
  2. package/README.md +278 -20
  3. package/dist/adapters/internal-request-response-factory.d.ts +1 -1
  4. package/dist/adapters/internal-request-response-factory.d.ts.map +1 -1
  5. package/dist/adapters/internal-request-response-factory.js +1 -1
  6. package/dist/adapters/request-response-factory.d.ts +20 -1
  7. package/dist/adapters/request-response-factory.d.ts.map +1 -1
  8. package/dist/adapters/request-response-factory.js +75 -22
  9. package/dist/bootstrap.d.ts +3 -9
  10. package/dist/bootstrap.d.ts.map +1 -1
  11. package/dist/bootstrap.js +179 -75
  12. package/dist/devtools/contracts.d.ts +3 -135
  13. package/dist/devtools/contracts.d.ts.map +1 -1
  14. package/dist/devtools/index.d.ts +2 -3
  15. package/dist/devtools/index.d.ts.map +1 -1
  16. package/dist/devtools/index.js +1 -2
  17. package/dist/devtools/snapshot.d.ts +1 -1
  18. package/dist/devtools/snapshot.d.ts.map +1 -1
  19. package/dist/devtools/snapshot.js +18 -17
  20. package/dist/devtools/studio-request-observer.d.ts +14 -0
  21. package/dist/devtools/studio-request-observer.d.ts.map +1 -0
  22. package/dist/devtools/studio-request-observer.js +112 -0
  23. package/dist/devtools/studio-runtime-config.d.ts +25 -0
  24. package/dist/devtools/studio-runtime-config.d.ts.map +1 -0
  25. package/dist/devtools/studio-runtime-config.js +59 -0
  26. package/dist/devtools/studio-runtime.d.ts +12 -18
  27. package/dist/devtools/studio-runtime.d.ts.map +1 -1
  28. package/dist/devtools/studio-runtime.js +25 -169
  29. package/dist/errors.d.ts +26 -0
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +38 -0
  32. package/dist/health/health.d.ts +6 -2
  33. package/dist/health/health.d.ts.map +1 -1
  34. package/dist/health/health.js +3 -2
  35. package/dist/http-adapter-shared.d.ts +1 -1
  36. package/dist/http-adapter-shared.d.ts.map +1 -1
  37. package/dist/http-adapter-shared.js +1 -1
  38. package/dist/index.d.ts +6 -3
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -1
  41. package/dist/internal/route-inspection-metadata.d.ts +30 -0
  42. package/dist/internal/route-inspection-metadata.d.ts.map +1 -0
  43. package/dist/internal/route-inspection-metadata.js +68 -0
  44. package/dist/internal.d.ts +6 -1
  45. package/dist/internal.d.ts.map +1 -1
  46. package/dist/internal.js +4 -1
  47. package/dist/module-definition.d.ts +10 -0
  48. package/dist/module-definition.d.ts.map +1 -0
  49. package/dist/module-definition.js +12 -0
  50. package/dist/module-graph.d.ts +41 -0
  51. package/dist/module-graph.d.ts.map +1 -1
  52. package/dist/module-graph.js +108 -13
  53. package/dist/multipart.d.ts +67 -3
  54. package/dist/multipart.d.ts.map +1 -1
  55. package/dist/multipart.js +665 -103
  56. package/dist/platform-component-registry.d.ts +9 -0
  57. package/dist/platform-component-registry.d.ts.map +1 -0
  58. package/dist/platform-component-registry.js +70 -0
  59. package/dist/platform-diagnostic-retention.d.ts +13 -0
  60. package/dist/platform-diagnostic-retention.d.ts.map +1 -0
  61. package/dist/platform-diagnostic-retention.js +43 -0
  62. package/dist/platform-shell-probe-reports.d.ts +8 -0
  63. package/dist/platform-shell-probe-reports.d.ts.map +1 -0
  64. package/dist/platform-shell-probe-reports.js +95 -0
  65. package/dist/platform-shell-probes.d.ts +16 -0
  66. package/dist/platform-shell-probes.d.ts.map +1 -0
  67. package/dist/platform-shell-probes.js +141 -0
  68. package/dist/platform-shell.d.ts +7 -13
  69. package/dist/platform-shell.d.ts.map +1 -1
  70. package/dist/platform-shell.js +58 -310
  71. package/dist/retryable-shutdown.d.ts +7 -0
  72. package/dist/retryable-shutdown.d.ts.map +1 -0
  73. package/dist/retryable-shutdown.js +20 -0
  74. package/dist/route-inspection.d.ts +56 -0
  75. package/dist/route-inspection.d.ts.map +1 -0
  76. package/dist/route-inspection.js +80 -0
  77. package/dist/tokens.d.ts +6 -4
  78. package/dist/tokens.d.ts.map +1 -1
  79. package/dist/tokens.js +5 -0
  80. package/dist/types.d.ts +53 -8
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/web.d.ts +22 -3
  83. package/dist/web.d.ts.map +1 -1
  84. package/dist/web.js +80 -34
  85. package/package.json +13 -26
  86. package/dist/internal-node.d.ts +0 -2
  87. package/dist/internal-node.d.ts.map +0 -1
  88. package/dist/internal-node.js +0 -1
  89. package/dist/logging/json-logger.d.ts +0 -8
  90. package/dist/logging/json-logger.d.ts.map +0 -1
  91. package/dist/logging/json-logger.js +0 -45
  92. package/dist/logging/logger.d.ts +0 -37
  93. package/dist/logging/logger.d.ts.map +0 -1
  94. package/dist/logging/logger.js +0 -103
  95. package/dist/node/internal-node-compression.d.ts +0 -22
  96. package/dist/node/internal-node-compression.d.ts.map +0 -1
  97. package/dist/node/internal-node-compression.js +0 -84
  98. package/dist/node/internal-node-request.d.ts +0 -162
  99. package/dist/node/internal-node-request.d.ts.map +0 -1
  100. package/dist/node/internal-node-request.js +0 -478
  101. package/dist/node/internal-node-response.d.ts +0 -28
  102. package/dist/node/internal-node-response.d.ts.map +0 -1
  103. package/dist/node/internal-node-response.js +0 -205
  104. package/dist/node/internal-node-shutdown.d.ts +0 -34
  105. package/dist/node/internal-node-shutdown.d.ts.map +0 -1
  106. package/dist/node/internal-node-shutdown.js +0 -83
  107. package/dist/node/internal-node.d.ts +0 -111
  108. package/dist/node/internal-node.d.ts.map +0 -1
  109. package/dist/node/internal-node.js +0 -283
  110. package/dist/node/node-compression.d.ts +0 -2
  111. package/dist/node/node-compression.d.ts.map +0 -1
  112. package/dist/node/node-compression.js +0 -1
  113. package/dist/node/node-request.d.ts +0 -2
  114. package/dist/node/node-request.d.ts.map +0 -1
  115. package/dist/node/node-request.js +0 -1
  116. package/dist/node/node-response.d.ts +0 -2
  117. package/dist/node/node-response.d.ts.map +0 -1
  118. package/dist/node/node-response.js +0 -1
  119. package/dist/node/node-shutdown.d.ts +0 -2
  120. package/dist/node/node-shutdown.d.ts.map +0 -1
  121. package/dist/node/node-shutdown.js +0 -1
  122. package/dist/node/node.d.ts +0 -2
  123. package/dist/node/node.d.ts.map +0 -1
  124. package/dist/node/node.js +0 -1
  125. package/dist/node.d.ts +0 -5
  126. package/dist/node.d.ts.map +0 -1
  127. package/dist/node.js +0 -3
package/README.ko.md CHANGED
@@ -4,12 +4,15 @@
4
4
 
5
5
  모듈 그래프를 컴파일하고 DI와 HTTP를 실행 가능한 애플리케이션 셸로 연결하는 어셈블리 레이어입니다.
6
6
 
7
+ Coordinated Node 24 릴리스를 준비한다면 패키지 업그레이드 전에 [소비자 마이그레이션 가이드](../../docs/getting-started/migrate-node24.ko.md)를 따르세요.
8
+
7
9
  ## 목차
8
10
 
9
11
  - [설치](#설치)
10
12
  - [사용 시점](#사용-시점)
11
13
  - [퀵 스타트](#퀵-스타트)
12
14
  - [주요 패턴](#주요-패턴)
15
+ - [Node 정적 에셋 source](#node-정적-에셋-source)
13
16
  - [동작 계약](#동작-계약)
14
17
  - [공개 API 개요](#공개-api-개요)
15
18
  - [관련 패키지](#관련-패키지)
@@ -21,13 +24,19 @@
21
24
  npm install @fluojs/runtime
22
25
  ```
23
26
 
27
+ 배포된 package는 의도적으로 package-wide `engines.node`를 선언하지 않습니다. Root, `./web`, runtime-neutral internal seam에는 eager Node builtin import가 없으며 Node, Bun, Deno, Cloudflare Workers 및 다른 Web 표준 host가 공유합니다. Node listener, filesystem, logger, compression, process-signal 책임은 검증된 Node engine 범위를 선언하는 `@fluojs/platform-nodejs`에 있습니다.
28
+
29
+ ## Node 정적 에셋 source
30
+
31
+ `@fluojs/http`는 portable static middleware와 representation-selection contract를 소유합니다. `@fluojs/platform-nodejs`는 Node filesystem `StaticAssetSource` 구현인 `createNodeFileSystemAssetSource(...)`를 export합니다. 이 helper는 configuration 단계에서 root directory를 검증하고 symlink 검사를 포함한 lexical 및 realpath 해석을 root 내부로 제한하며 `.br` 또는 `.gz` sibling을 선택할 수 있습니다. 선택된 각 regular-file representation은 검증된 파일을 열어 전체 파일을 immutable byte snapshot으로 즉시 복사하고 response write 전에 `FileHandle`을 닫습니다. 반환된 `source()`는 그 snapshot만 replay하며 pathname을 다시 열거나 lazy stream하지 않습니다. 따라서 애플리케이션 owner는 선택된 전체 파일 크기로 memory를 제한하고, `size`와 strong `ETag`는 정확히 그 byte를 설명합니다. Raw Node, Express, Fastify adapter는 이 portable middleware/source seam을 공유하고 adapter-specific re-encoding 대신 선택된 representation boundary를 보존합니다. 이 Node 전용 helper는 의도적으로 `@fluojs/runtime/web`에 없으며 Web 및 edge deployment는 애플리케이션이 소유한 source를 제공해야 합니다.
32
+
24
33
  ## 사용 시점
25
34
 
26
35
  다음과 같은 경우에 이 패키지를 사용합니다:
27
36
  - **fluo 애플리케이션 부트스트랩**: 모듈을 실행 중인 HTTP 서버나 마이크로서비스로 변환할 때.
28
37
  - **DI 및 라이프사이클 오케스트레이션**: 모듈 그래프 컴파일, 프로바이더 연결 및 애플리케이션 훅(`onModuleInit`, `onApplicationBootstrap`)을 관리할 때.
29
38
  - **독립형 컨텍스트 생성**: HTTP 서버는 필요 없지만 DI가 필요한 CLI task, script 또는 worker를 실행할 때.
30
- - **진단 및 검사**: CLI 내보내기를 위한 기계 읽기 가능한 플랫폼 snapshot diagnostic issue를 생산하되, 그래프 보기와 Mermaid 표현은 Studio에 맡길 때.
39
+ - **진단 및 검사**: CLI 내보내기를 위한 기계 읽기 가능한 플랫폼 snapshot, compiled route catalog, diagnostic issue를 생산하되, 그래프 보기와 Mermaid 표현은 Studio에 맡길 때.
31
40
 
32
41
  ## 퀵 스타트
33
42
 
@@ -62,8 +71,103 @@ const app = await fluoFactory.create(AppModule, {
62
71
  await app.listen();
63
72
  ```
64
73
 
74
+ 위 `@Get()`은 빈 상대 경로를 사용하므로 GET `/`을 제공합니다. `@Controller('cats')`라면
75
+ prefix를 무시하지 않고 `/cats`를 제공합니다. 빈 `@Module()`도 provider를 임의로 만들지
76
+ 않고 bootstrap하고 close할 수 있습니다.
77
+
65
78
  ## 주요 패턴
66
79
 
80
+ ### 헬스 엔드포인트 미들웨어
81
+
82
+ `HealthModule.forRoot()`는 생성된 health와 readiness route용 class 기반 `endpointMiddleware`를 받습니다. 미들웨어는 DI를 통해 해석되고 선언 순서대로 실행되며, 선택적 custom `path` 아래의 정규화된 두 엔드포인트 모두에 적용됩니다. 이 설정을 생략하면 기존 기본 동작이 유지됩니다.
83
+
84
+ ```typescript
85
+ HealthModule.forRoot({
86
+ endpointMiddleware: [HealthProbeAuthMiddleware],
87
+ path: '/internal/',
88
+ });
89
+ ```
90
+
91
+ 이 구성은 `HealthProbeAuthMiddleware`를 `/internal/health`와 `/internal/ready`에 적용하며, 관련 없는 애플리케이션 route에는 적용하지 않습니다.
92
+
93
+ ### 스트리밍 멀티파트 소비
94
+
95
+ 큰 업로드 파일을 `Uint8Array`로 실체화하지 않고 standalone raw `Request` 또는 request-like body에서
96
+ 소비할 때는 `@fluojs/runtime/web`의 `parseMultipartStream(...)`을 사용하세요. 기존
97
+ `parseMultipart(...)` API는 명시적인 buffered mode로 유지됩니다. 요청마다 정확히 하나의 mode를
98
+ 선택해야 하며, 같은 body를 buffered와 streaming으로 함께 파싱하면
99
+ `MultipartBodyConsumedError`로 reject됩니다.
100
+
101
+ ```typescript
102
+ import {
103
+ parseMultipartStream,
104
+ type MultipartFilePart,
105
+ } from '@fluojs/runtime/web';
106
+
107
+ for await (const part of parseMultipartStream(request, {
108
+ maxFieldSize: 1 * 1024 * 1024,
109
+ maxFields: 20,
110
+ maxFiles: 4,
111
+ maxFileSize: 20 * 1024 * 1024,
112
+ maxHeaderSize: 8 * 1024,
113
+ maxTotalSize: 25 * 1024 * 1024,
114
+ })) {
115
+ if (part.kind === 'field') {
116
+ continue; // part.name 및 part.value
117
+ }
118
+
119
+ const file: MultipartFilePart = part;
120
+ await store(file.stream); // 다음 part를 읽기 전에 끝까지 소비하거나 cancel
121
+ }
122
+ ```
123
+
124
+ Node.js, Express, Fastify, Web 애플리케이션 dispatch에서는 bootstrap의
125
+ `multipart.strategy: 'stream'`으로 opt in하세요. route는 `RequestContext.request.body`에서 같은
126
+ `AsyncIterable<MultipartPart>`를 받으며, adapter dispatch는 route가 소비하기 전 iterator만 만들고
127
+ pull 또는 buffering을 수행하지 않습니다.
128
+
129
+ ```typescript
130
+ const app = await bootstrapNodejsApplication(AppModule, {
131
+ multipart: {
132
+ strategy: 'stream',
133
+ maxTotalSize: 25 * 1024 * 1024,
134
+ },
135
+ });
136
+
137
+ @Controller('/uploads')
138
+ class UploadController {
139
+ @Post('/')
140
+ async upload(_input: undefined, context: RequestContext) {
141
+ for await (const part of context.request.body as AsyncIterable<MultipartPart>) {
142
+ // 다음 part로 진행하기 전에 각 file stream을 소비합니다.
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ Streaming mode에는 제한된 field/header 기본값이 적용됩니다. Buffered `parseMultipart(...)`는
149
+ `maxFieldSize`, `maxFields`, `maxHeaderSize`를 명시할 때만 해당 제한을 적용하여 기존 field/header
150
+ acceptance를 유지합니다.
151
+
152
+ `MultipartFilePart.stream`은 parser가 제어하는 backpressure를 가진 Web
153
+ `ReadableStream<Uint8Array>`입니다. Complete request가 도착하기 전에 file body를 yield하며,
154
+ active file stream이 다음 chunk를 필요로 할 때만 요청 chunk를 읽습니다. File stream cancel, request
155
+ abort, parser failure, 모든 size/count/header limit은 active source를 cancel하고 parser를 해제합니다.
156
+ Parser는 native Fetch `Request`와 native async-iterable Node/Express/Fastify request stream을 직접 또는
157
+ `MultipartRequestLike` wrapper로 받으며 adapter-native multipart object나 temporary-file API를 노출하지 않습니다.
158
+
159
+ ### Optional Early Hints capability
160
+
161
+ Runtime은 adapter가 소유하는 optional `context.response.earlyHints` capability를 보존하면서 이를 필수 response method surface로 만들지 않습니다. Node.js, Express, Fastify response는 writer를 제공하고 Web 표준 response factory는 이를 생략하므로 Bun, Deno, Workers, custom Fetch host가 unsupported임을 사용 전에 감지할 수 있습니다. Early write는 final status, header, body, commit ownership과 독립적입니다. 자세한 내용은 [`@fluojs/http` Early Hints 계약](../http/README.ko.md#early-hints)을 참고하세요.
162
+
163
+ ### Conditional request bootstrap
164
+
165
+ Runtime bootstrap은 `@fluojs/http`의 `conditionalRequest` option을 받습니다. Resolver는 명시적인 representation 존재 여부와 optional validator를 반환하며 middleware와 guard 뒤, interceptor와 controller 호출 전에 실행됩니다. Resolver shape, RFC 9110 precedence, `HEAD` 규칙은 [`@fluojs/http` Conditional Requests 계약](../http/README.ko.md#conditional-requests)을 참고하세요.
166
+
167
+ ### Access log observer
168
+
169
+ `createAccessLogObserver(...)`를 bootstrap `observers` option으로 전달하면 portable request lifecycle record를 애플리케이션 소유 structured logging으로 라우팅할 수 있습니다. Observer는 native adapter에서도 complete fallback path를 선택해 dispatcher lifecycle을 보존합니다. Trusted client identity와 header allowlist 요구사항은 [`@fluojs/http` Access logging 계약](../http/README.ko.md#access-logging)을 참고하세요.
170
+
67
171
  ### 애플리케이션 컨텍스트 (HTTP 제외)
68
172
 
69
173
  백그라운드 워커나 스크립트의 경우, `createApplicationContext`를 사용하여 HTTP 설정을 건너뛸 수 있습니다.
@@ -80,11 +184,42 @@ await userService.doWork();
80
184
  await context.close();
81
185
  ```
82
186
 
187
+ ### PlatformShell lifecycle overlap 마이그레이션
188
+
189
+ `RuntimePlatformShell.start()`와 `stop()`은 엄격한 exclusive transition입니다. 둘 중 하나가 active인 동안 겹치는 모든 `start()` 또는 `stop()` 호출은 `PlatformLifecycleConflictError`, code `PLATFORM_LIFECYCLE_CONFLICT`, 그리고 error field와 structured `meta` 모두에 있는 `activeOperation` / `requestedOperation`을 담은 즉시 reject된 promise를 반환합니다. Shell은 겹치는 작업을 공유하거나 queue 또는 coalesce하지 않습니다. Settle 이후의 순차 호출은 계속 idempotent하며, 실패한 transition은 exclusive gate를 해제하므로 caller가 명시적으로 retry할 수 있습니다.
190
+
191
+ `@fluojs/runtime` 2.x에서는 겹치는 `start()` 호출이 같은 component를 두 번 이상 시작할 수 있었고, in-flight startup 중 호출한 `stop()`이 startup settlement보다 먼저 반환하여 resource가 실행 중인 채로 남을 수 있었습니다. 업그레이드할 때는 하나의 application boundary가 각 lifecycle transition의 ownership을 갖게 하세요. 다른 경로가 겹칠 수 있다면 `PlatformLifecycleConflictError`를 catch하고 boundary-owned transition의 settlement를 기다린 다음, 원하는 상태가 여전히 필요할 때만 명시적으로 retry하세요. Callback reentry 주변에 숨은 queue를 다시 만들면 안 됩니다. Component lifecycle callback도 synchronous code 이후 또는 임의의 `await` boundary 이후 동일한 즉시 conflict를 받습니다.
192
+
193
+ ### NestJS 라이프사이클 훅 마이그레이션
194
+
195
+ 공개 runtime lifecycle 계약에는 네 개의 hook만 있습니다. Startup은 `onModuleInit()` 다음 `onApplicationBootstrap()`을 실행하고, shutdown은 lifecycle instance 역순으로 `onModuleDestroy()` 다음 `onApplicationShutdown(signal?)`을 실행합니다. 적격 singleton `multi: true` contribution은 각각 별도의 lifecycle instance이며, startup은 contribution 순서를 따르고 shutdown은 이를 뒤집습니다. NestJS `beforeApplicationShutdown`은 지원하지 않으며 fluo가 탐지하거나 호출하지 않습니다.
196
+
197
+ 종료 준비 작업은 소유하는 문서화된 phase로 옮기세요. Application-wide signal phase보다 먼저 끝나야 하는 module resource teardown에는 `onModuleDestroy()`를 사용하고, signal-aware application cleanup에는 `onApplicationShutdown(signal?)`을 사용합니다. `@fluojs/runtime`은 `beforeApplicationShutdown` compatibility shim, alias, fallback 또는 추가 runtime hook을 제공하지 않습니다.
198
+
199
+ NestJS `app.enableShutdownHooks()`는 모든 fluo bootstrap path에서 암묵적으로 적용되지 않습니다. 기본 Node `SIGINT` / `SIGTERM` wiring에는 `@fluojs/platform-nodejs`의 `runNodeApplication(...)`을 사용하세요. 이 helper가 기본 Node shutdown registration을 설치합니다. `FluoFactory.create(...)`, `bootstrapNodeApplication(...)`, adapter-first Node bootstrap은 signal ownership을 명시적으로 남겨 두므로 signal handling이 필요하면 해당 Node application boundary에서 `createNodeShutdownSignalRegistration(...)` 또는 `registerShutdownSignals(...)`를 사용하세요. Bun, Deno, Cloudflare Workers 같은 Fetch-style host는 자체 shutdown boundary를 소유합니다. 그 환경에 Node process signal을 설치하지 말고 host가 shutdown event를 받으면 `app.close(signal?)`를 호출하세요.
200
+
201
+ Lifecycle hook은 listener close 또는 connection drain phase가 아닙니다. `app.close(signal?)` 중 fluo는 `adapter.close(signal?)`보다 먼저 shutdown hook을 실행합니다. 닫힌 listener 또는 완료된 adapter drain이 필요한 migrated cleanup은 같은 이름의 lifecycle hook이 아니라 close 이후의 adapter 또는 host shutdown boundary에 두세요.
202
+
83
203
  ### Studio Devtools Bridge
84
204
 
85
- `@fluojs/runtime`은 live Studio snapshot과 request trace를 publish할 수 있지만 `process.env`를 직접 읽지 않습니다. `fluo dev --studio`가 애플리케이션 경계에서 sidecar를 시작하고 tokenized Studio config를 만든 뒤, 앱이 runtime을 import하기 전에 해당 명시적 configNode child에 주입합니다. CLI가 제공한 config가 없거나 잘못되었거나 tokenized endpoint가 없으면 Studio instrumentation은 no-op이며 bootstrap 동작은 바뀌지 않습니다.
205
+ `@fluojs/runtime`은 `process.env`를 직접 읽지 않고 live Studio snapshot과 request trace를 publish할 수 있습니다. `fluo dev --studio`는 기본 Node 경로로 유지됩니다. CLI가 sidecar를 시작하고 tokenized Studio config를 만든 앱이 runtime을 import하기 전에 주입합니다. Runtime은 주입된 field 번씩 읽고 HTTP(S) endpoint를 검증한 freeze된 private snapshot을 유지하므로 legacy process-global이 나중에 변경되어도 instrumentation input은 바뀌지 않습니다.
206
+
207
+ Package integration은 `@fluojs/runtime/devtools`에서 `StudioDevtoolsRuntime`과 transport contract를 import하고, host가 소유한 bridge를 `bootstrapApplication(...)`, `fluoFactory.create(...)`, 또는 `fluoFactory.createApplicationContext(...)`의 `studioDevtools`로 전달할 수 있습니다. 명시적 bridge는 CLI injection보다 우선하며 process-global mutation이 필요하지 않습니다.
86
208
 
87
- 이 MVP에서 전체 지원 대상은 Node dev runner 프로젝트입니다. Bun, Deno, Cloudflare Workers의 live Studio는 dedicated bridge를 구현하고 검증하기 전까지 unsupported입니다. 해당 런타임에서도 Studio config가 없으면 bootstrap은 no-op이어야 합니다. Request trace는 body, cookie, 전체 header를 의도적으로 제외하며, runtime은 local token이 Studio event history에 남지 않도록 publish 전에 trace `url`에서 query string과 fragment를 제거합니다.
209
+ ```typescript
210
+ import { fluoFactory } from '@fluojs/runtime';
211
+ import { StudioDevtoolsRuntime } from '@fluojs/runtime/devtools';
212
+
213
+ const studioDevtools = new StudioDevtoolsRuntime({
214
+ appId: 'my-bun-app',
215
+ runtime: 'bun',
216
+ transport: { publish: (event) => hostStudioTransport.send(event) },
217
+ });
218
+
219
+ const app = await fluoFactory.create(AppModule, { studioDevtools });
220
+ ```
221
+
222
+ 이 package는 transport-neutral seam을 publish하며 Bun, Deno, Cloudflare Workers sidecar 구현을 제공하지는 않습니다. Non-Node host는 소유자가 bridge와 executable host integration evidence를 제공한 경우에만 live Studio를 지원합니다. 그렇지 않으면 inspect/static artifact path를 사용하세요. Live route descriptor는 해당 route node의 정확한 `graphNodeId`를 포함합니다. Runtime은 기존 node-ID 형식을 유지하고 Studio는 이를 다시 구현하는 대신 명시적 correlation을 소비합니다. Request trace는 body, cookie, 전체 header를 의도적으로 제외하며, runtime은 local token이 Studio event history에 남지 않도록 publish 전에 trace `url`에서 query string과 fragment를 제거합니다. Failed-request event는 고정된 `Request failed` message만 사용하며 raw exception text, name, stack, cause, stringified value를 포함하지 않습니다.
88
223
 
89
224
  ### 전역 예외 필터
90
225
 
@@ -108,6 +243,77 @@ const app = await fluoFactory.create(AppModule, {
108
243
  });
109
244
  ```
110
245
 
246
+ ### Content negotiation
247
+
248
+ `FluoFactory.create(...)`와 `bootstrapApplication(...)`은 `contentNegotiation`을 받아 HTTP
249
+ dispatcher에 변경 없이 전달합니다. Application boundary에서 formatter를 한 번 구성하고 route의 허용
250
+ representation은 `@Produces(...)`로 선택하세요.
251
+
252
+ ```typescript
253
+ import { Controller, Get, Produces } from '@fluojs/http';
254
+ import { fluoFactory } from '@fluojs/runtime';
255
+
256
+ @Controller('/reports')
257
+ class ReportController {
258
+ @Produces('application/json', 'text/plain')
259
+ @Get('/')
260
+ getReport() {
261
+ return { ok: true };
262
+ }
263
+ }
264
+
265
+ const app = await fluoFactory.create(AppModule, {
266
+ contentNegotiation: {
267
+ defaultMediaType: 'application/json',
268
+ formatters: [
269
+ { mediaType: 'application/json', format: JSON.stringify },
270
+ { mediaType: 'text/plain', format: (value) => `plain:${JSON.stringify(value)}` },
271
+ ],
272
+ },
273
+ });
274
+ ```
275
+
276
+ Runtime은 `Accept`를 parse하거나 response policy를 소유하지 않습니다. `@fluojs/http`가 문서화된
277
+ quality, wildcard, suffix, default, malformed-input, 406 의미를 적용하고 성공한 모든 formatter 선택에
278
+ canonical `Vary: Accept`를 작성합니다. Standalone application context는 HTTP dispatcher를 만들지
279
+ 않으므로 이 option을 사용하지 않습니다. 자세한 내용은
280
+ [HTTP package contract](../http/README.ko.md#content-negotiation)를 참고하세요.
281
+
282
+ ### Optional HTML Error Representations
283
+
284
+ `FluoFactory.create(...)`와 `bootstrapApplication(...)`은 `errorRepresentation`을 받아 HTTP dispatcher에
285
+ 변경 없이 전달합니다. JSON을 canonical representation으로 유지하면서 negotiated browser request에 complete
286
+ HTML error/not-found document를 제공하려면 application-owned provider를 등록하세요.
287
+
288
+ ```typescript
289
+ function escapeHtml(value: string): string {
290
+ return value
291
+ .replaceAll('&', '&amp;')
292
+ .replaceAll('<', '&lt;')
293
+ .replaceAll('>', '&gt;')
294
+ .replaceAll('"', '&quot;')
295
+ .replaceAll("'", '&#39;');
296
+ }
297
+
298
+ const app = await fluoFactory.create(AppModule, {
299
+ adapter: createNodejsAdapter({ port: 3000 }),
300
+ errorRepresentation: {
301
+ html: {
302
+ render({ json }) {
303
+ return `<!doctype html><main>${json.error.status}: ${escapeHtml(json.error.message)}</main>`;
304
+ },
305
+ },
306
+ },
307
+ });
308
+ ```
309
+
310
+ Runtime은 이 option을 wiring만 합니다. Error classification, `Accept` negotiation, request scope, response status와
311
+ header, `HEAD`, abort, commit, canonical JSON fallback은 `@fluojs/http`가 소유합니다. 반환 string/byte는
312
+ application이 책임지는 trusted HTML입니다. Runtime은 request-derived 또는 error-derived value를 escape하거나
313
+ sanitize하지 않으므로 provider가 interpolation 전에 처리해야 합니다. Standalone application
314
+ context는 HTTP dispatcher를 생성하지 않으므로 이 option을 사용하지 않습니다. 자세한 내용은
315
+ [HTTP package contract](../http/README.ko.md#http-error-representations)를 참고하세요.
316
+
111
317
  ### Framework-managed response와 handler-owned response
112
318
 
113
319
  일반 request path는 framework-managed 방식입니다. Handler가 값을 반환하면 interceptor가 그 값을 변환할 수 있고, runtime response writer가 최종 결과를 commit합니다. `@fluojs/serialization`의 `SerializerInterceptor`가 반환 DTO에 적용되는 경로도 이 경로입니다.
@@ -134,8 +340,10 @@ class UsersModule {}
134
340
 
135
341
  ## 동작 계약
136
342
 
137
- - 요청 바디 파싱은 Web 표준 요청과 Node 기반 요청 모두에서 바이트가 스트리밍되는 동안 `maxBodySize`를 강제합니다.
138
- - `@fluojs/runtime/node`에서는 Node 요청 바디 파싱 전에 primary `content-type` media typenormalize한 JSON 멀티파트 여부를 판단하므로, 대소문자가 섞인 JSON/멀티파트 헤더도 문서화된 파서 동작을 그대로 유지합니다.
343
+ - Runtime lifecycle은 hook 계약을 유지합니다. Startup은 provider order의 `onModuleInit()` phase를 끝낸 `onApplicationBootstrap()`을 실행하고, shutdown은 lifecycle instance 역순으로 `onModuleDestroy()` 다음 `onApplicationShutdown(signal?)`을 실행합니다. 모든 적격 singleton `multi: true` contribution은 contribution 순서에 따른 별도 instance로 참여합니다. NestJS `beforeApplicationShutdown`은 지원하지 않으며 compatibility shim도 없습니다.
344
+ - 요청 바디 파싱은 Web 표준 요청과 Node 기반 요청 모두에서 바이트가 스트리밍되는 동안 `maxBodySize`를 강제합니다. 한도를 넘은 Web 바디는 stream cancellation기다리지 않고 HTTP 413으로 완료되며, cancellation 실패도 해당 응답을 가리지 않습니다. 계약은 원본 요청을 읽지 않는 기본 cloned-body 경로에도 적용됩니다.
345
+ - `preferNativeJsonBodyReader`는 deprecated adapter compatibility 옵션으로 `@fluojs/runtime/web`에서 계속 허용되지만 더 이상 파싱 동작을 바꾸지 않습니다. Web JSON 바디는 항상 bounded streaming reader를 사용하므로 native whole-body read가 `maxBodySize`를 우회할 수 없습니다.
346
+ - `@fluojs/platform-nodejs`에서는 Node 요청 바디 파싱 전에 primary `content-type` media type을 normalize한 뒤 JSON 및 멀티파트 여부를 판단하므로, 대소문자가 섞인 JSON/멀티파트 헤더도 문서화된 파서 동작을 그대로 유지합니다.
139
347
  - Node 기반 및 Web 표준 요청 wrapper는 바디 파싱 전에 저비용 요청 metadata를 snapshot으로 고정한 뒤 dispatch 경계에서 `body`/`rawBody`를 한 번 materialize하므로 userland는 계속 동기 parsed 값을 관찰합니다.
140
348
  - Node 기반 쿠키/쿼리 값과 Web 표준 헤더는 요청 wrapper가 생성되는 시점에 snapshot으로 고정된 뒤 요청별로 lazy하게 normalize되고 memoize됩니다. 이후 upstream 객체가 변경되어도 `FrameworkRequest` view는 바뀌지 않습니다.
141
349
  - Node 기반 request context ID는 `x-request-id`를 우선 사용하고, `x-request-id`가 없으면 `x-correlation-id`로 fallback합니다. 따라서 error response와 request-aware integration이 upstream correlation identifier를 유지합니다.
@@ -144,7 +352,9 @@ class UsersModule {}
144
352
  - `duplicateProviderPolicy`가 `warn` 또는 `ignore`일 때 context cache 적격성과 lifecycle hook 실행은 bootstrap이 선택한 effective winning provider를 기준으로 결정됩니다. stale losing provider는 cache entry나 lifecycle hook을 만들지 않습니다.
145
353
  - 모듈 그래프 컴파일은 cache key 생성이나 visibility 순회 전에 runtime provider와 `@Module(...)` provider 선언을 DI의 canonical normalization으로 검증합니다. 따라서 잘못된 `inject` 값, dependency wrapper/token, scope는 순회 단계 고유 오류를 노출하지 않고 `InvalidProviderError`로 실패합니다.
146
354
  - 애플리케이션 또는 컨텍스트 bootstrap이 런타임 리소스나 lifecycle instance 생성 이후 실패하면 fluo는 readiness를 초기화하고, 등록된 runtime cleanup callback을 실행하며, 그 시점까지 해석된 instance의 shutdown hook을 `bootstrap-failed`로 호출하고, 컨테이너를 dispose하고, cleanup 실패를 로그로 남긴 뒤 원래 bootstrap error를 다시 던집니다.
147
- - `Application.listen()`과 microservice `listen()`은 shutdown과 직렬화됩니다. 겹치는 startup 호출은 같은 in-flight startup을 공유하고, shutdown은 진행 중인 startup이 끝날 때까지 기다리며, shutdown과 경합한 startup은 close 시작 이후 shell을 다시 `ready`로 전이할 수 없습니다.
355
+ - `Application.listen()`과 microservice `listen()`은 shutdown과 직렬화됩니다. 겹치는 startup 호출은 같은 in-flight startup을 공유하고, shutdown은 진행 중인 startup이 끝날 때까지 기다리며, shutdown과 경합한 startup은 close 시작 이후 shell을 다시 `ready`로 전이할 수 없습니다. 공개 `Application.state` 계약은 teardown이 pending인 동안 `bootstrapped` 또는 `ready`를 유지하고 teardown이 성공적으로 완료된 뒤에만 `closed`로 바뀝니다. 이 상태와 별개로 application 또는 context close 시작은 terminal operation gate를 동기적으로 닫습니다. 따라서 `Application.get()`, `ApplicationContext.get()`, `connectMicroservice()`, `startAllMicroservices()`, application `listen()`은 teardown이 pending인 동안 reject되고 close 시도가 실패한 뒤에도 계속 reject됩니다. Close 직전에 admission된 provider lookup도 asynchronous resolution 뒤 이 gate를 다시 검사하므로 shutdown 시작 이후 stale value를 반환할 수 없습니다. 이후 `close()`는 완료된 runtime teardown phase를 건너뛰고 incomplete adapter 또는 lifecycle-hook stage를 각자의 retry contract에 따라 다시 실행합니다. Container-managed `onDestroy()` hook은 terminal best-effort cleanup입니다. 첫 container disposal에서 materialize된 hook을 모두 시도하고, 실패한 hook만 이후 명시적 application 또는 context `close()`에서 재시도하며, 성공한 hook은 다시 실행하지 않습니다. Microservice close가 시작되면 terminal ingress gate가 새 `send()`와 `emit()` 호출을 `listen()`이 아직 pending 상태이거나 close 시도가 실패한 뒤에도 runtime 또는 transport handoff 전에 reject합니다.
356
+ - `Application.dispatch()`도 같은 동기 terminal admission gate를 사용합니다. `Application.close()`가 시작된 뒤의 direct dispatch는 HTTP dispatcher에 들어가기 전에 reject되며, teardown이 pending인 동안, close가 실패한 뒤, 성공적으로 close된 뒤에도 마찬가지입니다. Gate가 닫히기 전에 admission된 dispatch는 dispatcher가 소유하며 close가 소급해 취소하지 않습니다.
357
+ - `@fluojs/platform-nodejs`는 pending 상태인 각 raw Node listen 작업과 해당 `EADDRINUSE` retry timer를 소유합니다. Startup이 retry 중일 때 adapter `close()`를 호출하면 retry를 취소하고 pending listen이 settle될 때까지 기다리며, shutdown 완료가 보고된 뒤 listener가 bind되지 않도록 보장합니다.
148
358
  - 종료 시그널 등록 실패는 사용자가 관찰할 수 있습니다. `runNodeApplication(...)`, `bootstrapNodeApplication(...)`, adapter 소유 runtime helper는 이미 시작된 애플리케이션을 `bootstrap-failed`로 닫고, close 실패가 있으면 별도로 로그로 남기며, 원래 registration error로 reject합니다.
149
359
  - 종료 시그널 등록 해제 실패는 애플리케이션 close를 건너뛰지 않습니다. `app.close()`는 항상 adapter shutdown, lifecycle hook, runtime cleanup callback, container dispose까지 계속 진행합니다. close 자체가 성공하면 unregistration error로 reject하고, close도 실패하면 두 실패를 모두 담은 aggregate로 reject합니다.
150
360
  - 연결된 microservice는 부모 `Application`이 소유하는 child입니다. `startAllMicroservices()`는 순차적으로 시작하며 이후 child 시작이 실패하면 이미 시작된 child를 `bootstrap-failed`로 rollback하고, `Application.close(signal)`은 부모 lifecycle hook, adapter 종료, container dispose보다 먼저 연결된 child를 닫습니다.
@@ -152,17 +362,22 @@ class UsersModule {}
152
362
  - Bootstrap은 독립적인 singleton lifecycle provider를 병렬로 해석한 뒤 lifecycle hook은 결정적인 provider 순서대로 실행합니다.
153
363
  - 멀티파트 파싱은 누적 바디 크기가 설정된 `multipart.maxTotalSize`를 넘으면 즉시 거부되며, 런타임 어댑터는 별도 재정의가 없으면 이 한도를 `maxBodySize`와 동일하게 맞춥니다.
154
364
  - `@fluojs/runtime/web` 멀티파트 파싱은 Node.js `Buffer` global 없이 Web 표준 `TextEncoder`와 `Uint8Array` primitive만 사용합니다. 업로드 파일의 `buffer` 값은 `Uint8Array`이며, Node 전용 consumer는 애플리케이션 경계에서 `Buffer.from(file.buffer)`로 명시적으로 변환할 수 있습니다.
365
+ - `@fluojs/runtime/web`은 서로 배타적인 두 멀티파트 소비 mode를 노출합니다. `parseMultipart(...)`는 field와 file을 buffer하고, `parseMultipartStream(...)`은 discriminated field/file part를 yield하며 complete file payload를 실체화하지 않습니다. Streaming mode는 byte를 읽는 동안 per-field, per-file, total-size, field-count, file-count, header limit을 강제하고 abort, cancellation, parser failure가 active source를 cancel합니다. 두 mode 중 하나가 선택한 body를 다시 buffered 또는 streaming으로 선택하면 `MultipartBodyConsumedError`로 reject됩니다.
155
366
  - `createNodeHttpAdapter(...)`, `bootstrapNodeApplication(...)`, `runNodeApplication(...)`는 `maxBodySize`를 0 이상의 정수 바이트 수로만 받으며, 값이 잘못되면 어댑터 생성/부트스트랩 단계에서 즉시 실패합니다.
156
367
  - 응답 스트림 백프레셔 헬퍼는 `drain`, `close`, `error` 중 어느 경우에도 `waitForDrain()`을 완료시켜 끊어진 연결에서 스트리밍 작성기가 멈추지 않도록 합니다.
368
+ - HTTP application bootstrap은 optional application-owned `errorRepresentation.html` provider를 representation ownership 없이 dispatcher에 전달합니다. Canonical JSON은 default로 유지되며 classification, negotiation, status/header, `HEAD`, abort, commit, fallback 의미는 HTTP가 소유합니다.
157
369
  - HTTP response writing은 단일 owner를 가집니다. Framework-managed handler 결과는 runtime이 commit하기 전에 interceptor가 변환할 수 있습니다. Handler나 response helper가 `RequestContext.response`를 commit한 뒤에는 dispatcher가 두 번째 success-response write를 건너뜁니다. `SerializerInterceptor`는 serialization을 우회하고 `next.handle()`에서 받은 값을 그대로 반환하지만, 다른 interceptor는 chain 결과를 계속 변환할 수 있습니다.
158
370
  - 런타임 health 모듈은 bootstrap이 ready로 표시하기 전까지 `/ready`를 HTTP 503과 `starting`으로 보고하며, 애플리케이션/컨텍스트 종료가 시작되는 즉시, 종료 시도가 실패하더라도 다시 `starting`으로 내려갑니다.
159
371
  - 런타임 health module readiness check는 현재 `RequestContext`를 받으므로, public integration이 internal runtime token을 import하지 않고도 runtime-exposed status provider를 해석할 수 있습니다.
160
372
  - 시그널 기반 종료 헬퍼는 bounded drain semantics를 유지하면서 timeout/실패 상황을 로그와 `process.exitCode`로 보고하지만, 최종 프로세스 종료 소유권은 주변 호스트 런타임에 남겨 둡니다.
161
373
  - 플랫폼 snapshot 및 diagnostic issue 생산은 런타임에 남아 있고, 그래프 보기, filtering 표현, Mermaid 렌더링은 CLI 및 자동화 호출자가 소비하는 Studio 소유 계약입니다.
162
- - Runtime-connected Studio instrumentation명시적인 CLI 주입 Studio config로만 활성화되며 runtime package source에서 `process.env`를 직접 읽지 않습니다. 유효한 config와 tokenized endpoint가 없으면 non-Node 런타임을 포함해 Studio 관점의 runtime bootstrapno-op입니다.
374
+ - Compiled route inspection`HandlerDescriptor` 값의 one-way projection입니다. Effective method, path, version, params, module, controller, handler field를 freeze된 entry로 복사합니다. 일반 route는 `kind: 'http'`를 사용하고 runtime-aware integration은 `react-page` 같은 구체적인 marker를 publish할 수 있습니다. Route inspectionmatching, conflict detection, dispatch에 관여하지 않으며 request body, cookie, header, query value 또는 다른 request-private data를 보관하지 않습니다.
375
+ - Runtime-connected Studio instrumentation은 명시적인 host-owned `studioDevtools` bridge 또는 기본 CLI 주입 Node config를 받고, runtime package source에서 `process.env`를 직접 읽지 않습니다. 문서화된 `@fluojs/runtime/devtools` subpath는 package integration이 private import나 process-global mutation 없이 사용할 transport-neutral bridge contract를 노출합니다. 명시적 bridge가 우선하며, CLI config는 한 번 캡처되어 검증되고 freeze된 private snapshot이 됩니다. bridge와 유효한 config가 모두 없을 때만 Studio 관점의 runtime bootstrap은 no-op입니다.
163
376
  - Studio request trace는 request/response body, cookie, 전체 header를 제외합니다. Trace `url`은 publish 전에 path-only 형태로 sanitize되어 query token과 fragment가 local Studio event history에 남지 않습니다.
164
377
  - 플랫폼 component snapshot은 런타임 소유 계약 payload입니다. 각 component는 `readiness`, `health`, dependency id, telemetry tag, diagnostic issue, 그리고 `ownership.ownsResources` / `ownership.externallyManaged`를 통해 리소스 소유권을 보고합니다. Runtime은 shell snapshot에서 이 ownership flag를 보존하므로 adapter와 package integration이 fluo가 종료해야 하는 리소스와 host가 소유한 외부 관리 리소스를 구분할 수 있습니다.
165
- - 모듈 그래프 컴파일 결과 캐시는 `moduleGraphCache: true`를 통한 opt-in입니다. 캐시 항목은 root module identity, runtime provider, validation token, module replacement pair, core metadata version, compile algorithm version으로 식별되며, 성공한 컴파일만 저장하고 호출자 mutation이 이후 bootstrap을 오염시키지 않도록 격리된 그래프 복사본을 반환합니다.
378
+ - Runtime은 validation, start, rollback, stop에서 발생한 서로 다른 lifecycle diagnostic을 보존합니다. 반복 가능한 `ready()`, `health()`, `snapshot()` probe가 생성한 실패는 component와 probe phase별 최신 실패 하나로 제한되므로, 장기 polling이 `PlatformShellSnapshot.diagnostics`를 무제한으로 늘리지 않으면서도 최신 cause는 계속 확인할 있습니다.
379
+ - `RuntimePlatformShell.start()`와 `stop()`은 하나의 엄격한 exclusive lifecycle transition을 강제합니다. 같은 operation 호출이나 임의의 await 이후 callback reentry를 포함한 모든 겹치는 operation은 shared/queued work 대신 즉시 `PlatformLifecycleConflictError` rejection을 받습니다. Active transition은 component work 시작 전에 publish되고 실패 시 identity 기준으로 해제됩니다. Settlement 이후의 명시적 retry는 sequential idempotency, dependency ordering, private startup rollback, cleanup retry behavior를 보존합니다.
380
+ - 모듈 그래프 컴파일 결과 캐시는 `moduleGraphCache: true`를 통한 opt-in입니다. Process-local cache는 최근 사용 순서 기준 최대 100개의 성공한 snapshot만 유지하고, cache 항목은 root module identity, runtime provider, validation token, module replacement pair, core metadata version, compile algorithm version으로 식별되며, 호출자 mutation이 이후 bootstrap을 오염시키지 않도록 격리된 그래프 복사본을 반환합니다. Application-owned lifetime control이 필요한 host는 `new ModuleGraphCompileCache(maxEntries)`를 `moduleGraphCache`로 전달하고 application teardown 중 `dispose()`를 호출할 수 있습니다.
166
381
  - `moduleReplacements`는 `bootstrapModule(...)` / `BootstrapModuleOptions`의 저수준 testing seam입니다. 원래 logical module identity를 보존하면서 replacement module metadata로 컴파일하고, replacement cycle은 일반 module graph validation 경로에서 거부하며, source module metadata를 mutate하지 않습니다.
167
382
  - `raceWithAbort(fn, signal)`은 `fn`이 settle된 후 항상 abort listener를 제거합니다. `fn`이 promise를 반환하기 전에 동기적으로 throw하는 경우도 포함합니다. 동기 throw는 settled rejection으로 변환되어 cleanup-dependent `finally` flow가 여전히 실행되고, 반복된 실패 작업에서 listener가 leak되지 않습니다.
168
383
 
@@ -173,43 +388,62 @@ class UsersModule {}
173
388
  - `Application`: `ApplicationContext`를 확장하며 `listen()`, `dispatch()`, `state`를 포함합니다.
174
389
  - `ApplicationContext`: `get<T>(token)`, `close()` 기능을 제공하며 `container`, `modules`, bootstrap diagnostics에 접근할 수 있습니다.
175
390
  - `LifecycleHooks`: `OnModuleInit`, `OnApplicationBootstrap`, `OnModuleDestroy`, `OnApplicationShutdown`를 묶는 편의 union 타입입니다.
391
+ - `MicroserviceRuntime`: `FluoFactory.createMicroservice(...)`가 해석하는 transport 계약입니다. 구현체는 `listen()`, 선택적 `send()`/`emit()`, 선택적 `close(signal?)`을 노출합니다. 선택적 `markShutdownStarted()` hook은 소유 shell이 종료를 시작할 때 동기적으로 호출되므로, 구현체는 어떤 awaited cleanup보다 먼저 자체 ingress gate를 닫을 수 있고, 경쟁 중인 `listen()`이 아직 settle되는 동안에도 새 `send()`/`emit()`/`listen()` 시도가 계속 reject됩니다.
176
392
  - `HealthModule.forRoot(options)`: bootstrap 및 shutdown 라이프사이클 전이에 맞춰 readiness marker를 관리하는 런타임 소유 `/health`, `/ready` 모듈 파사드입니다. `RuntimeHealthModule`을 반환하므로 first-party runtime-aware package가 internal runtime seam을 import하지 않고 `ReadinessCheck` function을 등록할 수 있습니다.
177
393
  - `createHealthModule(options)`: 같은 런타임 health module 계약을 위한 deprecated compatibility helper입니다. 애플리케이션-facing module import에서는 `HealthModule.forRoot(...)`를 우선 사용하세요.
178
394
  - `RuntimeHealthModule`: `HealthModule.forRoot(...)`가 반환하는 module class contract이며 `addReadinessCheck(...)`, `markReady()`, `markStarting()`을 포함합니다.
179
395
  - `ReadinessCheck`: runtime health module이 사용하는 function type입니다. Check는 `/ready` request context를 받고 boolean 또는 promise를 반환합니다.
180
396
  - `defineModule(cls, metadata)`: 프로그래밍 방식의 모듈 정의 헬퍼입니다.
181
- - `bootstrapApplication(options)`: 저수준 비동기 부트스트랩 함수입니다.
397
+ - `bootstrapApplication(options)`: 저수준 비동기 부트스트랩 함수입니다. `BootstrapApplicationOptions.errorRepresentation`은 optional HTTP-owned HTML representation provider를 등록하고 `BootstrapApplicationOptions.conditionalRequest`는 representation validation을 구성하며 `CreateApplicationOptions`는 `FluoFactory.create(...)`에서 두 field를 노출합니다.
398
+ - `@fluojs/runtime/devtools`: `StudioDevtoolsRuntime`, transport contract, live Studio event contract를 위한 package-integration subpath입니다. 생성한 bridge를 application 또는 context bootstrap의 `studioDevtools`로 전달합니다.
182
399
  - `bootstrapModule(...)`: 저수준 module graph bootstrap helper입니다. `BootstrapModuleOptions`에는 opt-in compile-result cache를 위한 `moduleGraphCache`와 authored module identity를 안정적으로 유지하는 testing-only module replacement compilation을 위한 `moduleReplacements` / `ModuleReplacementMap`이 포함됩니다.
400
+ - `ModuleGraphCompileCache`: Bounded caller-owned module graph compile cache입니다. `moduleGraphCache`로 instance를 전달하고 application 또는 host lifetime이 끝날 때 `dispose()`를 호출하세요.
183
401
  - `createBootstrapTimingDiagnostics(...)`, `createRuntimeDiagnosticsGraph(...)`: CLI/support tooling을 위한 runtime 소유 diagnostics snapshot helper입니다. 이 helper들은 기계 읽기 가능한 데이터를 생산하며, Studio가 viewer parsing, graph presentation, Mermaid rendering을 소유합니다.
402
+ - `createRuntimeRouteInspection(...)`, `createRuntimeRouteCatalog(...)`, `createRuntimeInspectionSnapshot(...)`: HTTP route behavior를 변경하지 않고 platform snapshot에 effective compiled route diagnostics를 추가하는 runtime-owned immutable projection입니다.
403
+ - `RuntimeRouteInspection`, `RuntimeInspectionSnapshot`: serializable read-only route 및 inspect artifact contract입니다. `RuntimeRouteInspection.params`에는 parameter name만 포함되고 request value는 포함되지 않습니다.
184
404
  - `PlatformShell`, `PlatformComponent`, `PlatformShellSnapshot`, `PlatformSnapshot`, `PlatformDiagnosticIssue` 및 관련 platform report 타입: runtime-aware package가 사용하는 공개 lifecycle diagnostics 및 resource-ownership 계약입니다. `RuntimePlatformShell`은 component가 제공한 ownership을 보존하고, consumer가 internal runtime token을 import하지 않아도 validation/readiness/health diagnostics를 내보냅니다.
405
+ - `PlatformLifecycleOperation`, `PlatformLifecycleConflictError`: root-exported lifecycle conflict 계약입니다. Error는 code `PLATFORM_LIFECYCLE_CONFLICT`를 사용하고 일치하는 `activeOperation` / `requestedOperation` field와 structured metadata를 노출합니다.
185
406
  - `createRequestAbortContext(...)`, `trackActiveRequestTransaction(...)`, `untrackActiveRequestTransaction(...)`: runtime-aware integration이 사용하는 request abort 및 active transaction helper입니다.
186
407
  - `UploadedFile`: 메모리 내 `buffer` payload를 Web 표준 `Uint8Array`로 제공하는 runtime-neutral 멀티파트 파일 descriptor입니다.
408
+ - `MultipartFieldPart`, `MultipartFilePart`, `MultipartPart`, `MultipartBodyConsumedError`: 타입화된 streaming 멀티파트 계약입니다. `MultipartFilePart.stream`은 single-consumer이며 iteration이 다음 part를 요청하기 전에 settle되어야 합니다.
409
+
410
+ ## 런타임 전용 진입점
411
+
412
+ Node host 책임에는 `@fluojs/platform-nodejs`를 사용하고 portable Web 표준 helper에는 `@fluojs/runtime/web`을 사용하세요. Runtime의 공개 `internal*` 서브경로는 first-party adapter와 runtime-aware package를 위한 runtime-neutral package-integration seam으로 유지됩니다.
187
413
 
188
- ## 플랫폼 전용 서브경로
414
+ Migration은 직접적이며 의도적으로 compatibility shim을 제공하지 않습니다.
415
+
416
+ | 제거된 import | 대체 import |
417
+ | :--- | :--- |
418
+ | `@fluojs/runtime/node` | `@fluojs/platform-nodejs` |
419
+ | `@fluojs/runtime/internal-node` | `@fluojs/platform-nodejs/internal` |
189
420
 
190
- 애플리케이션-facing 런타임 헬퍼에는 `@fluojs/runtime/node`와 `@fluojs/runtime/web`를 사용하세요. 공개된 `internal*` 서브경로는 first-party adapter와 runtime-aware package를 위한 package-integration seam으로 예약되어 있습니다. 이 표는 패키지 작성자가 경계를 식별할 수 있도록 문서화하지만, 해당 seam을 애플리케이션 수준 helper 계약으로 취급하지는 않습니다.
421
+ 이동한 모든 symbol은 대체 entrypoint에서 기존 이름을 유지하며, `@fluojs/platform-nodejs`는 기존 `Nodejs*` alias도 유지합니다.
191
422
 
192
423
  | 서브경로 | 용도 |
193
424
  | :--- | :--- |
194
- | `@fluojs/runtime/node` | 로거 팩토리, Node 어댑터/부트스트랩 헬퍼, 종료 시그널 등록을 위한 지원되는 Node.js 전용 진입점입니다. |
195
- | `@fluojs/runtime/web` | Bun, Deno, Cloudflare Workers를 위한 공유 Web 표준 요청/응답 유틸리티입니다. `createWebRequestResponseFactory`, `dispatchWebRequest`, `createWebFrameworkRequest`, `parseMultipart`를 포함합니다. |
196
- | `@fluojs/runtime/internal` | runtime wiring token과, compiled module graph의 provider scope와 정렬되어야 하는 first-party adapter가 사용하는 runtime-owned class metadata reader를 위한 internal package-integration seam입니다. |
197
- | `@fluojs/runtime/internal-node` | adapter/runtime plumbing을 위한 Node 전용 internal seam이며, 애플리케이션 코드에서는 `@fluojs/runtime/node`를 우선 사용하세요. |
425
+ | `@fluojs/platform-nodejs` | 로거 팩토리, Node 어댑터/부트스트랩 헬퍼, 종료 시그널 등록을 위한 지원되는 Node.js 전용 진입점입니다. |
426
+ | `@fluojs/runtime/web` | Bun, Deno, Cloudflare Workers를 위한 공유 Web 표준 요청/응답 유틸리티입니다. `createWebRequestResponseFactory`, `dispatchWebRequest`, `createWebFrameworkRequest`, buffered `parseMultipart`, streaming `parseMultipartStream`을 포함합니다. |
427
+ | `@fluojs/runtime/internal` | runtime wiring token, runtime-owned metadata route-inspection helper함께 compiled runtime descriptor에 정렬되어야 하는 first-party runtime-neutral integration을 위한 `defineModule(...)`, `createRuntimeRouteInspection(...)`을 제공하는 internal package-integration seam입니다. |
428
+ | `@fluojs/platform-nodejs/internal` | adapter/runtime plumbing을 위한 Node 전용 internal seam이며, 애플리케이션 코드에서는 `@fluojs/platform-nodejs`를 우선 사용하세요. |
198
429
  | `@fluojs/runtime/internal/http-adapter` | platform package를 위한 internal HTTP adapter seam입니다. |
199
430
  | `@fluojs/runtime/internal/request-response-factory` | platform package를 위한 internal request/response factory seam입니다. |
200
431
 
201
- ### Node 전용 서브경로 (`@fluojs/runtime/node`)
432
+ ### Node 전용 패키지 (`@fluojs/platform-nodejs`)
202
433
 
203
- 로거 팩토리와 지원되는 기타 Node 전용 헬퍼는 범용 루트 진입점에 포함되지 않습니다. `./node` 서브경로에서 가져오세요:
434
+ 로거 팩토리, eager immutable Node/Express/Fastify 정적 에셋 snapshot용 `createNodeFileSystemAssetSource({ root, precompressed })`, 지원되는 기타 Node 전용 헬퍼는 portable runtime root에 포함되지 않습니다. Node platform package에서 가져오세요:
204
435
 
205
436
  ```typescript
206
437
  import {
207
438
  bootstrapNodeApplication,
208
439
  createConsoleApplicationLogger,
209
440
  createJsonApplicationLogger,
441
+ createNodeFileSystemAssetSource,
210
442
  createNodeHttpAdapter,
211
443
  runNodeApplication,
212
- } from '@fluojs/runtime/node';
444
+ type NodeFileSystemAssetPrecompression,
445
+ type NodeFileSystemAssetSourceOptions,
446
+ } from '@fluojs/platform-nodejs';
213
447
  ```
214
448
 
215
449
  ```typescript
@@ -223,6 +457,7 @@ const adapter = createNodeHttpAdapter({
223
457
 
224
458
  - `createConsoleApplicationLogger()`: `process.stdout`/`process.stderr`를 사용하는 컬러 콘솔 로거입니다. 기본값은 pretty 형식입니다. 더 간결한 `[fluo] LEVEL [context] message` 줄을 원하면 `{ mode: 'minimal' }`, 런타임 로거 출력을 숨기려면 `{ mode: 'silent' }`, 낮은 심각도 메시지를 걸러내려면 `{ level: 'warn' }` 같은 threshold, 결정적인 비컬러 출력을 원하면 `{ color: false }`를 전달하세요.
225
459
  - `createJsonApplicationLogger()`: `process.stdout`/`process.stderr`를 사용하는 구조화된 JSON 로거.
460
+ - `createNodeFileSystemAssetSource(options)`: `@fluojs/http`의 `StaticAssetSource` contract를 구현하는 Node 전용 filesystem source입니다. `NodeFileSystemAssetSourceOptions`는 `{ root, precompressed }` 경계를 이름 붙이고 `NodeFileSystemAssetPrecompression`은 `.br` / `.gz` sibling 선택을 제어합니다. 허용된 각 representation은 안전하게 열어 immutable in-memory byte snapshot으로 즉시 복사하고 middleware response write 전에 `FileHandle`을 닫습니다. 반환된 `source()`는 그 snapshot만 replay하며 pathname을 다시 열지 않습니다. 따라서 애플리케이션 owner는 선택된 asset 크기로 memory를 제한하고, `size`와 strong `ETag`는 정확히 그 snapshot byte를 설명합니다.
226
461
  - `createNodeHttpAdapter()`: 어댑터 우선 런타임 구성을 위한 raw Node `http`/`https` 어댑터 팩토리입니다. primary Node 요청 `content-type`을 JSON/멀티파트 판별 전에 normalize하며, `maxBodySize`, `retryDelayMs`, `retryLimit`, `shutdownTimeoutMs`는 0 이상의 정수만 받습니다.
227
462
  - `bootstrapNodeApplication()` / `runNodeApplication()`: 직접 Node runtime flow에서 사용하는 Node 전용 부트스트랩 헬퍼.
228
463
  - `createNodeShutdownSignalRegistration()`, `defaultNodeShutdownSignals()`, `registerShutdownSignals()`: 호스트가 명시적으로 시그널 wiring을 제어할 때 쓰는 종료 등록 헬퍼.
@@ -230,7 +465,7 @@ const adapter = createNodeHttpAdapter({
230
465
  런타임 애플리케이션 로깅은 CLI lifecycle reporting과 별개입니다. 애플리케이션/런타임 자체가 내는 로그를 바꾸고 싶을 때 `ApplicationLogger`를 설정하세요:
231
466
 
232
467
  ```typescript
233
- import { createConsoleApplicationLogger, createJsonApplicationLogger } from '@fluojs/runtime/node';
468
+ import { createConsoleApplicationLogger, createJsonApplicationLogger } from '@fluojs/platform-nodejs';
234
469
 
235
470
  const minimalLogger = createConsoleApplicationLogger({ mode: 'minimal', level: 'warn' });
236
471
  const jsonLogger = createJsonApplicationLogger();
@@ -238,7 +473,30 @@ const jsonLogger = createJsonApplicationLogger();
238
473
 
239
474
  개발 명령의 raw child-process 출력이 필요하면 대신 `fluo dev --verbose` 같은 CLI reporter flag를 사용하세요.
240
475
 
241
- 저수준의 Node compression internals는 공개 `@fluojs/runtime/node` 계약이 아니라 `@fluojs/runtime/internal-node` seam 뒤에 둡니다.
476
+ ### Node 압축 실패 마이그레이션
477
+
478
+ **호환성 깨짐:** Node response가 commit되기 전에 압축이 실패하면
479
+ `FrameworkResponse.send()`는 reject됩니다. 어댑터 integration은 이 promise를 await하고
480
+ rejection을 처리해야 하며, 압축되지 않은 성공 response가 전송되었다고 가정하거나 오류를
481
+ 무시하면 안 됩니다.
482
+
483
+ dispatcher가 관리하는 request는 runtime의 JSON 500 envelope로 복구됩니다. 어댑터는 실패한
484
+ body에 자신이 추가한 `Content-Type`만 제거하므로 envelope는 `application/json`을 사용하고,
485
+ 애플리케이션 코드가 명시한 `Content-Type`은 변경되지 않습니다. fulfilled `send()`나
486
+ adapter가 추가한 오래된 `text/plain` 또는 `application/octet-stream` header에 의존한
487
+ consumer는 rejection 또는 fallback을 명시적으로 처리하고 필요한 application-owned header를
488
+ 직접 설정해야 합니다.
489
+
490
+ 더 저수준의 Node compression internals는 공개 `@fluojs/platform-nodejs` 계약이 아니라 `@fluojs/platform-nodejs/internal` seam 뒤에 둡니다.
491
+
492
+ ### Runtime cleanup callback
493
+
494
+ 내부 `RUNTIME_CLEANUP_REGISTRATION` token을 받는 provider는 `void` 또는 `Promise<void>`를
495
+ 반환하는 cleanup callback을 등록할 수 있습니다. Runtime close와 bootstrap-failure cleanup은
496
+ registration 순서대로 callback을 실행하고, 이후 cleanup phase로 넘어가기 전에 각각을 await합니다.
497
+ 실패해도 이후 callback은 계속 실행합니다. `close()`는 cleanup failure를 aggregate하고 완료되지 않은
498
+ phase를 명시적 retry 대상으로 남기며, bootstrap은 원래 failure를 보존하고 cleanup failure를
499
+ `ApplicationLogger`로 보고합니다.
242
500
 
243
501
  ## 관련 패키지
244
502