@fluojs/platform-cloudflare-workers 1.0.4 → 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
@@ -10,6 +10,7 @@
10
10
  - [사용 시점](#사용-시점)
11
11
  - [빠른 시작](#빠른-시작)
12
12
  - [주요 패턴](#주요-패턴)
13
+ - [Lifecycle 및 public seam 참고](#lifecycle-및-public-seam-참고)
13
14
  - [Conformance 커버리지](#conformance-커버리지)
14
15
  - [공개 API 개요](#공개-api-개요)
15
16
  - [관련 패키지](#관련-패키지)
@@ -27,9 +28,9 @@ npm install @fluojs/platform-cloudflare-workers
27
28
 
28
29
  fluo 애플리케이션을 [Cloudflare Workers](https://workers.cloudflare.com/)에 배포할 때 이 패키지를 사용합니다. 이 어댑터는 서버리스 엣지 환경에 맞게 설계되었으며, Worker isolate 제약 조건과 네이티브 Web API를 준수하는 가벼운 `fetch` 기반 어댑터를 제공합니다.
29
30
 
30
- 이 어댑터는 dispatcher가 binding된 뒤 각 요청 수명주기를 `executionContext.waitUntil(...)`에 연결하고, `close()` 중에도 진행 중인 디스패치와 SSE(`text/event-stream`) response body를 유지하여 Worker 종료 도중 활성 작업이 중간에 잘리지 않도록 보장합니다.
31
+ 이 어댑터는 dispatcher가 binding된 뒤 각 요청 수명주기를 `executionContext.waitUntil(...)`에 연결하고, `close()` 중에도 진행 중인 디스패치, terminal close까지의 upgraded server WebSocket, SSE(`text/event-stream`) response body를 유지하여 Worker 종료 도중 활성 작업이 중간에 잘리지 않도록 보장합니다.
31
32
 
32
- 애플리케이션 종료 중에는 즉시 새 ingress 수락을 중단하고, 활성 HTTP 핸들러가 정리될 수 있도록 최대 10초의 bounded drain window를 제공합니다. 이 시간을 넘기면 `close()`는 무기한 대기하지 않고 timeout 오류로 종료됩니다. 해당 drain이 아직 진행 중일 때 동시에 `listen()`을 호출하면 Worker를 다시 열지 않고 `Cloudflare Workers adapter cannot listen while shutdown is still draining.` 오류로 reject됩니다. 닫힌 뒤에는 어댑터가 명시적으로 다시 `listen()`될 때까지 후속 HTTP 및 WebSocket upgrade request가 동일한 JSON `503` shutdown response를 받습니다.
33
+ 애플리케이션 종료 중에는 즉시 새 ingress 수락을 중단하고, 활성 HTTP 핸들러가 정리될 수 있도록 최대 10초의 bounded drain window를 제공합니다. 이 시간을 넘기면 `close()`는 무기한 대기하지 않고 timeout 오류로 종료됩니다. 해당 drain이 아직 진행 중일 때 동시에 `listen()`을 호출하면 Worker를 다시 열지 않고 `Cloudflare Workers adapter cannot listen while shutdown is still draining.` 오류로 reject됩니다. 닫힌 뒤에는 어댑터가 명시적으로 다시 `listen()`될 때까지 후속 HTTP 및 WebSocket upgrade request가 동일한 JSON `503` shutdown response를 받습니다. Lazy entrypoint는 timed-out close가 아직 drain 중인 동안 shutdown response를 계속 반환하지만, underlying close가 나중에 settle되면 해당 임시 gate를 해제하여 이후 request가 새 Worker application을 bootstrap할 수 있게 합니다.
33
34
 
34
35
  ## 빠른 시작
35
36
 
@@ -65,16 +66,84 @@ export default {
65
66
  };
66
67
  ```
67
68
 
69
+ ### close 소유권과 lazy 재시작
70
+
71
+ Cloudflare Workers는 exported `fetch` 핸들러에 host가 호출하는 shutdown callback을 제공하지 않습니다. NestJS shutdown hook을 마이그레이션할 때는 application-owned close trigger를 선택하세요. `worker.fetch` 호출 밖에서 실행되는 out-of-band lifecycle trigger는 `await worker.close()`를 직접 호출할 수 있습니다. 같은 `worker.fetch` 호출 안에서 처리되는 management route는 `close()`를 await하지 않고 현재 response를 반환한 뒤 `executionContext.waitUntil(worker.close())` 또는 동등한 non-self-awaiting mechanism으로 close를 관찰해야 합니다. 그렇지 않으면 `close()`가 자기 자신의 active request drain을 기다리다 shutdown timeout에 도달합니다. `worker.fetch`만 export한다고 해서 close 호출이 마련되지는 않습니다.
72
+
73
+ 성공한 `worker.close()`는 의도적으로 재시작 가능합니다. 현재 lazy application을 해제하며, 이후의 `worker.fetch(...)`는 isolate 안에서 새 application을 bootstrap하여 bootstrap lifecycle hook을 다시 실행하고 application singleton provider를 다시 생성합니다. Env-aware entrypoint에서는 이 새 application generation도 factory를 다시 호출하지 않고 첫 environment에서 cache한 configuration을 사용합니다. `close()`를 terminal Worker shutdown signal로 취급하지 마세요. Application에 terminal behavior가 필요하면 해당 상태를 명시적으로 소유하고 강제해야 합니다.
74
+
75
+ ### Env-aware 지연 엔트리포인트
76
+ 첫 번째 Worker `env`로 root module 또는 bootstrap option을 선택해야 하면 `createCloudflareWorkerEnvEntrypoint(...)`를 사용하세요. 이 factory는 isolate마다 module registration 전에 한 번 실행되며, 반환한 root module과 option은 해당 isolate에 cache되고 실행 중인 각 application generation이 이를 재사용합니다.
77
+
78
+ ```typescript
79
+ import { createCloudflareWorkerEnvEntrypoint } from '@fluojs/platform-cloudflare-workers';
80
+ import { createAppModule } from './app.module';
81
+
82
+ interface WorkerEnv {
83
+ API_PREFIX: string;
84
+ DB: D1Database;
85
+ }
86
+
87
+ const worker = createCloudflareWorkerEnvEntrypoint<WorkerEnv>((env) => ({
88
+ rootModule: createAppModule({ database: env.DB }),
89
+ options: {
90
+ globalPrefix: env.API_PREFIX,
91
+ },
92
+ }));
93
+
94
+ export default {
95
+ fetch: worker.fetch,
96
+ };
97
+ ```
98
+
99
+ 같은 이유로 `worker.ready(env)`도 명시적 `env`를 요구합니다. 첫 번째로 제공한 environment가 singleton bootstrap configuration을 결정하며, 이후 request environment는 `request.cloudflare.env`에 계속 연결되지만 이를 재구성하지는 않습니다. 성공한 `worker.close()`는 현재 application generation만 해제합니다. 이후 `ready(env)` 또는 `fetch(...)`는 factory를 다시 호출하지 않고 보존된 첫 environment의 module과 option에서 새 application을 생성합니다. Bootstrap configuration을 첫 Worker request 전에 이미 사용할 수 있으면 기존 `createCloudflareWorkerEntrypoint(module, options)`를 사용하세요.
100
+
101
+ 표준 `createCloudflareWorkerEntrypoint(...)`의 request-bound `env` 경로에서는 fetch-time binding으로 `ConfigModule.forRoot(...)` 또는 singleton bootstrap provider를 구성할 수 없습니다. Request별 binding을 읽고 검증한 뒤 좁혀서 application-shaped 값으로 provider method에 전달하세요. 첫 environment가 module registration 전에 application을 구성해야 할 때만 env-aware entrypoint를 선택하세요.
102
+
68
103
  ## 주요 패턴
69
104
 
105
+ ### Early Hints 미지원
106
+
107
+ Workers `Response` API는 final response 이전 informational response를 request handler에서 write하는 primitive를 제공하지 않으므로 `context.response.earlyHints`가 없습니다. 사용 전에 capability 존재 여부를 확인하세요. Early Hints를 생성할 수 있는 Cloudflare deployment/cache feature는 host configuration이며 Fluo response writer로 노출되지 않습니다.
108
+
109
+ ### 스트리밍 멀티파트 소비
110
+
111
+ 애플리케이션 bootstrap에서 `multipart: { strategy: 'stream' }`을 설정하면 멀티파트 데이터를 점진적으로
112
+ 받습니다. 멀티파트 route에서 `RequestContext.request.body`는 `AsyncIterableIterator<MultipartPart>`입니다.
113
+ field part는 `kind: 'field'`, `name`, `value`, `headers`를, file part는 `kind: 'file'`, `name`, `filename`,
114
+ `contentType`, `headers`, 그리고 `stream`의 single-consumer `ReadableStream<Uint8Array>`를 제공합니다. 다음
115
+ part를 요청하기 전에 각 file stream을 끝까지 소비하거나 cancel하세요.
116
+
117
+ Runtime route dispatch는 route를 위해 만든 iterator를 소유하며 handler가 끝난 뒤 자동으로 `return()`을 호출해
118
+ active source를 cancel하고 release합니다. Standalone `parseMultipartStream(...)` consumer는 이 책임을 직접
119
+ 집니다. iterator를 끝까지 소비하거나 일찍 끝낼 때 `return()`을 호출하세요.
120
+
121
+ ### 바이트 범위와 캐시 검증
122
+
123
+ Workers는 fetch dispatch를 통해 공유 `@fluojs/http` 단일 byte-range 및 `If-Range` contract를 보존합니다. Conditional-request 평가가 cache validator를 선택한 뒤 유효한 `Range: bytes=` 요청은 portable `206` identity-byte response를 만들고, `If-Range`는 선택된 validator를 재사용합니다. Malformed 또는 multi-range field는 전체 response를 유지하고 충족 불가능한 range는 body 없는 `416`을 만들며, `HEAD`는 stream을 소비하지 않고 GET metadata를 반영합니다.
124
+
70
125
  ### WebSocketPair 활용
71
- 어댑터는 `@fluojs/websockets/cloudflare-workers` 바인딩을 통해 실시간 통신을 위한 Cloudflare의 네이티브 `WebSocketPair`를 지원합니다. Upgrade handling은 해당 binding을 통한 opt-in이며, non-hosted runtime test에서는 `createWebSocketPair`를 주입할 수 있습니다.
126
+ 어댑터는 `@fluojs/websockets/cloudflare-workers` 바인딩을 통해 실시간 통신을 위한 Cloudflare의 네이티브 `WebSocketPair`를 지원합니다. Upgrade handling은 해당 binding을 통한 opt-in이며, non-hosted runtime test에서는 `createWebSocketPair`를 주입할 수 있습니다. Binding은 `listen()`이 Worker dispatch boundary를 시작하기 전에 설정하세요. `listen()`이 한 번 실행된 뒤에는 해당 adapter instance의 binding identity가 frozen됩니다. 이미 public listen boundary를 지난 isolate 아래에서 upgrade ownership이 바뀌지 않도록, `close()` 이후에도 binding을 교체하거나 해제하려는 시도는 reject됩니다.
72
127
 
73
128
  ```typescript
129
+ import { Module } from '@fluojs/core';
130
+ import {
131
+ CloudflareWorkersWebSocketModule,
132
+ WebSocketGateway,
133
+ } from '@fluojs/websockets/cloudflare-workers';
134
+
74
135
  @WebSocketGateway({ path: '/ws' })
75
- export class MyGateway {}
136
+ export class EdgeGateway {}
137
+
138
+ @Module({
139
+ imports: [CloudflareWorkersWebSocketModule.forRoot()],
140
+ providers: [EdgeGateway],
141
+ })
142
+ export class RealtimeModule {}
76
143
  ```
77
144
 
145
+ Bootstrap 전에 application module graph에 `RealtimeModule`을 import하세요. Application bootstrap 중 `CloudflareWorkersWebSocketModule`이 gateway를 발견하고 `app.listen()`이 binding을 freeze하기 전에 versioned realtime capability를 통해 Worker adapter binding을 설치합니다. `configureWebSocketBinding()`은 compatibility facade로 유지됩니다. Listen boundary 이후에는 binding을 추가하거나 교체하지 마세요.
146
+
78
147
  ### 엣지 네이티브 미들웨어
79
148
  표준 fluo 미들웨어(CORS, Global Prefix 등)는 Worker bootstrap helper를 통해 완전히 지원되며 Cloudflare 환경에 최적화되어 있습니다. `createCloudflareWorkerAdapter(...)`는 adapter가 소유하는 parsing 및 websocket-pair 옵션만 받습니다. Routing 및 middleware 옵션은 `bootstrapCloudflareWorkerApplication(...)` 또는 `createCloudflareWorkerEntrypoint(...)`에 전달하세요.
80
149
 
@@ -87,29 +156,58 @@ const worker = createCloudflareWorkerEntrypoint(AppModule, {
87
156
 
88
157
  ### 동작 참고
89
158
 
90
- - `fetch()`는 `listen()` 또는 lazy entrypoint가 dispatcher를 binding한 active work를 `executionContext.waitUntil(...)`에 등록합니다. SSE(`text/event-stream`) response는 body가 끝나거나 cancel될 때까지 해당 lifecycle과 close drain을 유지합니다. lifecycle boundary 전에는 upgrade request HTTP dispatch가 application handler에 도달하지 않습니다.
159
+ - Public concrete `CloudflareWorkerHttpApplicationAdapter.fetch(request, env, executionContext)` 계약은 Worker `executionContext`를 필수로 요구합니다. Direct caller는 모든 HTTP, SSE, WebSocket ingress가 `executionContext.waitUntil(...)`에 active work를 등록하도록 실제 번째 `ctx` 인수를 전달해야 합니다. Migration: direct two-argument adapter call을 `adapter.fetch(request, env, ctx)`로 바꾸세요.
160
+ - `fetch()`는 `listen()` 또는 lazy entrypoint가 dispatcher를 binding한 뒤 active work를 `executionContext.waitUntil(...)`에 등록합니다. Upgraded server WebSocket은 terminal `close` event까지 해당 lifecycle과 close drain을 유지하고, SSE(`text/event-stream`) response는 body가 끝나거나 cancel될 때까지 이를 유지합니다. SSE reader 또는 tracked-stream setup이 동기적으로 실패하면 오류를 전파하기 전에 lifecycle을 release합니다. 그 lifecycle boundary 전에는 upgrade request와 HTTP dispatch가 application handler에 도달하지 않습니다.
91
161
  - `maxBodySize` 같은 adapter option은 Worker adapter 생성 시 검증됩니다. `globalPrefix`, `cors`, `middleware`, `securityHeaders` 같은 bootstrap 전용 옵션은 `createCloudflareWorkerAdapter(...)`가 아니라 Worker bootstrap helper에 전달해야 합니다.
92
- - WebSocket upgrade는 HTTP dispatch와 같은 listen boundary가 소유합니다. `listen()` 전의 upgrade request는 설정된 binding에 도달하지 않습니다.
93
- - `close()`는 shutdown 중 및 shutdown 이후 새 요청에 JSON `503` response를 반환하고, active request가 끝나지 않으면 10초 뒤 timeout됩니다. 해당 close drain이 아직 활성 상태일 때 `listen()`을 호출하면 Cloudflare Workers adapter shutdown-draining 오류로 reject됩니다.
162
+ - WebSocket upgrade는 HTTP dispatch와 같은 listen boundary가 소유합니다. `listen()` 전의 upgrade request는 설정된 binding에 도달하지 않으며, adapter가 한 번이라도 listen한 뒤 defined binding을 교체하거나 해제하려는 시도는 Worker upgrade ownership을 바꾸는 대신 빠르게 실패합니다. 다른 websocket binding이 필요하면 새 adapter를 생성하세요.
163
+ - `close()`는 shutdown 중 및 shutdown 이후 새 HTTP 및 WebSocket upgrade request에 JSON `503` response를 반환하고, active request가 끝나지 않으면 10초 뒤 timeout됩니다. 해당 close drain이 아직 활성 상태일 때 `listen()`을 호출하면 Cloudflare Workers adapter shutdown-draining 오류로 reject됩니다. Lazy entrypoint는 adapter의 underlying drain이 나중에 끝나면 이 timeout을 영구적으로 캐시하지 않습니다.
164
+ - Worker `fetch(...)` dispatch path는 body를 포함하는 RFC `QUERY` route와 `PURGE` 같은 uppercase extension method를 보존하며, method token과 parsed body는 동일한 fetch dispatch seam을 통해 등록된 route에 도달합니다.
94
165
  - Multipart request는 `rawBody`를 보존하지 않습니다.
95
- - Worker `env` 객체는 각 `FrameworkRequest`에 `request.cloudflare.env`로 연결되고 Worker execution context는 `request.cloudflare.executionContext`로 제공됩니다. Package-level config resolution은 application소유하므로, binding은 application boundary에서 명시적 provider 또는 `@fluojs/config`로 매핑하세요.
166
+ - Worker `env` 객체는 각 `FrameworkRequest`에 `request.cloudflare.env`로 연결되고 Worker execution context는 `request.cloudflare.executionContext`로 제공됩니다. `bootstrapCloudflareWorkerApplication(...)`은 exported `fetch(...)`가 traffic을 처리하기 전에 module registration을 완료합니다. `createCloudflareWorkerEntrypoint(...)`는 미리 선언한 root module과 option을 유지하므로 fetch-time `env`는 request dispatch 중에만 연결됩니다. 첫 명시적 Worker environment가 module registration 전에 root module 또는 final bootstrap option을 선택해야 하면 opt-in `createCloudflareWorkerEnvEntrypoint(...)`를 사용하세요. API의 `ready(env)`는 environment를 요구하고 첫 environment의 module과 option을 isolate마다 한 번 cache하며, 그 configuration에서 application generation마다 하나의 application을 생성합니다. 성공한 close 뒤에는 factory를 다시 실행하거나 이후 environment를 bootstrap configuration으로 수용하지 않고 application을 재시작합니다. 어느 경로든 의도적으로 request별인 binding에는 request-bound `request.cloudflare.env`를 사용하세요.
167
+
168
+ ## Lifecycle 및 public seam 참고
169
+
170
+ Root `@fluojs/platform-cloudflare-workers` export는 application code와 first-party Worker websocket integration이 사용하는 Worker public seam을 소유합니다. `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, `CloudflareWorkerWebSocketPair`, `CloudflareWorkerWebSocketPairFactory`, `CloudflareWorkerWebSocketUpgradeHost`, `CloudflareWorkerWebSocketUpgradeResult` 같은 Worker-specific public type은 consumer가 `@fluojs/http/internal` 또는 `@fluojs/runtime/internal*` subpath를 import하지 않아도 되도록 이 패키지에서 export됩니다.
171
+
172
+ 위의 listen, shutdown, SSE drain, websocket binding 규칙은 public lifecycle behavior입니다. 이러한 public seam type 또는 lifecycle semantic을 바꾸는 변경은 `@fluojs/platform-cloudflare-workers` release governance 대상이며, user-impacting update는 implementation, docs, tests와 함께 Changesets로 추적해야 합니다.
173
+
174
+ <!-- fluo-contract: realtime-capability -->
175
+ ```json
176
+ {
177
+ "closeOwnership": {
178
+ "inFetchManagement": "wait-until",
179
+ "outOfBand": "await",
180
+ "restart": "restartable"
181
+ },
182
+ "realtimeCapability": {
183
+ "bindingInstallationVersion": 1,
184
+ "contract": "raw-websocket-expansion",
185
+ "kind": "fetch-style",
186
+ "mode": "request-upgrade",
187
+ "support": "supported",
188
+ "version": 1
189
+ }
190
+ }
191
+ ```
96
192
 
97
193
  ## Conformance 커버리지
98
194
 
99
- `packages/platform-cloudflare-workers/src/adapter.test.ts`는 문서화된 Worker 계약을 검증하는 package-local regression 대상입니다. 이 파일은 shared Web dispatch delegation, Worker `env` request attachment, `executionContext.waitUntil(...)` SSE(`text/event-stream`) body tracking, websocket upgrade binding, listen-bound upgrade ownership, lazy entrypoint 재사용, shutdown gating, drain 중 `listen()` rejection, close 중 및 close 이후 JSON `503` response, bounded 10초 close timeout을 검증합니다.
195
+ `packages/platform-cloudflare-workers/src/adapter.test.ts`와 `packages/platform-cloudflare-workers/src/adapter-lifecycle.test.ts`는 문서화된 Worker 계약을 검증하는 package-local regression 대상입니다. 이 파일들은 shared Web dispatch delegation, Worker `env` request attachment, `executionContext.waitUntil(...)` SSE(`text/event-stream`) body tracking, body-cancellation 및 synchronous setup-failure drain, websocket upgrade binding, upgraded server-socket close tracking, pre-listen HTTP 및 websocket lifecycle guard, listen boundary 이후 websocket binding freeze, zero-config 및 env-aware lazy entrypoint 재사용, 명시적 env-aware readiness, 성공한 lazy 재시작 후 첫 environment configuration 보존, timeout recovery, shutdown gating, drain 중 `listen()` rejection, HTTP와 websocket upgrade 모두에 대한 close 중 및 close 이후 JSON `503` response, reliable fake-timer cleanup, public seam source import, structured realtime capability contract, bounded 10초 close timeout을 검증합니다.
100
196
 
101
- 공유 edge portability suite인 `packages/testing/src/portability/web-runtime-adapter-portability.test.ts`는 Cloudflare Workers를 Bun 및 Deno와 함께 실행해 malformed cookie 보존, query decoding, JSON/text raw-body capture, multipart raw-body 제외, SSE framing을 검증합니다. 패키지 테스트의 README parity assertion은 edge-runtime 커버리지 문서가 한국어 mirror와 계속 동기화되도록 확인합니다.
197
+ 공유 edge portability suite인 `packages/testing/src/portability/web-runtime-adapter-portability.test.ts`는 Cloudflare Workers를 Bun 및 Deno와 함께 실행해 conditional request, single-byte range 및 `If-Range`, body를 포함하는 `QUERY` 및 `PURGE` fetch dispatch, malformed cookie 보존, query decoding, JSON/text raw-body capture, multipart raw-body 제외, SSE framing을 검증합니다. 패키지 테스트는 README locale의 structured realtime capability contract를 parse하고 machine-consumed value를 adapter capability와 비교합니다.
102
198
 
103
199
  ## 공개 API 개요
104
200
 
105
201
  - `createCloudflareWorkerAdapter(options)`: Worker HTTP 어댑터를 위한 팩토리입니다.
106
202
  - `createCloudflareWorkerEntrypoint(module, options)`: 지연 부트스트랩 방식의 Worker 엔트리포인트를 생성합니다.
203
+ - `createCloudflareWorkerEnvEntrypoint(factory)`: 첫 명시적 Worker environment에서 지연 Worker 엔트리포인트를 생성합니다.
107
204
  - `bootstrapCloudflareWorkerApplication(module, options)`: Worker를 위한 비동기 부트스트랩 헬퍼입니다.
108
205
  - `CloudflareWorkerHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다.
109
206
  - `CloudflareWorkerHandler`: Worker application wrapper와 lazy entrypoint가 공유하는 fetch handler interface입니다.
110
207
  - `CloudflareWorkerApplication`: `adapter`, `app`, `fetch(...)`, `close(...)`를 제공하는 fully bootstrapped Worker application wrapper입니다.
111
208
  - `CloudflareWorkerEntrypoint`: `fetch`, `ready()`, `close()` lifecycle method를 제공하는 lazy entrypoint입니다.
112
- - Option 및 type: `CloudflareWorkerAdapterOptions`, `BootstrapCloudflareWorkerApplicationOptions`, `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, Worker websocket pair/upgrade type.
209
+ - `CloudflareWorkerEnvEntrypoint`: `fetch`, `ready(env)`, `close()` lifecycle method를 제공하는 env-aware lazy entrypoint입니다.
210
+ - Option 및 type: `CloudflareWorkerAdapterOptions`, `BootstrapCloudflareWorkerApplicationOptions`, `CloudflareWorkerEnvBootstrap`, `CloudflareWorkerEnvEntrypointFactory`, `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, `CloudflareWorkerWebSocketBindingHost`, `CloudflareWorkerWebSocket`, `CloudflareWorkerWebSocketMessage`, `CloudflareWorkerWebSocketPair`, `CloudflareWorkerWebSocketPairFactory`, `CloudflareWorkerWebSocketUpgradeHost`, `CloudflareWorkerWebSocketUpgradeResult`.
113
211
 
114
212
  ## 관련 패키지
115
213
 
package/README.md CHANGED
@@ -10,6 +10,7 @@ Cloudflare Workers HTTP adapter for the fluo runtime, optimized for the edge.
10
10
  - [When to Use](#when-to-use)
11
11
  - [Quick Start](#quick-start)
12
12
  - [Common Patterns](#common-patterns)
13
+ - [Lifecycle and Public Seam Notes](#lifecycle-and-public-seam-notes)
13
14
  - [Conformance Coverage](#conformance-coverage)
14
15
  - [Public API Overview](#public-api-overview)
15
16
  - [Related Packages](#related-packages)
@@ -27,9 +28,9 @@ This package is intended to run on Cloudflare Workers. The published manifest in
27
28
 
28
29
  Use this package when deploying fluo applications to [Cloudflare Workers](https://workers.cloudflare.com/). It is designed for the serverless edge environment, providing a lightweight `fetch`-based adapter that respects Worker isolate constraints and native Web APIs.
29
30
 
30
- The adapter binds each request lifecycle to `executionContext.waitUntil(...)` after the dispatcher is bound and keeps in-flight dispatches and SSE (`text/event-stream`) response bodies alive during `close()` so Worker shutdown does not drop active work mid-request.
31
+ The adapter binds each request lifecycle to `executionContext.waitUntil(...)` after the dispatcher is bound and keeps in-flight dispatches, WebSocket upgrades through the upgraded server socket's terminal close, and SSE (`text/event-stream`) response bodies alive during `close()` so Worker shutdown does not drop active work mid-request.
31
32
 
32
- During application shutdown, the adapter stops accepting new ingress immediately and gives active HTTP handlers a bounded 10-second drain window before `close()` fails with a timeout instead of hanging indefinitely. While that drain is still in progress, a concurrent `listen()` call rejects with `Cloudflare Workers adapter cannot listen while shutdown is still draining.` instead of reopening the Worker. Once closed, follow-up HTTP and WebSocket upgrade requests receive the same JSON `503` shutdown response until the adapter is explicitly listened again.
33
+ During application shutdown, the adapter stops accepting new ingress immediately and gives active HTTP handlers a bounded 10-second drain window before `close()` fails with a timeout instead of hanging indefinitely. While that drain is still in progress, a concurrent `listen()` call rejects with `Cloudflare Workers adapter cannot listen while shutdown is still draining.` instead of reopening the Worker. Once closed, follow-up HTTP and WebSocket upgrade requests receive the same JSON `503` shutdown response until the adapter is explicitly listened again. Lazy entrypoints keep returning shutdown responses while a timed-out close is still draining, but they clear that temporary gate once the underlying close eventually settles so a later request can bootstrap a fresh Worker application.
33
34
 
34
35
  ## Quick Start
35
36
 
@@ -65,16 +66,84 @@ export default {
65
66
  };
66
67
  ```
67
68
 
69
+ ### Close Ownership and Lazy Restart
70
+
71
+ Cloudflare Workers does not provide a host-invoked shutdown callback to the exported `fetch` handler. When migrating NestJS shutdown hooks, choose an application-owned close trigger. An out-of-band lifecycle trigger, running outside any `worker.fetch` invocation, may call `await worker.close()` directly. A management route handled inside the same `worker.fetch` invocation must return its current response without awaiting `close()`, then observe it with `executionContext.waitUntil(worker.close())` or an equivalent non-self-awaiting mechanism; otherwise `close()` waits for that active request to drain and reaches the shutdown timeout. Exporting `worker.fetch` alone does not arrange a close call.
72
+
73
+ A successful `worker.close()` is intentionally restartable. It releases the current lazy application; a later `worker.fetch(...)` bootstraps a new application in the isolate, rerunning bootstrap lifecycle hooks and reconstructing application singleton providers. For an env-aware entrypoint, that new application generation uses the cached configuration from the first environment without calling its factory again. Do not treat `close()` as a terminal Worker shutdown signal. If an application needs terminal behavior, it must own and enforce that state explicitly.
74
+
75
+ ### Env-Aware Lazy Entrypoint
76
+ Use `createCloudflareWorkerEnvEntrypoint(...)` when the first Worker `env` must select the root module or bootstrap options. Its factory runs once per isolate before module registration; the returned root module and options remain cached for that isolate, while each running application generation reuses them.
77
+
78
+ ```typescript
79
+ import { createCloudflareWorkerEnvEntrypoint } from '@fluojs/platform-cloudflare-workers';
80
+ import { createAppModule } from './app.module';
81
+
82
+ interface WorkerEnv {
83
+ API_PREFIX: string;
84
+ DB: D1Database;
85
+ }
86
+
87
+ const worker = createCloudflareWorkerEnvEntrypoint<WorkerEnv>((env) => ({
88
+ rootModule: createAppModule({ database: env.DB }),
89
+ options: {
90
+ globalPrefix: env.API_PREFIX,
91
+ },
92
+ }));
93
+
94
+ export default {
95
+ fetch: worker.fetch,
96
+ };
97
+ ```
98
+
99
+ `worker.ready(env)` requires an explicit `env` for the same reason. The first supplied environment determines the singleton bootstrap configuration; later request environments still attach to `request.cloudflare.env`, but do not reconfigure it. A successful `worker.close()` releases only the current application generation: the next `ready(env)` or `fetch(...)` creates a fresh application from the retained first-environment module and options without calling the factory again. Use the existing `createCloudflareWorkerEntrypoint(module, options)` when bootstrap configuration is already available before the first Worker request.
100
+
101
+ For the standard `createCloudflareWorkerEntrypoint(...)` request-bound `env` path, fetch-time bindings cannot supply `ConfigModule.forRoot(...)` or singleton bootstrap providers. Read, validate, and narrow request-varying bindings, then pass application-shaped values to provider methods. Choose the env-aware entrypoint only when the first environment must configure the application before module registration.
102
+
68
103
  ## Common Patterns
69
104
 
105
+ ### Early Hints are unsupported
106
+
107
+ The Workers `Response` API does not provide a request-handler write primitive for an informational response before the final response, so `context.response.earlyHints` is absent. Check for capability presence before use. Cloudflare deployment/cache features that may generate Early Hints are host configuration and are not exposed as a Fluo response writer.
108
+
109
+ ### Streaming multipart consumption
110
+
111
+ Set `multipart: { strategy: 'stream' }` at application bootstrap to receive multipart data incrementally. For
112
+ multipart routes, `RequestContext.request.body` is an `AsyncIterableIterator<MultipartPart>`: field parts expose
113
+ `kind: 'field'`, `name`, `value`, and `headers`; file parts expose `kind: 'file'`, `name`, `filename`,
114
+ `contentType`, `headers`, and a single-consumer `ReadableStream<Uint8Array>` at `stream`. Finish or cancel each file
115
+ stream before requesting the next part.
116
+
117
+ Runtime route dispatch owns an iterator created for a route and automatically calls `return()` after the handler
118
+ finishes, cancelling and releasing an active source. Standalone `parseMultipartStream(...)` consumers own that
119
+ responsibility: consume the iterator to completion or call `return()` when ending early.
120
+
121
+ ### Byte Ranges and Cache Validation
122
+
123
+ Workers preserves the shared `@fluojs/http` single-byte-range and `If-Range` contract through fetch dispatch. After conditional-request evaluation selects cache validators, a valid `Range: bytes=` request yields the portable `206` identity-byte response; `If-Range` reuses those selected validators, while malformed or multi-range fields retain the full response and an unsatisfiable range yields bodyless `416`. `HEAD` mirrors GET metadata without consuming a stream.
124
+
70
125
  ### Working with WebSocketPairs
71
- The adapter supports Cloudflare's native `WebSocketPair` for real-time communication via the `@fluojs/websockets/cloudflare-workers` binding. Upgrade handling is opt-in through that binding, and `createWebSocketPair` can be injected for non-hosted runtime tests.
126
+ The adapter supports Cloudflare's native `WebSocketPair` for real-time communication via the `@fluojs/websockets/cloudflare-workers` binding. Upgrade handling is opt-in through that binding, and `createWebSocketPair` can be injected for non-hosted runtime tests. Configure the binding before `listen()` starts the Worker dispatch boundary; once `listen()` has run, the binding identity is frozen for that adapter instance. Replacing or clearing it is rejected even after `close()`, so upgrade ownership cannot change underneath an isolate that has already crossed the public listen boundary.
72
127
 
73
128
  ```typescript
129
+ import { Module } from '@fluojs/core';
130
+ import {
131
+ CloudflareWorkersWebSocketModule,
132
+ WebSocketGateway,
133
+ } from '@fluojs/websockets/cloudflare-workers';
134
+
74
135
  @WebSocketGateway({ path: '/ws' })
75
- export class MyGateway {}
136
+ export class EdgeGateway {}
137
+
138
+ @Module({
139
+ imports: [CloudflareWorkersWebSocketModule.forRoot()],
140
+ providers: [EdgeGateway],
141
+ })
142
+ export class RealtimeModule {}
76
143
  ```
77
144
 
145
+ Import `RealtimeModule` into the application module graph before bootstrap. During application bootstrap, `CloudflareWorkersWebSocketModule` discovers the gateway and installs the Worker adapter binding through its versioned realtime capability before `app.listen()` freezes it; `configureWebSocketBinding()` remains a compatibility facade. Do not add or replace the binding after the listen boundary.
146
+
78
147
  ### Edge-Native Middleware
79
148
  Standard fluo middleware (CORS, Global Prefix, etc.) is fully supported through Worker bootstrap helpers and optimized for the Cloudflare environment. `createCloudflareWorkerAdapter(...)` only accepts adapter-owned parsing and websocket-pair options; pass routing and middleware options to `bootstrapCloudflareWorkerApplication(...)` or `createCloudflareWorkerEntrypoint(...)` instead.
80
149
 
@@ -87,29 +156,58 @@ const worker = createCloudflareWorkerEntrypoint(AppModule, {
87
156
 
88
157
  ### Behavior Notes
89
158
 
90
- - `fetch()` registers active work with `executionContext.waitUntil(...)` after `listen()` or the lazy entrypoint binds the dispatcher; SSE (`text/event-stream`) responses keep that lifecycle and the close drain open until the body finishes or is canceled. Before that lifecycle boundary, upgrade requests and HTTP dispatch do not reach application handlers.
159
+ - The public concrete `CloudflareWorkerHttpApplicationAdapter.fetch(request, env, executionContext)` contract requires the Worker `executionContext`; direct callers must pass the real third `ctx` argument so every HTTP, SSE, and WebSocket ingress registers active work with `executionContext.waitUntil(...)`. Migration: replace direct two-argument adapter calls with `adapter.fetch(request, env, ctx)`.
160
+ - `fetch()` registers active work with `executionContext.waitUntil(...)` after `listen()` or the lazy entrypoint binds the dispatcher; upgraded server WebSockets keep that lifecycle and the close drain open until their terminal `close` event, while SSE (`text/event-stream`) responses keep them open until the body finishes or is canceled. Synchronous SSE reader or tracked-stream setup failures release the lifecycle before propagating. Before that lifecycle boundary, upgrade requests and HTTP dispatch do not reach application handlers.
91
161
  - Adapter options such as `maxBodySize` are validated when the Worker adapter is created; bootstrap-only options such as `globalPrefix`, `cors`, `middleware`, and `securityHeaders` belong on Worker bootstrap helpers rather than `createCloudflareWorkerAdapter(...)`.
92
- - WebSocket upgrades are owned by the same listen boundary as HTTP dispatch; upgrade requests before `listen()` do not reach the configured binding.
93
- - `close()` returns JSON `503` responses for new requests during and after shutdown and times out after 10 seconds if active requests never settle. Calling `listen()` while that close drain is still active rejects with the Cloudflare Workers adapter shutdown-draining error.
162
+ - WebSocket upgrades are owned by the same listen boundary as HTTP dispatch; upgrade requests before `listen()` do not reach the configured binding, and attempts to replace or clear a defined binding after the adapter has ever listened fail fast instead of mutating Worker upgrade ownership. Create a new adapter when a host needs a different websocket binding.
163
+ - `close()` returns JSON `503` responses for new HTTP and WebSocket upgrade requests during and after shutdown and times out after 10 seconds if active requests never settle. Calling `listen()` while that close drain is still active rejects with the Cloudflare Workers adapter shutdown-draining error. Lazy entrypoints do not permanently cache that timeout once the adapter's underlying drain later finishes.
164
+ - The Worker `fetch(...)` dispatch path preserves body-bearing RFC `QUERY` routes and uppercase extension methods such as `PURGE`; their method token and parsed body reach the registered route through the same fetch dispatch seam.
94
165
  - Multipart requests do not preserve `rawBody`.
95
- - The Worker `env` object is attached to each `FrameworkRequest` as `request.cloudflare.env`, with the Worker execution context available as `request.cloudflare.executionContext`; package-level config resolution remains application-owned, so map bindings into explicit providers or `@fluojs/config` at the application boundary.
166
+ - The Worker `env` object is attached to each `FrameworkRequest` as `request.cloudflare.env`, with the Worker execution context available as `request.cloudflare.executionContext`. `bootstrapCloudflareWorkerApplication(...)` completes module registration before its exported `fetch(...)` handles traffic. `createCloudflareWorkerEntrypoint(...)` keeps its predeclared root module and options, so its fetch-time `env` attaches only during request dispatch. `createCloudflareWorkerEnvEntrypoint(...)` is the opt-in alternative when the first explicit Worker environment must select the root module or final bootstrap options before module registration. Its `ready(env)` method requires that environment, caches the first environment's module and options once per isolate, and builds one application per application generation from that configuration. A successful close restarts the application without rerunning the factory or accepting later environments as bootstrap configuration. In either path, use request-bound `request.cloudflare.env` for bindings that are intentionally per-request.
167
+
168
+ ## Lifecycle and Public Seam Notes
169
+
170
+ The root `@fluojs/platform-cloudflare-workers` export owns the Worker public seam for application code and first-party Worker websocket integrations. Worker-specific public types such as `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, `CloudflareWorkerWebSocketPair`, `CloudflareWorkerWebSocketPairFactory`, `CloudflareWorkerWebSocketUpgradeHost`, and `CloudflareWorkerWebSocketUpgradeResult` are exported from this package instead of asking consumers to import `@fluojs/http/internal` or `@fluojs/runtime/internal*` subpaths.
171
+
172
+ The listen, shutdown, SSE drain, and websocket binding rules above are public lifecycle behavior. Changes to those public seam types or lifecycle semantics are release-governed for `@fluojs/platform-cloudflare-workers`; user-impacting updates must be tracked with Changesets alongside the implementation, docs, and tests.
173
+
174
+ <!-- fluo-contract: realtime-capability -->
175
+ ```json
176
+ {
177
+ "closeOwnership": {
178
+ "inFetchManagement": "wait-until",
179
+ "outOfBand": "await",
180
+ "restart": "restartable"
181
+ },
182
+ "realtimeCapability": {
183
+ "bindingInstallationVersion": 1,
184
+ "contract": "raw-websocket-expansion",
185
+ "kind": "fetch-style",
186
+ "mode": "request-upgrade",
187
+ "support": "supported",
188
+ "version": 1
189
+ }
190
+ }
191
+ ```
96
192
 
97
193
  ## Conformance Coverage
98
194
 
99
- `packages/platform-cloudflare-workers/src/adapter.test.ts` is the package-local regression target for the documented Worker contract. It covers shared Web dispatch delegation, Worker `env` request attachment, `executionContext.waitUntil(...)` SSE (`text/event-stream`) body tracking, websocket upgrade binding, listen-bound upgrade ownership, lazy entrypoint reuse, shutdown gating, drain-time `listen()` rejection, JSON `503` responses while closing and after close, and the bounded 10-second close timeout.
195
+ `packages/platform-cloudflare-workers/src/adapter.test.ts` and `packages/platform-cloudflare-workers/src/adapter-lifecycle.test.ts` are the package-local regression targets for the documented Worker contract. They cover shared Web dispatch delegation, Worker `env` request attachment, `executionContext.waitUntil(...)` SSE (`text/event-stream`) body tracking, body-cancellation and synchronous setup-failure drains, websocket upgrade binding, upgraded server-socket close tracking, pre-listen HTTP and websocket lifecycle guards, websocket binding freeze after the listen boundary, zero-config and env-aware lazy entrypoint reuse, explicit env-aware readiness, first-environment configuration retention across successful lazy restarts, timeout recovery, shutdown gating, drain-time `listen()` rejection, JSON `503` responses while closing and after close for both HTTP and websocket upgrades, reliable fake-timer cleanup, public seam source imports, the structured realtime capability contract, and the bounded 10-second close timeout.
100
196
 
101
- The shared edge portability suite in `packages/testing/src/portability/web-runtime-adapter-portability.test.ts` exercises Cloudflare Workers beside Bun and Deno for malformed cookie preservation, query decoding, JSON/text raw-body capture, multipart raw-body exclusion, and SSE framing. The README parity assertion in the package test keeps these documented edge-runtime coverage claims synchronized with the Korean mirror.
197
+ The shared edge portability suite in `packages/testing/src/portability/web-runtime-adapter-portability.test.ts` exercises Cloudflare Workers beside Bun and Deno for conditional requests, single-byte ranges and `If-Range`, body-bearing `QUERY` and `PURGE` fetch dispatch, malformed cookie preservation, query decoding, JSON/text raw-body capture, multipart raw-body exclusion, and SSE framing. The package test parses the structured realtime capability contract in both README locales and compares its machine-consumed values with the adapter capability.
102
198
 
103
199
  ## Public API Overview
104
200
 
105
201
  - `createCloudflareWorkerAdapter(options)`: Factory for the Worker HTTP adapter.
106
202
  - `createCloudflareWorkerEntrypoint(module, options)`: Creates a lazy-bootstrapping Worker entrypoint.
203
+ - `createCloudflareWorkerEnvEntrypoint(factory)`: Creates a lazy Worker entrypoint from the first explicit Worker environment.
107
204
  - `bootstrapCloudflareWorkerApplication(module, options)`: Async bootstrap helper for Workers.
108
205
  - `CloudflareWorkerHttpApplicationAdapter`: The core adapter implementation.
109
206
  - `CloudflareWorkerHandler`: Fetch handler interface shared by Worker application wrappers and lazy entrypoints.
110
207
  - `CloudflareWorkerApplication`: Fully bootstrapped Worker application wrapper with `adapter`, `app`, `fetch(...)`, and `close(...)`.
111
208
  - `CloudflareWorkerEntrypoint`: Lazy entrypoint with `fetch`, `ready()`, and `close()` lifecycle methods.
112
- - Options and types: `CloudflareWorkerAdapterOptions`, `BootstrapCloudflareWorkerApplicationOptions`, `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, and Worker websocket pair/upgrade types.
209
+ - `CloudflareWorkerEnvEntrypoint`: Env-aware lazy entrypoint with `fetch`, `ready(env)`, and `close()` lifecycle methods.
210
+ - Options and types: `CloudflareWorkerAdapterOptions`, `BootstrapCloudflareWorkerApplicationOptions`, `CloudflareWorkerEnvBootstrap`, `CloudflareWorkerEnvEntrypointFactory`, `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, `CloudflareWorkerWebSocketBindingHost`, `CloudflareWorkerWebSocket`, `CloudflareWorkerWebSocketMessage`, `CloudflareWorkerWebSocketPair`, `CloudflareWorkerWebSocketPairFactory`, `CloudflareWorkerWebSocketUpgradeHost`, and `CloudflareWorkerWebSocketUpgradeResult`.
113
211
 
114
212
  ## Related Packages
115
213
 
package/dist/adapter.d.ts CHANGED
@@ -1,14 +1,20 @@
1
- import { type Dispatcher, type HttpApplicationAdapter } from '@fluojs/http/internal';
2
- import { type BootstrapHttpAdapterApplicationOptions } from '@fluojs/runtime/internal/http-adapter';
3
- import type { Application, ModuleType, UploadedFile } from '@fluojs/runtime';
1
+ import type { CorsOptions, Dispatcher, HttpApplicationAdapter, MiddlewareLike, SecurityHeadersOptions } from '@fluojs/http';
2
+ import type { Application, CreateApplicationOptions, ModuleType } from '@fluojs/runtime';
4
3
  import { type CreateWebRequestResponseFactoryOptions } from '@fluojs/runtime/web';
5
4
  declare module '@fluojs/http' {
6
5
  interface FrameworkRequest {
7
6
  cloudflare?: CloudflareWorkerRequestContext;
8
- files?: UploadedFile[];
9
- rawBody?: Uint8Array;
10
7
  }
11
8
  }
9
+ declare const ADAPTER_CLOSE_SETTLED: unique symbol;
10
+ type CloudflareWorkerCorsInput = false | string | string[] | CorsOptions;
11
+ interface CloudflareWorkerMiddlewareOptions {
12
+ cors?: CloudflareWorkerCorsInput;
13
+ globalPrefix?: string;
14
+ globalPrefixExclude?: readonly string[];
15
+ middleware?: MiddlewareLike[];
16
+ securityHeaders?: false | SecurityHeadersOptions;
17
+ }
12
18
  /** Minimal Worker execution context surface used by the adapter. */
13
19
  export interface CloudflareWorkerExecutionContext {
14
20
  passThroughOnException?(): void;
@@ -17,7 +23,7 @@ export interface CloudflareWorkerExecutionContext {
17
23
  /** Worker-specific request context attached to fluo HTTP requests by the Cloudflare adapter. */
18
24
  export interface CloudflareWorkerRequestContext<Env = unknown> {
19
25
  readonly env: Env;
20
- readonly executionContext?: CloudflareWorkerExecutionContext;
26
+ readonly executionContext: CloudflareWorkerExecutionContext;
21
27
  }
22
28
  /** Message payloads accepted by Cloudflare Worker websockets. */
23
29
  export type CloudflareWorkerWebSocketMessage = ArrayBuffer | ArrayBufferView | Blob | string;
@@ -55,7 +61,7 @@ export interface CloudflareWorkerAdapterOptions extends CreateWebRequestResponse
55
61
  createWebSocketPair?: CloudflareWorkerWebSocketPairFactory;
56
62
  }
57
63
  /** Bootstrap options for constructing a Cloudflare Worker application shell. */
58
- export interface BootstrapCloudflareWorkerApplicationOptions extends BootstrapHttpAdapterApplicationOptions, CloudflareWorkerAdapterOptions {
64
+ export interface BootstrapCloudflareWorkerApplicationOptions extends Omit<CreateApplicationOptions, 'adapter' | 'middleware'>, CloudflareWorkerMiddlewareOptions, CloudflareWorkerAdapterOptions {
59
65
  }
60
66
  /** Fetch handler shape exposed by Worker-backed application entrypoints. */
61
67
  export interface CloudflareWorkerHandler<Env = unknown> {
@@ -72,6 +78,26 @@ export interface CloudflareWorkerEntrypoint<Env = unknown> extends CloudflareWor
72
78
  close(signal?: string): Promise<void>;
73
79
  ready(): Promise<CloudflareWorkerApplication<Env>>;
74
80
  }
81
+ /** Isolate-lifetime root module and final bootstrap options selected from a Worker environment. */
82
+ export interface CloudflareWorkerEnvBootstrap {
83
+ readonly options?: BootstrapCloudflareWorkerApplicationOptions;
84
+ readonly rootModule: ModuleType;
85
+ }
86
+ /**
87
+ * Factory that derives the isolate-lifetime bootstrap configuration from the first supplied environment.
88
+ *
89
+ * Each application generation uses the returned configuration, including a generation restarted after a successful close.
90
+ */
91
+ export type CloudflareWorkerEnvEntrypointFactory<Env = unknown> = (env: Env) => CloudflareWorkerEnvBootstrap;
92
+ /**
93
+ * Lazy Cloudflare Worker entrypoint whose first explicit environment configures the isolate.
94
+ *
95
+ * A successful close starts a fresh application generation with the same cached configuration.
96
+ */
97
+ export interface CloudflareWorkerEnvEntrypoint<Env = unknown> extends CloudflareWorkerHandler<Env> {
98
+ close(signal?: string): Promise<void>;
99
+ ready(env: Env): Promise<CloudflareWorkerApplication<Env>>;
100
+ }
75
101
  /**
76
102
  * Cloudflare Workers HTTP adapter with waitUntil-aware request tracking and graceful close behavior.
77
103
  */
@@ -81,6 +107,7 @@ export declare class CloudflareWorkerHttpApplicationAdapter implements HttpAppli
81
107
  private inFlightDrain?;
82
108
  private inFlightRequestCount;
83
109
  private isClosed;
110
+ private isWebSocketBindingFrozen;
84
111
  private websocketBinding?;
85
112
  private readonly options;
86
113
  private readonly webRequestResponseFactory;
@@ -88,7 +115,16 @@ export declare class CloudflareWorkerHttpApplicationAdapter implements HttpAppli
88
115
  close(): Promise<void>;
89
116
  getRealtimeCapability(): import("@fluojs/http").FetchStyleHttpAdapterRealtimeCapability;
90
117
  configureWebSocketBinding(binding: CloudflareWorkerWebSocketBinding | undefined): void;
91
- fetch<Env = unknown>(request: Request, env?: Env, executionContext?: CloudflareWorkerExecutionContext): Promise<Response>;
118
+ [ADAPTER_CLOSE_SETTLED](): Promise<void>;
119
+ /**
120
+ * Dispatch a Worker request while registering its lifecycle with the Worker execution context.
121
+ *
122
+ * @param request Worker request to dispatch.
123
+ * @param env Worker environment bindings attached to the framework request.
124
+ * @param executionContext Worker lifecycle context used to retain active work.
125
+ * @returns The dispatched Worker response.
126
+ */
127
+ fetch<Env = unknown>(request: Request, env: Env, executionContext: CloudflareWorkerExecutionContext): Promise<Response>;
92
128
  listen(dispatcher: Dispatcher): Promise<void>;
93
129
  private upgradeWebSocket;
94
130
  private trackInFlightRequest;
@@ -118,6 +154,13 @@ export declare function bootstrapCloudflareWorkerApplication<Env = unknown>(root
118
154
  * @returns A Worker entrypoint exposing lazy `fetch(...)`, `ready()`, and `close(...)` helpers.
119
155
  */
120
156
  export declare function createCloudflareWorkerEntrypoint<Env = unknown>(rootModule: ModuleType, options?: BootstrapCloudflareWorkerApplicationOptions): CloudflareWorkerEntrypoint<Env>;
157
+ /**
158
+ * Create a lazy Cloudflare Worker entrypoint configured once per isolate from its first supplied environment.
159
+ *
160
+ * @param factory Factory that derives and caches the root module and final bootstrap options from one Worker environment.
161
+ * @returns A Worker entrypoint exposing env-aware lazy `fetch(...)`, `ready(env)`, and `close(...)` helpers for application generations using that cached configuration.
162
+ */
163
+ export declare function createCloudflareWorkerEnvEntrypoint<Env = unknown>(factory: CloudflareWorkerEnvEntrypointFactory<Env>): CloudflareWorkerEnvEntrypoint<Env>;
121
164
  declare global {
122
165
  interface ResponseInit {
123
166
  webSocket?: CloudflareWorkerWebSocket;
@@ -126,4 +169,5 @@ declare global {
126
169
  WebSocketPair?: new () => CloudflareWorkerWebSocketPair;
127
170
  }
128
171
  }
172
+ export {};
129
173
  //# sourceMappingURL=adapter.d.ts.map
@@ -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,8 +1,12 @@
1
1
  import { createFetchStyleHttpAdapterRealtimeCapability } from '@fluojs/http/internal';
2
2
  import { bootstrapHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
3
- import { createWebRequestResponseFactory, dispatchWebRequest } from '@fluojs/runtime/web';
3
+ import { createWebRequestResponseFactory, startWebRequestDispatch } from '@fluojs/runtime/web';
4
4
  const WORKER_DISPATCHER_NOT_READY_MESSAGE = 'Cloudflare Workers adapter received a request before dispatcher binding completed.';
5
5
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
6
+ const WEBSOCKET_CLOSED_READY_STATE = 3;
7
+ const ADAPTER_CLOSE_SETTLED = Symbol('CloudflareWorkerAdapterCloseSettled');
8
+ const WEBSOCKET_BINDING_RECONFIGURATION_MESSAGE = 'Cloudflare Workers websocket binding must be configured before listen() starts accepting Worker requests.';
9
+ const WEBSOCKET_BINDING_INSTALLATION_MESSAGE = 'Cloudflare Workers websocket binding installation requires a binding with a fetch(request, host) function.';
6
10
 
7
11
  /** Minimal Worker execution context surface used by the adapter. */
8
12
 
@@ -34,6 +38,20 @@ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
34
38
 
35
39
  /** Lazy Cloudflare Worker entrypoint that bootstraps on first use. */
36
40
 
41
+ /** Isolate-lifetime root module and final bootstrap options selected from a Worker environment. */
42
+
43
+ /**
44
+ * Factory that derives the isolate-lifetime bootstrap configuration from the first supplied environment.
45
+ *
46
+ * Each application generation uses the returned configuration, including a generation restarted after a successful close.
47
+ */
48
+
49
+ /**
50
+ * Lazy Cloudflare Worker entrypoint whose first explicit environment configures the isolate.
51
+ *
52
+ * A successful close starts a fresh application generation with the same cached configuration.
53
+ */
54
+
37
55
  /**
38
56
  * Cloudflare Workers HTTP adapter with waitUntil-aware request tracking and graceful close behavior.
39
57
  */
@@ -43,6 +61,7 @@ export class CloudflareWorkerHttpApplicationAdapter {
43
61
  inFlightDrain;
44
62
  inFlightRequestCount = 0;
45
63
  isClosed = false;
64
+ isWebSocketBindingFrozen = false;
46
65
  websocketBinding;
47
66
  options;
48
67
  webRequestResponseFactory;
@@ -71,12 +90,35 @@ export class CloudflareWorkerHttpApplicationAdapter {
71
90
  }
72
91
  getRealtimeCapability() {
73
92
  return createFetchStyleHttpAdapterRealtimeCapability('Cloudflare Workers exposes WebSocketPair isolate-local request-upgrade hosting. Use @fluojs/websockets/cloudflare-workers for the official raw websocket binding.', {
93
+ bindingInstallation: {
94
+ install: binding => {
95
+ if (binding !== undefined && !isCloudflareWorkerWebSocketBinding(binding)) {
96
+ throw new Error(WEBSOCKET_BINDING_INSTALLATION_MESSAGE);
97
+ }
98
+ this.configureWebSocketBinding(binding);
99
+ }
100
+ },
74
101
  support: 'supported'
75
102
  });
76
103
  }
77
104
  configureWebSocketBinding(binding) {
105
+ if (this.isWebSocketBindingFrozen && binding !== this.websocketBinding) {
106
+ throw new Error(WEBSOCKET_BINDING_RECONFIGURATION_MESSAGE);
107
+ }
78
108
  this.websocketBinding = binding;
79
109
  }
110
+ [ADAPTER_CLOSE_SETTLED]() {
111
+ return this.closeInFlight ?? Promise.resolve();
112
+ }
113
+
114
+ /**
115
+ * Dispatch a Worker request while registering its lifecycle with the Worker execution context.
116
+ *
117
+ * @param request Worker request to dispatch.
118
+ * @param env Worker environment bindings attached to the framework request.
119
+ * @param executionContext Worker lifecycle context used to retain active work.
120
+ * @returns The dispatched Worker response.
121
+ */
80
122
  async fetch(request, env, executionContext) {
81
123
  if (this.closeInFlight || this.isClosed) {
82
124
  return createShutdownResponse();
@@ -84,31 +126,32 @@ export class CloudflareWorkerHttpApplicationAdapter {
84
126
  const release = this.trackInFlightRequest();
85
127
  const dispatcher = this.dispatcher;
86
128
  if (dispatcher && this.websocketBinding && isWebSocketUpgradeRequest(request)) {
129
+ const socketLifecycles = [];
87
130
  try {
88
131
  const response = await this.websocketBinding.fetch(request, {
89
- upgrade: upgradeRequest => this.upgradeWebSocket(upgradeRequest)
132
+ upgrade: upgradeRequest => {
133
+ const upgrade = this.upgradeWebSocket(upgradeRequest);
134
+ socketLifecycles.push(createWebSocketCloseLifecycle(upgrade.serverSocket));
135
+ return upgrade;
136
+ }
90
137
  });
91
- executionContext?.waitUntil(Promise.resolve());
92
138
  return response;
93
139
  } finally {
94
- release();
140
+ const lifecycle = Promise.all(socketLifecycles).then(() => undefined).finally(release);
141
+ executionContext.waitUntil(lifecycle);
95
142
  }
96
143
  }
97
- const responsePromise = (async () => {
98
- return await dispatchWebRequest({
99
- dispatcher,
100
- dispatcherNotReadyMessage: WORKER_DISPATCHER_NOT_READY_MESSAGE,
101
- factory: this.createRequestResponseFactory(env, executionContext),
102
- request
103
- });
104
- })();
105
- const trackedResponsePromise = responsePromise.then(response => createLifecycleTrackedResponse(response, release), error => {
106
- release();
107
- throw error;
144
+ const dispatch = startWebRequestDispatch({
145
+ dispatcher,
146
+ dispatcherNotReadyMessage: WORKER_DISPATCHER_NOT_READY_MESSAGE,
147
+ factory: this.createRequestResponseFactory(env, executionContext),
148
+ request
108
149
  });
109
- executionContext?.waitUntil(trackedResponsePromise.then(({
110
- lifecycle
111
- }) => lifecycle).then(() => undefined, () => undefined));
150
+ const trackedResponsePromise = dispatch.response.then(createLifecycleTrackedResponse);
151
+ const lifecycle = Promise.allSettled([dispatch.completion, trackedResponsePromise.then(({
152
+ lifecycle: responseLifecycle
153
+ }) => responseLifecycle)]).then(() => undefined).finally(release);
154
+ executionContext.waitUntil(lifecycle);
112
155
  return (await trackedResponsePromise).response;
113
156
  }
114
157
  async listen(dispatcher) {
@@ -116,6 +159,7 @@ export class CloudflareWorkerHttpApplicationAdapter {
116
159
  throw new Error('Cloudflare Workers adapter cannot listen while shutdown is still draining.');
117
160
  }
118
161
  this.isClosed = false;
162
+ this.isWebSocketBindingFrozen = true;
119
163
  this.dispatcher = dispatcher;
120
164
  }
121
165
  upgradeWebSocket(_request) {
@@ -206,15 +250,61 @@ export async function bootstrapCloudflareWorkerApplication(rootModule, options =
206
250
  * @returns A Worker entrypoint exposing lazy `fetch(...)`, `ready()`, and `close(...)` helpers.
207
251
  */
208
252
  export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
253
+ const entrypoint = createLazyCloudflareWorkerEntrypoint({
254
+ createApplication() {
255
+ return bootstrapCloudflareWorkerApplication(rootModule, options);
256
+ },
257
+ getReadyArgument() {
258
+ return undefined;
259
+ }
260
+ });
261
+ return {
262
+ close: entrypoint.close,
263
+ fetch: entrypoint.fetch,
264
+ ready() {
265
+ return entrypoint.ready(undefined);
266
+ }
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Create a lazy Cloudflare Worker entrypoint configured once per isolate from its first supplied environment.
272
+ *
273
+ * @param factory Factory that derives and caches the root module and final bootstrap options from one Worker environment.
274
+ * @returns A Worker entrypoint exposing env-aware lazy `fetch(...)`, `ready(env)`, and `close(...)` helpers for application generations using that cached configuration.
275
+ */
276
+ export function createCloudflareWorkerEnvEntrypoint(factory) {
277
+ let bootstrap;
278
+ return createLazyCloudflareWorkerEntrypoint({
279
+ createApplication(env) {
280
+ bootstrap ??= factory(env);
281
+ return bootstrapCloudflareWorkerApplication(bootstrap.rootModule, bootstrap.options);
282
+ },
283
+ getReadyArgument(env) {
284
+ return env;
285
+ }
286
+ });
287
+ }
288
+ function createLazyCloudflareWorkerEntrypoint(options) {
209
289
  let closeError;
210
290
  let closeInFlight;
291
+ let closeRecovery;
211
292
  let runningApplication;
212
- const ready = async () => {
293
+ const ready = async readyArgument => {
294
+ if (closeRecovery) {
295
+ await closeRecovery;
296
+ }
213
297
  if (closeError) {
214
298
  throw closeError;
215
299
  }
216
300
  if (!runningApplication) {
217
- runningApplication = bootstrapCloudflareWorkerApplication(rootModule, options);
301
+ const application = Promise.resolve().then(() => options.createApplication(readyArgument));
302
+ runningApplication = application;
303
+ void application.catch(() => {
304
+ if (runningApplication === application) {
305
+ runningApplication = undefined;
306
+ }
307
+ });
218
308
  }
219
309
  return await runningApplication;
220
310
  };
@@ -224,6 +314,9 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
224
314
  await closeInFlight;
225
315
  return;
226
316
  }
317
+ if (closeRecovery) {
318
+ await closeRecovery;
319
+ }
227
320
  if (closeError) {
228
321
  throw closeError;
229
322
  }
@@ -232,13 +325,31 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
232
325
  return;
233
326
  }
234
327
  const closing = (async () => {
328
+ let currentApplication;
235
329
  try {
236
- await (await application).close(signal);
330
+ currentApplication = await application;
331
+ await currentApplication.close(signal);
237
332
  if (runningApplication === application) {
238
333
  runningApplication = undefined;
239
334
  }
240
335
  } catch (error) {
241
- closeError = error;
336
+ if (currentApplication && isShutdownTimeoutError(error)) {
337
+ closeRecovery = watchTimedOutCloseRecovery(currentApplication, {
338
+ clearRunningApplication() {
339
+ if (runningApplication === application) {
340
+ runningApplication = undefined;
341
+ }
342
+ },
343
+ setCloseError(error) {
344
+ closeError = error;
345
+ },
346
+ setCloseRecovery(recovery) {
347
+ closeRecovery = recovery;
348
+ }
349
+ });
350
+ } else {
351
+ closeError = error;
352
+ }
242
353
  throw error;
243
354
  } finally {
244
355
  closeInFlight = undefined;
@@ -248,10 +359,10 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
248
359
  await closing;
249
360
  },
250
361
  async fetch(request, env, executionContext) {
251
- if (closeError || closeInFlight) {
362
+ if (closeError || closeInFlight || closeRecovery) {
252
363
  return createShutdownResponse();
253
364
  }
254
- return await (await ready()).fetch(request, env, executionContext);
365
+ return await (await ready(options.getReadyArgument(env))).fetch(request, env, executionContext);
255
366
  },
256
367
  ready
257
368
  };
@@ -314,6 +425,22 @@ function validateNonNegativeIntegerOption(name, value) {
314
425
  function isWebSocketUpgradeRequest(request) {
315
426
  return request.headers.get('upgrade')?.toLowerCase() === 'websocket';
316
427
  }
428
+ function isCloudflareWorkerWebSocketBinding(binding) {
429
+ return typeof binding === 'object' && binding !== null && 'fetch' in binding && typeof binding.fetch === 'function';
430
+ }
431
+ function createWebSocketCloseLifecycle(socket) {
432
+ if (socket.readyState === WEBSOCKET_CLOSED_READY_STATE) {
433
+ return Promise.resolve();
434
+ }
435
+ const lifecycle = createDeferred();
436
+ socket.addEventListener('close', () => lifecycle.resolve(), {
437
+ once: true
438
+ });
439
+ if (socket.readyState === WEBSOCKET_CLOSED_READY_STATE) {
440
+ lifecycle.resolve();
441
+ }
442
+ return lifecycle.promise;
443
+ }
317
444
  function createDeferred() {
318
445
  let resolve;
319
446
  let reject;
@@ -327,6 +454,25 @@ function createDeferred() {
327
454
  resolve
328
455
  };
329
456
  }
457
+ function watchTimedOutCloseRecovery(currentApplication, callbacks) {
458
+ const recovery = currentApplication.adapter[ADAPTER_CLOSE_SETTLED]().then(() => {
459
+ callbacks.clearRunningApplication();
460
+ }, error => {
461
+ callbacks.setCloseError(error);
462
+ throw error;
463
+ });
464
+ const recoveryWithCleanup = recovery.finally(() => {
465
+ callbacks.setCloseRecovery(undefined);
466
+ });
467
+ void recoveryWithCleanup.catch(() => undefined);
468
+ return recoveryWithCleanup;
469
+ }
470
+ function createShutdownTimeoutMessage(timeoutMs) {
471
+ return `Cloudflare Workers adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`;
472
+ }
473
+ function isShutdownTimeoutError(error) {
474
+ return error instanceof Error && error.message === createShutdownTimeoutMessage(DEFAULT_SHUTDOWN_TIMEOUT_MS);
475
+ }
330
476
  function createShutdownResponse() {
331
477
  return new Response(JSON.stringify({
332
478
  error: {
@@ -341,9 +487,8 @@ function createShutdownResponse() {
341
487
  status: 503
342
488
  });
343
489
  }
344
- function createLifecycleTrackedResponse(response, release) {
490
+ function createLifecycleTrackedResponse(response) {
345
491
  if (!isLifecycleTrackedStreamingResponse(response)) {
346
- release();
347
492
  return {
348
493
  lifecycle: Promise.resolve(),
349
494
  response
@@ -352,42 +497,45 @@ function createLifecycleTrackedResponse(response, release) {
352
497
  const lifecycle = createDeferred();
353
498
  const responseBody = response.body;
354
499
  if (!responseBody) {
355
- release();
356
500
  return {
357
501
  lifecycle: Promise.resolve(),
358
502
  response
359
503
  };
360
504
  }
361
- const reader = responseBody.getReader();
362
- const trackedBody = new ReadableStream({
363
- async cancel(reason) {
364
- try {
365
- await reader.cancel(reason);
366
- lifecycle.resolve();
367
- } catch (error) {
368
- lifecycle.reject(error);
369
- throw error;
370
- }
371
- },
372
- async pull(controller) {
373
- try {
374
- const result = await reader.read();
375
- if (result.done) {
376
- controller.close();
505
+ try {
506
+ const reader = responseBody.getReader();
507
+ const trackedBody = new ReadableStream({
508
+ async cancel(reason) {
509
+ try {
510
+ await reader.cancel(reason);
377
511
  lifecycle.resolve();
378
- return;
512
+ } catch (error) {
513
+ lifecycle.reject(error);
514
+ throw error;
515
+ }
516
+ },
517
+ async pull(controller) {
518
+ try {
519
+ const result = await reader.read();
520
+ if (result.done) {
521
+ controller.close();
522
+ lifecycle.resolve();
523
+ return;
524
+ }
525
+ controller.enqueue(result.value);
526
+ } catch (error) {
527
+ controller.error(error);
528
+ lifecycle.reject(error);
379
529
  }
380
- controller.enqueue(result.value);
381
- } catch (error) {
382
- controller.error(error);
383
- lifecycle.reject(error);
384
530
  }
385
- }
386
- });
387
- return {
388
- lifecycle: lifecycle.promise.finally(release),
389
- response: new Response(trackedBody, response)
390
- };
531
+ });
532
+ return {
533
+ lifecycle: lifecycle.promise,
534
+ response: new Response(trackedBody, response)
535
+ };
536
+ } catch (error) {
537
+ throw error;
538
+ }
391
539
  }
392
540
  function isLifecycleTrackedStreamingResponse(response) {
393
541
  return response.body !== null && response.headers.get('content-type')?.toLowerCase().includes('text/event-stream') === true;
@@ -395,7 +543,7 @@ function isLifecycleTrackedStreamingResponse(response) {
395
543
  function waitForCloseWithTimeout(closePromise, timeoutMs) {
396
544
  return new Promise((resolve, reject) => {
397
545
  const timeoutHandle = setTimeout(() => {
398
- reject(new Error(`Cloudflare Workers adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`));
546
+ reject(new Error(createShutdownTimeoutMessage(timeoutMs)));
399
547
  }, timeoutMs);
400
548
  void closePromise.then(() => {
401
549
  clearTimeout(timeoutHandle);
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "platform",
10
10
  "fetch"
11
11
  ],
12
- "version": "1.0.4",
12
+ "version": "2.0.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -33,11 +33,12 @@
33
33
  "dist"
34
34
  ],
35
35
  "dependencies": {
36
- "@fluojs/http": "^1.1.2",
37
- "@fluojs/runtime": "^1.1.8"
36
+ "@fluojs/http": "^3.0.0",
37
+ "@fluojs/runtime": "^3.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "vitest": "^3.2.4"
40
+ "vitest": "^4.1.11",
41
+ "@fluojs/testing": "^3.0.0"
41
42
  },
42
43
  "scripts": {
43
44
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",