@fluojs/platform-bun 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +53 -9
- package/README.md +53 -9
- package/dist/adapter.d.ts +4 -3
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +67 -13
- package/package.json +5 -5
package/README.ko.md
CHANGED
|
@@ -28,7 +28,7 @@ npm install @fluojs/platform-bun
|
|
|
28
28
|
|
|
29
29
|
fluo 애플리케이션을 [Bun](https://bun.sh/) 런타임에서 실행할 때 이 패키지를 사용합니다. 이 어댑터는 Bun의 고성능 `Request`/`Response` 브리지와 네이티브 `fetch` 방식의 아키텍처를 활용하여 Bun 사용자에게 원활하고 빠른 경험을 제공합니다.
|
|
30
30
|
|
|
31
|
-
애플리케이션 종료 중에는 websocket upgrade 시도를 포함한 모든 새 유입을 `503` shutdown 응답으로 중단하고,
|
|
31
|
+
애플리케이션 종료 중에는 websocket upgrade 시도를 포함한 모든 새 유입을 `503` shutdown 응답으로 중단하고, realtime binding 평가부터 HTTP 응답 또는 upgrade 결과까지 수락된 작업을 drain하기 전에 `server.stop(stopActiveConnections)`를 시작합니다. bounded timeout은 caller-facing `close()` promise만 reject합니다. 수락된 작업과 adapter state는 underlying drain이 끝날 때까지 유지됩니다. 따라서 timeout이 추가 teardown을 강제하지 않습니다. 시그널 기반 종료가 `forceExitTimeoutMs`를 넘기거나 실패하면 fluo는 그 상태를 로그와 `process.exitCode`로 보고하고, 최종 프로세스 종료는 Bun 또는 주변 호스트에 맡깁니다.
|
|
32
32
|
|
|
33
33
|
## 빠른 시작
|
|
34
34
|
|
|
@@ -46,6 +46,26 @@ await app.listen();
|
|
|
46
46
|
|
|
47
47
|
## 주요 패턴
|
|
48
48
|
|
|
49
|
+
### Early Hints 미지원
|
|
50
|
+
|
|
51
|
+
Bun은 Fluo의 Web 표준 response facade를 사용하므로 `context.response.earlyHints`가 없습니다. 사용 전에 capability 존재 여부를 확인하세요. Adapter는 Early Hints를 조용히 무시하거나 early field를 final `Response`에 복사하지 않습니다. 애플리케이션 코드가 관찰 가능한 HTTP `103`을 emit해야 한다면 Node.js, Express, Fastify adapter를 사용하세요.
|
|
52
|
+
|
|
53
|
+
### 스트리밍 멀티파트 소비
|
|
54
|
+
|
|
55
|
+
애플리케이션 bootstrap에서 `multipart: { strategy: 'stream' }`을 설정하면 멀티파트 데이터를 점진적으로
|
|
56
|
+
받습니다. 멀티파트 route에서 `RequestContext.request.body`는 `AsyncIterableIterator<MultipartPart>`입니다.
|
|
57
|
+
field part는 `kind: 'field'`, `name`, `value`, `headers`를, file part는 `kind: 'file'`, `name`, `filename`,
|
|
58
|
+
`contentType`, `headers`, 그리고 `stream`의 single-consumer `ReadableStream<Uint8Array>`를 제공합니다. 다음
|
|
59
|
+
part를 요청하기 전에 각 file stream을 끝까지 소비하거나 cancel하세요.
|
|
60
|
+
|
|
61
|
+
Runtime route dispatch는 route를 위해 만든 iterator를 소유하며 handler가 끝난 뒤 자동으로 `return()`을 호출해
|
|
62
|
+
active source를 cancel하고 release합니다. Standalone `parseMultipartStream(...)` consumer는 이 책임을 직접
|
|
63
|
+
집니다. iterator를 끝까지 소비하거나 일찍 끝낼 때 `return()`을 호출하세요.
|
|
64
|
+
|
|
65
|
+
### 바이트 범위와 캐시 검증
|
|
66
|
+
|
|
67
|
+
Bun은 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를 반영합니다.
|
|
68
|
+
|
|
49
69
|
### 수동 Fetch 처리
|
|
50
70
|
Bun 서버를 직접 관리하려는 경우 fetch 핸들러를 직접 사용할 수 있습니다.
|
|
51
71
|
`dispatcher`는 이미 bootstrap된 application의 `app.getHttpDispatcher()`에서 가져와야 합니다. `createBunFetchHandler(...)`는 동기적으로 fetch bridge를 만들고 raw-body와 multipart request parsing을 보존하지만, shutdown ownership, websocket upgrade, native `routes` acceleration은 주변 `Bun.serve(...)` host 또는 managed adapter 경로가 소유합니다.
|
|
@@ -64,12 +84,20 @@ Bun.serve({
|
|
|
64
84
|
```
|
|
65
85
|
|
|
66
86
|
### 네이티브 WebSocket 업그레이드
|
|
67
|
-
어댑터는 `@fluojs/websockets/bun` 바인딩을 통해 Bun의 네이티브 `server.upgrade()`를 지원합니다.
|
|
87
|
+
어댑터는 `@fluojs/websockets/bun` 바인딩을 통해 Bun의 네이티브 `server.upgrade()`를 지원합니다. `getRealtimeCapability()`는 fetch-style capability version 1을 보존하면서 optional versioned `bindingInstallation` extension을 추가합니다. First-party WebSocket 및 Socket.IO module은 `app.listen()` 전에 이 protocol-neutral installer를 사용하고 adapter는 설치된 값을 Bun binding contract로 parse합니다. 런타임이 해당 바인딩을 설치하고 등록된 게이트웨이를 찾을 수 있도록 `app.listen()` 전에 application module에서 `BunWebSocketModule.forRoot(...)`를 import하세요.
|
|
68
88
|
|
|
69
89
|
```typescript
|
|
70
|
-
|
|
90
|
+
import { Module } from '@fluojs/core';
|
|
91
|
+
import { BunWebSocketModule, WebSocketGateway } from '@fluojs/websockets/bun';
|
|
92
|
+
|
|
71
93
|
@WebSocketGateway({ path: '/ws' })
|
|
72
|
-
|
|
94
|
+
class MyGateway {}
|
|
95
|
+
|
|
96
|
+
@Module({
|
|
97
|
+
imports: [BunWebSocketModule.forRoot()],
|
|
98
|
+
providers: [MyGateway],
|
|
99
|
+
})
|
|
100
|
+
export class AppModule {}
|
|
73
101
|
```
|
|
74
102
|
|
|
75
103
|
### 네이티브 `routes` Object 가속
|
|
@@ -79,6 +107,8 @@ Bun `>=1.2.3`에서는 어댑터가 의미 보존이 가능한 static/param fluo
|
|
|
79
107
|
|
|
80
108
|
Native handoff가 붙은 뒤 app middleware가 framework request의 method 또는 path를 rewrite하면 dispatcher는 stale handoff를 버리고 rewrite된 요청을 다시 매칭합니다. `OPTIONS` 같은 미지원 메서드와 CORS preflight 동작은 fluo route가 명시적으로 소유하지 않는 한 공유 dispatcher/middleware 경로가 계속 소유합니다.
|
|
81
109
|
|
|
110
|
+
`QUERY`, `PURGE` 같은 검증된 custom route는 Bun native `routes` 가속을 사용할 수 있어도 의도적으로 fetch fallback에 남습니다. Bun은 `Request`를 통해 원래 method와 body를 전달하고 shared dispatcher가 `ALL`보다 exact method를 먼저 매칭합니다. `CONNECT`는 일반 controller routing conformance 범위 밖에 유지됩니다.
|
|
111
|
+
|
|
82
112
|
## 공개 API 개요
|
|
83
113
|
|
|
84
114
|
- `createBunAdapter(options)`: Bun 어댑터를 위한 권장 팩토리입니다.
|
|
@@ -88,7 +118,7 @@ Native handoff가 붙은 뒤 app middleware가 framework request의 method 또
|
|
|
88
118
|
|
|
89
119
|
어댑터는 realtime 패키지가 사용하는 타입 지정 Bun 통합 seam도 함께 내보냅니다.
|
|
90
120
|
|
|
91
|
-
- `BunHttpApplicationAdapter`: `Bun.serve()`를 기반으로 동작하는 `HttpApplicationAdapter`
|
|
121
|
+
- `BunHttpApplicationAdapter`: `Bun.serve()`를 기반으로 동작하는 `HttpApplicationAdapter` 구현체이며 `getRealtimeCapability()`는 fetch-style capability version 1을 보존하면서 optional `bindingInstallation`을 포함합니다.
|
|
92
122
|
- `BunAdapterOptions`: `createBunAdapter()`가 받는 host, port, TLS, raw-body, multipart, shutdown 옵션입니다.
|
|
93
123
|
- `BootstrapBunApplicationOptions` 및 `RunBunApplicationOptions`: Bun 호스팅 애플리케이션의 bootstrap/run 옵션입니다.
|
|
94
124
|
- `BunWebSocketBinding`, `BunWebSocketUpgradeHost` 및 `BunRealtimeBindingHost`: 일반 HTTP dispatch 전에 `@fluojs/websockets/bun`이 사용하는 binding 계약입니다. Binding은 upgrade 가능한 host만 받으며 adapter가 소유하는 Bun server lifecycle이나 raw fetch handler는 받지 않습니다.
|
|
@@ -100,17 +130,31 @@ Native handoff가 붙은 뒤 app middleware가 framework request의 method 또
|
|
|
100
130
|
- **런타임 host**: 이 패키지는 listen 시점에 `globalThis.Bun.serve()`가 필요합니다. 테스트에서는 Bun 호환 test double을 제공할 수 있지만, production 사용은 Bun 전용입니다.
|
|
101
131
|
- **요청 portability**: Fetch 요청은 shared web dispatcher를 통해 변환되며 malformed cookie 값, query 배열, `rawBody: true`일 때 JSON/text raw body, custom `createBunFetchHandler(...)` 설정의 byte-exact request handoff, SSE framing을 보존합니다.
|
|
102
132
|
- **네이티브 route 가속**: Bun의 `routes` object를 사용할 수 있고 fluo route shape를 의미 보존 상태로 선등록할 수 있을 때만 Bun이 path matching을 먼저 처리하고, 이후 요청은 다시 shared dispatcher로 넘깁니다. 지원하지 않거나 모호한 route shape는 일반 `fetch` 경로로 폴백하며, middleware가 handler matching 전에 method/path를 rewrite하면 stale handoff는 무시됩니다.
|
|
103
|
-
- **네이티브 route gate**: Native route는 Bun `>=1.2.3`에서만 활성화됩니다. Adapter는 안전한 native-route entry가 실제로 활성화될 때만 `routes` 옵션을 전달하고, 그 외에는 `routes` 옵션 자체를 생략합니다. Versioned route, `ALL` handler, same-shape conflict, normalization-sensitive path, `OPTIONS`/CORS preflight는 fetch/shared-dispatch path에 남습니다.
|
|
133
|
+
- **네이티브 route gate**: Native route는 Bun `>=1.2.3`에서만 활성화됩니다. Adapter는 안전한 native-route entry가 실제로 활성화될 때만 `routes` 옵션을 전달하고, 그 외에는 `routes` 옵션 자체를 생략합니다. Versioned route, `ALL` handler, custom method, same-shape conflict, normalization-sensitive path, `OPTIONS`/CORS preflight는 fetch/shared-dispatch path에 남습니다.
|
|
104
134
|
- **Multipart 동작**: Multipart 요청은 `rawBody`를 노출하지 않으며 multipart limit은 shared runtime parser를 통해 계속 적용됩니다.
|
|
105
135
|
- **시작 target**: `hostname`, `port`, `tls`는 `Bun.serve()`로 전달됩니다. 시작 로그는 설정된 HTTP 또는 HTTPS listen URL을 보고합니다.
|
|
106
136
|
- **Lifecycle guard**: 이미 시작된 adapter에서 `listen()`을 다시 호출해도 원래 live dispatcher binding을 유지합니다. Realtime/websocket binding은 `listen()`이 시작되기 전에만 구성할 수 있으며, 이후 binding을 설정하거나 지우려는 시도는 live wiring에 영향을 주지 않은 채 수락되지 않고 빠르게 실패합니다.
|
|
107
|
-
- **종료 소유권**: `close()`는 새 HTTP 및 websocket-upgrade 유입을 `503` shutdown 응답으로
|
|
108
|
-
- **Realtime seam**: Bun websocket binding은 서버를 시작하는 `listen()` 전에 구성해야 합니다. Adapter가 새 유입을 받는 동안 Upgrade 요청은 HTTP dispatch로 넘어가기 전에 구성된 binding에 먼저 전달되며, 비동기 binding 평가 도중 shutdown이 시작되어도 이미 수락된 요청에는 dispatcher가 유지되고, binding이 response를 반환하거나 요청 업그레이드에 성공한 경우에만 HTTP fallback을 억제합니다. Binding host는 `upgrade(...)`만 노출하므로 adapter가 소유하는 `stop()`과 raw `fetch()` 제어는 realtime seam 밖에 남습니다.
|
|
137
|
+
- **종료 소유권**: `close()`는 새 HTTP 및 websocket-upgrade 유입을 `503` shutdown 응답으로 중단하고 `server.stop(stopActiveConnections)`를 시작한 뒤, Bun server 종료와 수락된 모든 요청을 realtime binding 평가부터 HTTP 응답 또는 upgrade 완료까지 기다립니다. bounded timeout은 caller-facing `close()` promise만 reject합니다. 수락된 작업과 adapter state는 underlying drain이 끝날 때까지 유지되며, 그 뒤에야 `close()`가 adapter state를 정리합니다. `runBunApplication()`은 등록한 signal listener를 `app.close()`가 시작될 때 adapter drain 이전에 제거합니다.
|
|
138
|
+
- **Realtime seam**: `getRealtimeCapability()`는 fetch-style version 1을 보존하면서 optional version 1 `bindingInstallation` contract를 노출합니다. Bun websocket binding은 서버를 시작하는 `listen()` 전에 구성해야 합니다. Capability installer는 protocol package를 위한 canonical configuration path이며 `fetch` 및 `websocket` host contract가 없는 값을 거부합니다. Startup 이후 live server의 binding은 고정되고 adapter `close()` boundary가 Bun 종료와 요청 drain이 끝난 후 retained binding state를 정리합니다. Adapter가 새 유입을 받는 동안 Upgrade 요청은 HTTP dispatch로 넘어가기 전에 구성된 binding에 먼저 전달되며, 비동기 binding 평가 도중 shutdown이 시작되어도 이미 수락된 요청에는 dispatcher가 유지되고, binding이 response를 반환하거나 요청 업그레이드에 성공한 경우에만 HTTP fallback을 억제합니다. Binding host는 `upgrade(...)`만 노출하므로 adapter가 소유하는 `stop()`과 raw `fetch()` 제어는 realtime seam 밖에 남습니다.
|
|
109
139
|
- **Adapter instance helper**: `BunHttpApplicationAdapter`는 `getServer()`, `getListenTarget()`, `getRealtimeCapability()`, `configureRealtimeBinding()`, `configureWebSocketBinding()`, `listen()`, `close()`를 노출합니다.
|
|
110
140
|
|
|
141
|
+
### 안정적인 진단 코드
|
|
142
|
+
|
|
143
|
+
패키지가 생성하는 caller-visible failure는 기존 `Error` 또는 `TypeError` class와 message를 유지하면서 `error.code`에 안정적인 문자열을 노출합니다.
|
|
144
|
+
|
|
145
|
+
| 코드 | Error class | 실패 조건 |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `BUN_ADAPTER_INVALID_OPTION` | `Error` | 숫자형 adapter 또는 shutdown option이 문서화된 범위를 벗어납니다. |
|
|
148
|
+
| `BUN_ADAPTER_REALTIME_BINDING_INVALID` | `TypeError` | Realtime capability installer가 필수 `fetch` 및 `websocket` contract가 없는 값을 받습니다. |
|
|
149
|
+
| `BUN_ADAPTER_REALTIME_BINDING_LOCKED` | `Error` | `listen()`이 Bun server를 시작한 뒤 caller가 realtime/websocket binding을 변경하려고 합니다. |
|
|
150
|
+
| `BUN_ADAPTER_RUNTIME_UNAVAILABLE` | `Error` | `listen()`이 호출 가능한 `globalThis.Bun.serve()`를 찾지 못합니다. |
|
|
151
|
+
| `BUN_ADAPTER_SHUTDOWN_TIMEOUT` | `Error` | Caller-facing `close()` 대기가 bounded shutdown timeout을 초과합니다. |
|
|
152
|
+
|
|
153
|
+
Bun 또는 application code에서 전파된 error에는 package-owned code를 덧붙이지 않고 원래 class, message, metadata를 유지합니다.
|
|
154
|
+
|
|
111
155
|
## Conformance 커버리지
|
|
112
156
|
|
|
113
|
-
`packages/platform-bun/src/adapter.test.ts`는 문서화된 계약을 검증하는 package-local regression 대상입니다. 이 파일은 malformed cookie, byte-exact JSON/text raw-body 보존, managed/custom fetch handler의 multipart raw-body 제외, SSE framing, native-route param parity, same-path multi-method handoff, middleware가 request path 또는 method를 rewrite한 뒤의 stale native handoff rematch, versioning fallback, normalization-sensitive fallback, OPTIONS/CORS ownership, same-shape route fallback, TLS listen-target reporting을 검증하는 Bun fetch-style portability assertion과 startup logging, duplicate listen idempotency, shutdown listener cleanup, in-flight drain, 비동기 realtime binding 평가 중 close, binding 완료 후 HTTP fallback, timeout validation/reporting, shutdown 503 ingress rejection, signal-driven close rejection reporting, upgrade-only host를 통한 websocket binding delegation/short-circuit 동작을 검증하는 집중 테스트를 포함합니다.
|
|
157
|
+
`packages/platform-bun/src/adapter.test.ts`는 문서화된 계약을 검증하는 package-local regression 대상입니다. 이 파일은 conditional request, single-byte range 및 `If-Range`, custom `QUERY`/extension-method fallback, malformed cookie, byte-exact JSON/text raw-body 보존, managed/custom fetch handler의 multipart raw-body 제외, SSE framing, native-route param parity, same-path multi-method handoff, middleware가 request path 또는 method를 rewrite한 뒤의 stale native handoff rematch, versioning fallback, normalization-sensitive fallback, OPTIONS/CORS ownership, same-shape route fallback, TLS listen-target reporting을 검증하는 Bun fetch-style portability assertion과 startup logging, duplicate listen idempotency, shutdown listener cleanup, in-flight drain, 비동기 realtime binding 평가 중 close, binding 완료 후 HTTP fallback, timeout validation/reporting, shutdown 503 ingress rejection, signal-driven close rejection reporting, upgrade-only host를 통한 websocket binding delegation/short-circuit 동작을 검증하는 집중 테스트를 포함합니다.
|
|
114
158
|
|
|
115
159
|
저장소의 더 넓은 suite도 `packages/testing/src/portability/web-runtime-adapter-portability.test.ts`에서 `createWebRuntimeHttpAdapterPortabilityHarness(...)`로 Bun을 Deno 및 Cloudflare Workers와 함께 실행해 fetch-style platform 간 shared web-runtime portability baseline을 맞춥니다.
|
|
116
160
|
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ This package is intended to run on Bun. The published manifest intentionally doe
|
|
|
28
28
|
|
|
29
29
|
Use this package when running fluo applications on the [Bun](https://bun.sh/) runtime. This adapter leverages Bun's high-performance `Request`/`Response` bridge and native `fetch`-style architecture, providing a seamless and fast experience for Bun users.
|
|
30
30
|
|
|
31
|
-
During application shutdown, the adapter stops all new ingress, including websocket upgrade attempts, with a `503` shutdown response and
|
|
31
|
+
During application shutdown, the adapter stops all new ingress, including websocket upgrade attempts, with a `503` shutdown response and starts `server.stop(stopActiveConnections)` before draining accepted work from realtime binding evaluation through its HTTP response or upgrade outcome. The bounded timeout only rejects the caller-facing `close()` promise: accepted work and adapter state remain retained until the underlying drain settles, so the timeout does not force another teardown. If signal-driven shutdown exceeds `forceExitTimeoutMs` or fails, fluo reports that condition through logging and `process.exitCode` while leaving final process termination to Bun or the surrounding host.
|
|
32
32
|
|
|
33
33
|
## Quick Start
|
|
34
34
|
|
|
@@ -46,6 +46,26 @@ await app.listen();
|
|
|
46
46
|
|
|
47
47
|
## Common Patterns
|
|
48
48
|
|
|
49
|
+
### Early Hints are unsupported
|
|
50
|
+
|
|
51
|
+
Bun uses Fluo's Web-standard response facade, so `context.response.earlyHints` is absent. Check for capability presence before use. The adapter does not silently ignore Early Hints and does not copy early fields into the final `Response`; use a Node.js, Express, or Fastify adapter when application code must emit observable HTTP `103` responses.
|
|
52
|
+
|
|
53
|
+
### Streaming multipart consumption
|
|
54
|
+
|
|
55
|
+
Set `multipart: { strategy: 'stream' }` at application bootstrap to receive multipart data incrementally. For
|
|
56
|
+
multipart routes, `RequestContext.request.body` is an `AsyncIterableIterator<MultipartPart>`: field parts expose
|
|
57
|
+
`kind: 'field'`, `name`, `value`, and `headers`; file parts expose `kind: 'file'`, `name`, `filename`,
|
|
58
|
+
`contentType`, `headers`, and a single-consumer `ReadableStream<Uint8Array>` at `stream`. Finish or cancel each file
|
|
59
|
+
stream before requesting the next part.
|
|
60
|
+
|
|
61
|
+
Runtime route dispatch owns an iterator created for a route and automatically calls `return()` after the handler
|
|
62
|
+
finishes, cancelling and releasing an active source. Standalone `parseMultipartStream(...)` consumers own that
|
|
63
|
+
responsibility: consume the iterator to completion or call `return()` when ending early.
|
|
64
|
+
|
|
65
|
+
### Byte Ranges and Cache Validation
|
|
66
|
+
|
|
67
|
+
Bun preserves the shared `@fluojs/http` single-byte-range and `If-Range` contract through its 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.
|
|
68
|
+
|
|
49
69
|
### Manual Fetch Handling
|
|
50
70
|
If you prefer to manage the Bun server yourself, you can use the fetch handler directly.
|
|
51
71
|
The `dispatcher` should come from the already bootstrapped application via `app.getHttpDispatcher()`. `createBunFetchHandler(...)` synchronously creates the fetch bridge and preserves raw-body and multipart request parsing, while shutdown ownership, websocket upgrades, and native `routes` acceleration remain responsibilities of the surrounding `Bun.serve(...)` host or the managed adapter path.
|
|
@@ -64,12 +84,20 @@ Bun.serve({
|
|
|
64
84
|
```
|
|
65
85
|
|
|
66
86
|
### Native WebSocket Upgrade
|
|
67
|
-
The adapter supports Bun's native `server.upgrade()` through the `@fluojs/websockets/bun` binding.
|
|
87
|
+
The adapter supports Bun's native `server.upgrade()` through the `@fluojs/websockets/bun` binding. `getRealtimeCapability()` preserves fetch-style capability version 1 and adds the optional versioned `bindingInstallation` extension; first-party WebSocket and Socket.IO modules use that protocol-neutral installer before `app.listen()`, and the adapter parses the installed value into its Bun binding contract. Import `BunWebSocketModule.forRoot(...)` into the application module before `app.listen()` so the runtime can install that binding and discover the registered gateways.
|
|
68
88
|
|
|
69
89
|
```typescript
|
|
70
|
-
|
|
90
|
+
import { Module } from '@fluojs/core';
|
|
91
|
+
import { BunWebSocketModule, WebSocketGateway } from '@fluojs/websockets/bun';
|
|
92
|
+
|
|
71
93
|
@WebSocketGateway({ path: '/ws' })
|
|
72
|
-
|
|
94
|
+
class MyGateway {}
|
|
95
|
+
|
|
96
|
+
@Module({
|
|
97
|
+
imports: [BunWebSocketModule.forRoot()],
|
|
98
|
+
providers: [MyGateway],
|
|
99
|
+
})
|
|
100
|
+
export class AppModule {}
|
|
73
101
|
```
|
|
74
102
|
|
|
75
103
|
### Native `routes` Object Acceleration
|
|
@@ -79,6 +107,8 @@ For semantically safe unversioned routes, Bun hands the pre-matched descriptor a
|
|
|
79
107
|
|
|
80
108
|
If app middleware rewrites the framework request method or path after a Bun native handoff is attached, the dispatcher discards that stale handoff and rematches the rewritten request. Unsupported methods such as `OPTIONS` and CORS preflight behavior remain owned by the shared dispatcher/middleware path unless a fluo route explicitly owns them.
|
|
81
109
|
|
|
110
|
+
Validated custom routes such as `QUERY` and `PURGE` intentionally remain on the fetch fallback even when Bun native `routes` acceleration is available. Bun receives the original method and body through `Request`, and the shared dispatcher performs exact-method matching before `ALL`. `CONNECT` remains outside ordinary controller routing conformance.
|
|
111
|
+
|
|
82
112
|
## Public API Overview
|
|
83
113
|
|
|
84
114
|
- `createBunAdapter(options)`: Recommended factory for the Bun adapter.
|
|
@@ -88,7 +118,7 @@ If app middleware rewrites the framework request method or path after a Bun nati
|
|
|
88
118
|
|
|
89
119
|
The adapter also exports the typed Bun integration seams used by realtime packages:
|
|
90
120
|
|
|
91
|
-
- `BunHttpApplicationAdapter`: `HttpApplicationAdapter` implementation backed by `Bun.serve()`.
|
|
121
|
+
- `BunHttpApplicationAdapter`: `HttpApplicationAdapter` implementation backed by `Bun.serve()`; its `getRealtimeCapability()` preserves fetch-style capability version 1 and includes optional `bindingInstallation`.
|
|
92
122
|
- `BunAdapterOptions`: host, port, TLS, raw-body, multipart, and shutdown options accepted by `createBunAdapter()`.
|
|
93
123
|
- `BootstrapBunApplicationOptions` and `RunBunApplicationOptions`: application bootstrap/run options for Bun-hosted apps.
|
|
94
124
|
- `BunWebSocketBinding`, `BunWebSocketUpgradeHost`, and `BunRealtimeBindingHost`: binding contracts used by `@fluojs/websockets/bun` before normal HTTP dispatch. Bindings receive only an upgrade-capable host, not the adapter-owned Bun server lifecycle or raw fetch handler.
|
|
@@ -100,17 +130,31 @@ The adapter also exports the typed Bun integration seams used by realtime packag
|
|
|
100
130
|
- **Runtime host**: This package requires `globalThis.Bun.serve()` at listen time. Tests may provide a Bun-compatible test double, but production use is Bun-only.
|
|
101
131
|
- **Request portability**: Fetch requests are translated through the shared web dispatcher, preserving malformed cookie values, query arrays, JSON/text raw bodies when `rawBody: true`, byte-exact request handoff for custom `createBunFetchHandler(...)` setups, and SSE framing.
|
|
102
132
|
- **Native route acceleration**: When Bun's `routes` object is available and a fluo route shape is semantically safe to pre-register, the adapter lets Bun short-circuit path matching before handing the request back to the shared dispatcher. Unsupported or ambiguous route shapes fall back to the regular `fetch` path, and stale handoffs are ignored if middleware rewrites method/path before handler matching.
|
|
103
|
-
- **Native route gate**: Native routes are enabled only on Bun `>=1.2.3`; the adapter omits the `routes` option entirely unless safe native-route entries are concretely enabled. Versioned routes, `ALL` handlers, same-shape conflicts, normalization-sensitive paths, and `OPTIONS`/CORS preflight stay on the fetch/shared-dispatch path.
|
|
133
|
+
- **Native route gate**: Native routes are enabled only on Bun `>=1.2.3`; the adapter omits the `routes` option entirely unless safe native-route entries are concretely enabled. Versioned routes, `ALL` handlers, custom methods, same-shape conflicts, normalization-sensitive paths, and `OPTIONS`/CORS preflight stay on the fetch/shared-dispatch path.
|
|
104
134
|
- **Multipart behavior**: Multipart requests never expose `rawBody`, and multipart limits continue to flow through the shared runtime parser.
|
|
105
135
|
- **Startup target**: `hostname`, `port`, and `tls` are forwarded to `Bun.serve()`. Startup logs report the configured HTTP or HTTPS listen URL.
|
|
106
136
|
- **Lifecycle guards**: `listen()` is idempotent for an already-started adapter and keeps the original live dispatcher binding. Realtime/websocket bindings must be configured before `listen()` starts; later attempts to set or clear the binding fail fast instead of being accepted without affecting live wiring.
|
|
107
|
-
- **Shutdown ownership**: `close()` stops new HTTP and websocket-upgrade ingress with a `503` shutdown response, waits for every accepted request from realtime binding evaluation through HTTP response or upgrade completion
|
|
108
|
-
- **Realtime seam**: Bun websocket bindings must be configured before `listen()` starts the server. Upgrade requests are offered to the configured binding before falling back to HTTP dispatch while the adapter is accepting new ingress; an accepted request keeps its dispatcher available if shutdown begins during asynchronous binding evaluation, and HTTP fallback is suppressed only after the binding returns a response or successfully upgrades the request. The binding host exposes only `upgrade(...)`, so adapter-owned `stop()` and raw `fetch()` control remain outside the realtime seam.
|
|
137
|
+
- **Shutdown ownership**: `close()` stops new HTTP and websocket-upgrade ingress with a `503` shutdown response, starts `server.stop(stopActiveConnections)`, and waits for Bun server termination and every accepted request from realtime binding evaluation through HTTP response or upgrade completion. The bounded timeout only rejects the caller-facing `close()` promise: accepted work and adapter state remain retained until the underlying drain settles, after which `close()` clears adapter state. `runBunApplication()` removes its registered signal listeners when `app.close()` begins, before the adapter starts draining.
|
|
138
|
+
- **Realtime seam**: `getRealtimeCapability()` preserves fetch-style version 1 and exposes its optional version 1 `bindingInstallation` contract. Bun websocket bindings must be configured before `listen()` starts the server. The capability installer is the canonical configuration path for protocol packages and rejects values without `fetch` and `websocket` host contracts. After startup the binding remains frozen for the live server; the adapter `close()` boundary clears its retained binding state after Bun termination and request drain settle. Upgrade requests are offered to the configured binding before falling back to HTTP dispatch while the adapter is accepting new ingress; an accepted request keeps its dispatcher available if shutdown begins during asynchronous binding evaluation, and HTTP fallback is suppressed only after the binding returns a response or successfully upgrades the request. The binding host exposes only `upgrade(...)`, so adapter-owned `stop()` and raw `fetch()` control remain outside the realtime seam.
|
|
109
139
|
- **Adapter instance helpers**: `BunHttpApplicationAdapter` exposes `getServer()`, `getListenTarget()`, `getRealtimeCapability()`, `configureRealtimeBinding()`, `configureWebSocketBinding()`, `listen()`, and `close()`.
|
|
110
140
|
|
|
141
|
+
### Stable diagnostic codes
|
|
142
|
+
|
|
143
|
+
Package-generated caller-visible failures retain their existing `Error` or `TypeError` class and message while exposing a stable string through `error.code`:
|
|
144
|
+
|
|
145
|
+
| Code | Error class | Failure |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `BUN_ADAPTER_INVALID_OPTION` | `Error` | A numeric adapter or shutdown option is outside its documented range. |
|
|
148
|
+
| `BUN_ADAPTER_REALTIME_BINDING_INVALID` | `TypeError` | The realtime capability installer receives a value without the required `fetch` and `websocket` contracts. |
|
|
149
|
+
| `BUN_ADAPTER_REALTIME_BINDING_LOCKED` | `Error` | A caller attempts to change the realtime/websocket binding after `listen()` starts the Bun server. |
|
|
150
|
+
| `BUN_ADAPTER_RUNTIME_UNAVAILABLE` | `Error` | `listen()` cannot find a callable `globalThis.Bun.serve()`. |
|
|
151
|
+
| `BUN_ADAPTER_SHUTDOWN_TIMEOUT` | `Error` | The caller-facing `close()` wait exceeds its bounded shutdown timeout. |
|
|
152
|
+
|
|
153
|
+
Errors propagated from Bun or application code keep their original class, message, and metadata instead of receiving a package-owned code.
|
|
154
|
+
|
|
111
155
|
## Conformance Coverage
|
|
112
156
|
|
|
113
|
-
`packages/platform-bun/src/adapter.test.ts` is the package-local regression target for the documented contract. It includes Bun fetch-style portability assertions for malformed cookies, byte-exact JSON/text raw-body preservation, multipart raw-body exclusion for managed and custom fetch handlers, SSE framing, native-route param parity, same-path multi-method handoff, stale native handoff rematching after middleware rewrites the request path or method, versioning fallback, normalization-sensitive fallback, OPTIONS/CORS ownership, same-shape route fallback, and TLS listen-target reporting, plus focused tests for startup logging, duplicate listen idempotency, shutdown listener cleanup, in-flight drain behavior, close during asynchronous realtime binding evaluation, HTTP fallback after binding completion, timeout validation/reporting, shutdown 503 ingress rejection, signal-driven close rejection reporting, and websocket binding delegation/short-circuit behavior through an upgrade-only host.
|
|
157
|
+
`packages/platform-bun/src/adapter.test.ts` is the package-local regression target for the documented contract. It includes Bun fetch-style portability assertions for conditional requests, single-byte ranges and `If-Range`, custom `QUERY`/extension-method fallback, malformed cookies, byte-exact JSON/text raw-body preservation, multipart raw-body exclusion for managed and custom fetch handlers, SSE framing, native-route param parity, same-path multi-method handoff, stale native handoff rematching after middleware rewrites the request path or method, versioning fallback, normalization-sensitive fallback, OPTIONS/CORS ownership, same-shape route fallback, and TLS listen-target reporting, plus focused tests for startup logging, duplicate listen idempotency, shutdown listener cleanup, in-flight drain behavior, close during asynchronous realtime binding evaluation, HTTP fallback after binding completion, timeout validation/reporting, shutdown 503 ingress rejection, signal-driven close rejection reporting, and websocket binding delegation/short-circuit behavior through an upgrade-only host.
|
|
114
158
|
|
|
115
159
|
The broader repository suite also exercises Bun through `createWebRuntimeHttpAdapterPortabilityHarness(...)` alongside Deno and Cloudflare Workers in `packages/testing/src/portability/web-runtime-adapter-portability.test.ts`, keeping the shared web-runtime portability baseline aligned across fetch-style platforms.
|
|
116
160
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CorsOptions, Dispatcher, HttpApplicationAdapter,
|
|
1
|
+
import type { CorsOptions, Dispatcher, HttpApplicationAdapter, MiddlewareLike, SecurityHeadersOptions } from '@fluojs/http';
|
|
2
2
|
import type { Application, CreateApplicationOptions, ModuleType, MultipartOptions } from '@fluojs/runtime';
|
|
3
3
|
import { type HttpAdapterListenTarget } from '@fluojs/runtime/internal/http-adapter';
|
|
4
4
|
type BunHostname = string;
|
|
@@ -6,7 +6,7 @@ type BunRequestLike = Request & {
|
|
|
6
6
|
params?: Readonly<Record<string, string>>;
|
|
7
7
|
};
|
|
8
8
|
type BunRouteHandler = (request: BunRequestLike, server: BunServerLike) => Response | Promise<Response> | undefined | Promise<Response | undefined>;
|
|
9
|
-
type BunRouteMethod =
|
|
9
|
+
type BunRouteMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
|
|
10
10
|
type BunRouteMethodMap = Partial<Record<BunRouteMethod, BunRouteHandler | Response>>;
|
|
11
11
|
type BunRouteValue = BunRouteHandler | Response | BunRouteMethodMap;
|
|
12
12
|
/** Shutdown signal names that `runBunApplication()` can register. */
|
|
@@ -90,7 +90,7 @@ export interface BunServerLike {
|
|
|
90
90
|
fetch?(request: Request): Response | Promise<Response> | undefined | Promise<Response | undefined>;
|
|
91
91
|
hostname?: BunHostname;
|
|
92
92
|
port?: number;
|
|
93
|
-
stop(closeActiveConnections?: boolean): void
|
|
93
|
+
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
94
94
|
upgrade<TData = unknown>(request: Request, options?: {
|
|
95
95
|
data?: TData;
|
|
96
96
|
headers?: HeadersInit;
|
|
@@ -187,6 +187,7 @@ export declare class BunHttpApplicationAdapter implements HttpApplicationAdapter
|
|
|
187
187
|
getListenTarget(): HttpAdapterListenTarget;
|
|
188
188
|
/** Reports Bun's fetch-style websocket capability for realtime package integration. */
|
|
189
189
|
getRealtimeCapability(): import("@fluojs/http").FetchStyleHttpAdapterRealtimeCapability;
|
|
190
|
+
private installRealtimeBinding;
|
|
190
191
|
/** Configures the official realtime binding before the Bun server starts. */
|
|
191
192
|
configureRealtimeBinding<TData>(binding: BunWebSocketBinding<TData> | undefined): void;
|
|
192
193
|
/** Configures a Bun websocket binding through the legacy websocket host name. */
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EAEV,sBAAsB,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bindRawRequestNativeRouteHandoff, createFetchStyleHttpAdapterRealtimeCapability, isRoutePathNormalizationSensitive } from '@fluojs/http/internal';
|
|
2
2
|
import { bootstrapHttpAdapterApplication, createDefaultApplicationLogger, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
|
|
3
|
-
import { createWebRequestResponseFactory, dispatchWebRequest } from '@fluojs/runtime/web';
|
|
3
|
+
import { createWebRequestResponseFactory, dispatchWebRequest, startWebRequestDispatch } from '@fluojs/runtime/web';
|
|
4
4
|
|
|
5
5
|
/** Shutdown signal names that `runBunApplication()` can register. */
|
|
6
6
|
|
|
@@ -41,7 +41,25 @@ const DEFAULT_FORCE_EXIT_TIMEOUT_MS = 30_000;
|
|
|
41
41
|
const MINIMUM_BUN_NATIVE_ROUTES_VERSION = '1.2.3';
|
|
42
42
|
const EMPTY_NATIVE_ROUTE_PARAMS = Object.freeze({});
|
|
43
43
|
const BUN_ADAPTER_CLOSE_TIMEOUT_MS = Symbol('fluo.bunAdapterCloseTimeoutMs');
|
|
44
|
+
const BUN_ADAPTER_DIAGNOSTIC_CODES = {
|
|
45
|
+
invalidOption: 'BUN_ADAPTER_INVALID_OPTION',
|
|
46
|
+
realtimeBindingInvalid: 'BUN_ADAPTER_REALTIME_BINDING_INVALID',
|
|
47
|
+
realtimeBindingLocked: 'BUN_ADAPTER_REALTIME_BINDING_LOCKED',
|
|
48
|
+
runtimeUnavailable: 'BUN_ADAPTER_RUNTIME_UNAVAILABLE',
|
|
49
|
+
shutdownTimeout: 'BUN_ADAPTER_SHUTDOWN_TIMEOUT'
|
|
50
|
+
};
|
|
44
51
|
const BUN_WEBSOCKET_SUPPORT_REASON = 'Bun exposes Bun.serve() + server.upgrade() request-upgrade hosting. Use @fluojs/websockets/bun for the official raw websocket binding.';
|
|
52
|
+
function attachBunAdapterDiagnosticCode(error, code) {
|
|
53
|
+
Object.defineProperty(error, 'code', {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
value: code
|
|
56
|
+
});
|
|
57
|
+
return error;
|
|
58
|
+
}
|
|
59
|
+
function isBunWebSocketBinding(value) {
|
|
60
|
+
return typeof value === 'object' && value !== null && typeof Reflect.get(value, 'fetch') === 'function' && typeof Reflect.get(value, 'websocket') === 'object' && Reflect.get(value, 'websocket') !== null;
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
/** HTTP application adapter backed by native `Bun.serve()`. */
|
|
46
64
|
export class BunHttpApplicationAdapter {
|
|
47
65
|
closeInFlight;
|
|
@@ -65,7 +83,6 @@ export class BunHttpApplicationAdapter {
|
|
|
65
83
|
consumeOriginalBody: true,
|
|
66
84
|
maxBodySize: options.maxBodySize,
|
|
67
85
|
multipart: options.multipart,
|
|
68
|
-
preferNativeJsonBodyReader: true,
|
|
69
86
|
rawBody: options.rawBody
|
|
70
87
|
});
|
|
71
88
|
}
|
|
@@ -91,14 +108,27 @@ export class BunHttpApplicationAdapter {
|
|
|
91
108
|
/** Reports Bun's fetch-style websocket capability for realtime package integration. */
|
|
92
109
|
getRealtimeCapability() {
|
|
93
110
|
return createFetchStyleHttpAdapterRealtimeCapability(BUN_WEBSOCKET_SUPPORT_REASON, {
|
|
111
|
+
bindingInstallation: {
|
|
112
|
+
install: binding => this.installRealtimeBinding(binding)
|
|
113
|
+
},
|
|
94
114
|
support: 'supported'
|
|
95
115
|
});
|
|
96
116
|
}
|
|
117
|
+
installRealtimeBinding(binding) {
|
|
118
|
+
if (binding === undefined) {
|
|
119
|
+
this.configureRealtimeBinding(undefined);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!isBunWebSocketBinding(binding)) {
|
|
123
|
+
throw attachBunAdapterDiagnosticCode(new TypeError('Bun realtime binding installation requires fetch and websocket host contracts.'), BUN_ADAPTER_DIAGNOSTIC_CODES.realtimeBindingInvalid);
|
|
124
|
+
}
|
|
125
|
+
this.configureRealtimeBinding(binding);
|
|
126
|
+
}
|
|
97
127
|
|
|
98
128
|
/** Configures the official realtime binding before the Bun server starts. */
|
|
99
129
|
configureRealtimeBinding(binding) {
|
|
100
130
|
if (this.server) {
|
|
101
|
-
throw new Error('Bun websocket binding must be configured before Bun adapter listen() starts the server.');
|
|
131
|
+
throw attachBunAdapterDiagnosticCode(new Error('Bun websocket binding must be configured before Bun adapter listen() starts the server.'), BUN_ADAPTER_DIAGNOSTIC_CODES.realtimeBindingLocked);
|
|
102
132
|
}
|
|
103
133
|
this.realtimeBinding = binding;
|
|
104
134
|
}
|
|
@@ -124,6 +154,7 @@ export class BunHttpApplicationAdapter {
|
|
|
124
154
|
return createShutdownResponse();
|
|
125
155
|
}
|
|
126
156
|
const release = this.trackInFlightRequest();
|
|
157
|
+
let releaseAfterResponse = true;
|
|
127
158
|
try {
|
|
128
159
|
if (realtimeBinding) {
|
|
129
160
|
const realtimeResult = await dispatchRealtimeBindingRequest(realtimeBinding, request, server);
|
|
@@ -131,14 +162,19 @@ export class BunHttpApplicationAdapter {
|
|
|
131
162
|
return realtimeResult.handled;
|
|
132
163
|
}
|
|
133
164
|
}
|
|
134
|
-
|
|
165
|
+
const dispatch = startWebRequestDispatch({
|
|
135
166
|
dispatcher: this.dispatcher,
|
|
136
167
|
dispatcherNotReadyMessage: DEFAULT_DISPATCHER_NOT_READY_MESSAGE,
|
|
137
168
|
factory: this.webRequestResponseFactory,
|
|
138
169
|
request
|
|
139
170
|
});
|
|
171
|
+
releaseAfterResponse = false;
|
|
172
|
+
void dispatch.completion.finally(release).catch(() => {});
|
|
173
|
+
return await dispatch.response;
|
|
140
174
|
} finally {
|
|
141
|
-
|
|
175
|
+
if (releaseAfterResponse) {
|
|
176
|
+
release();
|
|
177
|
+
}
|
|
142
178
|
}
|
|
143
179
|
};
|
|
144
180
|
const nativeRoutes = createBunNativeRoutes(dispatcher, handleRequest, bun);
|
|
@@ -166,6 +202,7 @@ export class BunHttpApplicationAdapter {
|
|
|
166
202
|
}
|
|
167
203
|
if (!this.server) {
|
|
168
204
|
this.dispatcher = undefined;
|
|
205
|
+
this.realtimeBinding = undefined;
|
|
169
206
|
return;
|
|
170
207
|
}
|
|
171
208
|
const server = this.server;
|
|
@@ -176,6 +213,7 @@ export class BunHttpApplicationAdapter {
|
|
|
176
213
|
}
|
|
177
214
|
this.closeInFlight = undefined;
|
|
178
215
|
this.dispatcher = undefined;
|
|
216
|
+
this.realtimeBinding = undefined;
|
|
179
217
|
});
|
|
180
218
|
this.closeInFlight = closeInFlight;
|
|
181
219
|
void closeInFlight.catch(() => {});
|
|
@@ -223,7 +261,6 @@ export function createBunFetchHandler({
|
|
|
223
261
|
consumeOriginalBody: true,
|
|
224
262
|
maxBodySize,
|
|
225
263
|
multipart,
|
|
226
|
-
preferNativeJsonBodyReader: true,
|
|
227
264
|
rawBody
|
|
228
265
|
});
|
|
229
266
|
return async function bunFetchHandler(request) {
|
|
@@ -352,7 +389,7 @@ async function closeBunApplicationFromSignal(app, logger, signal, forceExitTimeo
|
|
|
352
389
|
function requireBunGlobal() {
|
|
353
390
|
const bun = globalThis.Bun;
|
|
354
391
|
if (!bun || typeof bun.serve !== 'function') {
|
|
355
|
-
throw new Error('Bun adapter requires globalThis.Bun.serve(). Run this package inside Bun or provide a Bun-compatible test double.');
|
|
392
|
+
throw attachBunAdapterDiagnosticCode(new Error('Bun adapter requires globalThis.Bun.serve(). Run this package inside Bun or provide a Bun-compatible test double.'), BUN_ADAPTER_DIAGNOSTIC_CODES.runtimeUnavailable);
|
|
356
393
|
}
|
|
357
394
|
return bun;
|
|
358
395
|
}
|
|
@@ -362,7 +399,7 @@ function resolvePort(port) {
|
|
|
362
399
|
function validatePortOption(port) {
|
|
363
400
|
const resolved = port ?? DEFAULT_PORT;
|
|
364
401
|
if (!Number.isInteger(resolved) || resolved < 0 || resolved > 65535) {
|
|
365
|
-
throw new Error(`Invalid port value: ${String(resolved)}. Expected an integer between 0 and 65535.`);
|
|
402
|
+
throw attachBunAdapterDiagnosticCode(new Error(`Invalid port value: ${String(resolved)}. Expected an integer between 0 and 65535.`), BUN_ADAPTER_DIAGNOSTIC_CODES.invalidOption);
|
|
366
403
|
}
|
|
367
404
|
return resolved;
|
|
368
405
|
}
|
|
@@ -371,7 +408,7 @@ function validateNonNegativeIntegerOption(name, value) {
|
|
|
371
408
|
return;
|
|
372
409
|
}
|
|
373
410
|
if (!Number.isInteger(value) || value < 0) {
|
|
374
|
-
throw new Error(`Invalid ${name} value: ${String(value)}. Expected a non-negative integer.`);
|
|
411
|
+
throw attachBunAdapterDiagnosticCode(new Error(`Invalid ${name} value: ${String(value)}. Expected a non-negative integer.`), BUN_ADAPTER_DIAGNOSTIC_CODES.invalidOption);
|
|
375
412
|
}
|
|
376
413
|
}
|
|
377
414
|
async function dispatchRealtimeBindingRequest(binding, request, server) {
|
|
@@ -515,12 +552,29 @@ function collectVersionSensitiveRouteKeys(descriptors) {
|
|
|
515
552
|
return new Set([...grouped.entries()].filter(([, current]) => current.count > 1 || current.hasVersioned).map(([routeKey]) => routeKey));
|
|
516
553
|
}
|
|
517
554
|
function toBunRouteMethod(method) {
|
|
518
|
-
|
|
555
|
+
switch (method) {
|
|
556
|
+
case 'GET':
|
|
557
|
+
case 'POST':
|
|
558
|
+
case 'PUT':
|
|
559
|
+
case 'PATCH':
|
|
560
|
+
case 'DELETE':
|
|
561
|
+
case 'OPTIONS':
|
|
562
|
+
case 'HEAD':
|
|
563
|
+
return method;
|
|
564
|
+
default:
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
519
567
|
}
|
|
520
568
|
function closeBunServerWithDrain(server, stopActiveConnections, waitForDrain) {
|
|
521
569
|
return (async () => {
|
|
522
|
-
server.stop(stopActiveConnections);
|
|
523
|
-
|
|
570
|
+
const results = await Promise.allSettled([server.stop(stopActiveConnections), waitForDrain()]);
|
|
571
|
+
const rejections = results.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
|
|
572
|
+
if (rejections.length === 1) {
|
|
573
|
+
throw rejections[0];
|
|
574
|
+
}
|
|
575
|
+
if (rejections.length > 1) {
|
|
576
|
+
throw new AggregateError(rejections, 'Bun server shutdown failed.');
|
|
577
|
+
}
|
|
524
578
|
})();
|
|
525
579
|
}
|
|
526
580
|
function createDeferred() {
|
|
@@ -553,7 +607,7 @@ function createShutdownResponse() {
|
|
|
553
607
|
function waitForCloseWithTimeout(closePromise, timeoutMs) {
|
|
554
608
|
return new Promise((resolve, reject) => {
|
|
555
609
|
const timeoutHandle = setTimeout(() => {
|
|
556
|
-
reject(new Error(`Bun adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`));
|
|
610
|
+
reject(attachBunAdapterDiagnosticCode(new Error(`Bun adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`), BUN_ADAPTER_DIAGNOSTIC_CODES.shutdownTimeout));
|
|
557
611
|
}, timeoutMs);
|
|
558
612
|
void closePromise.then(() => {
|
|
559
613
|
clearTimeout(timeoutHandle);
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"platform",
|
|
9
9
|
"server"
|
|
10
10
|
],
|
|
11
|
-
"version": "
|
|
11
|
+
"version": "3.0.0",
|
|
12
12
|
"private": false,
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@fluojs/http": "^
|
|
36
|
-
"@fluojs/runtime": "^
|
|
35
|
+
"@fluojs/http": "^3.0.0",
|
|
36
|
+
"@fluojs/runtime": "^3.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"vitest": "^
|
|
40
|
-
"@fluojs/testing": "^
|
|
39
|
+
"vitest": "^4.1.11",
|
|
40
|
+
"@fluojs/testing": "^3.0.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|