@b9g/async-context 0.1.1 → 0.1.3
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.md +42 -22
- package/package.json +2 -2
- package/src/index.d.ts +72 -7
- package/src/index.js +72 -7
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The TC39 AsyncContext proposal aims to standardize async context propagation in
|
|
|
12
12
|
|
|
13
13
|
This package provides a **lightweight, maintainable polyfill** that:
|
|
14
14
|
|
|
15
|
-
✅ Implements the TC39 `AsyncContext.Variable`
|
|
15
|
+
✅ Implements the TC39 `AsyncContext.Variable` and `AsyncContext.Snapshot` APIs
|
|
16
16
|
✅ Uses battle-tested `AsyncLocalStorage` under the hood
|
|
17
17
|
✅ Zero dependencies (beyond Node.js built-ins)
|
|
18
18
|
✅ Full TypeScript support
|
|
@@ -121,19 +121,17 @@ console.log(themeContext.get()); // "light"
|
|
|
121
121
|
### Classes
|
|
122
122
|
|
|
123
123
|
- `AsyncVariable<T>` - Main class for creating async context variables
|
|
124
|
-
- `
|
|
124
|
+
- `AsyncSnapshot` - Captures and restores context state
|
|
125
|
+
- `AsyncContext.Variable<T>` - Alias for AsyncVariable (TC39 API)
|
|
126
|
+
- `AsyncContext.Snapshot` - Alias for AsyncSnapshot (TC39 API)
|
|
125
127
|
|
|
126
128
|
### Types
|
|
127
129
|
|
|
128
130
|
- `AsyncVariableOptions<T>` - Options for AsyncVariable constructor (defaultValue, name)
|
|
129
131
|
|
|
130
|
-
### Namespaces
|
|
131
|
-
|
|
132
|
-
- `AsyncContext` - Namespace containing Variable class (TC39 API)
|
|
133
|
-
|
|
134
132
|
### Default Export
|
|
135
133
|
|
|
136
|
-
- `AsyncContext` -
|
|
134
|
+
- `AsyncContext` - Object containing Variable and Snapshot classes
|
|
137
135
|
|
|
138
136
|
## API
|
|
139
137
|
|
|
@@ -147,13 +145,14 @@ Options:
|
|
|
147
145
|
- `defaultValue?: T` - Default value when no context is set
|
|
148
146
|
- `name?: string` - Optional name for debugging
|
|
149
147
|
|
|
150
|
-
#### `run<R>(value: T, fn: () => R): R`
|
|
148
|
+
#### `run<R>(value: T, fn: (...args) => R, ...args): R`
|
|
151
149
|
|
|
152
150
|
Execute a function with a context value. The value is available via `get()` throughout the entire async execution of `fn`.
|
|
153
151
|
|
|
154
152
|
**Parameters:**
|
|
155
153
|
- `value: T` - The context value to set
|
|
156
|
-
- `fn: () => R` - Function to execute (can be sync or async)
|
|
154
|
+
- `fn: (...args) => R` - Function to execute (can be sync or async)
|
|
155
|
+
- `...args` - Additional arguments to pass to fn
|
|
157
156
|
|
|
158
157
|
**Returns:** The return value of `fn`
|
|
159
158
|
|
|
@@ -165,14 +164,39 @@ Get the current context value. Returns `defaultValue` if no context is set.
|
|
|
165
164
|
|
|
166
165
|
Get the name of this variable (for debugging).
|
|
167
166
|
|
|
168
|
-
### `
|
|
167
|
+
### `AsyncSnapshot`
|
|
168
|
+
|
|
169
|
+
Captures the current values of all Variables at construction time. Use `run()` to restore that state later.
|
|
170
|
+
|
|
171
|
+
#### `constructor()`
|
|
172
|
+
|
|
173
|
+
Creates a snapshot of all current Variable values.
|
|
169
174
|
|
|
170
|
-
|
|
175
|
+
#### `run<R>(fn: (...args) => R, ...args): R`
|
|
176
|
+
|
|
177
|
+
Execute a function with the captured context values restored.
|
|
178
|
+
|
|
179
|
+
**Parameters:**
|
|
180
|
+
- `fn: (...args) => R` - Function to execute
|
|
181
|
+
- `...args` - Additional arguments to pass to fn
|
|
182
|
+
|
|
183
|
+
**Returns:** The return value of `fn`
|
|
184
|
+
|
|
185
|
+
#### `static wrap<F>(fn: F): F`
|
|
186
|
+
|
|
187
|
+
Wrap a function to preserve the current context. When the wrapped function is called later, it will execute with the context values that were active when `wrap()` was called.
|
|
171
188
|
|
|
172
189
|
```typescript
|
|
173
|
-
|
|
190
|
+
const userVar = new AsyncContext.Variable<string>();
|
|
174
191
|
|
|
175
|
-
const
|
|
192
|
+
const wrappedFn = userVar.run("alice", () => {
|
|
193
|
+
return AsyncContext.Snapshot.wrap(() => {
|
|
194
|
+
return userVar.get();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Later, even outside the run() context:
|
|
199
|
+
wrappedFn(); // returns "alice"
|
|
176
200
|
```
|
|
177
201
|
|
|
178
202
|
## How It Works
|
|
@@ -210,18 +234,14 @@ This package works in any JavaScript runtime that supports `AsyncLocalStorage`:
|
|
|
210
234
|
|
|
211
235
|
## Differences from TC39 Proposal
|
|
212
236
|
|
|
213
|
-
This polyfill
|
|
214
|
-
|
|
215
|
-
- ✅ `AsyncContext.Variable`
|
|
216
|
-
- ✅ `.run(value, fn)` method
|
|
217
|
-
- ✅ `.get()` method
|
|
237
|
+
This polyfill implements the core TC39 AsyncContext API:
|
|
218
238
|
|
|
219
|
-
|
|
239
|
+
- ✅ `AsyncContext.Variable` - context variables with `run()` and `get()`
|
|
240
|
+
- ✅ `AsyncContext.Snapshot` - context capture with `run()` and `wrap()`
|
|
220
241
|
|
|
221
|
-
-
|
|
222
|
-
- ⏳ `AsyncContext.Mapping`
|
|
242
|
+
The implementation uses Node.js `AsyncLocalStorage` rather than the pure-JS reference implementation, which means async context propagation works natively without monkey-patching `Promise.prototype.then`.
|
|
223
243
|
|
|
224
|
-
|
|
244
|
+
Test suite adapted from the [TC39 proposal repository](https://github.com/tc39/proposal-async-context).
|
|
225
245
|
|
|
226
246
|
## Migration Path
|
|
227
247
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/async-context",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Lightweight AsyncContext polyfill for JavaScript runtimes. Implements TC39 AsyncContext proposal using AsyncLocalStorage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"asynccontext",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@b9g/libuild": "^0.1.
|
|
19
|
+
"@b9g/libuild": "^0.1.18",
|
|
20
20
|
"@types/node": "^20.0.0",
|
|
21
21
|
"bun-types": "latest"
|
|
22
22
|
},
|
package/src/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* This implementation uses Node.js AsyncLocalStorage under the hood
|
|
8
8
|
* to provide async context propagation across promise chains and async callbacks.
|
|
9
9
|
*/
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
11
|
/**
|
|
11
12
|
* Options for creating an AsyncContext.Variable
|
|
12
13
|
*/
|
|
@@ -27,7 +28,7 @@ export interface AsyncVariableOptions<T> {
|
|
|
27
28
|
*
|
|
28
29
|
* @example
|
|
29
30
|
* ```ts
|
|
30
|
-
* const userContext = new
|
|
31
|
+
* const userContext = new AsyncContext.Variable<User>();
|
|
31
32
|
*
|
|
32
33
|
* userContext.run(currentUser, async () => {
|
|
33
34
|
* await someAsyncOperation();
|
|
@@ -44,9 +45,10 @@ export declare class AsyncVariable<T> {
|
|
|
44
45
|
*
|
|
45
46
|
* @param value - The context value to set
|
|
46
47
|
* @param fn - The function to execute with the context
|
|
48
|
+
* @param args - Additional arguments to pass to fn
|
|
47
49
|
* @returns The return value of fn
|
|
48
50
|
*/
|
|
49
|
-
run<R>(value: T, fn: () => R): R;
|
|
51
|
+
run<R, Args extends unknown[]>(value: T, fn: (...args: Args) => R, ...args: Args): R;
|
|
50
52
|
/**
|
|
51
53
|
* Get the current context value
|
|
52
54
|
* Returns the default value if no context is set
|
|
@@ -65,15 +67,78 @@ export declare class AsyncVariable<T> {
|
|
|
65
67
|
* Get the name of this variable (for debugging)
|
|
66
68
|
*/
|
|
67
69
|
get name(): string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Internal: Get the underlying storage (used by Snapshot)
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
_getStorage(): AsyncLocalStorage<T>;
|
|
68
75
|
}
|
|
69
76
|
/**
|
|
70
|
-
*
|
|
77
|
+
* AsyncContext.Snapshot - captures the current values of all Variables
|
|
78
|
+
*
|
|
79
|
+
* A Snapshot captures the state of all AsyncContext.Variable instances at the
|
|
80
|
+
* time of construction. Later, calling `run()` restores that state for the
|
|
81
|
+
* duration of the callback.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const userVar = new AsyncContext.Variable<string>();
|
|
86
|
+
*
|
|
87
|
+
* userVar.run("alice", () => {
|
|
88
|
+
* const snapshot = new AsyncContext.Snapshot();
|
|
89
|
+
*
|
|
90
|
+
* // Later, in a different context...
|
|
91
|
+
* userVar.run("bob", () => {
|
|
92
|
+
* console.log(userVar.get()); // "bob"
|
|
93
|
+
*
|
|
94
|
+
* snapshot.run(() => {
|
|
95
|
+
* console.log(userVar.get()); // "alice"
|
|
96
|
+
* });
|
|
97
|
+
* });
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
71
100
|
*/
|
|
72
|
-
export declare
|
|
101
|
+
export declare class AsyncSnapshot {
|
|
102
|
+
#private;
|
|
103
|
+
constructor();
|
|
73
104
|
/**
|
|
74
|
-
*
|
|
105
|
+
* Execute a function with the captured context values
|
|
106
|
+
*
|
|
107
|
+
* @param fn - The function to execute
|
|
108
|
+
* @param args - Additional arguments to pass to fn
|
|
109
|
+
* @returns The return value of fn
|
|
75
110
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
111
|
+
run<R, Args extends unknown[]>(fn: (...args: Args) => R, ...args: Args): R;
|
|
112
|
+
/**
|
|
113
|
+
* Wrap a function to capture the current context
|
|
114
|
+
*
|
|
115
|
+
* Creates a new function that, when called, will execute with the
|
|
116
|
+
* context values that were active when wrap() was called.
|
|
117
|
+
*
|
|
118
|
+
* @param fn - The function to wrap
|
|
119
|
+
* @returns A wrapped function that preserves context
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const userVar = new AsyncContext.Variable<string>();
|
|
124
|
+
*
|
|
125
|
+
* const wrappedFn = userVar.run("alice", () => {
|
|
126
|
+
* return AsyncContext.Snapshot.wrap(() => {
|
|
127
|
+
* return userVar.get();
|
|
128
|
+
* });
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* // Later, even outside the run() context:
|
|
132
|
+
* wrappedFn(); // returns "alice"
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
static wrap<T, A extends unknown[], R>(fn: (this: T, ...args: A) => R): (this: T, ...args: A) => R;
|
|
78
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* AsyncContext object matching the TC39 AsyncContext proposal
|
|
139
|
+
*/
|
|
140
|
+
export declare const AsyncContext: {
|
|
141
|
+
Variable: typeof AsyncVariable;
|
|
142
|
+
Snapshot: typeof AsyncSnapshot;
|
|
143
|
+
};
|
|
79
144
|
export default AsyncContext;
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
2
|
// src/index.ts
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
var variableRegistry = /* @__PURE__ */ new Set();
|
|
5
|
+
var NO_VALUE = Symbol("NO_VALUE");
|
|
4
6
|
var AsyncVariable = class {
|
|
5
7
|
#storage;
|
|
6
8
|
#defaultValue;
|
|
@@ -9,6 +11,7 @@ var AsyncVariable = class {
|
|
|
9
11
|
this.#storage = new AsyncLocalStorage();
|
|
10
12
|
this.#defaultValue = options?.defaultValue;
|
|
11
13
|
this.#name = options?.name;
|
|
14
|
+
variableRegistry.add(this);
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
17
|
* Execute a function with a context value
|
|
@@ -16,10 +19,11 @@ var AsyncVariable = class {
|
|
|
16
19
|
*
|
|
17
20
|
* @param value - The context value to set
|
|
18
21
|
* @param fn - The function to execute with the context
|
|
22
|
+
* @param args - Additional arguments to pass to fn
|
|
19
23
|
* @returns The return value of fn
|
|
20
24
|
*/
|
|
21
|
-
run(value, fn) {
|
|
22
|
-
return this.#storage.run(value, fn);
|
|
25
|
+
run(value, fn, ...args) {
|
|
26
|
+
return this.#storage.run(value, fn, ...args);
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* Get the current context value
|
|
@@ -46,16 +50,77 @@ var AsyncVariable = class {
|
|
|
46
50
|
get name() {
|
|
47
51
|
return this.#name;
|
|
48
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Internal: Get the underlying storage (used by Snapshot)
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
_getStorage() {
|
|
58
|
+
return this.#storage;
|
|
59
|
+
}
|
|
49
60
|
};
|
|
50
|
-
var
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
var AsyncSnapshot = class _AsyncSnapshot {
|
|
62
|
+
#captured;
|
|
63
|
+
constructor() {
|
|
64
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const variable of variableRegistry) {
|
|
66
|
+
const value = variable.getStore();
|
|
67
|
+
this.#captured.set(variable, value !== void 0 ? value : NO_VALUE);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Execute a function with the captured context values
|
|
72
|
+
*
|
|
73
|
+
* @param fn - The function to execute
|
|
74
|
+
* @param args - Additional arguments to pass to fn
|
|
75
|
+
* @returns The return value of fn
|
|
76
|
+
*/
|
|
77
|
+
run(fn, ...args) {
|
|
78
|
+
let result = () => fn(...args);
|
|
79
|
+
for (const [variable, value] of this.#captured) {
|
|
80
|
+
const prev = result;
|
|
81
|
+
const actualValue = value === NO_VALUE ? void 0 : value;
|
|
82
|
+
result = () => variable._getStorage().run(actualValue, prev);
|
|
83
|
+
}
|
|
84
|
+
return result();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Wrap a function to capture the current context
|
|
88
|
+
*
|
|
89
|
+
* Creates a new function that, when called, will execute with the
|
|
90
|
+
* context values that were active when wrap() was called.
|
|
91
|
+
*
|
|
92
|
+
* @param fn - The function to wrap
|
|
93
|
+
* @returns A wrapped function that preserves context
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* const userVar = new AsyncContext.Variable<string>();
|
|
98
|
+
*
|
|
99
|
+
* const wrappedFn = userVar.run("alice", () => {
|
|
100
|
+
* return AsyncContext.Snapshot.wrap(() => {
|
|
101
|
+
* return userVar.get();
|
|
102
|
+
* });
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Later, even outside the run() context:
|
|
106
|
+
* wrappedFn(); // returns "alice"
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
static wrap(fn) {
|
|
110
|
+
const snapshot = new _AsyncSnapshot();
|
|
111
|
+
return function(...args) {
|
|
112
|
+
return snapshot.run(() => fn.apply(this, args));
|
|
113
|
+
};
|
|
53
114
|
}
|
|
54
|
-
|
|
55
|
-
|
|
115
|
+
};
|
|
116
|
+
var AsyncContext = {
|
|
117
|
+
Variable: AsyncVariable,
|
|
118
|
+
Snapshot: AsyncSnapshot
|
|
119
|
+
};
|
|
56
120
|
var src_default = AsyncContext;
|
|
57
121
|
export {
|
|
58
122
|
AsyncContext,
|
|
123
|
+
AsyncSnapshot,
|
|
59
124
|
AsyncVariable,
|
|
60
125
|
src_default as default
|
|
61
126
|
};
|