@fluojs/http
Advanced tools
@@ -17,2 +17,9 @@ import type { RequestContextStore } from './request-context-store.js'; | ||
| /** | ||
| * Resolves host-provided `AsyncLocalStorage` without async imports or throwing host probes. | ||
| * | ||
| * @param host Host global-like object to inspect for synchronous async-context support. | ||
| * @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable. | ||
| */ | ||
| export declare function resolveImmediateAsyncLocalStorageConstructor(host?: AsyncLocalStorageResolutionHost): AsyncLocalStorageConstructor | undefined; | ||
| /** | ||
| * Resolves the host `AsyncLocalStorage` constructor without eagerly importing Node built-ins. | ||
@@ -25,3 +32,10 @@ * | ||
| export declare function resolveAsyncLocalStorageConstructor(host?: AsyncLocalStorageResolutionHost, loadNodeAsyncHooks?: NodeAsyncHooksLoader): Promise<AsyncLocalStorageConstructor | undefined>; | ||
| /** | ||
| * Reports whether the host can still resolve Node async-context storage asynchronously. | ||
| * | ||
| * @param host Host global-like object to inspect for Node runtime markers. | ||
| * @returns `true` when the host is Node.js and can use a lazy `node:async_hooks` import. | ||
| */ | ||
| export declare function canResolveAsyncLocalStorageDynamically(host?: AsyncLocalStorageResolutionHost): boolean; | ||
| export {}; | ||
| //# sourceMappingURL=request-context-node-store.d.ts.map |
@@ -1,1 +0,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"} | ||
| {"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"} |
| /** | ||
| * Resolves host-provided `AsyncLocalStorage` without async imports or throwing host probes. | ||
| * | ||
| * @param host Host global-like object to inspect for synchronous async-context support. | ||
| * @returns The resolved `AsyncLocalStorage` constructor, or `undefined` when unavailable. | ||
| */ | ||
| export function resolveImmediateAsyncLocalStorageConstructor(host = globalThis) { | ||
| if (typeof host.AsyncLocalStorage === 'function') { | ||
| return host.AsyncLocalStorage; | ||
| } | ||
| const getBuiltinModule = host.process?.getBuiltinModule; | ||
| if (typeof getBuiltinModule !== 'function') { | ||
| return undefined; | ||
| } | ||
| try { | ||
| const builtinAsyncLocalStorage = getBuiltinModule('node:async_hooks')?.AsyncLocalStorage; | ||
| if (typeof builtinAsyncLocalStorage === 'function') { | ||
| return builtinAsyncLocalStorage; | ||
| } | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** | ||
| * Resolves the host `AsyncLocalStorage` constructor without eagerly importing Node built-ins. | ||
@@ -9,9 +34,6 @@ * | ||
| export async function resolveAsyncLocalStorageConstructor(host = globalThis, loadNodeAsyncHooks = importNodeAsyncHooks) { | ||
| if (typeof host.AsyncLocalStorage === 'function') { | ||
| return host.AsyncLocalStorage; | ||
| const immediateAsyncLocalStorage = resolveImmediateAsyncLocalStorageConstructor(host); | ||
| if (typeof immediateAsyncLocalStorage === 'function') { | ||
| return immediateAsyncLocalStorage; | ||
| } | ||
| const builtinAsyncLocalStorage = host.process?.getBuiltinModule?.('node:async_hooks').AsyncLocalStorage; | ||
| if (typeof builtinAsyncLocalStorage === 'function') { | ||
| return builtinAsyncLocalStorage; | ||
| } | ||
| if (!isNodeHost(host)) { | ||
@@ -27,2 +49,12 @@ return undefined; | ||
| } | ||
| /** | ||
| * Reports whether the host can still resolve Node async-context storage asynchronously. | ||
| * | ||
| * @param host Host global-like object to inspect for Node runtime markers. | ||
| * @returns `true` when the host is Node.js and can use a lazy `node:async_hooks` import. | ||
| */ | ||
| export function canResolveAsyncLocalStorageDynamically(host = globalThis) { | ||
| return isNodeHost(host); | ||
| } | ||
| function isNodeHost(host) { | ||
@@ -29,0 +61,0 @@ return typeof host.process?.versions?.node === 'string'; |
@@ -1,1 +0,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"} | ||
| {"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;AAc9D;;;;;;;;;;GAUG;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"} |
| import { FluoError } from '@fluojs/core'; | ||
| import { resolveAsyncLocalStorageConstructor } from './request-context-node-store.js'; | ||
| import { canResolveAsyncLocalStorageDynamically, resolveAsyncLocalStorageConstructor, resolveImmediateAsyncLocalStorageConstructor } from './request-context-node-store.js'; | ||
| import { createStackRequestContextStore } from './request-context-stack-store.js'; | ||
| const requestContextStore = await createRequestContextStore(); | ||
| let requestContextStore; | ||
| let requestContextStoreResolution; | ||
| let fallbackRequestContextStore; | ||
| const dynamicResolutionFallbackStack = []; | ||
@@ -18,3 +21,13 @@ /** | ||
| export function runWithRequestContext(context, callback) { | ||
| return requestContextStore.run(context, callback); | ||
| const store = getResolvedRequestContextStore(); | ||
| if (store) { | ||
| return store.run(context, callback); | ||
| } | ||
| if (!canResolveAsyncLocalStorageDynamically()) { | ||
| return getFallbackRequestContextStore().run(context, callback); | ||
| } | ||
| if (!isAsyncCallback(callback)) { | ||
| return runWithDynamicResolutionFallbackContext(context, callback); | ||
| } | ||
| return runWithResolvedRequestContextStore(context, callback); | ||
| } | ||
@@ -28,3 +41,3 @@ | ||
| export function getCurrentRequestContext() { | ||
| return requestContextStore.getStore(); | ||
| return getRequestContextStore().getStore() ?? getDynamicResolutionFallbackContext(); | ||
| } | ||
@@ -97,8 +110,72 @@ | ||
| } | ||
| function getRequestContextStore() { | ||
| return getResolvedRequestContextStore() ?? getFallbackRequestContextStore(); | ||
| } | ||
| function getResolvedRequestContextStore() { | ||
| if (requestContextStore) { | ||
| return requestContextStore; | ||
| } | ||
| const AsyncLocalStorage = resolveImmediateAsyncLocalStorageConstructor(); | ||
| if (typeof AsyncLocalStorage === 'function') { | ||
| requestContextStore = new AsyncLocalStorage(); | ||
| return requestContextStore; | ||
| } | ||
| void resolveRequestContextStore(); | ||
| return undefined; | ||
| } | ||
| async function runWithResolvedRequestContextStore(context, callback) { | ||
| const store = await resolveRequestContextStore(); | ||
| return store.run(context, callback); | ||
| } | ||
| async function resolveRequestContextStore() { | ||
| requestContextStoreResolution ??= createRequestContextStore(); | ||
| return requestContextStoreResolution; | ||
| } | ||
| async function createRequestContextStore() { | ||
| const AsyncLocalStorage = await resolveAsyncLocalStorageConstructor(); | ||
| if (typeof AsyncLocalStorage === 'function') { | ||
| return new AsyncLocalStorage(); | ||
| requestContextStore = new AsyncLocalStorage(); | ||
| return requestContextStore; | ||
| } | ||
| return createStackRequestContextStore(); | ||
| requestContextStore = getFallbackRequestContextStore(); | ||
| return requestContextStore; | ||
| } | ||
| function getFallbackRequestContextStore() { | ||
| fallbackRequestContextStore ??= createStackRequestContextStore(); | ||
| return fallbackRequestContextStore; | ||
| } | ||
| function runWithDynamicResolutionFallbackContext(context, callback) { | ||
| dynamicResolutionFallbackStack.push(context); | ||
| try { | ||
| const result = callback(); | ||
| void resolveRequestContextStore(); | ||
| if (isPromiseLike(result)) { | ||
| return result.finally(() => { | ||
| removeDynamicResolutionFallbackContext(context); | ||
| }); | ||
| } | ||
| removeDynamicResolutionFallbackContext(context); | ||
| return result; | ||
| } catch (error) { | ||
| removeDynamicResolutionFallbackContext(context); | ||
| throw error; | ||
| } | ||
| } | ||
| function getDynamicResolutionFallbackContext() { | ||
| if (dynamicResolutionFallbackStack.length !== 1) { | ||
| return undefined; | ||
| } | ||
| return dynamicResolutionFallbackStack[0]; | ||
| } | ||
| function removeDynamicResolutionFallbackContext(context) { | ||
| const index = dynamicResolutionFallbackStack.lastIndexOf(context); | ||
| if (index >= 0) { | ||
| dynamicResolutionFallbackStack.splice(index, 1); | ||
| } | ||
| } | ||
| function isPromiseLike(value) { | ||
| return typeof value === 'object' && value !== null && 'then' in value && 'finally' in value; | ||
| } | ||
| function isAsyncCallback(callback) { | ||
| return callback.constructor.name === 'AsyncFunction'; | ||
| } |
+3
-3
@@ -13,3 +13,3 @@ { | ||
| ], | ||
| "version": "1.1.0", | ||
| "version": "1.1.1", | ||
| "private": false, | ||
@@ -46,4 +46,4 @@ "license": "MIT", | ||
| "@fluojs/core": "^1.0.3", | ||
| "@fluojs/validation": "^1.0.4", | ||
| "@fluojs/di": "^1.0.3" | ||
| "@fluojs/validation": "^1.0.5", | ||
| "@fluojs/di": "^1.1.0" | ||
| }, | ||
@@ -50,0 +50,0 @@ "devDependencies": { |
+1
-1
@@ -99,3 +99,3 @@ # @fluojs/http | ||
| `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이 재개되기 전에 컨텍스트를 비웁니다. | ||
| `runWithRequestContext(...)`는 호스트가 `globalThis.AsyncLocalStorage` 또는 Node 내장 `node:async_hooks` 모듈로 `AsyncLocalStorage`를 제공할 때 활성 컨텍스트를 `await` 이후까지 보존합니다. 루트 `@fluojs/http` import는 async-context storage를 probe하거나 instantiate하지 않습니다. Helper가 처음 사용될 때 storage를 lazy하게 해석하고, `process.getBuiltinModule(...)` 실패를 guard하며, 동기 probe를 노출하지 않는 Node host에서는 계속 `node:async_hooks`를 동적으로 해석할 수 있습니다. 비동기 컨텍스트 primitive가 없는 비 Node 호스트에서는 동기 stack fallback을 사용하며, 겹치는 비동기 요청이 서로의 컨텍스트를 관찰하지 않도록 awaited continuation이 재개되기 전에 컨텍스트를 비웁니다. | ||
@@ -102,0 +102,0 @@ ### 프록시 뒤의 속도 제한 |
+1
-1
@@ -101,3 +101,3 @@ # @fluojs/http | ||
| `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. | ||
| `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 can still resolve `node:async_hooks` dynamically for Node hosts that do not expose the synchronous probe. 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. | ||
@@ -104,0 +104,0 @@ ### Rate limiting behind proxies |
285879
1.82%5779
2.14%Updated
Updated