Skip to content

Commit b2664c5

Browse files
Expand on Snapshot and the function parameter (tc39#63)
Co-authored-by: Justin Ridgewell <[email protected]>
1 parent ec5b355 commit b2664c5

File tree

1 file changed

+118
-32
lines changed

1 file changed

+118
-32
lines changed

README.md

Lines changed: 118 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,14 @@ namespace AsyncContext {
184184
}
185185
```
186186

187+
## `AsyncContext.Variable`
188+
189+
`Variable` is a container for a value that is associated with the current
190+
execution flow. The value is propagated through async execution flows, and
191+
can be snapshot and restored with `Snapshot`.
192+
187193
`Variable.prototype.run()` and `Variable.prototype.get()` sets and gets
188-
the current value of an async execution flow. `Snapshot` allows you
189-
to opaquely capture the current value of all `Variable`s and execute a
190-
function at a later time with as if those values were still the current values
191-
(a snapshot and restore). Note that even with `Snapshot`, you can
192-
only access the value associated with an `Variable` instance if you have
193-
access to that instance.
194+
the current value of an async execution flow.
194195

195196
```typescript
196197
const asyncVar = new AsyncContext.Variable();
@@ -223,29 +224,55 @@ function main() {
223224

224225
// AsyncContext.Variable was restored after the previous run.
225226
console.log(asyncVar.get()); // => 'top'
226-
227-
// Captures the state of all AsyncContext.Variable's at this moment.
228-
const snapshotDuringTop = new AsyncContext.Snapshot();
229-
230-
asyncVar.run("C", () => {
231-
console.log(asyncVar.get()); // => 'C'
232-
233-
// The snapshotDuringTop will restore all AsyncContext.Variable to their snapshot
234-
// state and invoke the wrapped function. We pass a function which it will
235-
// invoke.
236-
snapshotDuringTop.run(() => {
237-
// Despite being lexically nested inside 'C', the snapshot restored us to
238-
// to the 'top' state.
239-
console.log(asyncVar.get()); // => 'top'
240-
});
241-
});
242227
}
243228

244229
function randomTimeout() {
245230
return Math.random() * 1000;
246231
}
247232
```
248233

234+
> Note: There are controversial thought on the dynamic scoping and
235+
> `Variable`, checkout [SCOPING.md][] for more details.
236+
237+
Hosts are expected to use the infrastructure in this proposal to allow tracking
238+
not only asynchronous callstacks, but other ways to schedule jobs on the event
239+
loop (such as `setTimeout`) to maximize the value of these use cases.
240+
241+
A detailed example of use cases can be found in the
242+
[Use cases document](./USE-CASES.md).
243+
244+
## `AsyncContext.Snapshot`
245+
246+
`Snapshot` allows you to opaquely capture the current values of all `Variable`s
247+
and execute a function at a later time as if those values were still the
248+
current values (a snapshot and restore).
249+
250+
Note that even with `Snapshot`, you can only access the value associated with
251+
a `Variable` instance if you have access to that instance.
252+
253+
```typescript
254+
const asyncVar = new AsyncContext.Variable();
255+
256+
let snapshot
257+
asyncVar.run("A", () => {
258+
// Captures the state of all AsyncContext.Variable's at this moment.
259+
snapshot = new AsyncContext.Snapshot();
260+
});
261+
262+
asyncVar.run("B", () => {
263+
console.log(asyncVar.get()); // => 'B'
264+
265+
// The snapshot will restore all AsyncContext.Variable to their snapshot
266+
// state and invoke the wrapped function. We pass a function which it will
267+
// invoke.
268+
snapshot.run(() => {
269+
// Despite being lexically nested inside 'B', the snapshot restored us to
270+
// to the snapshot 'A' state.
271+
console.log(asyncVar.get()); // => 'A'
272+
});
273+
});
274+
```
275+
249276
`Snapshot` is useful for implementing APIs that logically "schedule" a
250277
callback, so the callback will be called with the context that it logically
251278
belongs to, regardless of the context under which it actually runs:
@@ -269,16 +296,6 @@ runWhenIdle(() => {
269296
});
270297
```
271298

272-
> Note: There are controversial thought on the dynamic scoping and
273-
> `Variable`, checkout [SCOPING.md][] for more details.
274-
275-
Hosts are expected to use the infrastructure in this proposal to allow tracking
276-
not only asynchronous callstacks, but other ways to schedule jobs on the event
277-
loop (such as `setTimeout`) to maximize the value of these use cases.
278-
279-
A detailed example of use cases can be found in the
280-
[Use cases document](./USE-CASES.md).
281-
282299
# Examples
283300

284301
## Determine the initiator of a task
@@ -377,6 +394,75 @@ async function doStuffs(text) {
377394
}
378395
```
379396

397+
## User-land queues
398+
399+
User-land queues can be implemented with `AsyncContext.Snapshot` to propagate
400+
the values of all `AsyncContext.Variable`s without access to any of them. This
401+
allows the user-land queue to be implemented in a way that is decoupled from
402+
consumers of `AsyncContext.Variable`.
403+
404+
```typescript
405+
// The scheduler doesn't access to any AsyncContext.Variable.
406+
const scheduler = {
407+
queue: [],
408+
postTask(task) {
409+
// Each callback is stored with the context at which it was enqueued.
410+
const snapshot = new AsyncContext.Snapshot();
411+
queue.push(() => snapshot.run(task));
412+
},
413+
runWhenIdle() {
414+
// All callbacks in the queue would be run with the current context if they
415+
// hadn't been wrapped.
416+
for (const cb of this.queue) {
417+
cb();
418+
}
419+
this.queue = [];
420+
}
421+
};
422+
423+
function userAction() {
424+
scheduler.postTask(function userTask() {
425+
console.log(traceContext.get());
426+
});
427+
}
428+
429+
// Tracing libraries can use AsyncContext.Variable to store tracing contexts.
430+
const traceContext = new AsyncContext.Variable();
431+
traceContext.run("trace-id-a", userAction);
432+
traceContext.run("trace-id-b", userAction);
433+
434+
runWhenIdle();
435+
// The userTask will be run with the trace context it was enqueued with.
436+
// => 'trace-id-a'
437+
// => 'trace-id-b'
438+
```
439+
440+
# FAQ
441+
442+
## Why take a function in `run`?
443+
444+
The `Variable.prototype.run` and `Snapshot.prototype.run` methods take a
445+
function to execute because it ensures async context variables
446+
will always contain consistent values in a given execution flow. Any modification
447+
must be taken in a sub-graph of an async execution flow, and can not affect
448+
their parent or sibling scopes.
449+
450+
```typescript
451+
const asyncVar = new AsyncContext.Variable();
452+
asyncVar.run("A", async () => {
453+
asyncVar.get(); // => 'A'
454+
455+
// ...arbitrary synchronous codes.
456+
// ...or await-ed asynchronous calls.
457+
458+
// The value can not be modified at this point.
459+
asyncVar.get(); // => 'A'
460+
});
461+
```
462+
463+
This increases the integrity of async context variables, and makes them
464+
easier to reason about where a value of an async variable comes from.
465+
380466
# Prior Arts
381467

382468
## zones.js

0 commit comments

Comments
 (0)