@fluojs/platform-deno 1.0.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +78 -17
- package/README.md +78 -17
- package/dist/adapter.d.ts +10 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +136 -43
- package/dist/fetch-handler.d.ts +26 -0
- package/dist/fetch-handler.d.ts.map +1 -0
- package/dist/fetch-handler.js +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/options.d.ts +8 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +14 -0
- package/dist/testing-web-runtime-adapter-portability.d.js +0 -0
- package/package.json +5 -5
package/README.ko.md
CHANGED
|
@@ -43,33 +43,75 @@ await runDenoApplication(AppModule, {
|
|
|
43
43
|
});
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
Managed startup 경로는 network listener를 열고 기본적으로 `SIGINT`/`SIGTERM` listener를 등록합니다. Network 접근 권한을 부여해 entrypoint를 실행하세요.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
deno run --allow-net main.ts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Signal listener 등록에는 별도의 Deno permission이 필요하지 않습니다. Adapter 자체는 environment variable을 읽지 않습니다. 애플리케이션 코드가 해당 key를 읽을 때만 `--allow-env=PORT,DATABASE_URL`처럼 범위를 제한한 권한을 추가하세요. Signal로 트리거된 애플리케이션 close 실패는 helper가 log한 뒤 swallow하며 exit status를 설정하지 않습니다. Failure-status propagation 또는 forced termination이 필요한 host는 `runDenoApplication(...)`에 `shutdownSignals: false`를 전달하고 signal과 shutdown을 직접 조율해야 합니다.
|
|
53
|
+
|
|
46
54
|
## 주요 패턴
|
|
47
55
|
|
|
48
|
-
###
|
|
49
|
-
|
|
56
|
+
### Early Hints 미지원
|
|
57
|
+
|
|
58
|
+
Deno의 Fetch `Response`는 final response 이전 informational response를 표현할 수 없으므로 `context.response.earlyHints`가 없습니다. 사용 전에 capability 존재 여부를 확인하세요. Adapter는 요청된 `103`을 silent no-op 또는 final-response header로 바꾸지 않습니다.
|
|
59
|
+
|
|
60
|
+
### 스트리밍 멀티파트 소비
|
|
61
|
+
|
|
62
|
+
애플리케이션 bootstrap에서 `multipart: { strategy: 'stream' }`을 설정하면 멀티파트 데이터를 점진적으로
|
|
63
|
+
받습니다. 멀티파트 route에서 `RequestContext.request.body`는 `AsyncIterableIterator<MultipartPart>`입니다.
|
|
64
|
+
field part는 `kind: 'field'`, `name`, `value`, `headers`를, file part는 `kind: 'file'`, `name`, `filename`,
|
|
65
|
+
`contentType`, `headers`, 그리고 `stream`의 single-consumer `ReadableStream<Uint8Array>`를 제공합니다. 다음
|
|
66
|
+
part를 요청하기 전에 각 file stream을 끝까지 소비하거나 cancel하세요.
|
|
67
|
+
|
|
68
|
+
Runtime route dispatch는 route를 위해 만든 iterator를 소유하며 handler가 끝난 뒤 자동으로 `return()`을 호출해
|
|
69
|
+
active source를 cancel하고 release합니다. Standalone `parseMultipartStream(...)` consumer는 이 책임을 직접
|
|
70
|
+
집니다. iterator를 끝까지 소비하거나 일찍 끝낼 때 `return()`을 호출하세요.
|
|
71
|
+
|
|
72
|
+
### Host-Owned Deno.serve
|
|
73
|
+
애플리케이션이 `Deno.serve(...)`를 소유한다면 `app.listen()`을 호출하지 않고 fluo 애플리케이션을 bootstrap한 뒤 public dispatcher로 request handler를 만드세요. `createDenoFetchHandler(...)`는 request 변환과 dispatch만 수행하며 server를 시작하거나 shutdown, signal, websocket upgrade를 소유하지 않습니다.
|
|
50
74
|
|
|
51
75
|
```typescript
|
|
52
|
-
import { createDenoAdapter } from '@fluojs/platform-deno';
|
|
76
|
+
import { createDenoAdapter, createDenoFetchHandler } from '@fluojs/platform-deno';
|
|
53
77
|
import { fluoFactory } from '@fluojs/runtime';
|
|
54
78
|
|
|
55
|
-
const adapter = createDenoAdapter(
|
|
79
|
+
const adapter = createDenoAdapter();
|
|
56
80
|
const app = await fluoFactory.create(AppModule, { adapter });
|
|
81
|
+
const handler = createDenoFetchHandler({
|
|
82
|
+
dispatcher: app.dispatcher,
|
|
83
|
+
rawBody: true,
|
|
84
|
+
});
|
|
57
85
|
|
|
58
|
-
|
|
86
|
+
const server = Deno.serve({ port: 3000 }, handler);
|
|
59
87
|
|
|
60
|
-
|
|
88
|
+
try {
|
|
89
|
+
await server.finished;
|
|
90
|
+
} finally {
|
|
91
|
+
await app.close();
|
|
92
|
+
}
|
|
61
93
|
```
|
|
62
94
|
|
|
63
|
-
|
|
95
|
+
주변 host는 `server` 중지와 process signal 조율을 소유하고 websocket upgrade를 별도로 처리할지 결정해야 합니다. Fluo가 managed server lifecycle을 소유해야 한다면 `app.listen()`을 사용하고, shutdown signal listener까지 추가로 설치해야 한다면 `runDenoApplication(...)`을 사용하세요. `adapter.handle(...)`은 managed adapter의 `listen(dispatcher)` binding이 끝난 뒤에만 사용할 수 있습니다.
|
|
96
|
+
|
|
97
|
+
### 직접 어댑터 생성
|
|
98
|
+
애플리케이션 코드는 보통 `createDenoAdapter(options)`, `bootstrapDenoApplication(...)`, `runDenoApplication(...)`을 사용해 adapter setup 경계를 명확히 유지하는 편을 권장합니다. 커스텀 orchestration이나 테스트에서는 `new DenoHttpApplicationAdapter(options?)`를 직접 사용할 수 있으며, constructor는 factory와 같은 optional public `DenoAdapterOptions`를 받고 options가 생략되면 기본 port를 적용하며, portable `host` alias보다 `hostname`을 우선하고, 잘못된 `port` 또는 `maxBodySize` 값은 setup 시점에 거절합니다.
|
|
99
|
+
|
|
100
|
+
### Opt-in Deno WebSocket 바인딩
|
|
64
101
|
어댑터는 애플리케이션이 `@fluojs/websockets/deno` 바인딩을 import하고 설정한 뒤에 Deno의 네이티브 `Deno.upgradeWebSocket`을 지원합니다. 해당 바인딩이 없으면 websocket upgrade 요청은 암묵적으로 upgrade되지 않고 일반 HTTP dispatch 경로를 계속 따릅니다.
|
|
65
102
|
|
|
66
103
|
```typescript
|
|
67
104
|
import { Module } from '@fluojs/core';
|
|
68
|
-
import { WebSocketGateway } from '@fluojs/websockets';
|
|
69
|
-
import {
|
|
105
|
+
import { DenoWebSocketModule, OnMessage, WebSocketGateway } from '@fluojs/websockets/deno';
|
|
106
|
+
import type { DenoServerWebSocket } from '@fluojs/websockets/deno';
|
|
70
107
|
|
|
71
108
|
@WebSocketGateway({ path: '/ws' })
|
|
72
|
-
export class MyGateway {
|
|
109
|
+
export class MyGateway {
|
|
110
|
+
@OnMessage('ping')
|
|
111
|
+
handlePing(_payload: unknown, socket: DenoServerWebSocket) {
|
|
112
|
+
socket.send(JSON.stringify({ event: 'pong', data: 'hello from deno' }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
73
115
|
|
|
74
116
|
@Module({
|
|
75
117
|
imports: [DenoWebSocketModule.forRoot()],
|
|
@@ -78,6 +120,14 @@ export class MyGateway {}
|
|
|
78
120
|
export class RealtimeModule {}
|
|
79
121
|
```
|
|
80
122
|
|
|
123
|
+
#### Deno websocket 수신 payload
|
|
124
|
+
|
|
125
|
+
`DenoWebSocketMessage`는 Deno binding이 전달할 수 있는 모든 payload form,
|
|
126
|
+
`ArrayBuffer | ArrayBufferView | Blob | string`을 나타냅니다. 기존에 이 public
|
|
127
|
+
union을 `Blob | string`으로 exhaustive narrowing한 handler는 `ArrayBuffer`와
|
|
128
|
+
`ArrayBufferView` branch를 추가해야 합니다. `@fluojs/websockets/deno` binding은
|
|
129
|
+
이미 gateway handler를 dispatch하기 전에 이 binary payload를 수용하고 normalize합니다.
|
|
130
|
+
|
|
81
131
|
## HTTPS와 런타임 이식성
|
|
82
132
|
|
|
83
133
|
`https` 옵션으로 Deno TLS 인증서 자료를 전달하면 `Deno.serve`를 HTTPS 모드로 시작할 수 있습니다. 어댑터는 `https.cert`와 `https.key`를 Deno의 `cert` 및 `key`로 전달하며, 시작 로그도 `https://` listen URL을 보고하므로 Deno 패키지가 공유 HTTP 어댑터 이식성 계약과 정렬됩니다.
|
|
@@ -95,23 +145,33 @@ await runDenoApplication(AppModule, {
|
|
|
95
145
|
|
|
96
146
|
`hostname`은 Deno 네이티브 옵션 이름으로 유지됩니다. 공유 HTTP 어댑터 테스트와 교차 런타임 설정 헬퍼를 위해 `host`도 이식성 alias로 허용하며, 둘 다 제공하면 `Deno.serve(...)` bind target과 보고되는 listen URL에는 `hostname`이 우선합니다.
|
|
97
147
|
|
|
98
|
-
Advanced option에는 test 또는 non-hosted runtime을 위한 injectable `serve`, `upgradeWebSocket` seam, `rawBody`, `maxBodySize`, `multipart`, `shutdownSignals`가 포함됩니다. Seam을 주입하지 않으면
|
|
148
|
+
Advanced option에는 test 또는 non-hosted runtime을 위한 injectable `serve`, `upgradeWebSocket` seam, `rawBody`, `maxBodySize`, `multipart`, `shutdownSignals`가 포함됩니다. `createDenoFetchHandler(...)`는 같은 request parsing option을 받으며 byte-exact JSON/text raw body를 보존하고 multipart request에서는 `rawBody`를 제외합니다. Seam을 주입하지 않으면 managed adapter는 listen/upgrade 시점에 `globalThis.Deno.serve`와 `globalThis.Deno.upgradeWebSocket`으로 fallback합니다. `runDenoApplication(...)`은 기본적으로 `SIGINT`/`SIGTERM`을 연결하고, `shutdownSignals: false`는 signal registration을 끄며, 여러 signal을 등록하다 실패하면 이미 연결한 listener를 rollback합니다. 이미 실행 중인 adapter에 대한 중복 `listen(...)` 호출은 원래 dispatcher pipeline을 보존하는 no-op입니다. Close는 Deno serve signal을 abort하기 전에 active request drain을 최대 10초 기다립니다. Graceful shutdown이 reject되면 adapter는 ingress를 abort하지만 `server.finished`가 settle될 때까지 active server controller를 유지하고, 그 뒤에만 managed lifecycle을 해제한 다음 원래 shutdown error를 다시 throw합니다. `handle(...)`은 websocket upgrade 요청을 포함해 `listen()`이 dispatcher를 bind하기 전에는 JSON `500`, shutdown 진행 중에는 JSON `503`을 반환합니다.
|
|
99
149
|
|
|
100
150
|
## Conformance 커버리지
|
|
101
151
|
|
|
102
|
-
`packages/platform-deno/src/adapter.test.ts`는
|
|
152
|
+
`packages/platform-deno/src/adapter.test.ts`는 managed Deno 계약을 검증하는 package-local regression 대상입니다. 이 파일은 shared Web dispatch delegation, `listen(dispatcher)` 이후 direct `adapter.handle(...)` success-path dispatch, 직접 constructor/factory option normalization, HTTPS startup forwarding, `Deno.serve(...)` bind target과 startup log에 대한 `host` alias 및 `hostname` 우선순위, 중복 `listen(...)` no-op dispatcher 보존, 기본 `SIGINT`/`SIGTERM` signal listener 등록, `shutdownSignals: false`, partial signal-registration failure 이후 listener rollback, websocket upgrade binding 및 no-binding HTTP fallback, websocket listen 전 bootstrap gating, global Deno serve/upgrade fallback seam, listen 전 `500` 처리, shutdown 중 `503` 처리, serve-signal abort 전 in-flight request drain, `server.finished`까지 shutdown failure ownership 유지, bounded 10초 close timeout을 검증합니다. `packages/platform-deno/src/fetch-handler.test.ts`는 host-owned handler에 shared web-runtime portability harness를 적용하여 cookie/query decoding, JSON/text와 byte-exact raw body, multipart exclusion, SSE framing, dispatch가 `Deno.serve(...)`를 호출하지 않는다는 사실을 검증합니다. `packages/platform-deno/src/declaration-surface.test.ts`는 package를 다시 build하고 manifest가 export하는 declaration을 검증합니다.
|
|
153
|
+
|
|
154
|
+
공유 edge portability suite인 `packages/testing/src/portability/web-runtime-adapter-portability.test.ts`는 Deno를 Bun 및 Cloudflare Workers와 함께 실행해 malformed cookie 보존, query decoding, JSON/text raw-body capture, 단일 byte-range status/header/body semantic, multipart raw-body 제외, SSE framing을 검증합니다. 패키지 테스트의 README parity assertion은 이 edge-runtime 커버리지 문서가 한국어 mirror와 계속 동기화되도록 확인합니다.
|
|
103
155
|
|
|
104
|
-
|
|
156
|
+
Deno 2 smoke lane은 public `npm:@fluojs/platform-deno` root import를 검사하고 built adapter closure를 네이티브로 실행합니다. Listener 없이 host-owned `createDenoFetchHandler(...)` request를 dispatch한 뒤, port `0`과 `shutdownSignals: false`로 `runDenoApplication(...)`을 시작하고 실제 route를 fetch한 다음 애플리케이션을 닫습니다. 이 managed-listener test에서는 signal ownership을 의도적으로 비활성화하며, signal registration은 별도의 package-local contract suite에서 다룹니다.
|
|
105
157
|
|
|
106
158
|
## 공개 API 개요
|
|
107
159
|
|
|
108
|
-
- `createDenoAdapter(options)`: Deno HTTP 어댑터를 위한
|
|
160
|
+
- `createDenoAdapter(options)`: Deno HTTP 어댑터를 위한 팩토리이며, 직접 생성과 같은 validation 및 normalization을 공유합니다.
|
|
161
|
+
- `createDenoFetchHandler(options)`: 이미 bootstrap된 `app.dispatcher`에서 `Deno.serve(...)`를 시작하거나 소유하지 않는 `Request` handler를 동기적으로 생성합니다.
|
|
109
162
|
- `bootstrapDenoApplication(module, options)`: 커스텀 오케스트레이션을 위한 고급 부트스트랩입니다.
|
|
110
163
|
- `runDenoApplication(module, options)`: Deno를 위한 권장 빠른 시작 헬퍼입니다.
|
|
111
|
-
- `DenoHttpApplicationAdapter`: `
|
|
112
|
-
- `
|
|
164
|
+
- `DenoHttpApplicationAdapter(options?)`: 핵심 adapter 구현체입니다. Direct `new DenoHttpApplicationAdapter()` 또는 `new DenoHttpApplicationAdapter(options)`는 `createDenoAdapter(options)`와 같은 기본 port, `host` alias 처리, `hostname` 우선순위, 숫자 option validation을 적용합니다.
|
|
165
|
+
- `listen(dispatcher)`: fluo HTTP dispatcher를 bind하고 `Deno.serve`를 시작합니다. 중복 호출은 원래 dispatcher를 보존하는 no-op입니다.
|
|
166
|
+
- `close()`: 새 유입을 중단하고 active request를 최대 10초 drain한 뒤, shutdown이 끝나지 않으면 Deno serve signal을 abort합니다. Graceful shutdown이 reject되면 `server.finished`가 settle될 때까지 `getServer()` ownership을 유지하고, 이후 managed lifecycle을 해제한 다음 원래 shutdown error를 다시 throw합니다.
|
|
167
|
+
- `handle(request)`: 수동 `Request` to `Response` 디스패처입니다. `listen(dispatcher)`가 runtime dispatcher를 bind한 뒤에는 성공 경로를 실행하고, bind 전에는 JSON `500`, shutdown 중에는 JSON `503`을 반환합니다.
|
|
168
|
+
- `getListenTarget()`: Deno `hostname` 또는 portable `host` alias를 사용해 bind target과 public URL을 보고합니다.
|
|
169
|
+
- `getRealtimeCapability()`: runtime integration을 위한 fetch-style Deno websocket upgrade capability를 보고합니다.
|
|
170
|
+
- `getServer()`: adapter가 listen 중일 때 active `Deno.serve` controller를 반환합니다.
|
|
171
|
+
- `configureWebSocketBinding(...)`: `listen(dispatcher)`가 server를 시작하기 전에 `@fluojs/websockets/deno` binding을 설치합니다.
|
|
172
|
+
- `DenoWebSocketMessage`: 전체 수신 websocket payload union인 `ArrayBuffer | ArrayBufferView | Blob | string`입니다.
|
|
113
173
|
- `https: { cert, key }`: `Deno.serve`로 전달되고 보고되는 listen URL에 반영되는 HTTPS 시작 옵션입니다.
|
|
114
|
-
- Option 및 seam type: `DenoServeOptions`, `DenoServeController`, `DenoServerWebSocket`, websocket binding interface, bootstrap/run option, listen-target helper.
|
|
174
|
+
- Option 및 seam type: `CreateDenoFetchHandlerOptions`, `DenoServeOptions`, `DenoServeController`, `DenoServerWebSocket`, websocket binding interface, bootstrap/run option, listen-target helper.
|
|
115
175
|
|
|
116
176
|
## 관련 패키지
|
|
117
177
|
|
|
@@ -122,4 +182,5 @@ Advanced option에는 test 또는 non-hosted runtime을 위한 injectable `serve
|
|
|
122
182
|
## 예제 소스
|
|
123
183
|
|
|
124
184
|
- `packages/platform-deno/src/adapter.test.ts`
|
|
185
|
+
- `packages/platform-deno/src/fetch-handler.test.ts`
|
|
125
186
|
- `packages/websockets/src/deno/deno.test.ts`
|
package/README.md
CHANGED
|
@@ -43,33 +43,75 @@ await runDenoApplication(AppModule, {
|
|
|
43
43
|
});
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
The managed startup path opens a network listener and registers `SIGINT`/`SIGTERM` listeners by default. Run the entrypoint with network access:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
deno run --allow-net main.ts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Signal listener registration does not require a separate Deno permission. The adapter does not read environment variables. Add a scoped grant such as `--allow-env=PORT,DATABASE_URL` only when application code reads those keys. Signal-triggered application-close failures are logged and swallowed by the helper; it does not set an exit status. Hosts that require failure-status propagation or forced termination must pass `shutdownSignals: false` to `runDenoApplication(...)` and coordinate signals and shutdown themselves.
|
|
53
|
+
|
|
46
54
|
## Common Patterns
|
|
47
55
|
|
|
48
|
-
###
|
|
49
|
-
|
|
56
|
+
### Early Hints are unsupported
|
|
57
|
+
|
|
58
|
+
Deno's Fetch `Response` cannot represent an informational response before the final response, so `context.response.earlyHints` is absent. Check for capability presence before use. The adapter never turns a requested `103` into a silent no-op or a final-response header.
|
|
59
|
+
|
|
60
|
+
### Streaming multipart consumption
|
|
61
|
+
|
|
62
|
+
Set `multipart: { strategy: 'stream' }` at application bootstrap to receive multipart data incrementally. For
|
|
63
|
+
multipart routes, `RequestContext.request.body` is an `AsyncIterableIterator<MultipartPart>`: field parts expose
|
|
64
|
+
`kind: 'field'`, `name`, `value`, and `headers`; file parts expose `kind: 'file'`, `name`, `filename`,
|
|
65
|
+
`contentType`, `headers`, and a single-consumer `ReadableStream<Uint8Array>` at `stream`. Finish or cancel each file
|
|
66
|
+
stream before requesting the next part.
|
|
67
|
+
|
|
68
|
+
Runtime route dispatch owns an iterator created for a route and automatically calls `return()` after the handler
|
|
69
|
+
finishes, cancelling and releasing an active source. Standalone `parseMultipartStream(...)` consumers own that
|
|
70
|
+
responsibility: consume the iterator to completion or call `return()` when ending early.
|
|
71
|
+
|
|
72
|
+
### Host-Owned Deno.serve
|
|
73
|
+
If your application owns `Deno.serve(...)`, bootstrap the fluo application without calling `app.listen()` and create a request handler from its public dispatcher. `createDenoFetchHandler(...)` only translates and dispatches requests; it never starts a server or owns shutdown, signals, or websocket upgrades.
|
|
50
74
|
|
|
51
75
|
```typescript
|
|
52
|
-
import { createDenoAdapter } from '@fluojs/platform-deno';
|
|
76
|
+
import { createDenoAdapter, createDenoFetchHandler } from '@fluojs/platform-deno';
|
|
53
77
|
import { fluoFactory } from '@fluojs/runtime';
|
|
54
78
|
|
|
55
|
-
const adapter = createDenoAdapter(
|
|
79
|
+
const adapter = createDenoAdapter();
|
|
56
80
|
const app = await fluoFactory.create(AppModule, { adapter });
|
|
81
|
+
const handler = createDenoFetchHandler({
|
|
82
|
+
dispatcher: app.dispatcher,
|
|
83
|
+
rawBody: true,
|
|
84
|
+
});
|
|
57
85
|
|
|
58
|
-
|
|
86
|
+
const server = Deno.serve({ port: 3000 }, handler);
|
|
59
87
|
|
|
60
|
-
|
|
88
|
+
try {
|
|
89
|
+
await server.finished;
|
|
90
|
+
} finally {
|
|
91
|
+
await app.close();
|
|
92
|
+
}
|
|
61
93
|
```
|
|
62
94
|
|
|
63
|
-
|
|
95
|
+
The surrounding host must stop `server`, coordinate process signals, and decide whether websocket upgrades are handled separately. Use `app.listen()` when fluo should own the managed server lifecycle, or `runDenoApplication(...)` when fluo should additionally install shutdown signal listeners. `adapter.handle(...)` remains available only after the managed adapter's `listen(dispatcher)` binding has completed.
|
|
96
|
+
|
|
97
|
+
### Direct Adapter Construction
|
|
98
|
+
Application code should usually prefer `createDenoAdapter(options)`, `bootstrapDenoApplication(...)`, or `runDenoApplication(...)` so adapter setup stays explicit. Custom orchestration and tests may use `new DenoHttpApplicationAdapter(options?)` directly; the constructor accepts the same optional public `DenoAdapterOptions` as the factory, applies the default port when options are omitted, preserves `hostname` over the portable `host` alias, and rejects invalid `port` or `maxBodySize` values during setup.
|
|
99
|
+
|
|
100
|
+
### Opt-in Deno WebSocket Binding
|
|
64
101
|
The adapter supports Deno's native `Deno.upgradeWebSocket` after the application imports and configures the `@fluojs/websockets/deno` binding. Without that binding, websocket upgrade requests continue through normal HTTP dispatch instead of being upgraded implicitly.
|
|
65
102
|
|
|
66
103
|
```typescript
|
|
67
104
|
import { Module } from '@fluojs/core';
|
|
68
|
-
import { WebSocketGateway } from '@fluojs/websockets';
|
|
69
|
-
import {
|
|
105
|
+
import { DenoWebSocketModule, OnMessage, WebSocketGateway } from '@fluojs/websockets/deno';
|
|
106
|
+
import type { DenoServerWebSocket } from '@fluojs/websockets/deno';
|
|
70
107
|
|
|
71
108
|
@WebSocketGateway({ path: '/ws' })
|
|
72
|
-
export class MyGateway {
|
|
109
|
+
export class MyGateway {
|
|
110
|
+
@OnMessage('ping')
|
|
111
|
+
handlePing(_payload: unknown, socket: DenoServerWebSocket) {
|
|
112
|
+
socket.send(JSON.stringify({ event: 'pong', data: 'hello from deno' }));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
73
115
|
|
|
74
116
|
@Module({
|
|
75
117
|
imports: [DenoWebSocketModule.forRoot()],
|
|
@@ -78,6 +120,14 @@ export class MyGateway {}
|
|
|
78
120
|
export class RealtimeModule {}
|
|
79
121
|
```
|
|
80
122
|
|
|
123
|
+
#### Deno websocket inbound payloads
|
|
124
|
+
|
|
125
|
+
`DenoWebSocketMessage` represents every payload form the Deno binding can deliver:
|
|
126
|
+
`ArrayBuffer | ArrayBufferView | Blob | string`. Existing handlers that narrowed this
|
|
127
|
+
public union exhaustively for `Blob | string` must add `ArrayBuffer` and
|
|
128
|
+
`ArrayBufferView` branches. The `@fluojs/websockets/deno` binding already accepts and
|
|
129
|
+
normalizes these binary payloads before it dispatches gateway handlers.
|
|
130
|
+
|
|
81
131
|
## HTTPS and Runtime Portability
|
|
82
132
|
|
|
83
133
|
Pass Deno TLS certificate material through the `https` option to start `Deno.serve` in HTTPS mode. The adapter forwards `https.cert` and `https.key` to Deno as `cert` and `key`, and startup logging reports an `https://` listen URL so the Deno package stays aligned with the shared HTTP adapter portability contract.
|
|
@@ -95,23 +145,33 @@ await runDenoApplication(AppModule, {
|
|
|
95
145
|
|
|
96
146
|
`hostname` remains the Deno-native option name. The adapter also accepts `host` as a portability alias for shared HTTP adapter tests and cross-runtime configuration helpers; when both are provided, `hostname` wins for the `Deno.serve(...)` bind target and reported listen URL.
|
|
97
147
|
|
|
98
|
-
Advanced options include injectable `serve` and `upgradeWebSocket` seams for tests or non-hosted runtimes, `rawBody`, `maxBodySize`, `multipart`, and `shutdownSignals`. When a seam is not injected, the adapter falls back to `globalThis.Deno.serve` and `globalThis.Deno.upgradeWebSocket` at listen/upgrade time. `runDenoApplication(...)` wires `SIGINT`/`SIGTERM` by default, `shutdownSignals: false` disables signal registration, and failed multi-signal registration rolls back listeners that were already attached. Close waits up to 10 seconds for active requests to drain before aborting the Deno serve signal. `handle(...)` returns a JSON `500` before `listen()` binds the dispatcher and a JSON `503` while shutdown is in progress.
|
|
148
|
+
Advanced options include injectable `serve` and `upgradeWebSocket` seams for tests or non-hosted runtimes, `rawBody`, `maxBodySize`, `multipart`, and `shutdownSignals`. `createDenoFetchHandler(...)` accepts the same request parsing options and preserves byte-exact JSON/text raw bodies while excluding `rawBody` for multipart requests. When a seam is not injected, the managed adapter falls back to `globalThis.Deno.serve` and `globalThis.Deno.upgradeWebSocket` at listen/upgrade time. `runDenoApplication(...)` wires `SIGINT`/`SIGTERM` by default, `shutdownSignals: false` disables signal registration, and failed multi-signal registration rolls back listeners that were already attached. Duplicate `listen(...)` calls on an already-running adapter are no-ops that preserve the original dispatcher pipeline. Close waits up to 10 seconds for active requests to drain before aborting the Deno serve signal. If graceful shutdown rejects, the adapter aborts ingress but retains the active server controller until `server.finished` settles; only then does it release the managed lifecycle and rethrow the original shutdown error. `handle(...)` returns a JSON `500` before `listen()` binds the dispatcher, including websocket upgrade requests, and a JSON `503` while shutdown is in progress.
|
|
99
149
|
|
|
100
150
|
## Conformance Coverage
|
|
101
151
|
|
|
102
|
-
`packages/platform-deno/src/adapter.test.ts` is the package-local regression target for the
|
|
152
|
+
`packages/platform-deno/src/adapter.test.ts` is the package-local regression target for the managed Deno contract. It covers shared Web dispatch delegation, direct `adapter.handle(...)` success-path dispatch after `listen(dispatcher)`, direct constructor/factory option normalization, HTTPS startup forwarding, `host` alias and `hostname` precedence for the `Deno.serve(...)` bind target and startup log, duplicate `listen(...)` no-op dispatcher preservation, default `SIGINT`/`SIGTERM` signal listener registration, `shutdownSignals: false`, listener rollback after partial signal-registration failure, websocket upgrade binding and no-binding HTTP fallback, websocket pre-listen bootstrap gating, global Deno serve/upgrade fallback seams, pre-listen `500` handling, shutdown `503` handling, in-flight request drain before serve-signal abort, shutdown-failure ownership until `server.finished`, and the bounded 10-second close timeout. `packages/platform-deno/src/fetch-handler.test.ts` applies the shared web-runtime portability harness to the host-owned handler, covering cookies/query decoding, JSON/text and byte-exact raw bodies, multipart exclusion, SSE framing, and proof that dispatch does not call `Deno.serve(...)`. `packages/platform-deno/src/declaration-surface.test.ts` rebuilds the package and verifies the manifest-exported declarations.
|
|
153
|
+
|
|
154
|
+
The shared edge portability suite in `packages/testing/src/portability/web-runtime-adapter-portability.test.ts` exercises Deno beside Bun and Cloudflare Workers for malformed cookie preservation, query decoding, JSON/text raw-body capture, single byte-range status/header/body semantics, 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.
|
|
103
155
|
|
|
104
|
-
The
|
|
156
|
+
The Deno 2 smoke lane checks the public `npm:@fluojs/platform-deno` root import and executes the built adapter closure natively. It dispatches a host-owned `createDenoFetchHandler(...)` request without a listener, then starts `runDenoApplication(...)` on port `0` with `shutdownSignals: false`, fetches a real route, and closes the application. Signal ownership remains disabled for that managed-listener test; signal registration is covered separately by the package-local contract suite.
|
|
105
157
|
|
|
106
158
|
## Public API Overview
|
|
107
159
|
|
|
108
|
-
- `createDenoAdapter(options)`: Factory for the Deno HTTP adapter.
|
|
160
|
+
- `createDenoAdapter(options)`: Factory for the Deno HTTP adapter; it shares validation and normalization with direct construction.
|
|
161
|
+
- `createDenoFetchHandler(options)`: Synchronously creates a `Request` handler from an already bootstrapped `app.dispatcher` without starting or owning `Deno.serve(...)`.
|
|
109
162
|
- `bootstrapDenoApplication(module, options)`: Advanced bootstrap for custom orchestration.
|
|
110
163
|
- `runDenoApplication(module, options)`: Recommended quick-start helper for Deno.
|
|
111
|
-
- `DenoHttpApplicationAdapter`: Core adapter implementation
|
|
112
|
-
- `
|
|
164
|
+
- `DenoHttpApplicationAdapter(options?)`: Core adapter implementation. Direct `new DenoHttpApplicationAdapter()` or `new DenoHttpApplicationAdapter(options)` applies the same default port, `host` alias handling, `hostname` precedence, and numeric option validation as `createDenoAdapter(options)`.
|
|
165
|
+
- `listen(dispatcher)`: Binds the fluo HTTP dispatcher and starts `Deno.serve`; duplicate calls are no-ops while preserving the original dispatcher.
|
|
166
|
+
- `close()`: Stops ingress, drains active requests for up to 10 seconds, and aborts the Deno serve signal if shutdown does not settle. A rejected graceful shutdown retains `getServer()` ownership until `server.finished` settles, then releases the managed lifecycle and rethrows the original shutdown error.
|
|
167
|
+
- `handle(request)`: Manual `Request` to `Response` dispatcher. It succeeds after `listen(dispatcher)` binds the runtime dispatcher, returns JSON `500` before binding, and returns JSON `503` while shutdown is in progress.
|
|
168
|
+
- `getListenTarget()`: Reports the bind target and public URL using Deno `hostname` or the portable `host` alias.
|
|
169
|
+
- `getRealtimeCapability()`: Reports the fetch-style Deno websocket upgrade capability for runtime integration.
|
|
170
|
+
- `getServer()`: Returns the active `Deno.serve` controller while the adapter is listening.
|
|
171
|
+
- `configureWebSocketBinding(...)`: Installs the `@fluojs/websockets/deno` binding before `listen(dispatcher)` starts the server.
|
|
172
|
+
- `DenoWebSocketMessage`: The full inbound websocket payload union: `ArrayBuffer | ArrayBufferView | Blob | string`.
|
|
113
173
|
- `https: { cert, key }`: HTTPS startup options forwarded to `Deno.serve` and reflected in the reported listen URL.
|
|
114
|
-
- Option and seam types: `DenoServeOptions`, `DenoServeController`, `DenoServerWebSocket`, websocket binding interfaces, bootstrap/run options, and listen-target helpers.
|
|
174
|
+
- Option and seam types: `CreateDenoFetchHandlerOptions`, `DenoServeOptions`, `DenoServeController`, `DenoServerWebSocket`, websocket binding interfaces, bootstrap/run options, and listen-target helpers.
|
|
115
175
|
|
|
116
176
|
## Related Packages
|
|
117
177
|
|
|
@@ -122,4 +182,5 @@ The shared edge portability suite in `packages/testing/src/portability/web-runti
|
|
|
122
182
|
## Example Sources
|
|
123
183
|
|
|
124
184
|
- `packages/platform-deno/src/adapter.test.ts`
|
|
185
|
+
- `packages/platform-deno/src/fetch-handler.test.ts`
|
|
125
186
|
- `packages/websockets/src/deno/deno.test.ts`
|
package/dist/adapter.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface DenoServeController {
|
|
|
23
23
|
/** Deno shutdown signals supported by `runDenoApplication(...)`. */
|
|
24
24
|
export type DenoApplicationSignal = 'SIGINT' | 'SIGTERM';
|
|
25
25
|
/** Message payloads delivered through Deno server websocket bindings. */
|
|
26
|
-
export type DenoWebSocketMessage = Blob | string;
|
|
26
|
+
export type DenoWebSocketMessage = ArrayBuffer | ArrayBufferView | Blob | string;
|
|
27
27
|
/** Server-side websocket shape used by the Deno platform binding seam. */
|
|
28
28
|
export interface DenoServerWebSocket extends Pick<WebSocket, 'addEventListener' | 'close' | 'removeEventListener' | 'send'> {
|
|
29
29
|
readonly readyState: number;
|
|
@@ -91,6 +91,9 @@ declare global {
|
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
93
|
* Deno-backed HTTP adapter that preserves request draining and websocket binding seams.
|
|
94
|
+
*
|
|
95
|
+
* Direct construction accepts the same public options as `createDenoAdapter(...)` and applies the
|
|
96
|
+
* same default port, `host` alias, `hostname` precedence, and numeric option validation.
|
|
94
97
|
*/
|
|
95
98
|
export declare class DenoHttpApplicationAdapter implements HttpApplicationAdapter {
|
|
96
99
|
private abortController?;
|
|
@@ -103,7 +106,12 @@ export declare class DenoHttpApplicationAdapter implements HttpApplicationAdapte
|
|
|
103
106
|
private websocketBinding?;
|
|
104
107
|
private readonly options;
|
|
105
108
|
private readonly webRequestResponseFactory;
|
|
106
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Create a Deno adapter with the same normalization rules as `createDenoAdapter(...)`.
|
|
111
|
+
*
|
|
112
|
+
* @param options Transport, parsing, and websocket-host configuration for the Deno runtime.
|
|
113
|
+
*/
|
|
114
|
+
constructor(options?: DenoAdapterOptions);
|
|
107
115
|
getServer(): DenoServeController | undefined;
|
|
108
116
|
getListenTarget(): HttpAdapterListenTarget;
|
|
109
117
|
getRealtimeCapability(): import("@fluojs/http").FetchStyleHttpAdapterRealtimeCapability;
|
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,EAAiD,KAAK,UAAU,EAAE,KAAK,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpI,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpG,OAAO,EACL,KAAK,sCAAsC,EAG3C,KAAK,uBAAuB,EAC5B,KAAK,gCAAgC,EAEtC,MAAM,uCAAuC,CAAC;
|
|
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,7 @@
|
|
|
1
1
|
import { createFetchStyleHttpAdapterRealtimeCapability } from '@fluojs/http/internal';
|
|
2
2
|
import { bootstrapHttpAdapterApplication, createDefaultApplicationLogger, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
|
|
3
|
-
import { createWebRequestResponseFactory,
|
|
3
|
+
import { createWebRequestResponseFactory, startWebRequestDispatch } from '@fluojs/runtime/web';
|
|
4
|
+
import { validateNonNegativeIntegerOption } from './options.js';
|
|
4
5
|
|
|
5
6
|
/** Listen target metadata reported by `Deno.serve(...)` callbacks. */
|
|
6
7
|
|
|
@@ -41,7 +42,11 @@ const DEFAULT_PORT = 3000;
|
|
|
41
42
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
42
43
|
/**
|
|
43
44
|
* Deno-backed HTTP adapter that preserves request draining and websocket binding seams.
|
|
45
|
+
*
|
|
46
|
+
* Direct construction accepts the same public options as `createDenoAdapter(...)` and applies the
|
|
47
|
+
* same default port, `host` alias, `hostname` precedence, and numeric option validation.
|
|
44
48
|
*/
|
|
49
|
+
// allow: SIZE_OK — Deno serve, websocket, signal, and shutdown state form one adapter lifecycle state machine.
|
|
45
50
|
export class DenoHttpApplicationAdapter {
|
|
46
51
|
abortController;
|
|
47
52
|
closeInFlight;
|
|
@@ -53,12 +58,19 @@ export class DenoHttpApplicationAdapter {
|
|
|
53
58
|
websocketBinding;
|
|
54
59
|
options;
|
|
55
60
|
webRequestResponseFactory;
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a Deno adapter with the same normalization rules as `createDenoAdapter(...)`.
|
|
64
|
+
*
|
|
65
|
+
* @param options Transport, parsing, and websocket-host configuration for the Deno runtime.
|
|
66
|
+
*/
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
const resolvedOptions = normalizeDenoAdapterOptions(options);
|
|
69
|
+
this.options = resolvedOptions;
|
|
58
70
|
this.webRequestResponseFactory = createWebRequestResponseFactory({
|
|
59
|
-
maxBodySize:
|
|
60
|
-
multipart:
|
|
61
|
-
rawBody:
|
|
71
|
+
maxBodySize: resolvedOptions.maxBodySize,
|
|
72
|
+
multipart: resolvedOptions.multipart,
|
|
73
|
+
rawBody: resolvedOptions.rawBody
|
|
62
74
|
});
|
|
63
75
|
}
|
|
64
76
|
getServer() {
|
|
@@ -83,47 +95,75 @@ export class DenoHttpApplicationAdapter {
|
|
|
83
95
|
return createShutdownResponse();
|
|
84
96
|
}
|
|
85
97
|
const release = this.trackInFlightRequest();
|
|
98
|
+
let releaseAfterResponse = true;
|
|
86
99
|
try {
|
|
100
|
+
if (!this.dispatcher) {
|
|
101
|
+
const dispatch = startWebRequestDispatch({
|
|
102
|
+
dispatcher: this.dispatcher,
|
|
103
|
+
dispatcherNotReadyMessage: 'Deno adapter received a request before dispatcher binding completed.',
|
|
104
|
+
factory: this.webRequestResponseFactory,
|
|
105
|
+
request
|
|
106
|
+
});
|
|
107
|
+
releaseAfterResponse = false;
|
|
108
|
+
void dispatch.completion.finally(release).catch(() => {});
|
|
109
|
+
return await dispatch.response;
|
|
110
|
+
}
|
|
87
111
|
if (this.websocketBinding && isWebSocketUpgradeRequest(request)) {
|
|
88
112
|
const upgradeWebSocket = resolveUpgradeWebSocket(this.options.upgradeWebSocket);
|
|
89
113
|
return await this.websocketBinding.fetch(request, {
|
|
90
114
|
upgrade: upgradeRequest => upgradeWebSocket(upgradeRequest)
|
|
91
115
|
});
|
|
92
116
|
}
|
|
93
|
-
|
|
117
|
+
const dispatch = startWebRequestDispatch({
|
|
94
118
|
dispatcher: this.dispatcher,
|
|
95
119
|
dispatcherNotReadyMessage: 'Deno adapter received a request before dispatcher binding completed.',
|
|
96
120
|
factory: this.webRequestResponseFactory,
|
|
97
121
|
request
|
|
98
122
|
});
|
|
123
|
+
releaseAfterResponse = false;
|
|
124
|
+
void dispatch.completion.finally(release).catch(() => {});
|
|
125
|
+
return await dispatch.response;
|
|
99
126
|
} finally {
|
|
100
|
-
|
|
127
|
+
if (releaseAfterResponse) {
|
|
128
|
+
release();
|
|
129
|
+
}
|
|
101
130
|
}
|
|
102
131
|
}
|
|
103
132
|
async listen(dispatcher) {
|
|
104
|
-
this.dispatcher = dispatcher;
|
|
105
133
|
if (this.server) {
|
|
106
134
|
return;
|
|
107
135
|
}
|
|
136
|
+
const previousAbortController = this.abortController;
|
|
137
|
+
const previousDispatcher = this.dispatcher;
|
|
138
|
+
const previousListenAddress = this.listenAddress;
|
|
108
139
|
const abortController = new AbortController();
|
|
109
|
-
const serve = resolveServe(this.options.serve);
|
|
110
140
|
const listenReady = this.options.port === 0 ? createDeferred() : undefined;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
try {
|
|
142
|
+
const serve = resolveServe(this.options.serve);
|
|
143
|
+
this.dispatcher = dispatcher;
|
|
144
|
+
this.abortController = abortController;
|
|
145
|
+
this.server = serve({
|
|
146
|
+
cert: this.options.https?.cert,
|
|
147
|
+
hostname: this.options.hostname,
|
|
148
|
+
key: this.options.https?.key,
|
|
149
|
+
onListen: localAddr => {
|
|
150
|
+
this.listenAddress = localAddr;
|
|
151
|
+
listenReady?.resolve();
|
|
152
|
+
this.options.onListen?.(localAddr);
|
|
153
|
+
},
|
|
154
|
+
port: this.options.port,
|
|
155
|
+
signal: abortController.signal
|
|
156
|
+
}, async request => {
|
|
157
|
+
return await this.handle(request);
|
|
158
|
+
});
|
|
159
|
+
await listenReady?.promise;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
abortController.abort();
|
|
162
|
+
this.abortController = previousAbortController;
|
|
163
|
+
this.dispatcher = previousDispatcher;
|
|
164
|
+
this.listenAddress = previousListenAddress;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
127
167
|
}
|
|
128
168
|
async close() {
|
|
129
169
|
if (this.closeInFlight) {
|
|
@@ -185,12 +225,15 @@ export class DenoHttpApplicationAdapter {
|
|
|
185
225
|
* @returns A Deno-backed `HttpApplicationAdapter`.
|
|
186
226
|
*/
|
|
187
227
|
export function createDenoAdapter(options = {}) {
|
|
228
|
+
return new DenoHttpApplicationAdapter(options);
|
|
229
|
+
}
|
|
230
|
+
function normalizeDenoAdapterOptions(options) {
|
|
188
231
|
validateNonNegativeIntegerOption('maxBodySize', options.maxBodySize);
|
|
189
|
-
return
|
|
232
|
+
return {
|
|
190
233
|
...options,
|
|
191
234
|
hostname: options.hostname ?? options.host ?? DEFAULT_HOSTNAME,
|
|
192
235
|
port: resolveDenoPort(options.port)
|
|
193
|
-
}
|
|
236
|
+
};
|
|
194
237
|
}
|
|
195
238
|
|
|
196
239
|
/**
|
|
@@ -247,7 +290,11 @@ function createDenoShutdownSignalRegistration(signals) {
|
|
|
247
290
|
});
|
|
248
291
|
}
|
|
249
292
|
} catch (error) {
|
|
250
|
-
|
|
293
|
+
try {
|
|
294
|
+
removeDenoSignalBindings(denoGlobal, bindings);
|
|
295
|
+
} catch (cleanupError) {
|
|
296
|
+
throw new AggregateError([error, cleanupError], 'Failed to register Deno shutdown signals and roll back registered listeners.');
|
|
297
|
+
}
|
|
251
298
|
throw error;
|
|
252
299
|
}
|
|
253
300
|
return () => {
|
|
@@ -256,8 +303,19 @@ function createDenoShutdownSignalRegistration(signals) {
|
|
|
256
303
|
};
|
|
257
304
|
}
|
|
258
305
|
function removeDenoSignalBindings(denoGlobal, bindings) {
|
|
306
|
+
const errors = [];
|
|
259
307
|
for (const binding of bindings) {
|
|
260
|
-
|
|
308
|
+
try {
|
|
309
|
+
denoGlobal.removeSignalListener(binding.signal, binding.handler);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
errors.push(error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (errors.length === 1) {
|
|
315
|
+
throw errors[0];
|
|
316
|
+
}
|
|
317
|
+
if (errors.length > 1) {
|
|
318
|
+
throw new AggregateError(errors, 'Failed to remove Deno shutdown signal listeners.');
|
|
261
319
|
}
|
|
262
320
|
}
|
|
263
321
|
function createListenTarget(hostname, port, usesHttps) {
|
|
@@ -277,14 +335,6 @@ function resolveDenoPort(value) {
|
|
|
277
335
|
}
|
|
278
336
|
return port;
|
|
279
337
|
}
|
|
280
|
-
function validateNonNegativeIntegerOption(name, value) {
|
|
281
|
-
if (value === undefined) {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (!Number.isInteger(value) || value < 0) {
|
|
285
|
-
throw new Error(`Invalid ${name} value: ${String(value)}. Expected a non-negative integer.`);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
338
|
function formatHostForAuthority(hostname) {
|
|
289
339
|
return hostname.includes(':') && !hostname.startsWith('[') ? `[${hostname}]` : hostname;
|
|
290
340
|
}
|
|
@@ -313,15 +363,58 @@ function isWebSocketUpgradeRequest(request) {
|
|
|
313
363
|
}
|
|
314
364
|
function closeDenoServerWithDrain(server, abortController, waitForDrain) {
|
|
315
365
|
return (async () => {
|
|
366
|
+
let closeFailure;
|
|
367
|
+
const shutdown = Promise.resolve(server.shutdown());
|
|
368
|
+
const drain = waitForDrain();
|
|
369
|
+
const gracefulClose = Promise.allSettled([shutdown, drain]).then(results => ({
|
|
370
|
+
kind: 'graceful',
|
|
371
|
+
results
|
|
372
|
+
}));
|
|
373
|
+
const forcedClose = abortController ? waitForAbort(abortController.signal).then(() => ({
|
|
374
|
+
kind: 'forced'
|
|
375
|
+
})) : undefined;
|
|
376
|
+
const closeResult = forcedClose ? await Promise.race([gracefulClose, forcedClose]) : await gracefulClose;
|
|
377
|
+
if (closeResult.kind === 'graceful') {
|
|
378
|
+
const rejected = closeResult.results.find(result => result.status === 'rejected');
|
|
379
|
+
if (rejected?.status === 'rejected') {
|
|
380
|
+
abortController?.abort();
|
|
381
|
+
closeFailure = {
|
|
382
|
+
error: rejected.reason
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
try {
|
|
387
|
+
await drain;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
closeFailure = {
|
|
390
|
+
error
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
316
394
|
try {
|
|
317
|
-
await server.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
395
|
+
await server.finished;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (!closeFailure) {
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (closeFailure) {
|
|
402
|
+
throw closeFailure.error;
|
|
321
403
|
}
|
|
322
|
-
await server.finished;
|
|
323
404
|
})();
|
|
324
405
|
}
|
|
406
|
+
function waitForAbort(signal) {
|
|
407
|
+
if (signal.aborted) {
|
|
408
|
+
return Promise.resolve();
|
|
409
|
+
}
|
|
410
|
+
return new Promise(resolve => {
|
|
411
|
+
signal.addEventListener('abort', () => {
|
|
412
|
+
resolve();
|
|
413
|
+
}, {
|
|
414
|
+
once: true
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
}
|
|
325
418
|
function createDeferred() {
|
|
326
419
|
let resolve;
|
|
327
420
|
let reject;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Dispatcher } from '@fluojs/http/internal';
|
|
2
|
+
import type { MultipartOptions } from '@fluojs/runtime';
|
|
3
|
+
import type { DenoServeHandler } from './adapter.js';
|
|
4
|
+
/** Options for creating a Deno request handler for a host-owned `Deno.serve(...)` lifecycle. */
|
|
5
|
+
export interface CreateDenoFetchHandlerOptions {
|
|
6
|
+
/** Already bootstrapped dispatcher that receives translated fluo framework requests. */
|
|
7
|
+
readonly dispatcher: Dispatcher;
|
|
8
|
+
/** Maximum request body size enforced by the shared Web request parser. */
|
|
9
|
+
readonly maxBodySize?: number;
|
|
10
|
+
/** Multipart parsing limits enforced by the shared Web request parser. */
|
|
11
|
+
readonly multipart?: MultipartOptions;
|
|
12
|
+
/** Preserves byte-exact raw bodies for JSON and text requests when enabled. */
|
|
13
|
+
readonly rawBody?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a Deno `Request` handler without starting or owning `Deno.serve(...)`.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* The surrounding host owns server startup, shutdown, signals, and websocket upgrades. The
|
|
20
|
+
* dispatcher must come from an already bootstrapped fluo application, such as `app.dispatcher`.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Bootstrapped dispatcher and shared Web request parsing options.
|
|
23
|
+
* @returns A handler suitable for a host-owned `Deno.serve(handler)` call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createDenoFetchHandler({ dispatcher, maxBodySize, multipart, rawBody, }: CreateDenoFetchHandlerOptions): DenoServeHandler;
|
|
26
|
+
//# sourceMappingURL=fetch-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-handler.d.ts","sourceRoot":"","sources":["../src/fetch-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,gGAAgG;AAChG,MAAM,WAAW,6BAA6B;IAC5C,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,UAAU,EACV,WAAW,EACX,SAAS,EACT,OAAO,GACR,EAAE,6BAA6B,GAAG,gBAAgB,CAiBlD"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createWebRequestResponseFactory, dispatchWebRequest } from '@fluojs/runtime/web';
|
|
2
|
+
import { validateNonNegativeIntegerOption } from './options.js';
|
|
3
|
+
|
|
4
|
+
/** Options for creating a Deno request handler for a host-owned `Deno.serve(...)` lifecycle. */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a Deno `Request` handler without starting or owning `Deno.serve(...)`.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The surrounding host owns server startup, shutdown, signals, and websocket upgrades. The
|
|
11
|
+
* dispatcher must come from an already bootstrapped fluo application, such as `app.dispatcher`.
|
|
12
|
+
*
|
|
13
|
+
* @param options - Bootstrapped dispatcher and shared Web request parsing options.
|
|
14
|
+
* @returns A handler suitable for a host-owned `Deno.serve(handler)` call.
|
|
15
|
+
*/
|
|
16
|
+
export function createDenoFetchHandler({
|
|
17
|
+
dispatcher,
|
|
18
|
+
maxBodySize,
|
|
19
|
+
multipart,
|
|
20
|
+
rawBody
|
|
21
|
+
}) {
|
|
22
|
+
validateNonNegativeIntegerOption('maxBodySize', maxBodySize);
|
|
23
|
+
const factory = createWebRequestResponseFactory({
|
|
24
|
+
maxBodySize,
|
|
25
|
+
multipart,
|
|
26
|
+
rawBody
|
|
27
|
+
});
|
|
28
|
+
return async function denoFetchHandler(request) {
|
|
29
|
+
return await dispatchWebRequest({
|
|
30
|
+
dispatcher,
|
|
31
|
+
dispatcherNotReadyMessage: 'Deno fetch handler received a request before dispatcher binding completed.',
|
|
32
|
+
factory,
|
|
33
|
+
request
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from './adapter.js';
|
|
1
|
+
export * from './adapter.js';
|
|
2
|
+
export * from './fetch-handler.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate an optional non-negative integer adapter option.
|
|
3
|
+
*
|
|
4
|
+
* @param name - Option name included in validation errors.
|
|
5
|
+
* @param value - Optional numeric value to validate.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateNonNegativeIntegerOption(name: string, value: number | undefined): void;
|
|
8
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAQ9F"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate an optional non-negative integer adapter option.
|
|
3
|
+
*
|
|
4
|
+
* @param name - Option name included in validation errors.
|
|
5
|
+
* @param value - Optional numeric value to validate.
|
|
6
|
+
*/
|
|
7
|
+
export function validateNonNegativeIntegerOption(name, value) {
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
12
|
+
throw new Error(`Invalid ${name} value: ${String(value)}. Expected a non-negative integer.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"platform",
|
|
9
9
|
"server"
|
|
10
10
|
],
|
|
11
|
-
"version": "
|
|
11
|
+
"version": "2.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",
|