@fluojs/http 1.0.0-beta.9 → 1.1.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 -11
- package/README.md +78 -11
- package/dist/context/request-context-node-store.d.ts +25 -0
- package/dist/context/request-context-node-store.d.ts.map +1 -0
- package/dist/context/request-context-node-store.js +32 -0
- package/dist/context/request-context-stack-store.d.ts +8 -0
- package/dist/context/request-context-stack-store.d.ts.map +1 -0
- package/dist/context/request-context-stack-store.js +29 -0
- package/dist/context/request-context-store.d.ts +7 -0
- package/dist/context/request-context-store.d.ts.map +1 -0
- package/dist/context/request-context-store.js +1 -0
- package/dist/context/request-context.d.ts +7 -3
- package/dist/context/request-context.d.ts.map +1 -1
- package/dist/context/request-context.js +17 -5
- package/dist/context/sse.d.ts +20 -0
- package/dist/context/sse.d.ts.map +1 -1
- package/dist/context/sse.js +19 -0
- package/dist/decorators.d.ts +10 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +279 -53
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +142 -2
- package/dist/dispatch/fast-path/eligibility-checker.d.ts +22 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -1
- package/dist/dispatch/fast-path/eligibility-checker.js +32 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/middleware/correlation.d.ts.map +1 -1
- package/dist/middleware/correlation.js +8 -2
- package/package.json +4 -4
package/README.ko.md
CHANGED
|
@@ -96,6 +96,8 @@ function someDeepHelper() {
|
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
`runWithRequestContext(...)`는 호스트가 `globalThis.AsyncLocalStorage` 또는 Node 내장 `node:async_hooks` 모듈로 `AsyncLocalStorage`를 제공할 때 활성 컨텍스트를 `await` 이후까지 보존합니다. 선언된 `>=20.0.0` 지원 범위의 Node 런타임은 `process.getBuiltinModule(...)`이 없어도 `node:async_hooks`를 동적으로 해석해 ALS 의미론을 유지합니다. 비동기 컨텍스트 primitive가 없는 비 Node 호스트에서는 동기 stack fallback을 사용하며, 겹치는 비동기 요청이 서로의 컨텍스트를 관찰하지 않도록 awaited continuation이 재개되기 전에 컨텍스트를 비웁니다.
|
|
100
|
+
|
|
99
101
|
### 프록시 뒤의 속도 제한
|
|
100
102
|
|
|
101
103
|
`createRateLimitMiddleware(...)`는 기본적으로 raw socket `remoteAddress`만으로 클라이언트 식별자를 해석합니다. `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 신뢰하려면 해당 헤더를 신뢰 가능한 프록시가 덮어쓰는 환경에서만 `trustProxyHeaders: true`를 명시적으로 켜세요. 어댑터가 신뢰 가능한 프록시 체인도 raw socket 식별자도 제공하지 않는다면 공유 fallback 버킷에 의존하지 말고 명시적인 `keyResolver`를 설정하세요.
|
|
@@ -103,30 +105,93 @@ function someDeepHelper() {
|
|
|
103
105
|
### 서버 전송 이벤트
|
|
104
106
|
|
|
105
107
|
```ts
|
|
106
|
-
import {
|
|
108
|
+
import { Controller, Sse, type SseMessage } from '@fluojs/http';
|
|
109
|
+
|
|
110
|
+
@Controller('/orders')
|
|
111
|
+
export class OrdersEventsController {
|
|
112
|
+
@Sse('/events')
|
|
113
|
+
async *stream(): AsyncIterable<SseMessage<{ status: string }> | { heartbeat: true }> {
|
|
114
|
+
yield { data: { status: 'connected' }, event: 'ready', id: 'orders-ready' };
|
|
115
|
+
|
|
116
|
+
while (true) {
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 15_000));
|
|
118
|
+
yield { heartbeat: true };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`@Sse(path)`는 `GET` 라우트를 등록하고 `text/event-stream` produced media type metadata를 선언합니다. Handler는 수동 stream 제어가 필요하면 `SseResponse`를 반환할 수 있고, managed streaming이 필요하면 `AsyncIterable<SseMessage<T> | T>`를 반환할 수 있습니다. Managed async iterable은 `SseResponse`와 같은 `encodeSseMessage(...)` 동작으로 변환됩니다. 일반 yield 값은 `data:` frame이 되고, `data` 필드가 있는 객체는 `event`, `id`, `retry`도 함께 제공할 수 있습니다. Dispatcher는 `RequestContext.request.signal`이 abort되거나 response stream이 닫히면 source 소비를 중단하고, write가 backpressure를 보고하면 `FrameworkResponseStream.waitForDrain()`을 기다리며, 완료 또는 source error 시 stream을 닫고, source에서 던진 오류는 이미 commit된 SSE response를 닫은 뒤 일반 dispatcher error/observer seam으로 전달합니다. Observable 값은 계속 범위 밖이며 RxJS dependency는 필요하지 않습니다.
|
|
125
|
+
|
|
126
|
+
브라우저 쪽에서는 해당 연결을 소유하는 React effect 안에서 `EventSource`를 만들고 cleanup 함수에서 항상 닫아야 합니다. 그래야 route 변경, Strict Mode remount, component unmount가 중복 stream을 남기지 않습니다.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { useEffect, useState } from 'react';
|
|
130
|
+
|
|
131
|
+
export function OrderEvents({ orderId }: { orderId: string }) {
|
|
132
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const source = new EventSource(`/orders/events?orderId=${encodeURIComponent(orderId)}`, {
|
|
136
|
+
withCredentials: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
source.addEventListener('ready', (event) => {
|
|
140
|
+
setEvents((current) => [...current, event.data]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
source.onerror = () => {
|
|
144
|
+
// 서버가 terminal status로 닫지 않는 한 브라우저가 자동으로 재연결합니다.
|
|
145
|
+
console.warn('Order event stream disconnected; waiting for browser retry.');
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
source.close();
|
|
150
|
+
};
|
|
151
|
+
}, [orderId]);
|
|
107
152
|
|
|
108
|
-
|
|
109
|
-
stream(_input: undefined, ctx: RequestContext) {
|
|
110
|
-
const sse = new SseResponse(ctx);
|
|
111
|
-
sse.send({ message: 'hello' });
|
|
112
|
-
return sse;
|
|
153
|
+
return <output>{events.join('\n')}</output>;
|
|
113
154
|
}
|
|
114
155
|
```
|
|
115
156
|
|
|
157
|
+
브라우저 `EventSource`는 호출자가 임의의 `Authorization` 헤더를 붙일 수 없습니다. SSE 엔드포인트는 same-origin cookie, `withCredentials`와 명시적인 CORS credentials 정책, 또는 guard가 검증하는 짧은 수명의 signed URL/query token으로 인증하세요. 내장 `EventSource` API가 아니라 fetch 기반 custom SSE client를 쓰는 경우가 아니라면 bearer header 브라우저 예제를 문서화하지 마세요.
|
|
158
|
+
|
|
159
|
+
운영 환경에서는 SSE 연결을 buffering 없이 오래 유지해야 합니다. 신뢰한 origin에 대해서만 CORS credentials를 허용하고, proxy buffering과 response transform을 비활성화하며(`SseResponse`는 `Cache-Control: no-cache, no-transform` 및 `X-Accel-Buffering: no`를 설정합니다), `text/event-stream`을 buffering하는 compression middleware를 피하고, load balancer 또는 platform idle timeout을 heartbeat interval보다 길게 두고, `sse.comment('heartbeat')` 같은 comment heartbeat를 보내며, 클라이언트가 재연결 후 replay가 필요할 때 `Last-Event-ID`를 처리할 수 있도록 충분한 event history를 보존하세요.
|
|
160
|
+
|
|
161
|
+
### Versioning
|
|
162
|
+
|
|
163
|
+
`createHandlerMapping(...)`은 `VersioningType`과 `versioning` option을 통해 URI, header, media-type, custom versioning strategy를 지원합니다. Route registration은 exact/static match를 fallback보다 앞에 두고, 동등하게 정규화된 route는 registration order를 보존합니다.
|
|
164
|
+
|
|
165
|
+
### Request context helper
|
|
166
|
+
|
|
167
|
+
Framework integration이 명시적인 request context boundary나 typed per-request storage가 필요할 때 `runWithRequestContext(...)`, `assertRequestContext()`, `createRequestContext(...)`, `createContextKey(...)`, `getContextValue(...)`, `setContextValue(...)`를 사용합니다.
|
|
168
|
+
|
|
169
|
+
### Fast-path observability
|
|
170
|
+
|
|
171
|
+
Dispatcher는 adapter와 diagnostics를 위해 `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, `getDispatcherFastPathStats(...)`로 fast-path observability를 노출합니다.
|
|
172
|
+
|
|
173
|
+
### Bun decorator bundling compatibility
|
|
174
|
+
|
|
175
|
+
Fluo의 HTTP 데코레이터는 TC39 표준 데코레이터이며, runtime 또는 compiler가 표준 decorator context를 제공하면 계속 `context.metadata`를 통해 metadata를 기록합니다. Bun이 legacy TypeScript decorator transform으로 애플리케이션을 번들링하는 경우에도 controller, route, DTO binding, guard/interceptor, header, redirect, versioning, status, request DTO, `@Produces(...)` metadata를 Fluo 내부 metadata store에 기록하여 생성된 Bun bundle의 route mapping 동작을 보존합니다.
|
|
176
|
+
|
|
177
|
+
이 호환 경로는 Bun bundle output을 위한 실행 fallback입니다. 애플리케이션 소스는 계속 Fluo 표준 데코레이터를 사용해야 하며, `emitDecoratorMetadata`를 켜거나 `reflect-metadata`에 의존해서는 안 됩니다.
|
|
178
|
+
|
|
116
179
|
## 요청 정리와 런타임 이식성
|
|
117
180
|
|
|
118
|
-
디스패처는 활성 dispatch 동안에만
|
|
181
|
+
디스패처는 활성 dispatch 동안에만 호스트 비동기 컨텍스트 저장소로 `RequestContext`를 바인딩합니다. 지원되는 Node 20+ 런타임을 포함해 `AsyncLocalStorage`가 있는 호스트에서는 컨텍스트가 awaited work 이후까지 유지됩니다. 비동기 컨텍스트 primitive가 없는 비 Node 호스트에서는 fallback 컨텍스트가 동기 프레임에만 유효하고, 겹치는 요청이 서로의 컨텍스트를 관찰하지 않도록 `await` 이후에는 의도적으로 사용할 수 없습니다. 요청이 controller graph, middleware, guard, interceptor, observer, DTO converter, custom binder 또는 수동 `getCurrentRequestContext()` / `assertRequestContext()` container 접근을 통해 request-scoped DI를 사용할 수 있으면, 디스패처는 요청 observer가 끝난 뒤 `finally` 경로에서 isolated request-scoped DI 컨테이너를 생성하고 dispose합니다. Singleton-only route는 `RequestContext.container`가 접근되기 전까지 이 컨테이너 lifecycle을 건너뛰어 baseline 경로의 불필요한 per-request allocation을 피하면서도, graph가 모호하거나 request-scoped이면 request-scoped provider isolation을 유지합니다. 따라서 공개 `RequestContext.container` 읽기는 request-scoped provider resolve에 항상 안전합니다. singleton-only fast path는 내부 dispatcher 최적화일 뿐, 공개 context가 root container를 노출한다는 약속이 아닙니다.
|
|
119
182
|
|
|
120
183
|
어댑터는 플랫폼이 제공한다면 `FrameworkRequest.signal`에 `AbortSignal`을 전달해야 합니다. SSE에서는 가능하면 `FrameworkResponse.stream.onClose(...)`도 노출해야 합니다. `SseResponse`는 request abort와 raw stream close를 모두 구독하고, 멱등하게 닫히며, 어느 쪽이 먼저 종료되더라도 등록한 listener를 제거합니다.
|
|
121
184
|
|
|
122
185
|
## 공개 API
|
|
123
186
|
|
|
124
|
-
- **라우팅 데코레이터**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
187
|
+
- **라우팅 데코레이터**: `Controller`, `Get`, `Sse`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
125
188
|
- **바인딩 데코레이터**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`, `Optional`, `Convert`
|
|
126
189
|
- **실행 데코레이터**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`, `Produces`
|
|
127
|
-
- **핵심 런타임 타입**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
|
|
128
|
-
-
|
|
129
|
-
-
|
|
190
|
+
- **핵심 런타임 타입**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`, `SseMessage`, `Middleware`, `MiddlewareContext`, `MiddlewareRouteConfig`, `Next`, `Guard`, `GuardContext`, `Interceptor`, `InterceptorContext`, `CallHandler`, `RequestObserver`, `DispatcherLogger`
|
|
191
|
+
- **Adapter API**: `HttpApplicationAdapter`, `createNoopHttpApplicationAdapter`, `createServerBackedHttpAdapterRealtimeCapability`, `createUnsupportedHttpAdapterRealtimeCapability`, `createFetchStyleHttpAdapterRealtimeCapability`
|
|
192
|
+
- **예외와 오류**: `HttpException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `ConflictException`, `NotAcceptableException`, `TooManyRequestsException`, `InternalServerErrorException`, `PayloadTooLargeException`, `createErrorResponse`, `RouteConflictError`, `InvalidRoutePathError`, `HandlerNotFoundError`, `RequestAbortedError`
|
|
193
|
+
- **헬퍼**: `createHandlerMapping`, `createDispatcher`, `forRoutes`, `normalizeRoutePattern`, `matchRoutePattern`, `isMiddlewareRouteConfig`, `createCorrelationMiddleware`, `createCorsMiddleware`, `createRateLimitMiddleware`, `createMemoryRateLimitStore`, `createSecurityHeadersMiddleware`, `runWithRequestContext`, `getCurrentRequestContext`, `assertRequestContext`, `createRequestContext`, `createContextKey`, `getContextValue`, `setContextValue`, `encodeSseComment`, `encodeSseMessage`, `isSseMessage`
|
|
194
|
+
- **Option 및 store type**: `CorsOptions`, `RateLimitOptions`, `RateLimitStore`, `RateLimitStoreEntry`, `SecurityHeadersOptions`, `SseSendOptions`
|
|
130
195
|
|
|
131
196
|
## 내부 서브경로 (`@fluojs/http/internal`)
|
|
132
197
|
|
|
@@ -134,9 +199,11 @@ stream(_input: undefined, ctx: RequestContext) {
|
|
|
134
199
|
|
|
135
200
|
- `DefaultBinder`: 런타임 부트스트랩 경로에서 사용하는 기본 DTO/요청 바인더.
|
|
136
201
|
- `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: public dispatcher API를 넓히지 않고 의미 보존이 가능한 native route match를 재사용하기 위한 내부 adapter/runtime 헬퍼.
|
|
202
|
+
- `consumeRawRequestNativeRouteHandoff(...)` / `readFrameworkRequestNativeRouteHandoff(...)`: native route handoff를 읽거나 소비하기 위한 내부 helper.
|
|
137
203
|
- Native route handoff는 framework request에 붙는 시점의 method와 path를 함께 스냅샷합니다. app middleware가 handler matching 전에 둘 중 하나를 rewrite하면 dispatcher는 stale handoff를 무시하고 일반 route matching으로 fallback합니다.
|
|
138
204
|
- `isRoutePathNormalizationSensitive(path)`: duplicate slash와 trailing slash 요청을 generic dispatcher 경로에 남기기 위한 내부 guard.
|
|
139
205
|
- `resolveClientIdentity(request)`: 속도 제한과 런타임 통합에서 사용하는 보수적 클라이언트 식별 해석기.
|
|
206
|
+
- `createFetchStyleHttpAdapterRealtimeCapability(...)`, `Dispatcher`, `HttpApplicationAdapter`: 전체 HTTP root barrel을 instantiate하면 안 되는 edge/fetch-style platform package를 위한 내부 adapter seam.
|
|
140
207
|
|
|
141
208
|
## 관련 패키지
|
|
142
209
|
|
package/README.md
CHANGED
|
@@ -98,6 +98,8 @@ function someDeepHelper() {
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
`runWithRequestContext(...)` preserves the active context across awaited work when the host provides `AsyncLocalStorage` through `globalThis.AsyncLocalStorage` or Node's built-in `node:async_hooks` module. Node runtimes in the declared `>=20.0.0` support range keep ALS semantics even when `process.getBuiltinModule(...)` is unavailable by resolving `node:async_hooks` dynamically. Non-Node hosts without an async-context primitive use a synchronous stack fallback that clears the context before awaited continuations resume, avoiding cross-request leaks instead of pretending to isolate overlapping async work.
|
|
102
|
+
|
|
101
103
|
### Rate limiting behind proxies
|
|
102
104
|
|
|
103
105
|
`createRateLimitMiddleware(...)` resolves client identity from the raw socket `remoteAddress` by default. To trust `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`, opt in with `trustProxyHeaders: true` only when your adapter sits behind a trusted proxy that overwrites those headers. If your adapter exposes neither a trusted proxy chain nor a raw socket identity, provide an explicit `keyResolver`.
|
|
@@ -105,30 +107,93 @@ function someDeepHelper() {
|
|
|
105
107
|
### Server-sent events
|
|
106
108
|
|
|
107
109
|
```ts
|
|
108
|
-
import {
|
|
110
|
+
import { Controller, Sse, type SseMessage } from '@fluojs/http';
|
|
111
|
+
|
|
112
|
+
@Controller('/orders')
|
|
113
|
+
export class OrdersEventsController {
|
|
114
|
+
@Sse('/events')
|
|
115
|
+
async *stream(): AsyncIterable<SseMessage<{ status: string }> | { heartbeat: true }> {
|
|
116
|
+
yield { data: { status: 'connected' }, event: 'ready', id: 'orders-ready' };
|
|
117
|
+
|
|
118
|
+
while (true) {
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 15_000));
|
|
120
|
+
yield { heartbeat: true };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`@Sse(path)` registers a `GET` route and declares `text/event-stream` produced media type metadata. Handlers may either return `SseResponse` for manual stream control or return `AsyncIterable<SseMessage<T> | T>` for managed streaming. Managed async iterables are converted with the same `encodeSseMessage(...)` behavior as `SseResponse`: plain yielded values become `data:` frames, while yielded objects with a `data` field may also provide `event`, `id`, and `retry`. The dispatcher stops consuming the source when `RequestContext.request.signal` aborts or the response stream closes, calls `FrameworkResponseStream.waitForDrain()` when a write reports backpressure, closes the stream on completion or source errors, and routes thrown source errors through the normal dispatcher error/observer seam after the already-committed SSE response is closed. Observable values remain out of scope and no RxJS dependency is required.
|
|
127
|
+
|
|
128
|
+
On the browser side, create the `EventSource` inside the React effect that owns it and always close it from the cleanup function so route changes, Strict Mode remounts, and component unmounts do not leave duplicate streams open:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { useEffect, useState } from 'react';
|
|
132
|
+
|
|
133
|
+
export function OrderEvents({ orderId }: { orderId: string }) {
|
|
134
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const source = new EventSource(`/orders/events?orderId=${encodeURIComponent(orderId)}`, {
|
|
138
|
+
withCredentials: true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
source.addEventListener('ready', (event) => {
|
|
142
|
+
setEvents((current) => [...current, event.data]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
source.onerror = () => {
|
|
146
|
+
// Browsers reconnect automatically unless the server closes with a terminal status.
|
|
147
|
+
console.warn('Order event stream disconnected; waiting for browser retry.');
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
source.close();
|
|
152
|
+
};
|
|
153
|
+
}, [orderId]);
|
|
109
154
|
|
|
110
|
-
|
|
111
|
-
stream(_input: undefined, ctx: RequestContext) {
|
|
112
|
-
const sse = new SseResponse(ctx);
|
|
113
|
-
sse.send({ message: 'hello' });
|
|
114
|
-
return sse;
|
|
155
|
+
return <output>{events.join('\n')}</output>;
|
|
115
156
|
}
|
|
116
157
|
```
|
|
117
158
|
|
|
159
|
+
Browser `EventSource` does not let callers attach arbitrary `Authorization` headers. Authenticate SSE endpoints with same-origin cookies, `withCredentials` plus explicit CORS credentials policy, or a short-lived signed URL/query token that your guard validates. Do not document a bearer-header browser example unless you are using a custom fetch-based SSE client instead of the built-in `EventSource` API.
|
|
160
|
+
|
|
161
|
+
Operationally, keep SSE connections unbuffered and long-lived: allow credentials in CORS only for trusted origins, disable proxy buffering and response transforms (`SseResponse` sets `Cache-Control: no-cache, no-transform` and `X-Accel-Buffering: no`), avoid compression middleware that buffers `text/event-stream`, set load balancer or platform idle timeouts above your heartbeat interval, send comment heartbeats such as `sse.comment('heartbeat')`, and persist enough event history to honor `Last-Event-ID` when clients reconnect and need replay.
|
|
162
|
+
|
|
163
|
+
### Versioning
|
|
164
|
+
|
|
165
|
+
`createHandlerMapping(...)` supports URI, header, media-type, and custom versioning strategies through `VersioningType` and the `versioning` option. Route registration keeps exact/static matches ahead of fallbacks while preserving registration order for equivalent normalized routes.
|
|
166
|
+
|
|
167
|
+
### Request context helpers
|
|
168
|
+
|
|
169
|
+
Use `runWithRequestContext(...)`, `assertRequestContext()`, `createRequestContext(...)`, `createContextKey(...)`, `getContextValue(...)`, and `setContextValue(...)` when framework integrations need explicit request context boundaries or typed per-request storage.
|
|
170
|
+
|
|
171
|
+
### Fast-path observability
|
|
172
|
+
|
|
173
|
+
The dispatcher exposes fast-path observability for adapters and diagnostics through `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, and `getDispatcherFastPathStats(...)`.
|
|
174
|
+
|
|
175
|
+
### Bun decorator bundling compatibility
|
|
176
|
+
|
|
177
|
+
Fluo's HTTP decorators are standard TC39 decorators and continue to record metadata through `context.metadata` when the runtime or compiler provides the standard decorator context. When Bun bundles an application through its legacy TypeScript decorator transform, the same controller, route, DTO binding, guard/interceptor, header, redirect, versioning, status, request DTO, and `@Produces(...)` metadata is recorded through Fluo's internal metadata stores so generated Bun bundles preserve route mapping behavior.
|
|
178
|
+
|
|
179
|
+
This compatibility path is an execution fallback for Bun bundle output; application source should still use Fluo's standard decorators and should not enable `emitDecoratorMetadata` or rely on `reflect-metadata`.
|
|
180
|
+
|
|
118
181
|
## Request Cleanup and Portability
|
|
119
182
|
|
|
120
|
-
The dispatcher binds `RequestContext` with
|
|
183
|
+
The dispatcher binds `RequestContext` with host async-context storage for the active dispatch only. On hosts with `AsyncLocalStorage`, including supported Node 20+ runtimes, the context remains available across awaited work. On non-Node hosts without an async-context primitive, the fallback context is synchronous-only and intentionally unavailable after `await` so overlapping requests cannot observe one another's context. When a request may use request-scoped DI through its controller graph, middleware, guards, interceptors, observers, DTO converters, a custom binder, or manual `getCurrentRequestContext()` / `assertRequestContext()` container access, the dispatcher creates and disposes an isolated request-scoped DI container from its `finally` path after request observers finish. Singleton-only routes skip that container lifecycle until `RequestContext.container` is accessed, so the baseline path avoids unnecessary per-request allocation while preserving request-scoped provider isolation whenever the graph is ambiguous or request-scoped. Public `RequestContext.container` reads are therefore always safe for resolving request-scoped providers; the singleton-only fast path is an internal dispatcher optimization, not a promise that the public context exposes the root container.
|
|
121
184
|
|
|
122
185
|
Adapters should pass an `AbortSignal` on `FrameworkRequest.signal` when the platform exposes one. For SSE, adapters should also expose `FrameworkResponse.stream.onClose(...)` when possible; `SseResponse` listens to both request abort and raw stream close, closes idempotently, and removes registered listeners when either side terminates first.
|
|
123
186
|
|
|
124
187
|
## Public API
|
|
125
188
|
|
|
126
|
-
- **Routing decorators**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
189
|
+
- **Routing decorators**: `Controller`, `Get`, `Sse`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
127
190
|
- **Binding decorators**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`, `Optional`, `Convert`
|
|
128
191
|
- **Execution decorators**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`, `Produces`
|
|
129
|
-
- **Core runtime types**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
192
|
+
- **Core runtime types**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`, `SseMessage`, `Middleware`, `MiddlewareContext`, `MiddlewareRouteConfig`, `Next`, `Guard`, `GuardContext`, `Interceptor`, `InterceptorContext`, `CallHandler`, `RequestObserver`, `DispatcherLogger`
|
|
193
|
+
- **Adapter API**: `HttpApplicationAdapter`, `createNoopHttpApplicationAdapter`, `createServerBackedHttpAdapterRealtimeCapability`, `createUnsupportedHttpAdapterRealtimeCapability`, `createFetchStyleHttpAdapterRealtimeCapability`
|
|
194
|
+
- **Exceptions and errors**: `HttpException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `ConflictException`, `NotAcceptableException`, `TooManyRequestsException`, `InternalServerErrorException`, `PayloadTooLargeException`, `createErrorResponse`, `RouteConflictError`, `InvalidRoutePathError`, `HandlerNotFoundError`, `RequestAbortedError`
|
|
195
|
+
- **Helpers**: `createHandlerMapping`, `createDispatcher`, `forRoutes`, `normalizeRoutePattern`, `matchRoutePattern`, `isMiddlewareRouteConfig`, `createCorrelationMiddleware`, `createCorsMiddleware`, `createRateLimitMiddleware`, `createMemoryRateLimitStore`, `createSecurityHeadersMiddleware`, `runWithRequestContext`, `getCurrentRequestContext`, `assertRequestContext`, `createRequestContext`, `createContextKey`, `getContextValue`, `setContextValue`, `encodeSseComment`, `encodeSseMessage`, `isSseMessage`
|
|
196
|
+
- **Option and store types**: `CorsOptions`, `RateLimitOptions`, `RateLimitStore`, `RateLimitStoreEntry`, `SecurityHeadersOptions`, `SseSendOptions`
|
|
132
197
|
|
|
133
198
|
## Internal Subpath (`@fluojs/http/internal`)
|
|
134
199
|
|
|
@@ -136,9 +201,11 @@ The `./internal` subpath exports only the low-level utilities used by platform a
|
|
|
136
201
|
|
|
137
202
|
- `DefaultBinder`: Default DTO/request binder used by the runtime bootstrap path.
|
|
138
203
|
- `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: Internal adapter/runtime helpers for reusing semantically safe native route matches without widening the public dispatcher API.
|
|
204
|
+
- `consumeRawRequestNativeRouteHandoff(...)` / `readFrameworkRequestNativeRouteHandoff(...)`: Internal helpers for reading or consuming native route handoffs.
|
|
139
205
|
- Native route handoffs snapshot the framework request method and path when attached; if app middleware rewrites either value before handler matching, the dispatcher ignores the stale handoff and falls back to normal route matching.
|
|
140
206
|
- `isRoutePathNormalizationSensitive(path)`: Internal guard for keeping duplicate-slash and trailing-slash requests on the generic dispatcher path.
|
|
141
207
|
- `resolveClientIdentity(request)`: Conservative client identity resolver used by rate limiting and other runtime integrations.
|
|
208
|
+
- `createFetchStyleHttpAdapterRealtimeCapability(...)`, `Dispatcher`, and `HttpApplicationAdapter`: internal adapter seams for edge/fetch-style platform packages that must avoid instantiating the full HTTP root barrel.
|
|
142
209
|
|
|
143
210
|
## Related Packages
|
|
144
211
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestContextStore } from './request-context-store.js';
|
|
2
|
+
type AsyncLocalStorageConstructor = new () => RequestContextStore;
|
|
3
|
+
type NodeAsyncHooksModule = {
|
|
4
|
+
AsyncLocalStorage?: AsyncLocalStorageConstructor;
|
|
5
|
+
};
|
|
6
|
+
type AsyncLocalStorageResolutionHost = {
|
|
7
|
+
AsyncLocalStorage?: AsyncLocalStorageConstructor;
|
|
8
|
+
process?: {
|
|
9
|
+
getBuiltinModule?(id: 'node:async_hooks'): NodeAsyncHooksModule;
|
|
10
|
+
versions?: {
|
|
11
|
+
node?: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
type NodeAsyncHooksLoader = () => Promise<NodeAsyncHooksModule>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the host `AsyncLocalStorage` constructor without eagerly importing Node built-ins.
|
|
18
|
+
*
|
|
19
|
+
* @param host Host global-like object to inspect for async-context support.
|
|
20
|
+
* @param loadNodeAsyncHooks Lazy loader for Node's `node:async_hooks` module.
|
|
21
|
+
* @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveAsyncLocalStorageConstructor(host?: AsyncLocalStorageResolutionHost, loadNodeAsyncHooks?: NodeAsyncHooksLoader): Promise<AsyncLocalStorageConstructor | undefined>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=request-context-node-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context-node-store.d.ts","sourceRoot":"","sources":["../../src/context/request-context-node-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,KAAK,4BAA4B,GAAG,UAAU,mBAAmB,CAAC;AAElE,KAAK,oBAAoB,GAAG;IAC1B,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD,CAAC;AAEF,KAAK,+BAA+B,GAAG;IACrC,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;IACjD,OAAO,CAAC,EAAE;QACR,gBAAgB,CAAC,CAAC,EAAE,EAAE,kBAAkB,GAAG,oBAAoB,CAAC;QAChE,QAAQ,CAAC,EAAE;YACT,IAAI,CAAC,EAAE,MAAM,CAAC;SACf,CAAC;KACH,CAAC;CACH,CAAC;AAEF,KAAK,oBAAoB,GAAG,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC;AAEhE;;;;;;GAMG;AACH,wBAAsB,mCAAmC,CACvD,IAAI,GAAE,+BAA4C,EAClD,kBAAkB,GAAE,oBAA2C,GAC9D,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAsBnD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the host `AsyncLocalStorage` constructor without eagerly importing Node built-ins.
|
|
3
|
+
*
|
|
4
|
+
* @param host Host global-like object to inspect for async-context support.
|
|
5
|
+
* @param loadNodeAsyncHooks Lazy loader for Node's `node:async_hooks` module.
|
|
6
|
+
* @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable.
|
|
7
|
+
*/
|
|
8
|
+
export async function resolveAsyncLocalStorageConstructor(host = globalThis, loadNodeAsyncHooks = importNodeAsyncHooks) {
|
|
9
|
+
if (typeof host.AsyncLocalStorage === 'function') {
|
|
10
|
+
return host.AsyncLocalStorage;
|
|
11
|
+
}
|
|
12
|
+
const builtinAsyncLocalStorage = host.process?.getBuiltinModule?.('node:async_hooks').AsyncLocalStorage;
|
|
13
|
+
if (typeof builtinAsyncLocalStorage === 'function') {
|
|
14
|
+
return builtinAsyncLocalStorage;
|
|
15
|
+
}
|
|
16
|
+
if (!isNodeHost(host)) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const nodeAsyncHooks = await loadNodeAsyncHooks();
|
|
21
|
+
return nodeAsyncHooks.AsyncLocalStorage;
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isNodeHost(host) {
|
|
27
|
+
return typeof host.process?.versions?.node === 'string';
|
|
28
|
+
}
|
|
29
|
+
function importNodeAsyncHooks() {
|
|
30
|
+
const nodeAsyncHooksSpecifier = `node:${'async_hooks'}`;
|
|
31
|
+
return import(nodeAsyncHooksSpecifier);
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RequestContextStore } from './request-context-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates the synchronous fallback request-context store used when no async-context primitive exists.
|
|
4
|
+
*
|
|
5
|
+
* @returns A stack-backed request-context store scoped to synchronous callback frames.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createStackRequestContextStore(): RequestContextStore;
|
|
8
|
+
//# sourceMappingURL=request-context-stack-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context-stack-store.d.ts","sourceRoot":"","sources":["../../src/context/request-context-stack-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,mBAAmB,CAmBpE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates the synchronous fallback request-context store used when no async-context primitive exists.
|
|
3
|
+
*
|
|
4
|
+
* @returns A stack-backed request-context store scoped to synchronous callback frames.
|
|
5
|
+
*/
|
|
6
|
+
export function createStackRequestContextStore() {
|
|
7
|
+
const stack = [];
|
|
8
|
+
return {
|
|
9
|
+
getStore() {
|
|
10
|
+
return stack.at(-1);
|
|
11
|
+
},
|
|
12
|
+
run(context, callback) {
|
|
13
|
+
stack.push(context);
|
|
14
|
+
try {
|
|
15
|
+
return callback();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw error;
|
|
18
|
+
} finally {
|
|
19
|
+
removeStackContext(stack, context);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function removeStackContext(stack, context) {
|
|
25
|
+
const index = stack.lastIndexOf(context);
|
|
26
|
+
if (index >= 0) {
|
|
27
|
+
stack.splice(index, 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RequestContext } from '../types.js';
|
|
2
|
+
/** Store abstraction shared by host async-context implementations and the synchronous fallback. */
|
|
3
|
+
export type RequestContextStore = {
|
|
4
|
+
getStore(): RequestContext | undefined;
|
|
5
|
+
run<T>(context: RequestContext, callback: () => T): T;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=request-context-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context-store.d.ts","sourceRoot":"","sources":["../../src/context/request-context-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,mGAAmG;AACnG,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,IAAI,cAAc,GAAG,SAAS,CAAC;IACvC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;CACvD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ContextKey, RequestContext } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Runs a callback inside the request-scoped
|
|
3
|
+
* Runs a callback inside the request-scoped async context.
|
|
4
|
+
*
|
|
5
|
+
* Hosts with `AsyncLocalStorage` preserve the context across awaited work. Hosts without an async
|
|
6
|
+
* context primitive use a stack fallback that keeps the context only for the synchronous callback
|
|
7
|
+
* frame and clears it before awaited continuations resume.
|
|
4
8
|
*
|
|
5
9
|
* @param context Request context snapshot to bind to the current async execution chain.
|
|
6
10
|
* @param callback Callback executed with `context` available through request-context helpers.
|
|
@@ -21,9 +25,9 @@ export declare function getCurrentRequestContext(): RequestContext | undefined;
|
|
|
21
25
|
*/
|
|
22
26
|
export declare function assertRequestContext(): RequestContext;
|
|
23
27
|
/**
|
|
24
|
-
* Creates a defensive clone of a request context for
|
|
28
|
+
* Creates a defensive clone of a request context for async-context storage.
|
|
25
29
|
*
|
|
26
|
-
* @param context Request context to clone before storing in
|
|
30
|
+
* @param context Request context to clone before storing in the active async-context store.
|
|
27
31
|
* @returns A shallow clone with copied metadata map to avoid cross-request mutation.
|
|
28
32
|
*/
|
|
29
33
|
export declare function createRequestContext(context: RequestContext): RequestContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/context/request-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/context/request-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAO9D;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtF;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,cAAc,GAAG,SAAS,CAErE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAUrD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAK5E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAKtE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAE7F;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAE9F"}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
1
|
import { FluoError } from '@fluojs/core';
|
|
3
|
-
|
|
2
|
+
import { resolveAsyncLocalStorageConstructor } from './request-context-node-store.js';
|
|
3
|
+
import { createStackRequestContextStore } from './request-context-stack-store.js';
|
|
4
|
+
const requestContextStore = await createRequestContextStore();
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Runs a callback inside the request-scoped
|
|
7
|
+
* Runs a callback inside the request-scoped async context.
|
|
8
|
+
*
|
|
9
|
+
* Hosts with `AsyncLocalStorage` preserve the context across awaited work. Hosts without an async
|
|
10
|
+
* context primitive use a stack fallback that keeps the context only for the synchronous callback
|
|
11
|
+
* frame and clears it before awaited continuations resume.
|
|
7
12
|
*
|
|
8
13
|
* @param context Request context snapshot to bind to the current async execution chain.
|
|
9
14
|
* @param callback Callback executed with `context` available through request-context helpers.
|
|
@@ -39,9 +44,9 @@ export function assertRequestContext() {
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
|
-
* Creates a defensive clone of a request context for
|
|
47
|
+
* Creates a defensive clone of a request context for async-context storage.
|
|
43
48
|
*
|
|
44
|
-
* @param context Request context to clone before storing in
|
|
49
|
+
* @param context Request context to clone before storing in the active async-context store.
|
|
45
50
|
* @returns A shallow clone with copied metadata map to avoid cross-request mutation.
|
|
46
51
|
*/
|
|
47
52
|
export function createRequestContext(context) {
|
|
@@ -86,4 +91,11 @@ export function getContextValue(context, key) {
|
|
|
86
91
|
*/
|
|
87
92
|
export function setContextValue(context, key, value) {
|
|
88
93
|
context.metadata[key.id] = value;
|
|
94
|
+
}
|
|
95
|
+
async function createRequestContextStore() {
|
|
96
|
+
const AsyncLocalStorage = await resolveAsyncLocalStorageConstructor();
|
|
97
|
+
if (typeof AsyncLocalStorage === 'function') {
|
|
98
|
+
return new AsyncLocalStorage();
|
|
99
|
+
}
|
|
100
|
+
return createStackRequestContextStore();
|
|
89
101
|
}
|
package/dist/context/sse.d.ts
CHANGED
|
@@ -8,6 +8,26 @@ export interface SseSendOptions {
|
|
|
8
8
|
/** Optional client retry delay in milliseconds. Non-finite or negative values are ignored. */
|
|
9
9
|
retry?: number;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Structured message emitted by a managed `@Sse()` async iterable handler.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Returning `AsyncIterable<SseMessage<T> | T>` from an `@Sse()` route lets the
|
|
16
|
+
* dispatcher write each item as an SSE frame. Plain values become `data:`
|
|
17
|
+
* frames. Objects with a `data` field plus optional `event`, `id`, or `retry`
|
|
18
|
+
* fields use those SSE metadata fields for the frame.
|
|
19
|
+
*/
|
|
20
|
+
export interface SseMessage<T = unknown> extends SseSendOptions {
|
|
21
|
+
/** Payload encoded into the event frame's `data:` lines. */
|
|
22
|
+
data: T;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runtime guard for structured managed SSE messages.
|
|
26
|
+
*
|
|
27
|
+
* @param value Candidate value yielded by an async iterable SSE source.
|
|
28
|
+
* @returns `true` when the value follows the public `SseMessage<T>` shape.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isSseMessage<T = unknown>(value: unknown): value is SseMessage<T>;
|
|
11
31
|
/**
|
|
12
32
|
* Encodes a comment as a canonical server-sent event comment frame.
|
|
13
33
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/context/sse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8C,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9F,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,8FAA8F;IAC9F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAoCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CAoBpF;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IASV,OAAO,CAAC,QAAQ,CAAC,OAAO;IARpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,mBAAmB,CAAC,CAAa;IAEzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAEtB;gBAE2B,OAAO,EAAE,cAAc;IAmCpD;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO;IAI1D;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIjC,uFAAuF;IACvF,KAAK,IAAI,IAAI;IAiBb,OAAO,CAAC,UAAU;CAYnB"}
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/context/sse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8C,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9F,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,8FAA8F;IAC9F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO,CAAE,SAAQ,cAAc;IAC7D,4DAA4D;IAC5D,IAAI,EAAE,CAAC,CAAC;CACT;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAAC,CAAC,CAAC,CAEhF;AAoCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CAoBpF;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IASV,OAAO,CAAC,QAAQ,CAAC,OAAO;IARpC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,mBAAmB,CAAC,CAAa;IAEzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAEtB;gBAE2B,OAAO,EAAE,cAAc;IAmCpD;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO;IAI1D;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIjC,uFAAuF;IACvF,KAAK,IAAI,IAAI;IAiBb,OAAO,CAAC,UAAU;CAYnB"}
|
package/dist/context/sse.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
/** Options that customize the fields emitted for one server-sent event frame. */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Structured message emitted by a managed `@Sse()` async iterable handler.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Returning `AsyncIterable<SseMessage<T> | T>` from an `@Sse()` route lets the
|
|
8
|
+
* dispatcher write each item as an SSE frame. Plain values become `data:`
|
|
9
|
+
* frames. Objects with a `data` field plus optional `event`, `id`, or `retry`
|
|
10
|
+
* fields use those SSE metadata fields for the frame.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runtime guard for structured managed SSE messages.
|
|
15
|
+
*
|
|
16
|
+
* @param value Candidate value yielded by an async iterable SSE source.
|
|
17
|
+
* @returns `true` when the value follows the public `SseMessage<T>` shape.
|
|
18
|
+
*/
|
|
19
|
+
export function isSseMessage(value) {
|
|
20
|
+
return typeof value === 'object' && value !== null && 'data' in value;
|
|
21
|
+
}
|
|
3
22
|
function sanitizeSseField(value) {
|
|
4
23
|
return value.replace(/\r/g, '').replace(/\n/g, '');
|
|
5
24
|
}
|
package/dist/decorators.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ export declare function Version(version: string): ClassOrMethodDecoratorLike;
|
|
|
28
28
|
* @returns A method decorator that registers a `GET` handler mapping.
|
|
29
29
|
*/
|
|
30
30
|
export declare const Get: (path: string) => MethodDecoratorLike;
|
|
31
|
+
/**
|
|
32
|
+
* Registers a server-sent events route handler as `GET` with `text/event-stream` produces metadata.
|
|
33
|
+
*
|
|
34
|
+
* @param path Route path relative to the controller base path.
|
|
35
|
+
* @returns A method decorator that registers a `GET` SSE handler mapping.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Handlers may return `SseResponse` for manual control or `AsyncIterable<SseMessage<T> | T>` for managed dispatcher streaming.
|
|
39
|
+
*/
|
|
40
|
+
export declare const Sse: (path: string) => MethodDecoratorLike;
|
|
31
41
|
/**
|
|
32
42
|
* Registers a `POST` route handler.
|
|
33
43
|
*
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,mBAAmB,EAEzB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"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"}
|