@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 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
+ };