Skip to content

Commit 733cb4f

Browse files
Readd AsyncContext.Snapshot.wrap() helper (#68)
* Add back AsyncContext.wrap() * Update readme * Add Wrapped function exotic objects section * Update wrap length * Add Realm to wrapper * Move to Snapshot, simplify spec * Add "wrapped" to function name * Explicitly support rest params in Abstract Closures * Remove CBF change, ecma262 will be updated * Update example * Update spec.html Co-authored-by: Andreu Botella <[email protected]> * Move to "Properties of X Constructor" clause --------- Co-authored-by: Andreu Botella <[email protected]>
1 parent c9714a6 commit 733cb4f

File tree

2 files changed

+91
-5
lines changed

2 files changed

+91
-5
lines changed

README.md

+69-5
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,19 @@ logically-connected sync/async code execution.
163163
namespace AsyncContext {
164164
class Variable<T> {
165165
constructor(options: AsyncVariableOptions<T>);
166-
167166
get name(): string;
168-
169167
run<R>(value: T, fn: (...args: any[])=> R, ...args: any[]): R;
170-
171168
get(): T | undefined;
172169
}
173-
174170
interface AsyncVariableOptions<T> {
175171
name?: string;
176172
defaultValue?: T;
177173
}
178174

179175
class Snapshot {
180176
constructor();
181-
182177
run<R>(fn: (...args: any[]) => R, ...args: any[]): R;
178+
wrap<T, R>(fn: (this: T, ...args: any[]) => R): (this: T, ...args: any[]) => R;
183179
}
184180
}
185181
```
@@ -299,6 +295,74 @@ runWhenIdle(() => {
299295
A detailed explanation of why `AsyncContext.Snapshot` is a requirement can be
300296
found in [SNAPSHOT.md](./SNAPSHOT.md).
301297

298+
### `AsyncContext.Snapshot.wrap`
299+
300+
`AsyncContext.Snapshot.wrap` is a helper which captures the current values of all
301+
`Variable`s and returns a wrapped function. When invoked, this wrapped function
302+
restores the state of all `Variable`s and executes the inner function.
303+
304+
```typescript
305+
const asyncVar = new AsyncContext.Variable();
306+
307+
function fn() {
308+
return asyncVar.get();
309+
}
310+
311+
let wrappedFn;
312+
asyncVar.run("A", () => {
313+
// Captures the state of all AsyncContext.Variable's at this moment, returning
314+
// wrapped closure that restores that state.
315+
wrappedFn = AsyncContext.Snapshot.wrap(fn)
316+
});
317+
318+
319+
console.log(fn()); // => undefined
320+
console.log(wrappedFn()); // => 'A'
321+
```
322+
323+
You can think of this as a more convenient version of `Snapshot`, where only a
324+
single function needs to be wrapped. It also serves as a convenient way for
325+
consumers of libraries that don't support `AsyncContext` to ensure that function
326+
is executed in the correct execution context.
327+
328+
```typescript
329+
// User code that uses a legacy library
330+
const asyncVar = new AsyncContext.Variable();
331+
332+
function fn() {
333+
return asyncVar.get();
334+
}
335+
336+
asyncVar.run("A", () => {
337+
defer(fn); // setTimeout schedules during "A" context.
338+
})
339+
asyncVar.run("B", () => {
340+
defer(fn); // setTimeout is not called, fn will still see "A" context.
341+
})
342+
asyncVar.run("C", () => {
343+
const wrapped = AsyncContext.Snapshot.wrap(fn);
344+
defer(wrapped); // wrapped callback captures "C" context.
345+
})
346+
347+
348+
// Some legacy library that queues multiple callbacks per macrotick
349+
// Because the setTimeout is called a single time per queue batch,
350+
// all callbacks will be invoked with _that_ context regardless of
351+
// whatever context is active during the call to `defer`.
352+
const queue = [];
353+
function defer(callback) {
354+
if (queue.length === 0) setTimeout(processQueue, 1);
355+
queue.push(callback);
356+
}
357+
function processQueue() {
358+
for (const cb of queue) {
359+
cb();
360+
}
361+
queue.length = 0;
362+
}
363+
```
364+
365+
302366
# Examples
303367

304368
## Determine the initiator of a task

spec.html

+22
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,28 @@ <h1>AsyncContext.Snapshot.prototype</h1>
908908
<p>The initial value of `AsyncContext.Snapshot.prototype` is the AsyncContext.Snapshot prototype object.</p>
909909
<p>This property has the attributes { [[Writable]]: *false*, [[Enumerable]]: *false*, [[Configurable]]: *false* }.</p>
910910
</emu-clause>
911+
912+
<emu-clause id="sec-asynccontext-snapshot.wrap">
913+
<h1>AsyncContext.Snapshot.wrap ( _fn_ )</h1>
914+
<p>This function returns a new function which restores the current value of all AsyncContext.Variable values when being invoked.</p>
915+
916+
<emu-alg>
917+
1. If IsCallable(_fn_) is *false*, throw a *TypeError* exception.
918+
1. Let _snapshot_ be AsyncContextSnapshot().
919+
1. Let _closure_ be a new Abstract Closure with parameters (..._args_) that captures _fn_ and _snapshot_ and performs the following steps when called:
920+
1. Let _thisArgument_ be the *this* value.
921+
1. Let _previousContextMapping_ be AsyncContextSwap(_snapshot_).
922+
1. Let _result_ be Completion(Call(_fn_, _thisArgument_, _args_)).
923+
1. AsyncContextSwap(_previousContextMapping_).
924+
1. Return _result_.
925+
1. Let _length_ be ? LengthOfArrayLike(_fn_).
926+
1. Let _name_ be ? Get(_fn_, *"name"*).
927+
1. If _name_ is not a String, set _name_ to the empty String.
928+
1. Let _realm_ be the current Realm Record.
929+
1. Let _prototype_ be _realm_.[[Intrinsics]].[[%Function.prototype%]].
930+
1. Return CreateBuiltinFunction(_closure_, _length_, _name_, &laquo; &raquo;, _realm_, _prototype_, *"wrapped"*).
931+
</emu-alg>
932+
</emu-clause>
911933
</emu-clause>
912934

913935
<emu-clause id="sec-properties-of-the-asynccontext-snapshot-prototype-object">

0 commit comments

Comments
 (0)