Skip to content

Commit d269f3a

Browse files
legendecasandreubotellajridgewell
authored
Add AsyncContext namespace and Snapshot and Variable (tc39#55)
Co-authored-by: Andreu Botella <[email protected]> Co-authored-by: Justin Ridgewell <[email protected]>
1 parent f167ac1 commit d269f3a

File tree

3 files changed

+331
-327
lines changed

3 files changed

+331
-327
lines changed

README.md

+57-54
Original file line numberDiff line numberDiff line change
@@ -154,81 +154,83 @@ Non-goals:
154154
logically-connected sync/async code execution.
155155

156156
```typescript
157-
class AsyncContext<T> {
158-
static wrap<R>(callback: (...args: any[]) => R): (...args: any[]) => R;
157+
namespace AsyncContext {
158+
class Variable<T> {
159+
constructor(options: AsyncVariableOptions<T>);
159160

160-
constructor(options: AsyncContextOptions<T>);
161+
get name(): string;
161162

162-
get name(): string;
163+
run<R>(value: T, fn: (...args: any[])=> R, ...args: any[]): R;
163164

164-
run<R>(value: T, callback: () => R): R;
165+
get(): T | undefined;
166+
}
165167

166-
get(): T | undefined;
167-
}
168+
interface AsyncVariableOptions<T> {
169+
name?: string;
170+
defaultValue?: T;
171+
}
172+
173+
class Snapshot {
174+
constructor();
168175

169-
interface AsyncContextOptions<T> {
170-
name?: string;
171-
defaultValue?: T;
176+
run<R>(fn: (...args: any[]) => R, ...args: any[]): R;
177+
}
172178
}
173179
```
174180

175-
`AsyncContext.prototype.run()` and `AsyncContext.prototype.get()` sets and gets
176-
the current value of an async execution flow. `AsyncContext.wrap()` allows you
177-
to opaquely capture the current value of all `AsyncContext`s and execute the
178-
callback at a later time with as if those values were still the current values
179-
(a snapshot and restore). Note that even with `AsyncContext.wrap()`, you can
180-
only access the value associated with an `AsyncContext` instance if you have
181+
`Variable.prototype.run()` and `Variable.prototype.get()` sets and gets
182+
the current value of an async execution flow. `Snapshot` allows you
183+
to opaquely capture the current value of all `Variable`s and execute a
184+
function at a later time with as if those values were still the current values
185+
(a snapshot and restore). Note that even with `Snapshot`, you can
186+
only access the value associated with an `Variable` instance if you have
181187
access to that instance.
182188

183189
```typescript
184-
const context = new AsyncContext();
190+
const asyncVar = new AsyncContext.Variable();
185191

186192
// Sets the current value to 'top', and executes the `main` function.
187-
context.run("top", main);
193+
asyncVar.run("top", main);
188194

189195
function main() {
190-
// Context is maintained through other platform queueing.
196+
// AsyncContext.Variable is maintained through other platform queueing.
191197
setTimeout(() => {
192-
console.log(context.get()); // => 'top'
198+
console.log(asyncVar.get()); // => 'top'
193199

194-
context.run("A", () => {
195-
console.log(context.get()); // => 'A'
200+
asyncVar.run("A", () => {
201+
console.log(asyncVar.get()); // => 'A'
196202

197203
setTimeout(() => {
198-
console.log(context.get()); // => 'A'
204+
console.log(asyncVar.get()); // => 'A'
199205
}, randomTimeout());
200206
});
201207
}, randomTimeout());
202208

203-
// Context runs can be nested.
204-
context.run("B", () => {
205-
console.log(context.get()); // => 'B'
209+
// AsyncContext.Variable runs can be nested.
210+
asyncVar.run("B", () => {
211+
console.log(asyncVar.get()); // => 'B'
206212

207213
setTimeout(() => {
208-
console.log(context.get()); // => 'B'
214+
console.log(asyncVar.get()); // => 'B'
209215
}, randomTimeout());
210216
});
211217

212-
// Context was restored after the previous run.
213-
console.log(context.get()); // => 'top'
218+
// AsyncContext.Variable was restored after the previous run.
219+
console.log(asyncVar.get()); // => 'top'
214220

215-
// Captures the state of all AsyncContext's at this moment.
216-
const snapshotDuringTop = AsyncContext.wrap((cb) => {
217-
console.log(context.get()); // => 'top'
218-
cb();
219-
});
221+
// Captures the state of all AsyncContext.Variable's at this moment.
222+
const snapshotDuringTop = new AsyncContext.Snapshot();
220223

221-
// Context runs can be nested.
222-
context.run("C", () => {
223-
console.log(context.get()); // => 'C'
224+
asyncVar.run("C", () => {
225+
console.log(asyncVar.get()); // => 'C'
224226

225-
// The snapshotDuringTop will restore all AsyncContext to their snapshot
226-
// state and invoke the wrapped function. We pass a callback which it will
227+
// The snapshotDuringTop will restore all AsyncContext.Variable to their snapshot
228+
// state and invoke the wrapped function. We pass a function which it will
227229
// invoke.
228-
snapshotDuringTop(() => {
230+
snapshotDuringTop.run(() => {
229231
// Despite being lexically nested inside 'C', the snapshot restored us to
230232
// to the 'top' state.
231-
console.log(context.get()); // => 'top'
233+
console.log(asyncVar.get()); // => 'top'
232234
});
233235
});
234236
}
@@ -238,7 +240,7 @@ function randomTimeout() {
238240
}
239241
```
240242

241-
`AsyncContext.wrap` is useful for implementing APIs that logically "schedule" a
243+
`Snapshot` is useful for implementing APIs that logically "schedule" a
242244
callback, so the callback will be called with the context that it logically
243245
belongs to, regardless of the context under which it actually runs:
244246

@@ -247,7 +249,8 @@ let queue = [];
247249

248250
export function enqueueCallback(cb: () => void) {
249251
// Each callback is stored with the context at which it was enqueued.
250-
queue.push(AsyncContext.wrap(cb));
252+
const snapshot = new AsyncContext.Snapshot();
253+
queue.push(() => snapshot.run(cb));
251254
}
252255

253256
runWhenIdle(() => {
@@ -261,11 +264,11 @@ runWhenIdle(() => {
261264
```
262265

263266
> Note: There are controversial thought on the dynamic scoping and
264-
> `AsyncContext`, checkout [SCOPING.md][] for more details.
267+
> `Variable`, checkout [SCOPING.md][] for more details.
265268
266269
## Use cases
267270

268-
Use cases for `AsyncContext` include:
271+
Use cases for async context include:
269272

270273
- Annotating logs with information related to an asynchronous callstack.
271274

@@ -300,7 +303,7 @@ A detailed example usecase can be found [here](./USE-CASES.md)
300303
## Determine the initiator of a task
301304

302305
Application monitoring tools like OpenTelemetry save their tracing spans in the
303-
`AsyncContext` and retrieve the span when they need to determine what started
306+
`AsyncContext.Variable` and retrieve the span when they need to determine what started
304307
this chain of interaction.
305308

306309
These libraries can not intrude the developer APIs for seamless monitoring. The
@@ -309,20 +312,20 @@ tracing span doesn't need to be manually passing around by usercodes.
309312
```typescript
310313
// tracer.js
311314

312-
const context = new AsyncContext();
315+
const asyncVar = new AsyncContext.Variable();
313316
export function run(cb) {
314317
// (a)
315318
const span = {
316319
startTime: Date.now(),
317320
traceId: randomUUID(),
318321
spanId: randomUUID(),
319322
};
320-
context.run(span, cb);
323+
asyncVar.run(span, cb);
321324
}
322325

323326
export function end() {
324327
// (b)
325-
const span = context.get();
328+
const span = asyncVar.get();
326329
span?.endTime = Date.now();
327330
}
328331
```
@@ -358,20 +361,20 @@ concurrent multi-tracking.
358361

359362
## Transitive task attribution
360363

361-
User tasks can be scheduled with attributions. With `AsyncContext`, task
364+
User tasks can be scheduled with attributions. With `AsyncContext.Variable`, task
362365
attributions are propagated in the async task flow and sub-tasks can be
363366
scheduled with the same priority.
364367

365368
```typescript
366369
const scheduler = {
367-
context: new AsyncContext(),
370+
asyncVar: new AsyncContext.Variable(),
368371
postTask(task, options) {
369372
// In practice, the task execution may be deferred.
370-
// Here we simply run the task immediately with the context.
371-
return this.context.run({ priority: options.priority }, task);
373+
// Here we simply run the task immediately.
374+
return this.asyncVar.run({ priority: options.priority }, task);
372375
},
373376
currentTask() {
374-
return this.context.get() ?? { priority: "default" };
377+
return this.asyncVar.get() ?? { priority: "default" };
375378
},
376379
};
377380

SCOPING.md

+37-36
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
# Scoping of AsyncContext
1+
# Scoping of AsyncContext.Variable
22

3-
The major concerns of `AsyncContext` advancing to Stage 1 of TC39 proposal
3+
The major concerns of `AsyncContext.Variable` advancing to Stage 1 of TC39 proposal
44
process is that there are potential dynamic scoping of the semantics of
5-
`AsyncContext`. This document is about defining the scoping of `AsyncContext`.
5+
`AsyncContext.Variable`. This document is about defining the scoping of
6+
`AsyncContext.Variable`.
67

78
### Dynamic Scoping
89

@@ -22,61 +23,61 @@ $ echo $x # does this print 1, or 2?
2223
1
2324
```
2425

25-
However, the naming scope of an async context is identical to a regular variable
26+
However, the naming scope of an `AsyncContext.Variable` is identical to a regular variable
2627
in JavaScript. Since JavaScript variables are lexically scoped, the naming of
27-
async context instances are lexically scoped too. It is not possible to access a
28-
value inside an async context without explicit access to the async context
28+
`AsyncContext.Variable` instances are lexically scoped too. It is not possible to access a
29+
value inside an `AsyncContext.Variable` without explicit access to the `AsyncContext.Variable` instance
2930
itself.
3031

3132
```typescript
32-
const context = new AsyncContext();
33+
const asyncVar = new AsyncContext.Variable();
3334

34-
context.run(1, f);
35-
console.log(context.get()); // => undefined
35+
asyncVar.run(1, f);
36+
console.log(asyncVar.get()); // => undefined
3637

3738
function g() {
38-
console.log(context.get()); // => 1
39+
console.log(asyncVar.get()); // => 1
3940
}
4041

4142
function f() {
42-
// Intentionally named the same "context"
43-
const context = new AsyncContext();
44-
context.run(2, g);
43+
// Intentionally named the same "asyncVar"
44+
const asyncVar = new AsyncContext.Variable();
45+
asyncVar.run(2, g);
4546
}
4647
```
4748

48-
Hence, knowing the name of an async context variable does not give you the
49-
ability to change that context. You must have direct access to it in order to
50-
affect it.
49+
Hence, knowing the name of an `AsyncContext.Variable` variable does not give you the
50+
ability to change the value of that variable. You must have direct access to it
51+
in order to affect it.
5152

5253
```typescript
53-
const context = new AsyncContext();
54+
const asyncVar = new AsyncContext.Variable();
5455

55-
context.run(1, f);
56+
asyncVar.run(1, f);
5657

57-
console.log(context.get()); // => undefined;
58+
console.log(asyncVar.get()); // => undefined;
5859

5960
function f() {
60-
const context = new AsyncContext();
61-
context.run(2, g);
61+
const asyncVar = new AsyncContext.Variable();
62+
asyncVar.run(2, g);
6263

6364
function g() {
64-
console.log(context.get(); // => 2;
65+
console.log(asyncVar.get()); // => 2;
6566
}
6667
}
6768
```
6869

6970
### Dynamic Scoping: dependency on caller
7071

71-
One argument on the dynamic scoping is that the values in `AsyncContext` can be
72+
One argument on the dynamic scoping is that the values in `AsyncContext.Variable` can be
7273
changed depending on which the caller is.
7374

74-
However, the definition of whether the value of an async context can be changed
75+
However, the definition of whether the value of an `AsyncContext.Variable` can be changed
7576
has the same meaning with a regular JavaScript variable: anyone with direct
7677
access to a variable has the ability to change the variable.
7778

7879
```typescript
79-
class SyncContext {
80+
class SyncVariable {
8081
#current;
8182

8283
get() {
@@ -94,30 +95,30 @@ class SyncContext {
9495
}
9596
}
9697

97-
const context = new SyncContext();
98+
const syncVar = new SyncVariable();
9899

99-
context.run(1, f);
100+
syncVar.run(1, f);
100101

101-
console.log(context.get()); // => undefined;
102+
console.log(syncVar.get()); // => undefined;
102103

103104
function g() {
104-
console.log(context.get()); // => 1
105+
console.log(syncVar.get()); // => 1
105106
}
106107

107108
function f() {
108-
// Intentionally named the same "context"
109-
const context = new AsyncContext();
110-
context.run(2, g);
109+
// Intentionally named the same "syncVar"
110+
const syncVar = new AsyncContext.Variable();
111+
syncVar.run(2, g);
111112
}
112113
```
113114

114-
If this userland `SyncContext` is acceptable, than adding an `AsyncContext`
115+
If this userland `SyncVariable` is acceptable, than adding an `AsyncContext.Variable`
115116
that can operate across sync/async execution should be no different.
116117

117118
### Summary
118119

119-
There are no differences regarding naming scope of async contexts compared to
120-
regular JavaScript variables. Only code with direct access to `AsyncContex`
120+
There are no differences regarding naming scope of `AsyncContext.Variable` compared to
121+
regular JavaScript variables. Only code with direct access to `AsyncContext.Variable`
121122
instances can modify the value, and only for code execution nested inside a new
122-
`context.run()`. Further, the capability to modify a local variable which you
123+
`asyncVar.run()`. Further, the capability to modify an AsyncVariable which you
123124
have direct access to is already possible in sync code execution.

0 commit comments

Comments
 (0)