You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
vm: "afterEvaluate", evaluate() return a promise from the outer context
Consider the default context A with a microtask queue QA, and a
context B with its own microtask queue QB.
Context B is constructed with vm.createContext(..., {microtaskMode:
"afterEvaluate"}). The evaluation in context B can be performed via
vm.Script or vm.SourceTextModule.
The standard (https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob)
dictates that, when resolving a {promise} with {resolution}, from any
context, the {then} method on {promise} should be called within a task
enqueued on the microtask queue from the context associated with {then}.
Specifically, after evaluating a script or module in context B, any
promises created within B, if later resolved within A, will result in a
task to be enqueued back onto QB, even long after we are done evaluating
any code within B.
This creates a challenge for users of node:vm in "afterEvaluate" mode.
In ContextifyScript::EvalMachine() and in ModuleWrap::Evaluate(), we
only drain the microtask queue QB a single time after running the script
or evaluating the module. After that point, the queue will not be
drained unless another script or module is evaluated in the same
context.
In the following scenario, prior to this patch, the log statement will
not be printed:
const microtaskMode = "afterEvaluate";
const context = vm.createContext({}, {microtaskMode});
const source = "";
const module = new vm.SourceTextModule(source, {context});
await module.link(() => null);
await module.evaluate();
console.log("NOT PRINTED");
Within `evaluate()`, there is this `await` statement:
await this[kWrap].evaluate(timeout, breakOnSigint)
Since the promise returned by ModuleWrap::Evaluate() is the top-level
capability for {module}, a promise created within B, V8 will enqueue a
task on QB. But since this is after the PerformCheckpoint() call in
ModuleWrap::Evaluate(), the task in QB is never run. In the meantime,
since QA is empty, the Node process simply exits (with a warning about
the unsettled promise, if it happened to be a top-level await).
While being unable to do `await module.evaluate()` is clearly a problem,
more generally, it is intended that in "afterEvaluate" mode, promises
created in the inner context cannot make progress if, and until, the
microtask queue of the inner context is checkpointed.
Therefore, to address this issue, the fix is narrow:
When the module has its own microtask queue, i.e. in "afterEvaluate"
mode, the inner-context promise returned by
v8::SourceTextModule::Evaluate() is first resolved to an outer-context
promise, then we checkpoint the microtask queue of the inner context,
then we return the outer-context promise we just built.
This ensures that in the statement `await this[kWrap].evaluate(...)`,
the promise returned can be resolved within the outer context, without
involving the microtask queue in the inner context.
Fixes: #59541
Refs: https://issues.chromium.org/issues/441679231
Refs: https://groups.google.com/g/v8-dev/c/YIeRg8CUNS8/m/rEQdFuNZAAAJ
PR-URL: #59801
Refs: https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
Reviewed-By: Anna Henningsen <anna@addaleax.net>
0 commit comments