From a73b4322f77c56e715d11b5cf1f5979ff59a29ed Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 3 Apr 2024 03:48:06 -0400 Subject: [PATCH] Add new AsyncContext.callingContext() API --- README.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++-- spec.html | 98 ++++++++++++++++++++++++++++++++++++------------------- 2 files changed, 150 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9bc5af1..7d6896e 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ logically-connected sync/async code execution. ```typescript namespace AsyncContext { - class Variable { + export class Variable { constructor(options: AsyncVariableOptions); get name(): string; get(): T | undefined; @@ -172,11 +172,13 @@ namespace AsyncContext { defaultValue?: T; } - class Snapshot { + export class Snapshot { constructor(); run(fn: (...args: any[]) => R, ...args: any[]): R; static wrap(fn: (this: T, ...args: any[]) => R): (this: T, ...args: any[]) => R; } + + export function callingContext(fn: (...args: any[]) => R, ...args: any[]): R; } ``` @@ -362,6 +364,87 @@ function processQueue() { } ``` +## `AsyncContext.callingContext` + +`AsyncContext.callingContext` is a helper which allows you to +temporarily return all `Variable`s to the execution state immediately +before the current one. + +Generally, APIs which defer execution will capture the context at the +time of registration to be used when that function is later executed. +Eg, `obj.addEventListener('foo', fn)` immediately captures the context +when `addEventListener` is called to be restored later when the `foo` +event eventually happens. This is called **registration-time** context +propagation. + +In certain circumstances, you may wish to use **call-time** context +propagation. Ie, the context that is active when the event is actually +dispatched. Unfortunately, because the API will restore the registration +time context before invoking `fn`, the calling context will have already +been replaced. + +`AsyncContext.callingContext` helper function allows you to restore the +context state to what it was immediately prior to the current state, +allowing both **registration-time** and **call-time** use cases to work. + +```typescript +const asyncVar = new AsyncContext.Variable(); + +const obj = new EventEmitter(); + +asyncVar.run("registration", () => { + obj.on("foo", () => { + // EventEmitter restored the registration time context before + // invoking our callback. If the callback wanted to get the context + // active during the `emit()` call, it would have to receive a + // Snapshot instance passed by the caller. + console.log(asyncVar.get()); // => 'registration' + + // But with `AsyncContext.callingContext()`, we're able to restore + // the caller's context without changing EventEmitter's API. + // EventEmitter can continue to assume that registration is default + // that is most useful to developers. + AsyncContext.callingContext(() => { + console.log(asyncVar.get()); // => 'call' + }); + }); +}); + +asyncVar.run("call", () => { + obj.emit("foo"); +}); +``` + +Calling `AsyncContext.callingContext` works similarly to invoking +`AsyncContext.Snapshot.prototype.run`, meaning that we temporarily +restore a different global state while the passed in callback executes, +then immediately restore the prior state. + +This also works with `Generator`/`AsyncGenerator` functions, allowing +you to restore the context that was active when `it.next()` was called. + +```typescript +function* gen() { + console.log(asyncVar.get()); // => 'init' + + yield 1; + + // Generators and AsyncGenerators always restore the context that was + // active when they were initialized. + console.log(asyncVar.get()); // => 'init' + + AsyncContext.callingContext(() => { + console.log(asyncVar.get()); // => 'second' + }); +} + +const it = asyncVar.run("init", () => { + return gen(); +}); + +asyncVar.run("first", () => it.next()); +asyncVar.run("second", () => it.next()); +``` # Examples diff --git a/spec.html b/spec.html index 6191ef1..31f0b1b 100644 --- a/spec.html +++ b/spec.html @@ -125,6 +125,17 @@

Agents

A map from the AsyncContext.Variable instances to the saved ECMAScript language value. Every Record in the List contains a unique [[AsyncContextKey]]. The map is initially empty. + + + [[CallingAsyncContextMapping]] + + + a List of Async Context Mapping Records + + + A map from the AsyncContext.Variable instances to the saved ECMAScript language value. Every Record in the List contains a unique [[AsyncContextKey]]. The map is initially empty. + + @@ -145,9 +156,9 @@

1. Choose any such _cell_. 1. Remove _cell_ from _finalizationRegistry_.[[Cells]]. 1. Perform ? HostCallJobCallback(_callback_, *undefined*, « _cell_.[[HeldValue]] »). - 1. Let _previousContextMapping_ be AsyncContextSwap(_finalizationRegistry_.[[FinalizationRegistryAsyncContextSnapshot]]). + 1. Let _entranceState_ be AsyncContextEnter(_finalizationRegistry_.[[FinalizationRegistryAsyncContextSnapshot]]). 1. Let _result_ be Completion(HostCallJobCallback(_callback_, *undefined*, « _cell_.[[HeldValue]] »)). - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Perform ? _result_. 1. Return ~unused~. @@ -302,8 +313,8 @@

An implementation of HostPromiseRejectionTracker that delays notifying developers of unhandled rejections must conform to the following requirements

  • It must perform AsyncContextSnapshot() at the call of HostPromiseRejectionTracker,
  • -
  • It must perform AsyncContextSwap before the event notification, with the result of the AsyncContextSnapshot operation,
  • -
  • It must perform AsyncContextSwap after the event notification, with the result of the earlier AsyncContextSwap operation.
  • +
  • It must perform AsyncContextEnter() before the event notification, with the result of the AsyncContextSnapshot operation,
  • +
  • It must perform AsyncContextExit() after the event notification, with the result of the earlier AsyncContextEnter operation.
@@ -328,7 +339,7 @@

1. Let _promiseCapability_ be _reaction_.[[Capability]]. 1. Let _type_ be _reaction_.[[Type]]. 1. Let _handler_ be _reaction_.[[Handler]]. - 1. Let _previousContextMapping_ be AsyncContextSwap(_reaction_.[[PromiseAsyncContextSnapshot]]). + 1. Let _entranceState_ be AsyncContextEnter(_reaction_.[[PromiseAsyncContextSnapshot]]). 1. If _handler_ is ~empty~, then 1. If _type_ is ~fulfill~, then 1. let _handlerResult_ be NormalCompletion(_argument_). @@ -339,7 +350,7 @@

1. let _handlerResult_ be Completion(HostCallJobCallback(_handler_, *undefined*, « _argument_ »)). 1. If _promiseCapability_ is *undefined*, then 1. Assert: _handlerResult_ is not an abrupt completion. - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return ~empty~. 1. Assert: _promiseCapability_ is a PromiseCapability Record. 1. If _handlerResult_ is an abrupt completion, then @@ -348,7 +359,7 @@

1. Else, 1. Return ? Call(_promiseCapability_.[[Resolve]], *undefined*, « _handlerResult_.[[Value]] »). 1. Let _resolvingFunctionResult_ be Completion(Call(_promiseCapability_.[[Resolve]], *undefined*, « _handlerResult_.[[Value]] »)). - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return _resolvingFunctionResult_. 1. Let _handlerRealm_ be *null*. 1. If _reaction_.[[Handler]] is not ~empty~, then @@ -374,14 +385,14 @@

1. Let _snapshot_ be AsyncContextSnapshot(). 1. Let _job_ be a new Job Abstract Closure with no parameters that captures _promiseToResolve_, _thenable_, _then_, and _snapshot_ and performs the following steps when called: 1. Let _resolvingFunctions_ be CreateResolvingFunctions(_promiseToResolve_). - 1. Let _previousContextMapping_ be AsyncContextSwap(_snapshot_). + 1. Let _entranceState_ be AsyncContextEnter(_snapshot_). 1. Let _thenCallResult_ be Completion(HostCallJobCallback(_then_, _thenable_, « _resolvingFunctions_.[[Resolve]], _resolvingFunctions_.[[Reject]] »)). 1. If _thenCallResult_ is an abrupt completion, then 1. Return ? Call(_resolvingFunctions_.[[Reject]], *undefined*, « _thenCallResult_.[[Value]] »). 1. Let _rejectResult_ be Completion(Call(_resolvingFunctions_.[[Reject]], *undefined*, « _thenCallResult_.[[Value]] »)). - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return _rejectResult_. - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return ? _thenCallResult_. 1. Let _getThenRealmResult_ be Completion(GetFunctionRealm(_then_.[[Callback]])). 1. If _getThenRealmResult_ is a normal completion, let _thenRealm_ be _getThenRealmResult_.[[Value]]. @@ -622,15 +633,15 @@

1. Suspend _methodContext_. 1. Set _generator_.[[GeneratorState]] to ~executing~. 1. If _generator_.[[GeneratorAsyncContextMapping]] is ~empty~, then - 1. Let _previousContextMapping_ be ~empty~. + 1. Let _entranceState_ be ~empty~. 1. Else, - 1. Let _previousContextMapping_ be AsyncContextSwap(_generator_.[[GeneratorAsyncContextMapping]]). + 1. Let _entranceState_ be AsyncContextEnter(_generator_.[[GeneratorAsyncContextMapping]]). 1. Push _genContext_ onto the execution context stack; _genContext_ is now the running execution context. 1. Resume the suspended evaluation of _genContext_ using NormalCompletion(_value_) as the result of the operation that suspended it. Let _result_ be the value returned by the resumed computation. 1. Assert: When we return here, _genContext_ has already been removed from the execution context stack and _methodContext_ is the currently running execution context. - 1. If _previousContextMapping_ is not ~empty~, then + 1. If _entranceState_ is not ~empty~, then 1. Assert: The result of AsyncContextSnapshot() is _generator_.[[GeneratorAsyncContextMapping]]. - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return ? _result_. @@ -661,15 +672,15 @@

1. Suspend _methodContext_. 1. Set _generator_.[[GeneratorState]] to ~executing~. 1. If _generator_.[[GeneratorAsyncContextMapping]] is ~empty~, then - 1. Let _previousContextMapping_ be ~empty~. + 1. Let _entranceState_ be ~empty~. 1. Else, - 1. Let _previousContextMapping_ be AsyncContextSwap(_generator_.[[GeneratorAsyncContextMapping]]). + 1. Let _entranceState_ be AsyncContextEnter(_generator_.[[GeneratorAsyncContextMapping]]). 1. Push _genContext_ onto the execution context stack; _genContext_ is now the running execution context. 1. Resume the suspended evaluation of _genContext_ using _abruptCompletion_ as the result of the operation that suspended it. Let _result_ be the Completion Record returned by the resumed computation. 1. Assert: When we return here, _genContext_ has already been removed from the execution context stack and _methodContext_ is the currently running execution context. - 1. If _previousContextMapping_ is not ~empty~, then + 1. If _entranceState_ is not ~empty~, then 1. Assert: The result of AsyncContextSnapshot() is _generator_.[[GeneratorAsyncContextMapping]]. - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return ? _result_. @@ -826,16 +837,16 @@

1. Suspend _callerContext_. 1. Set _generator_.[[AsyncGeneratorState]] to ~executing~. 1. If _generator_.[[AsyncGeneratorAsyncContextMapping]] is ~empty~, then - 1. Let _previousContextMapping_ be ~empty~. + 1. Let _entranceState_ be ~empty~. 1. Else, - 1. Let _previousContextMapping_ be AsyncContextSwap(_generator_.[[AsyncGeneratorAsyncContextMapping]]). + 1. Let _entranceState_ be AsyncContextEnter(_generator_.[[AsyncGeneratorAsyncContextMapping]]). 1. Push _genContext_ onto the execution context stack; _genContext_ is now the running execution context. 1. Resume the suspended evaluation of _genContext_ using _completion_ as the result of the operation that suspended it. Let _result_ be the Completion Record returned by the resumed computation. 1. Assert: _result_ is never an abrupt completion. 1. Assert: When we return here, _genContext_ has already been removed from the execution context stack and _callerContext_ is the currently running execution context. - 1. If _previousContextMapping_ is not ~empty~, then + 1. If _entranceState_ is not ~empty~, then 1. Assert: The result of AsyncContextSnapshot() is _generator_.[[AsyncGeneratorAsyncContextMapping]]. - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return ~unused~. @@ -902,23 +913,44 @@

- +

- AsyncContextSwap ( + AsyncContextEnter ( _snapshotMapping_: a List of Async Context Mapping Records - ): a List of Async Context Mapping Records + ): a Record with fields [[AsyncContextMapping]] (a List of Async Context Mapping Records) and [[CallingAsyncContextMapping]] (a List of Async Context Mapping Records)

description
-
It is used to swap the surrounding agent's Agent Record's [[AsyncContextMapping]] with the _snapshotMapping_.
+
It is used to set the surrounding agent's Agent Record's [[AsyncContextMapping]] to a value returned by AsyncContextSwap.
1. Let _agentRecord_ be the surrounding agent's Agent Record. 1. Let _asyncContextMapping_ be _agentRecord_.[[AsyncContextMapping]]. + 1. Let _callingAsyncContextMapping_ be _agentRecord_.[[CallingAsyncContextMapping]]. 1. Set _agentRecord_.[[AsyncContextMapping]] to _snapshotMapping_. + 1. Set _agentRecord_.[[CallingAsyncContextMapping]] to _asyncContextMapping_. + 1. Return the Record { [[AsyncContextMapping]]: _asyncContextMapping_, [[CallingAsyncContextMapping]]: _callingAsyncContextMapping_ }. 1. Return _asyncContextMapping_.
+ + +

+ AsyncContextExit ( + _entranceState_: a Record with fields [[AsyncContextMapping]] (a List of Async Context Mapping Records) and [[CallingAsyncContextMapping]] (a List of Async Context Mapping Records) + ): ~unused~ +

+
+
description
+
It is used to restore the surrounding agent's Agent Record's [[AsyncContextMapping]] and [[CallingAsyncContextMapping]] to a value returned by AsyncContextEnter.
+
+ + 1. Let _agentRecord_ be the surrounding agent's Agent Record. + 1. Set _agentRecord_.[[AsyncContextMapping]] to _entranceState_.[[AsyncContextMapping]]. + 1. Set _agentRecord_.[[CallingAsyncContextMapping]] to _entranceState_.[[CallingAsyncContextMapping]]. + 1. Return ~unused~. + +
@@ -997,9 +1029,9 @@

AsyncContext.Snapshot.wrap ( _fn_ )

1. Let _snapshot_ be AsyncContextSnapshot(). 1. Let _closure_ be a new Abstract Closure with parameters (..._args_) that captures _fn_ and _snapshot_ and performs the following steps when called: 1. Let _thisArgument_ be the *this* value. - 1. Let _previousContextMapping_ be AsyncContextSwap(_snapshot_). + 1. Let _entranceState_ be AsyncContextEnter(_snapshot_). 1. Let _result_ be Completion(Call(_fn_, _thisArgument_, _args_)). - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return _result_. 1. Let _length_ be ? LengthOfArrayLike(_fn_). 1. Let _name_ be ? Get(_fn_, *"name"*). @@ -1032,9 +1064,9 @@

AsyncContext.Snapshot.prototype.run ( _func_, ..._args_ )

1. Let _asyncSnapshot_ be the *this* value. 1. Perform ? RequireInternalSlot(_asyncSnapshot_, [[AsyncSnapshotMapping]]). - 1. Let _previousContextMapping_ be AsyncContextSwap(_asyncSnapshot_.[[AsyncSnapshotMapping]]). + 1. Let _entranceState_ be AsyncContextEnter(_asyncSnapshot_.[[AsyncSnapshotMapping]]). 1. Let _result_ be Completion(Call(_func_, *undefined*, _args_)). - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return _result_.
@@ -1153,16 +1185,16 @@

AsyncContext.Variable.prototype.run ( _value_, _func_, ..._args_ )

1. Perform ? RequireInternalSlot(_asyncVariable_, [[AsyncVariableName]]). 1. Let _previousContextMapping_ be AsyncContextSnapshot(). 1. Let _asyncContextMapping_ be a new empty List. - 1. For each Async Context Mapping Record _p_ of _previousContextMapping_, do + 1. For each Async Context Mapping Record _p_ of _entranceState_, do 1. If SameValueZero(_p_.[[AsyncContextKey]], _asyncVariable_) is *false*, then 1. Let _q_ be the Async Context Mapping Record { [[AsyncContextKey]]: _p_.[[AsyncContextKey]], [[AsyncContextValue]]: _p_.[[AsyncContextValue]] }. 1. Append _q_ to _asyncContextMapping_. 1. Assert: _asyncContextMapping_ does not contain an Async Context Mapping Record whose [[AsyncContextKey]] is _asyncVariable_. 1. Let _p_ be the Async Context Mapping Record { [[AsyncContextKey]]: _asyncVariable_, [[AsyncContextValue]]: _value_ }. 1. Append _p_ to _asyncContextMapping_. - 1. AsyncContextSwap(_asyncContextMapping_). + 1. Let _entranceState_ be AsyncContextSwap(_asyncContextMapping_). 1. Let _result_ be Completion(Call(_func_, *undefined*, _args_)). - 1. AsyncContextSwap(_previousContextMapping_). + 1. Perform AsyncContextExit(_entranceState_). 1. Return _result_.