@fluojs/http 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -41,6 +41,11 @@ class CreateUserDto {
41
41
  name!: string;
42
42
  }
43
43
 
44
+ class FindUserParamsDto {
45
+ @FromPath('id')
46
+ id!: string;
47
+ }
48
+
44
49
  @Controller('/users')
45
50
  export class UserController {
46
51
  @Post('/')
@@ -50,8 +55,9 @@ export class UserController {
50
55
  }
51
56
 
52
57
  @Get('/:id')
53
- getById(@FromPath('id') id: string) {
54
- return { id, name: 'John Doe' };
58
+ @RequestDto(FindUserParamsDto)
59
+ getById(input: FindUserParamsDto) {
60
+ return { id: input.id, name: 'John Doe' };
55
61
  }
56
62
  }
57
63
  ```
@@ -96,7 +102,7 @@ function someDeepHelper() {
96
102
  }
97
103
  ```
98
104
 
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이 재개되기 전에 컨텍스트를 비웁니다.
105
+ `runWithRequestContext(...)`는 호스트가 `globalThis.AsyncLocalStorage` 또는 Node 내장 `node:async_hooks` 모듈로 `AsyncLocalStorage`를 제공할 때 활성 컨텍스트를 `await` 이후까지 보존합니다. 루트 `@fluojs/http` import는 async-context storage를 probe하거나 instantiate하지 않습니다. Helper가 처음 사용될 때 storage를 lazy하게 해석하고, `process.getBuiltinModule(...)` 실패를 guard하며, Node async storage 해석이 끝나기 전에 등록된 promise continuation도 격리하면서 첫 호출의 동기 callback 반환 및 throw 동작은 그대로 유지합니다. 비동기 컨텍스트 primitive가 없는 비 Node 호스트에서는 동기 stack fallback을 사용하며, 겹치는 비동기 요청이 서로의 컨텍스트를 관찰하지 않도록 awaited continuation이 재개되기 전에 컨텍스트를 비웁니다.
100
106
 
101
107
  ### 프록시 뒤의 속도 제한
102
108
 
@@ -180,7 +186,7 @@ Fluo의 HTTP 데코레이터는 TC39 표준 데코레이터이며, runtime 또
180
186
 
181
187
  디스패처는 활성 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를 노출한다는 약속이 아닙니다.
182
188
 
183
- 어댑터는 플랫폼이 제공한다면 `FrameworkRequest.signal`에 `AbortSignal`을 전달해야 합니다. SSE에서는 가능하면 `FrameworkResponse.stream.onClose(...)`도 노출해야 합니다. `SseResponse`는 request abort와 raw stream close를 모두 구독하고, 멱등하게 닫히며, 어느 쪽이 먼저 종료되더라도 등록한 listener를 제거합니다.
189
+ 어댑터는 플랫폼이 제공한다면 `FrameworkRequest.signal`에 `AbortSignal`을 전달하고, signal allocation이 실용적이지 않다면 `isAborted()` probe를 제공해야 합니다. Dispatcher는 per-dispatch request clone에 두 abort surface를 모두 보존하고 handler 작업 전후에 검사하므로 `AbortSignal`이 없는 어댑터도 abandon된 요청을 중단할 수 있습니다. SSE에서는 가능하면 `FrameworkResponse.stream.onClose(...)`도 노출해야 합니다. `SseResponse`는 request abort와 raw stream close를 모두 구독하고, 멱등하게 닫히며, 어느 쪽이 먼저 종료되더라도 등록한 listener를 제거합니다.
184
190
 
185
191
  ## 공개 API
186
192
 
package/README.md CHANGED
@@ -43,6 +43,11 @@ class CreateUserDto {
43
43
  name!: string;
44
44
  }
45
45
 
46
+ class FindUserParamsDto {
47
+ @FromPath('id')
48
+ id!: string;
49
+ }
50
+
46
51
  @Controller('/users')
47
52
  export class UserController {
48
53
  @Post('/')
@@ -52,8 +57,9 @@ export class UserController {
52
57
  }
53
58
 
54
59
  @Get('/:id')
55
- getById(@FromPath('id') id: string) {
56
- return { id, name: 'John Doe' };
60
+ @RequestDto(FindUserParamsDto)
61
+ getById(input: FindUserParamsDto) {
62
+ return { id: input.id, name: 'John Doe' };
57
63
  }
58
64
  }
59
65
  ```
@@ -98,7 +104,7 @@ function someDeepHelper() {
98
104
  }
99
105
  ```
100
106
 
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.
107
+ `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. The root `@fluojs/http` import does not probe or instantiate async-context storage; helpers resolve storage lazily on first use, guard `process.getBuiltinModule(...)` failures, and keep the first-call synchronous callback return and throw behavior unchanged while isolating promise continuations registered before Node async storage finishes resolving. 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
108
 
103
109
  ### Rate limiting behind proxies
104
110
 
@@ -182,7 +188,7 @@ This compatibility path is an execution fallback for Bun bundle output; applicat
182
188
 
183
189
  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.
184
190
 
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.
191
+ Adapters should pass an `AbortSignal` on `FrameworkRequest.signal` when the platform exposes one, or an `isAborted()` probe when allocating a signal is not practical. The dispatcher preserves both abort surfaces on its per-dispatch request clone and checks them before and after handler work so adapters without `AbortSignal` can still stop abandoned requests. 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.
186
192
 
187
193
  ## Public API
188
194
 
@@ -13,6 +13,13 @@ type AsyncLocalStorageResolutionHost = {
13
13
  };
14
14
  };
15
15
  type NodeAsyncHooksLoader = () => Promise<NodeAsyncHooksModule>;
16
+ /**
17
+ * Resolves host-provided `AsyncLocalStorage` without async imports or throwing host probes.
18
+ *
19
+ * @param host Host global-like object to inspect for synchronous async-context support.
20
+ * @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable.
21
+ */
22
+ export declare function resolveImmediateAsyncLocalStorageConstructor(host?: AsyncLocalStorageResolutionHost): AsyncLocalStorageConstructor | undefined;
16
23
  /**
17
24
  * Resolves the host `AsyncLocalStorage` constructor without eagerly importing Node built-ins.
18
25
  *
@@ -21,5 +28,12 @@ type NodeAsyncHooksLoader = () => Promise<NodeAsyncHooksModule>;
21
28
  * @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable.
22
29
  */
23
30
  export declare function resolveAsyncLocalStorageConstructor(host?: AsyncLocalStorageResolutionHost, loadNodeAsyncHooks?: NodeAsyncHooksLoader): Promise<AsyncLocalStorageConstructor | undefined>;
31
+ /**
32
+ * Reports whether the host can still resolve Node async-context storage asynchronously.
33
+ *
34
+ * @param host Host global-like object to inspect for Node runtime markers.
35
+ * @returns `true` when the host is Node.js and can use a lazy `node:async_hooks` import.
36
+ */
37
+ export declare function canResolveAsyncLocalStorageDynamically(host?: AsyncLocalStorageResolutionHost): boolean;
24
38
  export {};
25
39
  //# sourceMappingURL=request-context-node-store.d.ts.map
@@ -1 +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"}
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;;;;;GAKG;AACH,wBAAgB,4CAA4C,CAC1D,IAAI,GAAE,+BAA4C,GACjD,4BAA4B,GAAG,SAAS,CAsB1C;AAED;;;;;;GAMG;AACH,wBAAsB,mCAAmC,CACvD,IAAI,GAAE,+BAA4C,EAClD,kBAAkB,GAAE,oBAA2C,GAC9D,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAkBnD;AAED;;;;;GAKG;AACH,wBAAgB,sCAAsC,CACpD,IAAI,GAAE,+BAA4C,GACjD,OAAO,CAET"}
@@ -1,3 +1,28 @@
1
+ /**
2
+ * Resolves host-provided `AsyncLocalStorage` without async imports or throwing host probes.
3
+ *
4
+ * @param host Host global-like object to inspect for synchronous async-context support.
5
+ * @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable.
6
+ */
7
+ export function resolveImmediateAsyncLocalStorageConstructor(host = globalThis) {
8
+ if (typeof host.AsyncLocalStorage === 'function') {
9
+ return host.AsyncLocalStorage;
10
+ }
11
+ const getBuiltinModule = host.process?.getBuiltinModule;
12
+ if (typeof getBuiltinModule !== 'function') {
13
+ return undefined;
14
+ }
15
+ try {
16
+ const builtinAsyncLocalStorage = getBuiltinModule('node:async_hooks')?.AsyncLocalStorage;
17
+ if (typeof builtinAsyncLocalStorage === 'function') {
18
+ return builtinAsyncLocalStorage;
19
+ }
20
+ } catch {
21
+ return undefined;
22
+ }
23
+ return undefined;
24
+ }
25
+
1
26
  /**
2
27
  * Resolves the host `AsyncLocalStorage` constructor without eagerly importing Node built-ins.
3
28
  *
@@ -6,12 +31,9 @@
6
31
  * @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable.
7
32
  */
8
33
  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;
34
+ const immediateAsyncLocalStorage = resolveImmediateAsyncLocalStorageConstructor(host);
35
+ if (typeof immediateAsyncLocalStorage === 'function') {
36
+ return immediateAsyncLocalStorage;
15
37
  }
16
38
  if (!isNodeHost(host)) {
17
39
  return undefined;
@@ -23,6 +45,16 @@ export async function resolveAsyncLocalStorageConstructor(host = globalThis, loa
23
45
  return undefined;
24
46
  }
25
47
  }
48
+
49
+ /**
50
+ * Reports whether the host can still resolve Node async-context storage asynchronously.
51
+ *
52
+ * @param host Host global-like object to inspect for Node runtime markers.
53
+ * @returns `true` when the host is Node.js and can use a lazy `node:async_hooks` import.
54
+ */
55
+ export function canResolveAsyncLocalStorageDynamically(host = globalThis) {
56
+ return isNodeHost(host);
57
+ }
26
58
  function isNodeHost(host) {
27
59
  return typeof host.process?.versions?.node === 'string';
28
60
  }
@@ -2,9 +2,11 @@ import type { ContextKey, RequestContext } from '../types.js';
2
2
  /**
3
3
  * Runs a callback inside the request-scoped async context.
4
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.
5
+ * Hosts with `AsyncLocalStorage` preserve the context across awaited work. During lazy
6
+ * `AsyncLocalStorage` resolution, promise continuations registered before the store resolves keep
7
+ * their request context until the returned promise settles. Hosts without an async context primitive
8
+ * use a stack fallback that keeps the context only for the synchronous callback frame and clears it
9
+ * before awaited continuations resume.
8
10
  *
9
11
  * @param context Request context snapshot to bind to the current async execution chain.
10
12
  * @param callback Callback executed with `context` available through request-context helpers.
@@ -1 +1 @@
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
+ {"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;AAoB9D;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAgBtF;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,21 +1,37 @@
1
1
  import { FluoError } from '@fluojs/core';
2
- import { resolveAsyncLocalStorageConstructor } from './request-context-node-store.js';
2
+ import { canResolveAsyncLocalStorageDynamically, resolveAsyncLocalStorageConstructor, resolveImmediateAsyncLocalStorageConstructor } from './request-context-node-store.js';
3
3
  import { createStackRequestContextStore } from './request-context-stack-store.js';
4
- const requestContextStore = await createRequestContextStore();
5
-
4
+ let requestContextStore;
5
+ let requestContextStoreResolution;
6
+ let fallbackRequestContextStore;
7
+ let originalPromiseThen;
8
+ let promiseThenPatchDepth = 0;
9
+ const dynamicResolutionFallbackStack = [];
6
10
  /**
7
11
  * Runs a callback inside the request-scoped async context.
8
12
  *
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.
13
+ * Hosts with `AsyncLocalStorage` preserve the context across awaited work. During lazy
14
+ * `AsyncLocalStorage` resolution, promise continuations registered before the store resolves keep
15
+ * their request context until the returned promise settles. Hosts without an async context primitive
16
+ * use a stack fallback that keeps the context only for the synchronous callback frame and clears it
17
+ * before awaited continuations resume.
12
18
  *
13
19
  * @param context Request context snapshot to bind to the current async execution chain.
14
20
  * @param callback Callback executed with `context` available through request-context helpers.
15
21
  * @returns The return value from `callback`.
16
22
  */
17
23
  export function runWithRequestContext(context, callback) {
18
- return requestContextStore.run(context, callback);
24
+ const store = getResolvedRequestContextStore();
25
+ if (store) {
26
+ return store.run(context, callback);
27
+ }
28
+ if (!canResolveAsyncLocalStorageDynamically()) {
29
+ return getFallbackRequestContextStore().run(context, callback);
30
+ }
31
+ if (!isAsyncCallback(callback)) {
32
+ return runWithDynamicResolutionFallbackContext(context, callback);
33
+ }
34
+ return runWithResolvedRequestContextStore(context, callback);
19
35
  }
20
36
 
21
37
  /**
@@ -24,7 +40,7 @@ export function runWithRequestContext(context, callback) {
24
40
  * @returns The active request context, or `undefined` when no request scope is bound.
25
41
  */
26
42
  export function getCurrentRequestContext() {
27
- return requestContextStore.getStore();
43
+ return getRequestContextStore().getStore() ?? getDynamicResolutionFallbackContext();
28
44
  }
29
45
 
30
46
  /**
@@ -92,10 +108,115 @@ export function getContextValue(context, key) {
92
108
  export function setContextValue(context, key, value) {
93
109
  context.metadata[key.id] = value;
94
110
  }
111
+ function getRequestContextStore() {
112
+ return getResolvedRequestContextStore() ?? getFallbackRequestContextStore();
113
+ }
114
+ function getResolvedRequestContextStore() {
115
+ if (requestContextStore) {
116
+ return requestContextStore;
117
+ }
118
+ const AsyncLocalStorage = resolveImmediateAsyncLocalStorageConstructor();
119
+ if (typeof AsyncLocalStorage === 'function') {
120
+ requestContextStore = new AsyncLocalStorage();
121
+ return requestContextStore;
122
+ }
123
+ void resolveRequestContextStore();
124
+ return undefined;
125
+ }
126
+ async function runWithResolvedRequestContextStore(context, callback) {
127
+ const store = await resolveRequestContextStore();
128
+ return store.run(context, callback);
129
+ }
130
+ async function resolveRequestContextStore() {
131
+ requestContextStoreResolution ??= createRequestContextStore();
132
+ return requestContextStoreResolution;
133
+ }
95
134
  async function createRequestContextStore() {
96
135
  const AsyncLocalStorage = await resolveAsyncLocalStorageConstructor();
97
136
  if (typeof AsyncLocalStorage === 'function') {
98
- return new AsyncLocalStorage();
137
+ requestContextStore = new AsyncLocalStorage();
138
+ return requestContextStore;
139
+ }
140
+ requestContextStore = getFallbackRequestContextStore();
141
+ return requestContextStore;
142
+ }
143
+ function getFallbackRequestContextStore() {
144
+ fallbackRequestContextStore ??= createStackRequestContextStore();
145
+ return fallbackRequestContextStore;
146
+ }
147
+ function runWithDynamicResolutionFallbackContext(context, callback) {
148
+ dynamicResolutionFallbackStack.push(context);
149
+ installPromiseThenContextBridge();
150
+ let cleanedUp = false;
151
+ const cleanup = () => {
152
+ if (cleanedUp) {
153
+ return;
154
+ }
155
+ cleanedUp = true;
156
+ removeDynamicResolutionFallbackContext(context);
157
+ restorePromiseThenContextBridge();
158
+ void resolveRequestContextStore();
159
+ };
160
+ try {
161
+ const result = callback();
162
+ if (isPromise(result)) {
163
+ const then = originalPromiseThen ?? Promise.prototype.then;
164
+ void then.call(result, cleanup, cleanup);
165
+ return result;
166
+ }
167
+ cleanup();
168
+ return result;
169
+ } catch (error) {
170
+ cleanup();
171
+ throw error;
172
+ }
173
+ }
174
+ function getDynamicResolutionFallbackContext() {
175
+ return dynamicResolutionFallbackStack.at(-1);
176
+ }
177
+ function removeDynamicResolutionFallbackContext(context) {
178
+ const index = dynamicResolutionFallbackStack.lastIndexOf(context);
179
+ if (index >= 0) {
180
+ dynamicResolutionFallbackStack.splice(index, 1);
181
+ }
182
+ }
183
+ function installPromiseThenContextBridge() {
184
+ if (promiseThenPatchDepth === 0) {
185
+ originalPromiseThen = Promise.prototype.then;
186
+ Object.defineProperty(Promise.prototype, 'then', {
187
+ configurable: true,
188
+ value: createContextBridgePromiseThen(originalPromiseThen),
189
+ writable: true
190
+ });
191
+ }
192
+ promiseThenPatchDepth += 1;
193
+ }
194
+ function restorePromiseThenContextBridge() {
195
+ promiseThenPatchDepth -= 1;
196
+ if (promiseThenPatchDepth === 0 && originalPromiseThen) {
197
+ Object.defineProperty(Promise.prototype, 'then', {
198
+ configurable: true,
199
+ value: originalPromiseThen,
200
+ writable: true
201
+ });
202
+ originalPromiseThen = undefined;
203
+ }
204
+ }
205
+ function createContextBridgePromiseThen(originalThen) {
206
+ return function contextBridgePromiseThen(onfulfilled, onrejected) {
207
+ const context = getDynamicResolutionFallbackContext();
208
+ return originalThen.call(this, wrapPromiseCallback(context, onfulfilled), wrapPromiseCallback(context, onrejected));
209
+ };
210
+ }
211
+ function wrapPromiseCallback(context, callback) {
212
+ if (!context || typeof callback !== 'function') {
213
+ return callback;
99
214
  }
100
- return createStackRequestContextStore();
215
+ return value => runWithDynamicResolutionFallbackContext(context, () => callback(value));
216
+ }
217
+ function isPromise(value) {
218
+ return value instanceof Promise;
219
+ }
220
+ function isAsyncCallback(callback) {
221
+ return callback.constructor.name === 'AsyncFunction';
101
222
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAC;AAQnE,OAAO,KAAK,EACV,MAAM,EACN,yBAAyB,EACzB,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAKjB,cAAc,EAEd,eAAe,EAEf,cAAc,EAId,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAKL,KAAK,aAAa,EAOnB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAE5F,gEAAgE;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpK,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,sDAAsD;IACtD,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,+DAA+D;IAC/D,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qCAAqC;IACrC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,sEAAsE;IACtE,YAAY,CAAC,EAAE;QACb,wDAAwD;QACxD,oBAAoB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;KACjD,CAAC;IACF,qDAAqD;IACrD,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,SAAS,CAAC;IACzB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAy8BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CAuF7E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,UAAU,GAAG,aAAa,GAAG,SAAS,CAE5F;AAED,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAC;AAQnE,OAAO,KAAK,EACV,MAAM,EACN,yBAAyB,EACzB,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAKjB,cAAc,EAEd,eAAe,EAEf,cAAc,EAId,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAKL,KAAK,aAAa,EAOnB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAE5F,gEAAgE;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpK,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,sDAAsD;IACtD,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,+DAA+D;IAC/D,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qCAAqC;IACrC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,sEAAsE;IACtE,YAAY,CAAC,EAAE;QACb,wDAAwD;QACxD,oBAAoB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;KACjD,CAAC;IACF,qDAAqD;IACrD,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,SAAS,CAAC;IACzB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA08BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CAuF7E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,UAAU,GAAG,aAAa,GAAG,SAAS,CAE5F;AAED,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -45,6 +45,7 @@ function createDispatchRequest(request) {
45
45
  raw: request.raw,
46
46
  rawBody: request.rawBody,
47
47
  requestId: request.requestId,
48
+ isAborted: request.isAborted,
48
49
  signal: request.signal,
49
50
  url: request.url
50
51
  };
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "controller",
11
11
  "rest"
12
12
  ],
13
- "version": "1.1.0",
13
+ "version": "1.1.2",
14
14
  "private": false,
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -42,8 +42,8 @@
42
42
  ],
43
43
  "dependencies": {
44
44
  "@fluojs/core": "^1.0.3",
45
- "@fluojs/validation": "^1.0.4",
46
- "@fluojs/di": "^1.0.3"
45
+ "@fluojs/validation": "^1.0.5",
46
+ "@fluojs/di": "^1.1.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "vitest": "^3.2.4"