Skip to content

Commit

Permalink
Expand on requirements of Snapshot (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
legendecas authored Dec 12, 2023
1 parent b2664c5 commit 427feca
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 1 deletion.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ runWhenIdle(() => {
});
```

A detailed explanation of why `AsyncContext.Snapshot` is a requirement can be
found in [SNAPSHOT.md](./SNAPSHOT.md).

# Examples

## Determine the initiator of a task
Expand Down Expand Up @@ -431,7 +434,7 @@ const traceContext = new AsyncContext.Variable();
traceContext.run("trace-id-a", userAction);
traceContext.run("trace-id-b", userAction);

runWhenIdle();
scheduler.runWhenIdle();
// The userTask will be run with the trace context it was enqueued with.
// => 'trace-id-a'
// => 'trace-id-b'
Expand Down
95 changes: 95 additions & 0 deletions SNAPSHOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Requirements of `AsyncContext.Snapshot`

`AsyncContext.Snapshot` presents two unique requirements:

- It does not expose the value associated with any `Variable` instances.
- It captures _all_ `Variable`s' current value and restores those values
at a later time.

The above requirements are essential to decouple a queueing implementation
from the consumers of `Variable` instances. For example, a scheduler can queue
an async task and take a snapshot of the current context:

```typescript
// The scheduler doesn't access 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, task });
},
runWhenIdle() {
const queue = this.queue;
this.queue = [];
for (const { snapshot, task } of queue) {
// All tasks in the queue would be run with the current context if they
// hadn't been wrapped with the snapshot.
snapshot.run(task);
}
}
};
```

In this example, the scheduler can propagate values of `Variable`s but doesn't
have access to any `Variable` instance. They are not coupled with a specific
consumer of `Variable`. A consumer of `Variable` will not be coupled with a
specific scheduler as well.

A consumer like a tracer can use `Variable` without knowing how the scheduler
is implemented:

```typescript
// tracer.js
const asyncVar = new AsyncContext.Variable();
export function run(cb) {
// Create a new span and run the callback with it.
const span = {
startTime: Date.now(),
traceId: randomUUID(),
spanId: randomUUID(),
};
asyncVar.run(span, cb);
}

export function end() {
// Get the current span from the AsyncContext.Variable and end it.
const span = asyncVar.get();
span?.endTime = Date.now();
}
```

The `Snapshot` API enables user-land queueing implementations to be cooperate
with any consumers of `Variable`. For instances, a queueing implementation can
be:

- A user-land Promise-like implementation,
- A user multiplexer that multiplexes an IO operation with a batch of async
tasks.

Without an API like `Snapshot`, a queueing implementation would have to be built
on top of the built-in `Promise`, as it is the only way to capture the current
`Variable` values and restore them later. This would limit the implementation
of a user-land queueing.

```typescript
const scheduler = {
queue: [],
postTask(task) {
const { promise, resolve } = Promise.withResolvers();
// Captures the current context by `Promise.prototype.then`.
promise.then(() => {
task();
});
// Defers the task execution by resolving the promise.
queue.push(resolve);
},
runWhenIdle() {
// LIMITATION: the tasks are not run synchronously.
for (const cb of this.queue) {
cb();
}
this.queue = [];
}
};
```

0 comments on commit 427feca

Please sign in to comment.