@fluojs/http 1.1.0 → 1.1.1

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
@@ -96,7 +96,7 @@ 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이 재개되기 전에 컨텍스트를 비웁니다.
99
+ `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이 재개되기 전에 컨텍스트를 비웁니다.
100
100
 
101
101
  ### 프록시 뒤의 속도 제한
102
102
 
package/README.md CHANGED
@@ -98,7 +98,7 @@ 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.
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. 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.
102
102
 
103
103
  ### Rate limiting behind proxies
104
104
 
@@ -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
  }
@@ -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;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"}
@@ -1,7 +1,10 @@
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();
4
+ let requestContextStore;
5
+ let requestContextStoreResolution;
6
+ let fallbackRequestContextStore;
7
+ const dynamicResolutionFallbackStack = [];
5
8
 
6
9
  /**
7
10
  * Runs a callback inside the request-scoped async context.
@@ -15,7 +18,17 @@ const requestContextStore = await createRequestContextStore();
15
18
  * @returns The return value from `callback`.
16
19
  */
17
20
  export function runWithRequestContext(context, callback) {
18
- return requestContextStore.run(context, callback);
21
+ const store = getResolvedRequestContextStore();
22
+ if (store) {
23
+ return store.run(context, callback);
24
+ }
25
+ if (!canResolveAsyncLocalStorageDynamically()) {
26
+ return getFallbackRequestContextStore().run(context, callback);
27
+ }
28
+ if (!isAsyncCallback(callback)) {
29
+ return runWithDynamicResolutionFallbackContext(context, callback);
30
+ }
31
+ return runWithResolvedRequestContextStore(context, callback);
19
32
  }
20
33
 
21
34
  /**
@@ -24,7 +37,7 @@ export function runWithRequestContext(context, callback) {
24
37
  * @returns The active request context, or `undefined` when no request scope is bound.
25
38
  */
26
39
  export function getCurrentRequestContext() {
27
- return requestContextStore.getStore();
40
+ return getRequestContextStore().getStore() ?? getDynamicResolutionFallbackContext();
28
41
  }
29
42
 
30
43
  /**
@@ -92,10 +105,74 @@ export function getContextValue(context, key) {
92
105
  export function setContextValue(context, key, value) {
93
106
  context.metadata[key.id] = value;
94
107
  }
108
+ function getRequestContextStore() {
109
+ return getResolvedRequestContextStore() ?? getFallbackRequestContextStore();
110
+ }
111
+ function getResolvedRequestContextStore() {
112
+ if (requestContextStore) {
113
+ return requestContextStore;
114
+ }
115
+ const AsyncLocalStorage = resolveImmediateAsyncLocalStorageConstructor();
116
+ if (typeof AsyncLocalStorage === 'function') {
117
+ requestContextStore = new AsyncLocalStorage();
118
+ return requestContextStore;
119
+ }
120
+ void resolveRequestContextStore();
121
+ return undefined;
122
+ }
123
+ async function runWithResolvedRequestContextStore(context, callback) {
124
+ const store = await resolveRequestContextStore();
125
+ return store.run(context, callback);
126
+ }
127
+ async function resolveRequestContextStore() {
128
+ requestContextStoreResolution ??= createRequestContextStore();
129
+ return requestContextStoreResolution;
130
+ }
95
131
  async function createRequestContextStore() {
96
132
  const AsyncLocalStorage = await resolveAsyncLocalStorageConstructor();
97
133
  if (typeof AsyncLocalStorage === 'function') {
98
- return new AsyncLocalStorage();
134
+ requestContextStore = new AsyncLocalStorage();
135
+ return requestContextStore;
99
136
  }
100
- return createStackRequestContextStore();
137
+ requestContextStore = getFallbackRequestContextStore();
138
+ return requestContextStore;
139
+ }
140
+ function getFallbackRequestContextStore() {
141
+ fallbackRequestContextStore ??= createStackRequestContextStore();
142
+ return fallbackRequestContextStore;
143
+ }
144
+ function runWithDynamicResolutionFallbackContext(context, callback) {
145
+ dynamicResolutionFallbackStack.push(context);
146
+ try {
147
+ const result = callback();
148
+ void resolveRequestContextStore();
149
+ if (isPromiseLike(result)) {
150
+ return result.finally(() => {
151
+ removeDynamicResolutionFallbackContext(context);
152
+ });
153
+ }
154
+ removeDynamicResolutionFallbackContext(context);
155
+ return result;
156
+ } catch (error) {
157
+ removeDynamicResolutionFallbackContext(context);
158
+ throw error;
159
+ }
160
+ }
161
+ function getDynamicResolutionFallbackContext() {
162
+ if (dynamicResolutionFallbackStack.length !== 1) {
163
+ return undefined;
164
+ }
165
+ return dynamicResolutionFallbackStack[0];
166
+ }
167
+ function removeDynamicResolutionFallbackContext(context) {
168
+ const index = dynamicResolutionFallbackStack.lastIndexOf(context);
169
+ if (index >= 0) {
170
+ dynamicResolutionFallbackStack.splice(index, 1);
171
+ }
172
+ }
173
+ function isPromiseLike(value) {
174
+ return typeof value === 'object' && value !== null && 'then' in value && 'finally' in value;
175
+ }
176
+ function isAsyncCallback(callback) {
177
+ return callback.constructor.name === 'AsyncFunction';
101
178
  }
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.1",
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"