@fluojs/platform-cloudflare-workers 1.0.5 → 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 +82 -5
- package/README.md +82 -5
- package/dist/adapter.d.ts +37 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +94 -24
- package/package.json +5 -4
package/README.ko.md
CHANGED
|
@@ -66,8 +66,62 @@ export default {
|
|
|
66
66
|
};
|
|
67
67
|
```
|
|
68
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
|
+
|
|
69
103
|
## 주요 패턴
|
|
70
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
|
+
|
|
71
125
|
### WebSocketPair 활용
|
|
72
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됩니다.
|
|
73
127
|
|
|
@@ -88,7 +142,7 @@ export class EdgeGateway {}
|
|
|
88
142
|
export class RealtimeModule {}
|
|
89
143
|
```
|
|
90
144
|
|
|
91
|
-
Bootstrap 전에 application module graph에 `RealtimeModule`을 import하세요. Application bootstrap 중 `CloudflareWorkersWebSocketModule`이 gateway를 발견하고 `app.listen()`이 binding을 freeze하기 전에 Worker adapter binding을
|
|
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을 추가하거나 교체하지 마세요.
|
|
92
146
|
|
|
93
147
|
### 엣지 네이티브 미들웨어
|
|
94
148
|
표준 fluo 미들웨어(CORS, Global Prefix 등)는 Worker bootstrap helper를 통해 완전히 지원되며 Cloudflare 환경에 최적화되어 있습니다. `createCloudflareWorkerAdapter(...)`는 adapter가 소유하는 parsing 및 websocket-pair 옵션만 받습니다. Routing 및 middleware 옵션은 `bootstrapCloudflareWorkerApplication(...)` 또는 `createCloudflareWorkerEntrypoint(...)`에 전달하세요.
|
|
@@ -102,12 +156,14 @@ const worker = createCloudflareWorkerEntrypoint(AppModule, {
|
|
|
102
156
|
|
|
103
157
|
### 동작 참고
|
|
104
158
|
|
|
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)`로 바꾸세요.
|
|
105
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에 도달하지 않습니다.
|
|
106
161
|
- `maxBodySize` 같은 adapter option은 Worker adapter 생성 시 검증됩니다. `globalPrefix`, `cors`, `middleware`, `securityHeaders` 같은 bootstrap 전용 옵션은 `createCloudflareWorkerAdapter(...)`가 아니라 Worker bootstrap helper에 전달해야 합니다.
|
|
107
162
|
- WebSocket upgrade는 HTTP dispatch와 같은 listen boundary가 소유합니다. `listen()` 전의 upgrade request는 설정된 binding에 도달하지 않으며, adapter가 한 번이라도 listen한 뒤 defined binding을 교체하거나 해제하려는 시도는 Worker upgrade ownership을 바꾸는 대신 빠르게 실패합니다. 다른 websocket binding이 필요하면 새 adapter를 생성하세요.
|
|
108
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에 도달합니다.
|
|
109
165
|
- Multipart request는 `rawBody`를 보존하지 않습니다.
|
|
110
|
-
- Worker `env` 객체는 각 `FrameworkRequest`에 `request.cloudflare.env`로 연결되고 Worker execution context는 `request.cloudflare.executionContext`로 제공됩니다.
|
|
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`를 사용하세요.
|
|
111
167
|
|
|
112
168
|
## Lifecycle 및 public seam 참고
|
|
113
169
|
|
|
@@ -115,22 +171,43 @@ Root `@fluojs/platform-cloudflare-workers` export는 application code와 first-p
|
|
|
115
171
|
|
|
116
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로 추적해야 합니다.
|
|
117
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
|
+
```
|
|
192
|
+
|
|
118
193
|
## Conformance 커버리지
|
|
119
194
|
|
|
120
|
-
`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, lazy entrypoint
|
|
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을 검증합니다.
|
|
121
196
|
|
|
122
|
-
공유 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을 검증합니다. 패키지
|
|
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와 비교합니다.
|
|
123
198
|
|
|
124
199
|
## 공개 API 개요
|
|
125
200
|
|
|
126
201
|
- `createCloudflareWorkerAdapter(options)`: Worker HTTP 어댑터를 위한 팩토리입니다.
|
|
127
202
|
- `createCloudflareWorkerEntrypoint(module, options)`: 지연 부트스트랩 방식의 Worker 엔트리포인트를 생성합니다.
|
|
203
|
+
- `createCloudflareWorkerEnvEntrypoint(factory)`: 첫 명시적 Worker environment에서 지연 Worker 엔트리포인트를 생성합니다.
|
|
128
204
|
- `bootstrapCloudflareWorkerApplication(module, options)`: Worker를 위한 비동기 부트스트랩 헬퍼입니다.
|
|
129
205
|
- `CloudflareWorkerHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다.
|
|
130
206
|
- `CloudflareWorkerHandler`: Worker application wrapper와 lazy entrypoint가 공유하는 fetch handler interface입니다.
|
|
131
207
|
- `CloudflareWorkerApplication`: `adapter`, `app`, `fetch(...)`, `close(...)`를 제공하는 fully bootstrapped Worker application wrapper입니다.
|
|
132
208
|
- `CloudflareWorkerEntrypoint`: `fetch`, `ready()`, `close()` lifecycle method를 제공하는 lazy entrypoint입니다.
|
|
133
|
-
-
|
|
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`.
|
|
134
211
|
|
|
135
212
|
## 관련 패키지
|
|
136
213
|
|
package/README.md
CHANGED
|
@@ -66,8 +66,62 @@ export default {
|
|
|
66
66
|
};
|
|
67
67
|
```
|
|
68
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
|
+
|
|
69
103
|
## Common Patterns
|
|
70
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
|
+
|
|
71
125
|
### Working with WebSocketPairs
|
|
72
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.
|
|
73
127
|
|
|
@@ -88,7 +142,7 @@ export class EdgeGateway {}
|
|
|
88
142
|
export class RealtimeModule {}
|
|
89
143
|
```
|
|
90
144
|
|
|
91
|
-
Import `RealtimeModule` into the application module graph before bootstrap. During application bootstrap, `CloudflareWorkersWebSocketModule` discovers the gateway and
|
|
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.
|
|
92
146
|
|
|
93
147
|
### Edge-Native Middleware
|
|
94
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.
|
|
@@ -102,12 +156,14 @@ const worker = createCloudflareWorkerEntrypoint(AppModule, {
|
|
|
102
156
|
|
|
103
157
|
### Behavior Notes
|
|
104
158
|
|
|
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)`.
|
|
105
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.
|
|
106
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(...)`.
|
|
107
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.
|
|
108
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.
|
|
109
165
|
- Multipart requests do not preserve `rawBody`.
|
|
110
|
-
- The Worker `env` object is attached to each `FrameworkRequest` as `request.cloudflare.env`, with the Worker execution context available as `request.cloudflare.executionContext
|
|
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.
|
|
111
167
|
|
|
112
168
|
## Lifecycle and Public Seam Notes
|
|
113
169
|
|
|
@@ -115,22 +171,43 @@ The root `@fluojs/platform-cloudflare-workers` export owns the Worker public sea
|
|
|
115
171
|
|
|
116
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.
|
|
117
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
|
+
```
|
|
192
|
+
|
|
118
193
|
## Conformance Coverage
|
|
119
194
|
|
|
120
|
-
`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, lazy entrypoint reuse
|
|
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.
|
|
121
196
|
|
|
122
|
-
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
|
|
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.
|
|
123
198
|
|
|
124
199
|
## Public API Overview
|
|
125
200
|
|
|
126
201
|
- `createCloudflareWorkerAdapter(options)`: Factory for the Worker HTTP adapter.
|
|
127
202
|
- `createCloudflareWorkerEntrypoint(module, options)`: Creates a lazy-bootstrapping Worker entrypoint.
|
|
203
|
+
- `createCloudflareWorkerEnvEntrypoint(factory)`: Creates a lazy Worker entrypoint from the first explicit Worker environment.
|
|
128
204
|
- `bootstrapCloudflareWorkerApplication(module, options)`: Async bootstrap helper for Workers.
|
|
129
205
|
- `CloudflareWorkerHttpApplicationAdapter`: The core adapter implementation.
|
|
130
206
|
- `CloudflareWorkerHandler`: Fetch handler interface shared by Worker application wrappers and lazy entrypoints.
|
|
131
207
|
- `CloudflareWorkerApplication`: Fully bootstrapped Worker application wrapper with `adapter`, `app`, `fetch(...)`, and `close(...)`.
|
|
132
208
|
- `CloudflareWorkerEntrypoint`: Lazy entrypoint with `fetch`, `ready()`, and `close()` lifecycle methods.
|
|
133
|
-
-
|
|
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`.
|
|
134
211
|
|
|
135
212
|
## Related Packages
|
|
136
213
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface CloudflareWorkerExecutionContext {
|
|
|
23
23
|
/** Worker-specific request context attached to fluo HTTP requests by the Cloudflare adapter. */
|
|
24
24
|
export interface CloudflareWorkerRequestContext<Env = unknown> {
|
|
25
25
|
readonly env: Env;
|
|
26
|
-
readonly executionContext
|
|
26
|
+
readonly executionContext: CloudflareWorkerExecutionContext;
|
|
27
27
|
}
|
|
28
28
|
/** Message payloads accepted by Cloudflare Worker websockets. */
|
|
29
29
|
export type CloudflareWorkerWebSocketMessage = ArrayBuffer | ArrayBufferView | Blob | string;
|
|
@@ -78,6 +78,26 @@ export interface CloudflareWorkerEntrypoint<Env = unknown> extends CloudflareWor
|
|
|
78
78
|
close(signal?: string): Promise<void>;
|
|
79
79
|
ready(): Promise<CloudflareWorkerApplication<Env>>;
|
|
80
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
|
+
}
|
|
81
101
|
/**
|
|
82
102
|
* Cloudflare Workers HTTP adapter with waitUntil-aware request tracking and graceful close behavior.
|
|
83
103
|
*/
|
|
@@ -96,7 +116,15 @@ export declare class CloudflareWorkerHttpApplicationAdapter implements HttpAppli
|
|
|
96
116
|
getRealtimeCapability(): import("@fluojs/http").FetchStyleHttpAdapterRealtimeCapability;
|
|
97
117
|
configureWebSocketBinding(binding: CloudflareWorkerWebSocketBinding | undefined): void;
|
|
98
118
|
[ADAPTER_CLOSE_SETTLED](): Promise<void>;
|
|
99
|
-
|
|
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>;
|
|
100
128
|
listen(dispatcher: Dispatcher): Promise<void>;
|
|
101
129
|
private upgradeWebSocket;
|
|
102
130
|
private trackInFlightRequest;
|
|
@@ -126,6 +154,13 @@ export declare function bootstrapCloudflareWorkerApplication<Env = unknown>(root
|
|
|
126
154
|
* @returns A Worker entrypoint exposing lazy `fetch(...)`, `ready()`, and `close(...)` helpers.
|
|
127
155
|
*/
|
|
128
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>;
|
|
129
164
|
declare global {
|
|
130
165
|
interface ResponseInit {
|
|
131
166
|
webSocket?: CloudflareWorkerWebSocket;
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createFetchStyleHttpAdapterRealtimeCapability } from '@fluojs/http/internal';
|
|
2
2
|
import { bootstrapHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
|
|
3
|
-
import { createWebRequestResponseFactory,
|
|
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
6
|
const WEBSOCKET_CLOSED_READY_STATE = 3;
|
|
7
7
|
const ADAPTER_CLOSE_SETTLED = Symbol('CloudflareWorkerAdapterCloseSettled');
|
|
8
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.';
|
|
9
10
|
|
|
10
11
|
/** Minimal Worker execution context surface used by the adapter. */
|
|
11
12
|
|
|
@@ -37,6 +38,20 @@ const WEBSOCKET_BINDING_RECONFIGURATION_MESSAGE = 'Cloudflare Workers websocket
|
|
|
37
38
|
|
|
38
39
|
/** Lazy Cloudflare Worker entrypoint that bootstraps on first use. */
|
|
39
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
|
+
|
|
40
55
|
/**
|
|
41
56
|
* Cloudflare Workers HTTP adapter with waitUntil-aware request tracking and graceful close behavior.
|
|
42
57
|
*/
|
|
@@ -75,6 +90,14 @@ export class CloudflareWorkerHttpApplicationAdapter {
|
|
|
75
90
|
}
|
|
76
91
|
getRealtimeCapability() {
|
|
77
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
|
+
},
|
|
78
101
|
support: 'supported'
|
|
79
102
|
});
|
|
80
103
|
}
|
|
@@ -87,6 +110,15 @@ export class CloudflareWorkerHttpApplicationAdapter {
|
|
|
87
110
|
[ADAPTER_CLOSE_SETTLED]() {
|
|
88
111
|
return this.closeInFlight ?? Promise.resolve();
|
|
89
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
|
+
*/
|
|
90
122
|
async fetch(request, env, executionContext) {
|
|
91
123
|
if (this.closeInFlight || this.isClosed) {
|
|
92
124
|
return createShutdownResponse();
|
|
@@ -106,24 +138,20 @@ export class CloudflareWorkerHttpApplicationAdapter {
|
|
|
106
138
|
return response;
|
|
107
139
|
} finally {
|
|
108
140
|
const lifecycle = Promise.all(socketLifecycles).then(() => undefined).finally(release);
|
|
109
|
-
executionContext
|
|
141
|
+
executionContext.waitUntil(lifecycle);
|
|
110
142
|
}
|
|
111
143
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
request
|
|
118
|
-
});
|
|
119
|
-
})();
|
|
120
|
-
const trackedResponsePromise = responsePromise.then(response => createLifecycleTrackedResponse(response, release), error => {
|
|
121
|
-
release();
|
|
122
|
-
throw error;
|
|
144
|
+
const dispatch = startWebRequestDispatch({
|
|
145
|
+
dispatcher,
|
|
146
|
+
dispatcherNotReadyMessage: WORKER_DISPATCHER_NOT_READY_MESSAGE,
|
|
147
|
+
factory: this.createRequestResponseFactory(env, executionContext),
|
|
148
|
+
request
|
|
123
149
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
127
155
|
return (await trackedResponsePromise).response;
|
|
128
156
|
}
|
|
129
157
|
async listen(dispatcher) {
|
|
@@ -222,11 +250,47 @@ export async function bootstrapCloudflareWorkerApplication(rootModule, options =
|
|
|
222
250
|
* @returns A Worker entrypoint exposing lazy `fetch(...)`, `ready()`, and `close(...)` helpers.
|
|
223
251
|
*/
|
|
224
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) {
|
|
225
289
|
let closeError;
|
|
226
290
|
let closeInFlight;
|
|
227
291
|
let closeRecovery;
|
|
228
292
|
let runningApplication;
|
|
229
|
-
const ready = async
|
|
293
|
+
const ready = async readyArgument => {
|
|
230
294
|
if (closeRecovery) {
|
|
231
295
|
await closeRecovery;
|
|
232
296
|
}
|
|
@@ -234,7 +298,13 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
|
|
|
234
298
|
throw closeError;
|
|
235
299
|
}
|
|
236
300
|
if (!runningApplication) {
|
|
237
|
-
|
|
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
|
+
});
|
|
238
308
|
}
|
|
239
309
|
return await runningApplication;
|
|
240
310
|
};
|
|
@@ -292,7 +362,7 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
|
|
|
292
362
|
if (closeError || closeInFlight || closeRecovery) {
|
|
293
363
|
return createShutdownResponse();
|
|
294
364
|
}
|
|
295
|
-
return await (await ready()).fetch(request, env, executionContext);
|
|
365
|
+
return await (await ready(options.getReadyArgument(env))).fetch(request, env, executionContext);
|
|
296
366
|
},
|
|
297
367
|
ready
|
|
298
368
|
};
|
|
@@ -355,6 +425,9 @@ function validateNonNegativeIntegerOption(name, value) {
|
|
|
355
425
|
function isWebSocketUpgradeRequest(request) {
|
|
356
426
|
return request.headers.get('upgrade')?.toLowerCase() === 'websocket';
|
|
357
427
|
}
|
|
428
|
+
function isCloudflareWorkerWebSocketBinding(binding) {
|
|
429
|
+
return typeof binding === 'object' && binding !== null && 'fetch' in binding && typeof binding.fetch === 'function';
|
|
430
|
+
}
|
|
358
431
|
function createWebSocketCloseLifecycle(socket) {
|
|
359
432
|
if (socket.readyState === WEBSOCKET_CLOSED_READY_STATE) {
|
|
360
433
|
return Promise.resolve();
|
|
@@ -414,9 +487,8 @@ function createShutdownResponse() {
|
|
|
414
487
|
status: 503
|
|
415
488
|
});
|
|
416
489
|
}
|
|
417
|
-
function createLifecycleTrackedResponse(response
|
|
490
|
+
function createLifecycleTrackedResponse(response) {
|
|
418
491
|
if (!isLifecycleTrackedStreamingResponse(response)) {
|
|
419
|
-
release();
|
|
420
492
|
return {
|
|
421
493
|
lifecycle: Promise.resolve(),
|
|
422
494
|
response
|
|
@@ -425,7 +497,6 @@ function createLifecycleTrackedResponse(response, release) {
|
|
|
425
497
|
const lifecycle = createDeferred();
|
|
426
498
|
const responseBody = response.body;
|
|
427
499
|
if (!responseBody) {
|
|
428
|
-
release();
|
|
429
500
|
return {
|
|
430
501
|
lifecycle: Promise.resolve(),
|
|
431
502
|
response
|
|
@@ -459,11 +530,10 @@ function createLifecycleTrackedResponse(response, release) {
|
|
|
459
530
|
}
|
|
460
531
|
});
|
|
461
532
|
return {
|
|
462
|
-
lifecycle: lifecycle.promise
|
|
533
|
+
lifecycle: lifecycle.promise,
|
|
463
534
|
response: new Response(trackedBody, response)
|
|
464
535
|
};
|
|
465
536
|
} catch (error) {
|
|
466
|
-
release();
|
|
467
537
|
throw error;
|
|
468
538
|
}
|
|
469
539
|
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"platform",
|
|
10
10
|
"fetch"
|
|
11
11
|
],
|
|
12
|
-
"version": "
|
|
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": "^
|
|
37
|
-
"@fluojs/runtime": "^
|
|
36
|
+
"@fluojs/http": "^3.0.0",
|
|
37
|
+
"@fluojs/runtime": "^3.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"vitest": "^
|
|
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",
|