@fluojs/platform-cloudflare-workers 1.0.3 → 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 +31 -10
- package/README.md +31 -10
- package/dist/adapter.d.ts +23 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +173 -20
- package/package.json +3 -3
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()` 중에도 진행 중인
|
|
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
|
|
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(...)`에 등록합니다. 그 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이 동기적으로 실패하면 오류를 전파하기 전에 lifecycle을 release합니다. 그 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 이후 새
|
|
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
|
-
- Worker `env` 객체는
|
|
110
|
+
- Worker `env` 객체는 각 `FrameworkRequest`에 `request.cloudflare.env`로 연결되고 Worker execution context는 `request.cloudflare.executionContext`로 제공됩니다. Package-level config resolution은 application이 소유하므로, binding은 application boundary에서 명시적 provider 또는 `@fluojs/config`로 매핑하세요.
|
|
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로 추적해야 합니다.
|
|
96
117
|
|
|
97
118
|
## Conformance 커버리지
|
|
98
119
|
|
|
99
|
-
`packages/platform-cloudflare-workers/src/adapter.test.ts`는 문서화된 Worker 계약을 검증하는 package-local regression 대상입니다. 이
|
|
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`, `CloudflareWorkerWebSocketBinding`,
|
|
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 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
|
|
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; 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
|
-
- The Worker `env` object is
|
|
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.
|
|
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.
|
|
96
117
|
|
|
97
118
|
## Conformance Coverage
|
|
98
119
|
|
|
99
|
-
`packages/platform-cloudflare-workers/src/adapter.test.ts`
|
|
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`, `CloudflareWorkerWebSocketBinding`,
|
|
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,18 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
8
|
-
rawBody?: Uint8Array;
|
|
6
|
+
cloudflare?: CloudflareWorkerRequestContext;
|
|
9
7
|
}
|
|
10
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
|
+
}
|
|
11
18
|
/** Minimal Worker execution context surface used by the adapter. */
|
|
12
19
|
export interface CloudflareWorkerExecutionContext {
|
|
13
20
|
passThroughOnException?(): void;
|
|
14
21
|
waitUntil(promise: Promise<unknown>): void;
|
|
15
22
|
}
|
|
23
|
+
/** Worker-specific request context attached to fluo HTTP requests by the Cloudflare adapter. */
|
|
24
|
+
export interface CloudflareWorkerRequestContext<Env = unknown> {
|
|
25
|
+
readonly env: Env;
|
|
26
|
+
readonly executionContext?: CloudflareWorkerExecutionContext;
|
|
27
|
+
}
|
|
16
28
|
/** Message payloads accepted by Cloudflare Worker websockets. */
|
|
17
29
|
export type CloudflareWorkerWebSocketMessage = ArrayBuffer | ArrayBufferView | Blob | string;
|
|
18
30
|
/** Server-side Cloudflare Worker websocket shape used by the raw binding seam. */
|
|
@@ -49,7 +61,7 @@ export interface CloudflareWorkerAdapterOptions extends CreateWebRequestResponse
|
|
|
49
61
|
createWebSocketPair?: CloudflareWorkerWebSocketPairFactory;
|
|
50
62
|
}
|
|
51
63
|
/** Bootstrap options for constructing a Cloudflare Worker application shell. */
|
|
52
|
-
export interface BootstrapCloudflareWorkerApplicationOptions extends
|
|
64
|
+
export interface BootstrapCloudflareWorkerApplicationOptions extends Omit<CreateApplicationOptions, 'adapter' | 'middleware'>, CloudflareWorkerMiddlewareOptions, CloudflareWorkerAdapterOptions {
|
|
53
65
|
}
|
|
54
66
|
/** Fetch handler shape exposed by Worker-backed application entrypoints. */
|
|
55
67
|
export interface CloudflareWorkerHandler<Env = unknown> {
|
|
@@ -75,6 +87,7 @@ export declare class CloudflareWorkerHttpApplicationAdapter implements HttpAppli
|
|
|
75
87
|
private inFlightDrain?;
|
|
76
88
|
private inFlightRequestCount;
|
|
77
89
|
private isClosed;
|
|
90
|
+
private isWebSocketBindingFrozen;
|
|
78
91
|
private websocketBinding?;
|
|
79
92
|
private readonly options;
|
|
80
93
|
private readonly webRequestResponseFactory;
|
|
@@ -82,11 +95,13 @@ export declare class CloudflareWorkerHttpApplicationAdapter implements HttpAppli
|
|
|
82
95
|
close(): Promise<void>;
|
|
83
96
|
getRealtimeCapability(): import("@fluojs/http").FetchStyleHttpAdapterRealtimeCapability;
|
|
84
97
|
configureWebSocketBinding(binding: CloudflareWorkerWebSocketBinding | undefined): void;
|
|
85
|
-
|
|
98
|
+
[ADAPTER_CLOSE_SETTLED](): Promise<void>;
|
|
99
|
+
fetch<Env = unknown>(request: Request, env?: Env, executionContext?: CloudflareWorkerExecutionContext): Promise<Response>;
|
|
86
100
|
listen(dispatcher: Dispatcher): Promise<void>;
|
|
87
101
|
private upgradeWebSocket;
|
|
88
102
|
private trackInFlightRequest;
|
|
89
103
|
private waitForInFlightRequests;
|
|
104
|
+
private createRequestResponseFactory;
|
|
90
105
|
}
|
|
91
106
|
/**
|
|
92
107
|
* Create the canonical Cloudflare Worker adapter instance.
|
|
@@ -119,4 +134,5 @@ declare global {
|
|
|
119
134
|
WebSocketPair?: new () => CloudflareWorkerWebSocketPair;
|
|
120
135
|
}
|
|
121
136
|
}
|
|
137
|
+
export {};
|
|
122
138
|
//# sourceMappingURL=adapter.d.ts.map
|
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
|
@@ -3,9 +3,14 @@ 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
|
|
|
12
|
+
/** Worker-specific request context attached to fluo HTTP requests by the Cloudflare adapter. */
|
|
13
|
+
|
|
9
14
|
/** Message payloads accepted by Cloudflare Worker websockets. */
|
|
10
15
|
|
|
11
16
|
/** Server-side Cloudflare Worker websocket shape used by the raw binding seam. */
|
|
@@ -41,6 +46,7 @@ export class CloudflareWorkerHttpApplicationAdapter {
|
|
|
41
46
|
inFlightDrain;
|
|
42
47
|
inFlightRequestCount = 0;
|
|
43
48
|
isClosed = false;
|
|
49
|
+
isWebSocketBindingFrozen = false;
|
|
44
50
|
websocketBinding;
|
|
45
51
|
options;
|
|
46
52
|
webRequestResponseFactory;
|
|
@@ -73,39 +79,59 @@ export class CloudflareWorkerHttpApplicationAdapter {
|
|
|
73
79
|
});
|
|
74
80
|
}
|
|
75
81
|
configureWebSocketBinding(binding) {
|
|
82
|
+
if (this.isWebSocketBindingFrozen && binding !== this.websocketBinding) {
|
|
83
|
+
throw new Error(WEBSOCKET_BINDING_RECONFIGURATION_MESSAGE);
|
|
84
|
+
}
|
|
76
85
|
this.websocketBinding = binding;
|
|
77
86
|
}
|
|
78
|
-
|
|
87
|
+
[ADAPTER_CLOSE_SETTLED]() {
|
|
88
|
+
return this.closeInFlight ?? Promise.resolve();
|
|
89
|
+
}
|
|
90
|
+
async fetch(request, env, executionContext) {
|
|
79
91
|
if (this.closeInFlight || this.isClosed) {
|
|
80
92
|
return createShutdownResponse();
|
|
81
93
|
}
|
|
82
94
|
const release = this.trackInFlightRequest();
|
|
83
|
-
const
|
|
95
|
+
const dispatcher = this.dispatcher;
|
|
96
|
+
if (dispatcher && this.websocketBinding && isWebSocketUpgradeRequest(request)) {
|
|
97
|
+
const socketLifecycles = [];
|
|
84
98
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
upgrade
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return await dispatchWebRequest({
|
|
92
|
-
dispatcher,
|
|
93
|
-
dispatcherNotReadyMessage: WORKER_DISPATCHER_NOT_READY_MESSAGE,
|
|
94
|
-
factory: this.webRequestResponseFactory,
|
|
95
|
-
request
|
|
99
|
+
const response = await this.websocketBinding.fetch(request, {
|
|
100
|
+
upgrade: upgradeRequest => {
|
|
101
|
+
const upgrade = this.upgradeWebSocket(upgradeRequest);
|
|
102
|
+
socketLifecycles.push(createWebSocketCloseLifecycle(upgrade.serverSocket));
|
|
103
|
+
return upgrade;
|
|
104
|
+
}
|
|
96
105
|
});
|
|
106
|
+
return response;
|
|
97
107
|
} finally {
|
|
98
|
-
|
|
108
|
+
const lifecycle = Promise.all(socketLifecycles).then(() => undefined).finally(release);
|
|
109
|
+
executionContext?.waitUntil(lifecycle);
|
|
99
110
|
}
|
|
111
|
+
}
|
|
112
|
+
const responsePromise = (async () => {
|
|
113
|
+
return await dispatchWebRequest({
|
|
114
|
+
dispatcher,
|
|
115
|
+
dispatcherNotReadyMessage: WORKER_DISPATCHER_NOT_READY_MESSAGE,
|
|
116
|
+
factory: this.createRequestResponseFactory(env, executionContext),
|
|
117
|
+
request
|
|
118
|
+
});
|
|
100
119
|
})();
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
const trackedResponsePromise = responsePromise.then(response => createLifecycleTrackedResponse(response, release), error => {
|
|
121
|
+
release();
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
executionContext?.waitUntil(trackedResponsePromise.then(({
|
|
125
|
+
lifecycle
|
|
126
|
+
}) => lifecycle).then(() => undefined, () => undefined));
|
|
127
|
+
return (await trackedResponsePromise).response;
|
|
103
128
|
}
|
|
104
129
|
async listen(dispatcher) {
|
|
105
130
|
if (this.closeInFlight) {
|
|
106
131
|
throw new Error('Cloudflare Workers adapter cannot listen while shutdown is still draining.');
|
|
107
132
|
}
|
|
108
133
|
this.isClosed = false;
|
|
134
|
+
this.isWebSocketBindingFrozen = true;
|
|
109
135
|
this.dispatcher = dispatcher;
|
|
110
136
|
}
|
|
111
137
|
upgradeWebSocket(_request) {
|
|
@@ -139,6 +165,20 @@ export class CloudflareWorkerHttpApplicationAdapter {
|
|
|
139
165
|
}
|
|
140
166
|
await this.inFlightDrain?.promise;
|
|
141
167
|
}
|
|
168
|
+
createRequestResponseFactory(env, executionContext) {
|
|
169
|
+
const baseFactory = this.webRequestResponseFactory;
|
|
170
|
+
return {
|
|
171
|
+
...baseFactory,
|
|
172
|
+
async createRequest(request, signal) {
|
|
173
|
+
const frameworkRequest = await baseFactory.createRequest(request, signal);
|
|
174
|
+
frameworkRequest.cloudflare = {
|
|
175
|
+
env,
|
|
176
|
+
executionContext
|
|
177
|
+
};
|
|
178
|
+
return frameworkRequest;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
/**
|
|
@@ -184,8 +224,12 @@ export async function bootstrapCloudflareWorkerApplication(rootModule, options =
|
|
|
184
224
|
export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
|
|
185
225
|
let closeError;
|
|
186
226
|
let closeInFlight;
|
|
227
|
+
let closeRecovery;
|
|
187
228
|
let runningApplication;
|
|
188
229
|
const ready = async () => {
|
|
230
|
+
if (closeRecovery) {
|
|
231
|
+
await closeRecovery;
|
|
232
|
+
}
|
|
189
233
|
if (closeError) {
|
|
190
234
|
throw closeError;
|
|
191
235
|
}
|
|
@@ -200,6 +244,9 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
|
|
|
200
244
|
await closeInFlight;
|
|
201
245
|
return;
|
|
202
246
|
}
|
|
247
|
+
if (closeRecovery) {
|
|
248
|
+
await closeRecovery;
|
|
249
|
+
}
|
|
203
250
|
if (closeError) {
|
|
204
251
|
throw closeError;
|
|
205
252
|
}
|
|
@@ -208,13 +255,31 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
|
|
|
208
255
|
return;
|
|
209
256
|
}
|
|
210
257
|
const closing = (async () => {
|
|
258
|
+
let currentApplication;
|
|
211
259
|
try {
|
|
212
|
-
|
|
260
|
+
currentApplication = await application;
|
|
261
|
+
await currentApplication.close(signal);
|
|
213
262
|
if (runningApplication === application) {
|
|
214
263
|
runningApplication = undefined;
|
|
215
264
|
}
|
|
216
265
|
} catch (error) {
|
|
217
|
-
|
|
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
|
+
}
|
|
218
283
|
throw error;
|
|
219
284
|
} finally {
|
|
220
285
|
closeInFlight = undefined;
|
|
@@ -224,7 +289,7 @@ export function createCloudflareWorkerEntrypoint(rootModule, options = {}) {
|
|
|
224
289
|
await closing;
|
|
225
290
|
},
|
|
226
291
|
async fetch(request, env, executionContext) {
|
|
227
|
-
if (closeError || closeInFlight) {
|
|
292
|
+
if (closeError || closeInFlight || closeRecovery) {
|
|
228
293
|
return createShutdownResponse();
|
|
229
294
|
}
|
|
230
295
|
return await (await ready()).fetch(request, env, executionContext);
|
|
@@ -290,6 +355,19 @@ function validateNonNegativeIntegerOption(name, value) {
|
|
|
290
355
|
function isWebSocketUpgradeRequest(request) {
|
|
291
356
|
return request.headers.get('upgrade')?.toLowerCase() === 'websocket';
|
|
292
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
|
+
}
|
|
293
371
|
function createDeferred() {
|
|
294
372
|
let resolve;
|
|
295
373
|
let reject;
|
|
@@ -303,6 +381,25 @@ function createDeferred() {
|
|
|
303
381
|
resolve
|
|
304
382
|
};
|
|
305
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
|
+
}
|
|
306
403
|
function createShutdownResponse() {
|
|
307
404
|
return new Response(JSON.stringify({
|
|
308
405
|
error: {
|
|
@@ -317,10 +414,66 @@ function createShutdownResponse() {
|
|
|
317
414
|
status: 503
|
|
318
415
|
});
|
|
319
416
|
}
|
|
417
|
+
function createLifecycleTrackedResponse(response, release) {
|
|
418
|
+
if (!isLifecycleTrackedStreamingResponse(response)) {
|
|
419
|
+
release();
|
|
420
|
+
return {
|
|
421
|
+
lifecycle: Promise.resolve(),
|
|
422
|
+
response
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const lifecycle = createDeferred();
|
|
426
|
+
const responseBody = response.body;
|
|
427
|
+
if (!responseBody) {
|
|
428
|
+
release();
|
|
429
|
+
return {
|
|
430
|
+
lifecycle: Promise.resolve(),
|
|
431
|
+
response
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const reader = responseBody.getReader();
|
|
436
|
+
const trackedBody = new ReadableStream({
|
|
437
|
+
async cancel(reason) {
|
|
438
|
+
try {
|
|
439
|
+
await reader.cancel(reason);
|
|
440
|
+
lifecycle.resolve();
|
|
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);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
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
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function isLifecycleTrackedStreamingResponse(response) {
|
|
471
|
+
return response.body !== null && response.headers.get('content-type')?.toLowerCase().includes('text/event-stream') === true;
|
|
472
|
+
}
|
|
320
473
|
function waitForCloseWithTimeout(closePromise, timeoutMs) {
|
|
321
474
|
return new Promise((resolve, reject) => {
|
|
322
475
|
const timeoutHandle = setTimeout(() => {
|
|
323
|
-
reject(new Error(
|
|
476
|
+
reject(new Error(createShutdownTimeoutMessage(timeoutMs)));
|
|
324
477
|
}, timeoutMs);
|
|
325
478
|
void closePromise.then(() => {
|
|
326
479
|
clearTimeout(timeoutHandle);
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"platform",
|
|
10
10
|
"fetch"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
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": "^
|
|
37
|
-
"@fluojs/runtime": "^
|
|
36
|
+
"@fluojs/http": "^2.0.1",
|
|
37
|
+
"@fluojs/runtime": "^2.0.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"vitest": "^3.2.4"
|