@fluojs/platform-cloudflare-workers 1.0.4 → 1.0.5

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
 
@@ -68,13 +69,27 @@ export default {
68
69
  ## 주요 패턴
69
70
 
70
71
  ### WebSocketPair 활용
71
- 어댑터는 `@fluojs/websockets/cloudflare-workers` 바인딩을 통해 실시간 통신을 위한 Cloudflare의 네이티브 `WebSocketPair`를 지원합니다. Upgrade handling은 해당 binding을 통한 opt-in이며, non-hosted runtime test에서는 `createWebSocketPair`를 주입할 수 있습니다.
72
+ 어댑터는 `@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
73
 
73
74
  ```typescript
75
+ import { Module } from '@fluojs/core';
76
+ import {
77
+ CloudflareWorkersWebSocketModule,
78
+ WebSocketGateway,
79
+ } from '@fluojs/websockets/cloudflare-workers';
80
+
74
81
  @WebSocketGateway({ path: '/ws' })
75
- export class MyGateway {}
82
+ export class EdgeGateway {}
83
+
84
+ @Module({
85
+ imports: [CloudflareWorkersWebSocketModule.forRoot()],
86
+ providers: [EdgeGateway],
87
+ })
88
+ export class RealtimeModule {}
76
89
  ```
77
90
 
91
+ Bootstrap 전에 application module graph에 `RealtimeModule`을 import하세요. Application bootstrap 중 `CloudflareWorkersWebSocketModule`이 gateway를 발견하고 `app.listen()`이 binding을 freeze하기 전에 Worker adapter binding을 구성합니다. Listen boundary 이후에는 binding을 추가하거나 교체하지 마세요.
92
+
78
93
  ### 엣지 네이티브 미들웨어
79
94
  표준 fluo 미들웨어(CORS, Global Prefix 등)는 Worker bootstrap helper를 통해 완전히 지원되며 Cloudflare 환경에 최적화되어 있습니다. `createCloudflareWorkerAdapter(...)`는 adapter가 소유하는 parsing 및 websocket-pair 옵션만 받습니다. Routing 및 middleware 옵션은 `bootstrapCloudflareWorkerApplication(...)` 또는 `createCloudflareWorkerEntrypoint(...)`에 전달하세요.
80
95
 
@@ -87,16 +102,22 @@ const worker = createCloudflareWorkerEntrypoint(AppModule, {
87
102
 
88
103
  ### 동작 참고
89
104
 
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에 도달하지 않습니다.
105
+ - `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이 동기적으로 실패하면 오류를 전파하기 전에 lifecyclerelease합니다. 그 lifecycle boundary 전에는 upgrade request와 HTTP dispatch가 application handler에 도달하지 않습니다.
91
106
  - `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됩니다.
107
+ - WebSocket upgrade는 HTTP dispatch와 같은 listen boundary가 소유합니다. `listen()` 전의 upgrade request는 설정된 binding에 도달하지 않으며, adapter가 한 번이라도 listen한 뒤 defined binding을 교체하거나 해제하려는 시도는 Worker upgrade ownership을 바꾸는 대신 빠르게 실패합니다. 다른 websocket binding이 필요하면 새 adapter를 생성하세요.
108
+ - `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을 영구적으로 캐시하지 않습니다.
94
109
  - Multipart request는 `rawBody`를 보존하지 않습니다.
95
110
  - Worker `env` 객체는 각 `FrameworkRequest`에 `request.cloudflare.env`로 연결되고 Worker execution context는 `request.cloudflare.executionContext`로 제공됩니다. Package-level config resolution은 application이 소유하므로, binding은 application boundary에서 명시적 provider 또는 `@fluojs/config`로 매핑하세요.
96
111
 
112
+ ## Lifecycle 및 public seam 참고
113
+
114
+ 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됩니다.
115
+
116
+ 위의 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
+
97
118
  ## Conformance 커버리지
98
119
 
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을 검증합니다.
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 재사용 및 timeout recovery, shutdown gating, drain 중 `listen()` rejection, HTTP와 websocket upgrade 모두에 대한 close 중 및 close 이후 JSON `503` response, reliable fake-timer cleanup, public seam source import, README parity, bounded 10초 close timeout을 검증합니다.
100
121
 
101
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을 검증합니다. 패키지 테스트의 README parity assertion은 이 edge-runtime 커버리지 문서가 한국어 mirror와 계속 동기화되도록 확인합니다.
102
123
 
@@ -109,7 +130,7 @@ const worker = createCloudflareWorkerEntrypoint(AppModule, {
109
130
  - `CloudflareWorkerHandler`: Worker application wrapper와 lazy entrypoint가 공유하는 fetch handler interface입니다.
110
131
  - `CloudflareWorkerApplication`: `adapter`, `app`, `fetch(...)`, `close(...)`를 제공하는 fully bootstrapped Worker application wrapper입니다.
111
132
  - `CloudflareWorkerEntrypoint`: `fetch`, `ready()`, `close()` lifecycle method를 제공하는 lazy entrypoint입니다.
112
- - Option 및 type: `CloudflareWorkerAdapterOptions`, `BootstrapCloudflareWorkerApplicationOptions`, `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, Worker websocket pair/upgrade type.
133
+ - Option 및 type: `CloudflareWorkerAdapterOptions`, `BootstrapCloudflareWorkerApplicationOptions`, `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, `CloudflareWorkerWebSocketBindingHost`, `CloudflareWorkerWebSocket`, `CloudflareWorkerWebSocketMessage`, `CloudflareWorkerWebSocketPair`, `CloudflareWorkerWebSocketPairFactory`, `CloudflareWorkerWebSocketUpgradeHost`, `CloudflareWorkerWebSocketUpgradeResult`.
113
134
 
114
135
  ## 관련 패키지
115
136
 
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
 
@@ -68,13 +69,27 @@ export default {
68
69
  ## Common Patterns
69
70
 
70
71
  ### 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.
72
+ 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
73
 
73
74
  ```typescript
75
+ import { Module } from '@fluojs/core';
76
+ import {
77
+ CloudflareWorkersWebSocketModule,
78
+ WebSocketGateway,
79
+ } from '@fluojs/websockets/cloudflare-workers';
80
+
74
81
  @WebSocketGateway({ path: '/ws' })
75
- export class MyGateway {}
82
+ export class EdgeGateway {}
83
+
84
+ @Module({
85
+ imports: [CloudflareWorkersWebSocketModule.forRoot()],
86
+ providers: [EdgeGateway],
87
+ })
88
+ export class RealtimeModule {}
76
89
  ```
77
90
 
91
+ Import `RealtimeModule` into the application module graph before bootstrap. During application bootstrap, `CloudflareWorkersWebSocketModule` discovers the gateway and configures the Worker adapter binding before `app.listen()` freezes it; do not add or replace the binding after the listen boundary.
92
+
78
93
  ### Edge-Native Middleware
79
94
  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
95
 
@@ -87,16 +102,22 @@ const worker = createCloudflareWorkerEntrypoint(AppModule, {
87
102
 
88
103
  ### Behavior Notes
89
104
 
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.
105
+ - `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
106
  - 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.
107
+ - 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
+ - `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.
94
109
  - Multipart requests do not preserve `rawBody`.
95
110
  - 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.
96
111
 
112
+ ## Lifecycle and Public Seam Notes
113
+
114
+ 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.
115
+
116
+ 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
+
97
118
  ## Conformance Coverage
98
119
 
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.
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 and 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, README parity, and the bounded 10-second close timeout.
100
121
 
101
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 README parity assertion in the package test keeps these documented edge-runtime coverage claims synchronized with the Korean mirror.
102
123
 
@@ -109,7 +130,7 @@ The shared edge portability suite in `packages/testing/src/portability/web-runti
109
130
  - `CloudflareWorkerHandler`: Fetch handler interface shared by Worker application wrappers and lazy entrypoints.
110
131
  - `CloudflareWorkerApplication`: Fully bootstrapped Worker application wrapper with `adapter`, `app`, `fetch(...)`, and `close(...)`.
111
132
  - `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.
133
+ - Options and types: `CloudflareWorkerAdapterOptions`, `BootstrapCloudflareWorkerApplicationOptions`, `CloudflareWorkerExecutionContext`, `CloudflareWorkerRequestContext`, `CloudflareWorkerWebSocketBinding`, `CloudflareWorkerWebSocketBindingHost`, `CloudflareWorkerWebSocket`, `CloudflareWorkerWebSocketMessage`, `CloudflareWorkerWebSocketPair`, `CloudflareWorkerWebSocketPairFactory`, `CloudflareWorkerWebSocketUpgradeHost`, and `CloudflareWorkerWebSocketUpgradeResult`.
113
134
 
114
135
  ## Related Packages
115
136
 
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;
@@ -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> {
@@ -81,6 +87,7 @@ export declare class CloudflareWorkerHttpApplicationAdapter implements HttpAppli
81
87
  private inFlightDrain?;
82
88
  private inFlightRequestCount;
83
89
  private isClosed;
90
+ private isWebSocketBindingFrozen;
84
91
  private websocketBinding?;
85
92
  private readonly options;
86
93
  private readonly webRequestResponseFactory;
@@ -88,6 +95,7 @@ export declare class CloudflareWorkerHttpApplicationAdapter implements HttpAppli
88
95
  close(): Promise<void>;
89
96
  getRealtimeCapability(): import("@fluojs/http").FetchStyleHttpAdapterRealtimeCapability;
90
97
  configureWebSocketBinding(binding: CloudflareWorkerWebSocketBinding | undefined): void;
98
+ [ADAPTER_CLOSE_SETTLED](): Promise<void>;
91
99
  fetch<Env = unknown>(request: Request, env?: Env, executionContext?: CloudflareWorkerExecutionContext): Promise<Response>;
92
100
  listen(dispatcher: Dispatcher): Promise<void>;
93
101
  private upgradeWebSocket;
@@ -126,4 +134,5 @@ declare global {
126
134
  WebSocketPair?: new () => CloudflareWorkerWebSocketPair;
127
135
  }
128
136
  }
137
+ export {};
129
138
  //# 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
@@ -3,6 +3,9 @@ import { bootstrapHttpAdapterApplication } from '@fluojs/runtime/internal/http-a
3
3
  import { createWebRequestResponseFactory, dispatchWebRequest } 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.';
6
9
 
7
10
  /** Minimal Worker execution context surface used by the adapter. */
8
11
 
@@ -43,6 +46,7 @@ export class CloudflareWorkerHttpApplicationAdapter {
43
46
  inFlightDrain;
44
47
  inFlightRequestCount = 0;
45
48
  isClosed = false;
49
+ isWebSocketBindingFrozen = false;
46
50
  websocketBinding;
47
51
  options;
48
52
  webRequestResponseFactory;
@@ -75,8 +79,14 @@ export class CloudflareWorkerHttpApplicationAdapter {
75
79
  });
76
80
  }
77
81
  configureWebSocketBinding(binding) {
82
+ if (this.isWebSocketBindingFrozen && binding !== this.websocketBinding) {
83
+ throw new Error(WEBSOCKET_BINDING_RECONFIGURATION_MESSAGE);
84
+ }
78
85
  this.websocketBinding = binding;
79
86
  }
87
+ [ADAPTER_CLOSE_SETTLED]() {
88
+ return this.closeInFlight ?? Promise.resolve();
89
+ }
80
90
  async fetch(request, env, executionContext) {
81
91
  if (this.closeInFlight || this.isClosed) {
82
92
  return createShutdownResponse();
@@ -84,14 +94,19 @@ export class CloudflareWorkerHttpApplicationAdapter {
84
94
  const release = this.trackInFlightRequest();
85
95
  const dispatcher = this.dispatcher;
86
96
  if (dispatcher && this.websocketBinding && isWebSocketUpgradeRequest(request)) {
97
+ const socketLifecycles = [];
87
98
  try {
88
99
  const response = await this.websocketBinding.fetch(request, {
89
- upgrade: upgradeRequest => this.upgradeWebSocket(upgradeRequest)
100
+ upgrade: upgradeRequest => {
101
+ const upgrade = this.upgradeWebSocket(upgradeRequest);
102
+ socketLifecycles.push(createWebSocketCloseLifecycle(upgrade.serverSocket));
103
+ return upgrade;
104
+ }
90
105
  });
91
- executionContext?.waitUntil(Promise.resolve());
92
106
  return response;
93
107
  } finally {
94
- release();
108
+ const lifecycle = Promise.all(socketLifecycles).then(() => undefined).finally(release);
109
+ executionContext?.waitUntil(lifecycle);
95
110
  }
96
111
  }
97
112
  const responsePromise = (async () => {
@@ -116,6 +131,7 @@ export class CloudflareWorkerHttpApplicationAdapter {
116
131
  throw new Error('Cloudflare Workers adapter cannot listen while shutdown is still draining.');
117
132
  }
118
133
  this.isClosed = false;
134
+ this.isWebSocketBindingFrozen = true;
119
135
  this.dispatcher = dispatcher;
120
136
  }
121
137
  upgradeWebSocket(_request) {
@@ -208,8 +224,12 @@ export async function bootstrapCloudflareWorkerApplication(rootModule, options =
208
224
  export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
209
225
  let closeError;
210
226
  let closeInFlight;
227
+ let closeRecovery;
211
228
  let runningApplication;
212
229
  const ready = async () => {
230
+ if (closeRecovery) {
231
+ await closeRecovery;
232
+ }
213
233
  if (closeError) {
214
234
  throw closeError;
215
235
  }
@@ -224,6 +244,9 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
224
244
  await closeInFlight;
225
245
  return;
226
246
  }
247
+ if (closeRecovery) {
248
+ await closeRecovery;
249
+ }
227
250
  if (closeError) {
228
251
  throw closeError;
229
252
  }
@@ -232,13 +255,31 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
232
255
  return;
233
256
  }
234
257
  const closing = (async () => {
258
+ let currentApplication;
235
259
  try {
236
- await (await application).close(signal);
260
+ currentApplication = await application;
261
+ await currentApplication.close(signal);
237
262
  if (runningApplication === application) {
238
263
  runningApplication = undefined;
239
264
  }
240
265
  } catch (error) {
241
- closeError = error;
266
+ if (currentApplication && isShutdownTimeoutError(error)) {
267
+ closeRecovery = watchTimedOutCloseRecovery(currentApplication, {
268
+ clearRunningApplication() {
269
+ if (runningApplication === application) {
270
+ runningApplication = undefined;
271
+ }
272
+ },
273
+ setCloseError(error) {
274
+ closeError = error;
275
+ },
276
+ setCloseRecovery(recovery) {
277
+ closeRecovery = recovery;
278
+ }
279
+ });
280
+ } else {
281
+ closeError = error;
282
+ }
242
283
  throw error;
243
284
  } finally {
244
285
  closeInFlight = undefined;
@@ -248,7 +289,7 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
248
289
  await closing;
249
290
  },
250
291
  async fetch(request, env, executionContext) {
251
- if (closeError || closeInFlight) {
292
+ if (closeError || closeInFlight || closeRecovery) {
252
293
  return createShutdownResponse();
253
294
  }
254
295
  return await (await ready()).fetch(request, env, executionContext);
@@ -314,6 +355,19 @@ function validateNonNegativeIntegerOption(name, value) {
314
355
  function isWebSocketUpgradeRequest(request) {
315
356
  return request.headers.get('upgrade')?.toLowerCase() === 'websocket';
316
357
  }
358
+ function createWebSocketCloseLifecycle(socket) {
359
+ if (socket.readyState === WEBSOCKET_CLOSED_READY_STATE) {
360
+ return Promise.resolve();
361
+ }
362
+ const lifecycle = createDeferred();
363
+ socket.addEventListener('close', () => lifecycle.resolve(), {
364
+ once: true
365
+ });
366
+ if (socket.readyState === WEBSOCKET_CLOSED_READY_STATE) {
367
+ lifecycle.resolve();
368
+ }
369
+ return lifecycle.promise;
370
+ }
317
371
  function createDeferred() {
318
372
  let resolve;
319
373
  let reject;
@@ -327,6 +381,25 @@ function createDeferred() {
327
381
  resolve
328
382
  };
329
383
  }
384
+ function watchTimedOutCloseRecovery(currentApplication, callbacks) {
385
+ const recovery = currentApplication.adapter[ADAPTER_CLOSE_SETTLED]().then(() => {
386
+ callbacks.clearRunningApplication();
387
+ }, error => {
388
+ callbacks.setCloseError(error);
389
+ throw error;
390
+ });
391
+ const recoveryWithCleanup = recovery.finally(() => {
392
+ callbacks.setCloseRecovery(undefined);
393
+ });
394
+ void recoveryWithCleanup.catch(() => undefined);
395
+ return recoveryWithCleanup;
396
+ }
397
+ function createShutdownTimeoutMessage(timeoutMs) {
398
+ return `Cloudflare Workers adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`;
399
+ }
400
+ function isShutdownTimeoutError(error) {
401
+ return error instanceof Error && error.message === createShutdownTimeoutMessage(DEFAULT_SHUTDOWN_TIMEOUT_MS);
402
+ }
330
403
  function createShutdownResponse() {
331
404
  return new Response(JSON.stringify({
332
405
  error: {
@@ -358,36 +431,41 @@ function createLifecycleTrackedResponse(response, release) {
358
431
  response
359
432
  };
360
433
  }
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();
434
+ try {
435
+ const reader = responseBody.getReader();
436
+ const trackedBody = new ReadableStream({
437
+ async cancel(reason) {
438
+ try {
439
+ await reader.cancel(reason);
377
440
  lifecycle.resolve();
378
- return;
441
+ } catch (error) {
442
+ lifecycle.reject(error);
443
+ throw error;
444
+ }
445
+ },
446
+ async pull(controller) {
447
+ try {
448
+ const result = await reader.read();
449
+ if (result.done) {
450
+ controller.close();
451
+ lifecycle.resolve();
452
+ return;
453
+ }
454
+ controller.enqueue(result.value);
455
+ } catch (error) {
456
+ controller.error(error);
457
+ lifecycle.reject(error);
379
458
  }
380
- controller.enqueue(result.value);
381
- } catch (error) {
382
- controller.error(error);
383
- lifecycle.reject(error);
384
459
  }
385
- }
386
- });
387
- return {
388
- lifecycle: lifecycle.promise.finally(release),
389
- response: new Response(trackedBody, response)
390
- };
460
+ });
461
+ return {
462
+ lifecycle: lifecycle.promise.finally(release),
463
+ response: new Response(trackedBody, response)
464
+ };
465
+ } catch (error) {
466
+ release();
467
+ throw error;
468
+ }
391
469
  }
392
470
  function isLifecycleTrackedStreamingResponse(response) {
393
471
  return response.body !== null && response.headers.get('content-type')?.toLowerCase().includes('text/event-stream') === true;
@@ -395,7 +473,7 @@ function isLifecycleTrackedStreamingResponse(response) {
395
473
  function waitForCloseWithTimeout(closePromise, timeoutMs) {
396
474
  return new Promise((resolve, reject) => {
397
475
  const timeoutHandle = setTimeout(() => {
398
- reject(new Error(`Cloudflare Workers adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`));
476
+ reject(new Error(createShutdownTimeoutMessage(timeoutMs)));
399
477
  }, timeoutMs);
400
478
  void closePromise.then(() => {
401
479
  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": "1.0.5",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "dist"
34
34
  ],
35
35
  "dependencies": {
36
- "@fluojs/http": "^1.1.2",
37
- "@fluojs/runtime": "^1.1.8"
36
+ "@fluojs/http": "^2.0.1",
37
+ "@fluojs/runtime": "^2.0.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "vitest": "^3.2.4"