Skip to content

Commit 67c76e2

Browse files
committed
✨ [node] 新增了acontext
1 parent 4720343 commit 67c76e2

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

node/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"name": "@gaubee/node",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"exports": {
5+
"./acontext": "./src/acontext/acontext.ts",
56
"./env": "./src/env.ts",
67
"./path": "./src/path.ts",
78
"./promise": "./src/promise.ts",

node/src/acontext/acontext.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { type AsyncContextStorage, createAsyncContextStorage } from "./async-context.ts";
2+
3+
export type ReadonlyAcontext<T> = {
4+
readonly name: string;
5+
get(): T;
6+
getOrNull(): T | null;
7+
getOrElse<TElse>(else_fn: () => TElse): T | TElse;
8+
};
9+
10+
export type Acontext<T> = ReadonlyAcontext<T> & {
11+
readonly default?: () => T;
12+
readonly storage: AsyncContextStorage<{ value: T }>;
13+
run<R>(value: T, cb: () => Promise<R>): Promise<R>;
14+
asReadonly(): ReadonlyAcontext<T>;
15+
};
16+
17+
export const createAcontext = <T extends unknown>(name: string, default_fn?: Acontext<T>["default"]): Acontext<T> => {
18+
const storage = createAsyncContextStorage<{ value: T }>();
19+
20+
const handle: Acontext<T> = {
21+
storage: storage,
22+
default: default_fn,
23+
name: name,
24+
getOrElse: (else_fn) => {
25+
const item = storage.getStore();
26+
if (item == null) {
27+
return default_fn ? default_fn() : else_fn();
28+
}
29+
return item?.value;
30+
},
31+
32+
get: () => {
33+
return handle.getOrElse(() => {
34+
throw new Error(`No factory registered for capability handle: ${name}`);
35+
});
36+
},
37+
getOrNull: () => {
38+
return handle.getOrElse(() => null);
39+
},
40+
run: (value, cb) => {
41+
return storage.run({ value }, cb);
42+
},
43+
44+
asReadonly: () => {
45+
return {
46+
name: handle.name,
47+
get: handle.get,
48+
getOrNull: handle.getOrNull,
49+
getOrElse: handle.getOrElse,
50+
};
51+
},
52+
};
53+
54+
return handle;
55+
};
56+
57+
type AContextValue<T> = T extends Acontext<infer R> ? R : never;
58+
59+
type AContextsValues<T extends Array<any>> = T extends []
60+
? []
61+
: T extends [infer Head, ...infer Tail]
62+
? [AContextValue<Head>, ...AContextsValues<Tail>]
63+
: T extends Array<infer Head>
64+
? AContextValue<Head>[]
65+
: never;
66+
67+
export const useAcontexts = <const ACS extends Array<Acontext<any>>>(ctxs: ACS) => {
68+
return <R extends unknown>(values: AContextsValues<ACS>, run: () => Promise<R>) => {
69+
// 递归的内部辅助函数
70+
const runner = async (index: number): Promise<R> => {
71+
// 基本情况:如果所有上下文都已应用,则执行最终的 run 函数
72+
if (index >= ctxs.length) {
73+
return run();
74+
}
75+
76+
// 获取当前的 Acontext 和要设置的值
77+
const context = ctxs[index];
78+
const value = values[index];
79+
80+
// 递归步骤:在当前上下文中运行,并递归调用 runner 处理下一个上下文
81+
return context.run(value, () => runner(index + 1));
82+
};
83+
84+
// 从第一个上下文开始启动递归
85+
return runner(0);
86+
};
87+
};
88+
89+
export class AcontextMap {
90+
#map = new Map<Acontext<any>, any>();
91+
set<T extends Acontext<unknown>>(context: T, value: AContextValue<T>) {
92+
this.#map.set(context, value);
93+
return this;
94+
}
95+
get<T>(context: Acontext<T>) {
96+
return this.#map.get(context);
97+
}
98+
has(context: Acontext<any>) {
99+
return this.#map.has(context);
100+
}
101+
run<T>(runner: () => Promise<T>) {
102+
return useAcontexts([...this.#map.keys()])([...this.#map.values()], runner);
103+
}
104+
}

node/src/acontext/async-context.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { AsyncLocalStorage } from "node:async_hooks";
2+
3+
/**
4+
* 异步上下文存储的通用接口,对齐 TC39 proposal-async-context。
5+
* 这层抽象允许我们在不同 JS 运行时之间切换实现。
6+
*/
7+
export interface AsyncContextStorage<T> {
8+
/**
9+
* 创建并进入一个新的异步上下文。
10+
* @param store 上下文中要存储的数据
11+
* @param callback 在此上下文中执行的函数
12+
*/
13+
run<R>(store: T, callback: () => R): R;
14+
15+
/**
16+
* 获取当前异步上下文中的数据。
17+
* @returns 如果在上下文中,则返回存储的数据;否则返回 undefined。
18+
*/
19+
getStore(): T | undefined;
20+
}
21+
22+
/**
23+
* 基于 Node.js 原生 AsyncLocalStorage 的实现。
24+
*/
25+
class NodeAsyncContextStorage<T> implements AsyncContextStorage<T> {
26+
private readonly als: AsyncLocalStorage<T>;
27+
28+
constructor() {
29+
this.als = new AsyncLocalStorage<T>();
30+
}
31+
32+
run<R>(store: T, callback: () => R): R {
33+
return this.als.run(store, callback);
34+
}
35+
36+
getStore(): T | undefined {
37+
return this.als.getStore();
38+
}
39+
}
40+
41+
/**
42+
* 一个简单的 Polyfill,用于不支持 AsyncLocalStorage 的环境。
43+
* 注意:这个 Polyfill 不支持跨异步任务的上下文传递,主要用于简化测试或基础场景。
44+
*/
45+
class FallbackAsyncContextStorage<T> implements AsyncContextStorage<T> {
46+
private currentStore: T | undefined = undefined;
47+
48+
run<R>(store: T, callback: () => R): R {
49+
const previousStore = this.currentStore;
50+
this.currentStore = store;
51+
try {
52+
return callback();
53+
} finally {
54+
this.currentStore = previousStore;
55+
}
56+
}
57+
58+
getStore(): T | undefined {
59+
return this.currentStore;
60+
}
61+
}
62+
63+
export const createAsyncContextStorage = <T,>(): AsyncContextStorage<T> => {
64+
// 优先使用 Node.js 的原生实现
65+
if (typeof AsyncLocalStorage === "function") {
66+
return new NodeAsyncContextStorage<T>();
67+
}
68+
// 否则使用备用方案
69+
console.warn("AsyncLocalStorage is not available. Falling back to a simple polyfill. Concurrency issues may occur.");
70+
return new FallbackAsyncContextStorage<T>();
71+
};

node/src/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./acontext/acontext.ts";
12
export * from "./env.ts";
23
export * from "./path.ts";
34
export * from "./promise.ts";

0 commit comments

Comments
 (0)