Skip to content

Commit

Permalink
Expand on Snapshot and the function parameter (#63)
Browse files Browse the repository at this point in the history
Co-authored-by: Justin Ridgewell <[email protected]>
  • Loading branch information
legendecas and jridgewell authored Nov 15, 2023
1 parent ec5b355 commit b2664c5
Showing 1 changed file with 118 additions and 32 deletions.
150 changes: 118 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,14 @@ namespace AsyncContext {
}
```

## `AsyncContext.Variable`

`Variable` is a container for a value that is associated with the current
execution flow. The value is propagated through async execution flows, and
can be snapshot and restored with `Snapshot`.

`Variable.prototype.run()` and `Variable.prototype.get()` sets and gets
the current value of an async execution flow. `Snapshot` allows you
to opaquely capture the current value of all `Variable`s and execute a
function at a later time with as if those values were still the current values
(a snapshot and restore). Note that even with `Snapshot`, you can
only access the value associated with an `Variable` instance if you have
access to that instance.
the current value of an async execution flow.

```typescript
const asyncVar = new AsyncContext.Variable();
Expand Down Expand Up @@ -223,29 +224,55 @@ function main() {

// AsyncContext.Variable was restored after the previous run.
console.log(asyncVar.get()); // => 'top'

// Captures the state of all AsyncContext.Variable's at this moment.
const snapshotDuringTop = new AsyncContext.Snapshot();

asyncVar.run("C", () => {
console.log(asyncVar.get()); // => 'C'

// The snapshotDuringTop will restore all AsyncContext.Variable to their snapshot
// state and invoke the wrapped function. We pass a function which it will
// invoke.
snapshotDuringTop.run(() => {
// Despite being lexically nested inside 'C', the snapshot restored us to
// to the 'top' state.
console.log(asyncVar.get()); // => 'top'
});
});
}

function randomTimeout() {
return Math.random() * 1000;
}
```

> Note: There are controversial thought on the dynamic scoping and
> `Variable`, checkout [SCOPING.md][] for more details.
Hosts are expected to use the infrastructure in this proposal to allow tracking
not only asynchronous callstacks, but other ways to schedule jobs on the event
loop (such as `setTimeout`) to maximize the value of these use cases.

A detailed example of use cases can be found in the
[Use cases document](./USE-CASES.md).

## `AsyncContext.Snapshot`

`Snapshot` allows you to opaquely capture the current values of all `Variable`s
and execute a function at a later time as if those values were still the
current values (a snapshot and restore).

Note that even with `Snapshot`, you can only access the value associated with
a `Variable` instance if you have access to that instance.

```typescript
const asyncVar = new AsyncContext.Variable();

let snapshot
asyncVar.run("A", () => {
// Captures the state of all AsyncContext.Variable's at this moment.
snapshot = new AsyncContext.Snapshot();
});

asyncVar.run("B", () => {
console.log(asyncVar.get()); // => 'B'

// The snapshot will restore all AsyncContext.Variable to their snapshot
// state and invoke the wrapped function. We pass a function which it will
// invoke.
snapshot.run(() => {
// Despite being lexically nested inside 'B', the snapshot restored us to
// to the snapshot 'A' state.
console.log(asyncVar.get()); // => 'A'
});
});
```

`Snapshot` is useful for implementing APIs that logically "schedule" a
callback, so the callback will be called with the context that it logically
belongs to, regardless of the context under which it actually runs:
Expand All @@ -269,16 +296,6 @@ runWhenIdle(() => {
});
```

> Note: There are controversial thought on the dynamic scoping and
> `Variable`, checkout [SCOPING.md][] for more details.
Hosts are expected to use the infrastructure in this proposal to allow tracking
not only asynchronous callstacks, but other ways to schedule jobs on the event
loop (such as `setTimeout`) to maximize the value of these use cases.

A detailed example of use cases can be found in the
[Use cases document](./USE-CASES.md).

# Examples

## Determine the initiator of a task
Expand Down Expand Up @@ -377,6 +394,75 @@ async function doStuffs(text) {
}
```

## User-land queues

User-land queues can be implemented with `AsyncContext.Snapshot` to propagate
the values of all `AsyncContext.Variable`s without access to any of them. This
allows the user-land queue to be implemented in a way that is decoupled from
consumers of `AsyncContext.Variable`.

```typescript
// The scheduler doesn't access to any AsyncContext.Variable.
const scheduler = {
queue: [],
postTask(task) {
// Each callback is stored with the context at which it was enqueued.
const snapshot = new AsyncContext.Snapshot();
queue.push(() => snapshot.run(task));
},
runWhenIdle() {
// All callbacks in the queue would be run with the current context if they
// hadn't been wrapped.
for (const cb of this.queue) {
cb();
}
this.queue = [];
}
};

function userAction() {
scheduler.postTask(function userTask() {
console.log(traceContext.get());
});
}

// Tracing libraries can use AsyncContext.Variable to store tracing contexts.
const traceContext = new AsyncContext.Variable();
traceContext.run("trace-id-a", userAction);
traceContext.run("trace-id-b", userAction);

runWhenIdle();
// The userTask will be run with the trace context it was enqueued with.
// => 'trace-id-a'
// => 'trace-id-b'
```

# FAQ

## Why take a function in `run`?

The `Variable.prototype.run` and `Snapshot.prototype.run` methods take a
function to execute because it ensures async context variables
will always contain consistent values in a given execution flow. Any modification
must be taken in a sub-graph of an async execution flow, and can not affect
their parent or sibling scopes.

```typescript
const asyncVar = new AsyncContext.Variable();
asyncVar.run("A", async () => {
asyncVar.get(); // => 'A'

// ...arbitrary synchronous codes.
// ...or await-ed asynchronous calls.

// The value can not be modified at this point.
asyncVar.get(); // => 'A'
});
```

This increases the integrity of async context variables, and makes them
easier to reason about where a value of an async variable comes from.

# Prior Arts

## zones.js
Expand Down

0 comments on commit b2664c5

Please sign in to comment.