@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 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` API
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
- - `AsyncContext.Variable<T>` - Alias matching TC39 proposal namespace
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` - The AsyncContext namespace
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
- ### `AsyncContext.Variable<T>`
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
- Alias for `AsyncVariable<T>` that matches the TC39 proposal namespace.
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
- import { AsyncContext } from "@b9g/async-context";
190
+ const userVar = new AsyncContext.Variable<string>();
174
191
 
175
- const ctx = new AsyncContext.Variable<string>();
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 currently implements:
214
-
215
- - ✅ `AsyncContext.Variable`
216
- - ✅ `.run(value, fn)` method
217
- - ✅ `.get()` method
237
+ This polyfill implements the core TC39 AsyncContext API:
218
238
 
219
- Not yet implemented (future additions):
239
+ - `AsyncContext.Variable` - context variables with `run()` and `get()`
240
+ - ✅ `AsyncContext.Snapshot` - context capture with `run()` and `wrap()`
220
241
 
221
- - `AsyncContext.Snapshot`
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
- These may be added in future versions as the proposal evolves.
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.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.11",
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 AsyncVariable<User>();
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
- * Namespace matching the TC39 AsyncContext proposal
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 namespace AsyncContext {
101
+ export declare class AsyncSnapshot {
102
+ #private;
103
+ constructor();
73
104
  /**
74
- * AsyncContext.Variable - stores a value that propagates through async operations
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
- class Variable<T> extends AsyncVariable<T> {
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 AsyncContext;
51
- ((AsyncContext2) => {
52
- class Variable extends AsyncVariable {
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
- AsyncContext2.Variable = Variable;
55
- })(AsyncContext || (AsyncContext = {}));
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
  };