@b9g/async-context 0.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.md +256 -0
- package/package.json +41 -0
- package/src/index.d.ts +79 -0
- package/src/index.js +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# @b9g/async-context
|
|
2
|
+
|
|
3
|
+
Lightweight polyfill for the [TC39 AsyncContext proposal](https://github.com/tc39/proposal-async-context) using Node.js `AsyncLocalStorage`.
|
|
4
|
+
|
|
5
|
+
## Why This Package?
|
|
6
|
+
|
|
7
|
+
The TC39 AsyncContext proposal aims to standardize async context propagation in JavaScript. However:
|
|
8
|
+
|
|
9
|
+
- The proposal is still Stage 2 (not yet standardized)
|
|
10
|
+
- No native browser/runtime support yet
|
|
11
|
+
- Node.js already has `AsyncLocalStorage` which solves the same problem
|
|
12
|
+
|
|
13
|
+
This package provides a **lightweight, maintainable polyfill** that:
|
|
14
|
+
|
|
15
|
+
✅ Implements the TC39 `AsyncContext.Variable` API
|
|
16
|
+
✅ Uses battle-tested `AsyncLocalStorage` under the hood
|
|
17
|
+
✅ Zero dependencies (beyond Node.js built-ins)
|
|
18
|
+
✅ Full TypeScript support
|
|
19
|
+
✅ Production-ready and well-tested
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @b9g/async-context
|
|
25
|
+
# or
|
|
26
|
+
bun add @b9g/async-context
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Basic Example
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { AsyncContext } from "@b9g/async-context";
|
|
35
|
+
|
|
36
|
+
// Create a context variable
|
|
37
|
+
const userContext = new AsyncContext.Variable<User>();
|
|
38
|
+
|
|
39
|
+
// Set a value that propagates through async operations
|
|
40
|
+
userContext.run(currentUser, async () => {
|
|
41
|
+
await someAsyncOperation();
|
|
42
|
+
|
|
43
|
+
const user = userContext.get(); // returns currentUser
|
|
44
|
+
console.log(user.name);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Request Context (Web Server)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { AsyncVariable } from "@b9g/async-context";
|
|
52
|
+
|
|
53
|
+
interface RequestContext {
|
|
54
|
+
requestId: string;
|
|
55
|
+
userId?: string;
|
|
56
|
+
startTime: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const requestContext = new AsyncVariable<RequestContext>();
|
|
60
|
+
|
|
61
|
+
// In your request handler
|
|
62
|
+
async function handleRequest(request: Request) {
|
|
63
|
+
return requestContext.run(
|
|
64
|
+
{
|
|
65
|
+
requestId: crypto.randomUUID(),
|
|
66
|
+
userId: await getUserId(request),
|
|
67
|
+
startTime: Date.now(),
|
|
68
|
+
},
|
|
69
|
+
async () => {
|
|
70
|
+
// Context is available throughout the async call chain
|
|
71
|
+
await authenticate();
|
|
72
|
+
const result = await processRequest();
|
|
73
|
+
await logMetrics();
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logMetrics() {
|
|
80
|
+
const ctx = requestContext.get();
|
|
81
|
+
const duration = Date.now() - ctx.startTime;
|
|
82
|
+
console.log(`Request ${ctx.requestId} took ${duration}ms`);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Multiple Independent Contexts
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const userContext = new AsyncVariable<User>();
|
|
90
|
+
const tenantContext = new AsyncVariable<Tenant>();
|
|
91
|
+
|
|
92
|
+
userContext.run(user, () => {
|
|
93
|
+
tenantContext.run(tenant, async () => {
|
|
94
|
+
// Both contexts are available
|
|
95
|
+
const currentUser = userContext.get();
|
|
96
|
+
const currentTenant = tenantContext.get();
|
|
97
|
+
|
|
98
|
+
await doWork(currentUser, currentTenant);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Default Values
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const themeContext = new AsyncVariable<string>({
|
|
107
|
+
defaultValue: "light"
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(themeContext.get()); // "light"
|
|
111
|
+
|
|
112
|
+
themeContext.run("dark", () => {
|
|
113
|
+
console.log(themeContext.get()); // "dark"
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(themeContext.get()); // "light"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Exports
|
|
120
|
+
|
|
121
|
+
### Classes
|
|
122
|
+
|
|
123
|
+
- `AsyncVariable<T>` - Main class for creating async context variables
|
|
124
|
+
- `AsyncContext.Variable<T>` - Alias matching TC39 proposal namespace
|
|
125
|
+
|
|
126
|
+
### Types
|
|
127
|
+
|
|
128
|
+
- `AsyncVariableOptions<T>` - Options for AsyncVariable constructor (defaultValue, name)
|
|
129
|
+
|
|
130
|
+
### Namespaces
|
|
131
|
+
|
|
132
|
+
- `AsyncContext` - Namespace containing Variable class (TC39 API)
|
|
133
|
+
|
|
134
|
+
### Default Export
|
|
135
|
+
|
|
136
|
+
- `AsyncContext` - The AsyncContext namespace
|
|
137
|
+
|
|
138
|
+
## API
|
|
139
|
+
|
|
140
|
+
### `AsyncVariable<T>`
|
|
141
|
+
|
|
142
|
+
Main class for creating async context variables.
|
|
143
|
+
|
|
144
|
+
#### `constructor(options?: AsyncVariableOptions<T>)`
|
|
145
|
+
|
|
146
|
+
Options:
|
|
147
|
+
- `defaultValue?: T` - Default value when no context is set
|
|
148
|
+
- `name?: string` - Optional name for debugging
|
|
149
|
+
|
|
150
|
+
#### `run<R>(value: T, fn: () => R): R`
|
|
151
|
+
|
|
152
|
+
Execute a function with a context value. The value is available via `get()` throughout the entire async execution of `fn`.
|
|
153
|
+
|
|
154
|
+
**Parameters:**
|
|
155
|
+
- `value: T` - The context value to set
|
|
156
|
+
- `fn: () => R` - Function to execute (can be sync or async)
|
|
157
|
+
|
|
158
|
+
**Returns:** The return value of `fn`
|
|
159
|
+
|
|
160
|
+
#### `get(): T | undefined`
|
|
161
|
+
|
|
162
|
+
Get the current context value. Returns `defaultValue` if no context is set.
|
|
163
|
+
|
|
164
|
+
#### `name: string | undefined`
|
|
165
|
+
|
|
166
|
+
Get the name of this variable (for debugging).
|
|
167
|
+
|
|
168
|
+
### `AsyncContext.Variable<T>`
|
|
169
|
+
|
|
170
|
+
Alias for `AsyncVariable<T>` that matches the TC39 proposal namespace.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { AsyncContext } from "@b9g/async-context";
|
|
174
|
+
|
|
175
|
+
const ctx = new AsyncContext.Variable<string>();
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## How It Works
|
|
179
|
+
|
|
180
|
+
This polyfill wraps Node.js's `AsyncLocalStorage` to provide the TC39 AsyncContext API:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// AsyncContext API (this polyfill)
|
|
184
|
+
const ctx = new AsyncContext.Variable<number>();
|
|
185
|
+
ctx.run(42, () => {
|
|
186
|
+
console.log(ctx.get()); // 42
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// AsyncLocalStorage (Node.js native)
|
|
190
|
+
const storage = new AsyncLocalStorage<number>();
|
|
191
|
+
storage.run(42, () => {
|
|
192
|
+
console.log(storage.getStore()); // 42
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The polyfill provides:
|
|
197
|
+
- Cleaner API matching the future standard
|
|
198
|
+
- Default value support
|
|
199
|
+
- Better TypeScript types
|
|
200
|
+
- Future-proof (easy migration when AsyncContext lands in browsers)
|
|
201
|
+
|
|
202
|
+
## Runtime Support
|
|
203
|
+
|
|
204
|
+
This package works in any JavaScript runtime that supports `AsyncLocalStorage`:
|
|
205
|
+
|
|
206
|
+
- ✅ Node.js 12.17+ (native support)
|
|
207
|
+
- ✅ Bun (native support)
|
|
208
|
+
- ✅ Cloudflare Workers (via Node.js compatibility)
|
|
209
|
+
- ⚠️ Deno (via Node.js compatibility layer: `import { AsyncLocalStorage } from "node:async_hooks"`)
|
|
210
|
+
|
|
211
|
+
## Differences from TC39 Proposal
|
|
212
|
+
|
|
213
|
+
This polyfill currently implements:
|
|
214
|
+
|
|
215
|
+
- ✅ `AsyncContext.Variable`
|
|
216
|
+
- ✅ `.run(value, fn)` method
|
|
217
|
+
- ✅ `.get()` method
|
|
218
|
+
|
|
219
|
+
Not yet implemented (future additions):
|
|
220
|
+
|
|
221
|
+
- ⏳ `AsyncContext.Snapshot`
|
|
222
|
+
- ⏳ `AsyncContext.Mapping`
|
|
223
|
+
|
|
224
|
+
These may be added in future versions as the proposal evolves.
|
|
225
|
+
|
|
226
|
+
## Migration Path
|
|
227
|
+
|
|
228
|
+
### From `AsyncLocalStorage`
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// Before
|
|
232
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
233
|
+
const storage = new AsyncLocalStorage<User>();
|
|
234
|
+
|
|
235
|
+
storage.run(user, () => {
|
|
236
|
+
const current = storage.getStore();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// After
|
|
240
|
+
import { AsyncVariable } from "@b9g/async-context";
|
|
241
|
+
const userContext = new AsyncVariable<User>();
|
|
242
|
+
|
|
243
|
+
userContext.run(user, () => {
|
|
244
|
+
const current = userContext.get();
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
251
|
+
|
|
252
|
+
## See Also
|
|
253
|
+
|
|
254
|
+
- [TC39 AsyncContext Proposal](https://github.com/tc39/proposal-async-context)
|
|
255
|
+
- [Node.js AsyncLocalStorage](https://nodejs.org/api/async_context.html#class-asynclocalstorage)
|
|
256
|
+
- [Shovel Framework](https://github.com/b9g/shovel)
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@b9g/async-context",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Lightweight AsyncContext polyfill for JavaScript runtimes. Implements TC39 AsyncContext proposal using AsyncLocalStorage.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"asynccontext",
|
|
7
|
+
"async-context",
|
|
8
|
+
"context",
|
|
9
|
+
"async-local-storage",
|
|
10
|
+
"asynclocalstorage",
|
|
11
|
+
"tc39",
|
|
12
|
+
"polyfill",
|
|
13
|
+
"async",
|
|
14
|
+
"continuation",
|
|
15
|
+
"shovel"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@b9g/libuild": "^0.1.11",
|
|
20
|
+
"@types/node": "^20.0.0",
|
|
21
|
+
"bun-types": "latest"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"types": "src/index.d.ts",
|
|
25
|
+
"module": "src/index.js",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./src/index.d.ts",
|
|
29
|
+
"import": "./src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
"./index": {
|
|
33
|
+
"types": "./src/index.d.ts",
|
|
34
|
+
"import": "./src/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./index.js": {
|
|
37
|
+
"types": "./src/index.d.ts",
|
|
38
|
+
"import": "./src/index.js"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @b9g/async-context
|
|
3
|
+
*
|
|
4
|
+
* Lightweight polyfill for the TC39 AsyncContext proposal
|
|
5
|
+
* https://github.com/tc39/proposal-async-context
|
|
6
|
+
*
|
|
7
|
+
* This implementation uses Node.js AsyncLocalStorage under the hood
|
|
8
|
+
* to provide async context propagation across promise chains and async callbacks.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Options for creating an AsyncContext.Variable
|
|
12
|
+
*/
|
|
13
|
+
export interface AsyncVariableOptions<T> {
|
|
14
|
+
/**
|
|
15
|
+
* Default value returned when no context value is set
|
|
16
|
+
*/
|
|
17
|
+
defaultValue?: T;
|
|
18
|
+
/**
|
|
19
|
+
* Optional name for debugging purposes
|
|
20
|
+
*/
|
|
21
|
+
name?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* AsyncContext.Variable - stores a value that propagates through async operations
|
|
25
|
+
*
|
|
26
|
+
* Based on the TC39 AsyncContext proposal (Stage 2)
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const userContext = new AsyncVariable<User>();
|
|
31
|
+
*
|
|
32
|
+
* userContext.run(currentUser, async () => {
|
|
33
|
+
* await someAsyncOperation();
|
|
34
|
+
* const user = userContext.get(); // returns currentUser
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class AsyncVariable<T> {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(options?: AsyncVariableOptions<T>);
|
|
41
|
+
/**
|
|
42
|
+
* Execute a function with a context value
|
|
43
|
+
* The value is available via get() throughout the entire async execution
|
|
44
|
+
*
|
|
45
|
+
* @param value - The context value to set
|
|
46
|
+
* @param fn - The function to execute with the context
|
|
47
|
+
* @returns The return value of fn
|
|
48
|
+
*/
|
|
49
|
+
run<R>(value: T, fn: () => R): R;
|
|
50
|
+
/**
|
|
51
|
+
* Get the current context value
|
|
52
|
+
* Returns the default value if no context is set
|
|
53
|
+
*
|
|
54
|
+
* @returns The current context value or default value
|
|
55
|
+
*/
|
|
56
|
+
get(): T | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Get the current context value (AsyncLocalStorage-compatible)
|
|
59
|
+
* This method provides compatibility with libraries expecting AsyncLocalStorage
|
|
60
|
+
*
|
|
61
|
+
* @returns The current context value (without default value)
|
|
62
|
+
*/
|
|
63
|
+
getStore(): T | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Get the name of this variable (for debugging)
|
|
66
|
+
*/
|
|
67
|
+
get name(): string | undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Namespace matching the TC39 AsyncContext proposal
|
|
71
|
+
*/
|
|
72
|
+
export declare namespace AsyncContext {
|
|
73
|
+
/**
|
|
74
|
+
* AsyncContext.Variable - stores a value that propagates through async operations
|
|
75
|
+
*/
|
|
76
|
+
class Variable<T> extends AsyncVariable<T> {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export default AsyncContext;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/// <reference types="./index.d.ts" />
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
var AsyncVariable = class {
|
|
5
|
+
#storage;
|
|
6
|
+
#defaultValue;
|
|
7
|
+
#name;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.#storage = new AsyncLocalStorage();
|
|
10
|
+
this.#defaultValue = options?.defaultValue;
|
|
11
|
+
this.#name = options?.name;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Execute a function with a context value
|
|
15
|
+
* The value is available via get() throughout the entire async execution
|
|
16
|
+
*
|
|
17
|
+
* @param value - The context value to set
|
|
18
|
+
* @param fn - The function to execute with the context
|
|
19
|
+
* @returns The return value of fn
|
|
20
|
+
*/
|
|
21
|
+
run(value, fn) {
|
|
22
|
+
return this.#storage.run(value, fn);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the current context value
|
|
26
|
+
* Returns the default value if no context is set
|
|
27
|
+
*
|
|
28
|
+
* @returns The current context value or default value
|
|
29
|
+
*/
|
|
30
|
+
get() {
|
|
31
|
+
const value = this.#storage.getStore();
|
|
32
|
+
return value !== void 0 ? value : this.#defaultValue;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the current context value (AsyncLocalStorage-compatible)
|
|
36
|
+
* This method provides compatibility with libraries expecting AsyncLocalStorage
|
|
37
|
+
*
|
|
38
|
+
* @returns The current context value (without default value)
|
|
39
|
+
*/
|
|
40
|
+
getStore() {
|
|
41
|
+
return this.#storage.getStore();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the name of this variable (for debugging)
|
|
45
|
+
*/
|
|
46
|
+
get name() {
|
|
47
|
+
return this.#name;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var AsyncContext;
|
|
51
|
+
((AsyncContext2) => {
|
|
52
|
+
class Variable extends AsyncVariable {
|
|
53
|
+
}
|
|
54
|
+
AsyncContext2.Variable = Variable;
|
|
55
|
+
})(AsyncContext || (AsyncContext = {}));
|
|
56
|
+
var src_default = AsyncContext;
|
|
57
|
+
export {
|
|
58
|
+
AsyncContext,
|
|
59
|
+
AsyncVariable,
|
|
60
|
+
src_default as default
|
|
61
|
+
};
|