vm: explain how to share promises between contexts w/ afterEvaluate · nodejs/node@9347ddd
@@ -1946,6 +1946,68 @@ inside a `vm.Context`, functions passed to them will be added to global queues,
19461946which are shared by all contexts. Therefore, callbacks passed to those functions
19471947are not controllable through the timeout either.
194819481949+### When `microtaskMode` is `'afterEvaluate'`, beware sharing Promises between Contexts
1950+1951+In `'afterEvaluate'` mode, the `Context` has its own microtask queue, separate
1952+from the global microtask queue used by the outer (main) context. While this
1953+mode is necessary to enforce `timeout` and enable `breakOnSigint` with
1954+asynchronous tasks, it also makes sharing promises between contexts challenging.
1955+1956+In the example below, a promise is created in the inner context and shared with
1957+the outer context. When the outer context `await` on the promise, the execution
1958+flow of the outer context is disrupted in a surprising way: the log statement
1959+is never executed.
1960+1961+```mjs
1962+import * as vm from 'node:vm';
1963+1964+const inner_context = vm.createContext({}, { microtaskMode: 'afterEvaluate' });
1965+1966+// runInContext() returns a Promise created in the inner context.
1967+const inner_promise = vm.runInContext(
1968+'Promise.resolve()',
1969+ context,
1970+);
1971+1972+// As part of performing `await`, the JavaScript runtime must enqueue a task
1973+// on the microtask queue of the context where `inner_promise` was created.
1974+// A task is added on the inner microtask queue, but **it will not be run
1975+// automatically**: this task will remain pending indefinitely.
1976+//
1977+// Since the outer microtask queue is empty, execution in the outer module
1978+// falls through, and the log statement below is never executed.
1979+await inner_promise;
1980+1981+console.log('this will NOT be printed');
1982+```
1983+1984+To successfully share promises between contexts with different microtask queues,
1985+it is necessary to ensure that tasks on the inner microtask queue will be run
1986+**whenever** the outer context enqueues a task on the inner microtask queue.
1987+1988+The tasks on the microtask queue of a given context are run whenever
1989+`runInContext()` or `SourceTextModule.evaluate()` are invoked on a script or
1990+module using this context. In our example, the normal execution flow can be
1991+restored by scheduling a second call to `runInContext()` _before_ `await
1992+inner_promise`.
1993+1994+```mjs
1995+// Schedule `runInContext()` to manually drain the inner context microtask
1996+// queue; it will run after the `await` statement below.
1997+setImmediate(() => {
1998+vm.runInContext('', context);
1999+});
2000+2001+await inner_promise;
2002+2003+console.log('OK');
2004+```
2005+2006+**Note:** Strictly speaking, in this mode, `node:vm` departs from the letter of
2007+the ECMAScript specification for [enqueing jobs][], by allowing asynchronous
2008+tasks from different contexts to run in a different order than they were
2009+enqueued.
2010+19492011## Support of dynamic `import()` in compilation APIs
1950201219512013The following APIs support an `importModuleDynamically` option to enable dynamic
@@ -2183,6 +2245,7 @@ const { Script, SyntheticModule } = require('node:vm');
21832245[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
21842246[`vm.runInThisContext()`]: #vmruninthiscontextcode-options
21852247[contextified]: #what-does-it-mean-to-contextify-an-object
2248+[enqueing jobs]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob
21862249[global object]: https://tc39.es/ecma262/#sec-global-object
21872250[indirect `eval()` call]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#direct_and_indirect_eval
21882251[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin