@@ -1815,6 +1815,68 @@ inside a `vm.Context`, functions passed to them will be added to global queues,
18151815which are shared by all contexts. Therefore, callbacks passed to those functions
18161816are not controllable through the timeout either.
18171817
1818+ ### When `microtaskMode` is `'afterEvaluate'`, beware sharing Promises between Contexts
1819+
1820+ In `'afterEvaluate'` mode, the `Context` has its own microtask queue, separate
1821+ from the global microtask queue used by the outer (main) context. While this
1822+ mode is necessary to enforce `timeout` and enable `breakOnSigint` with
1823+ asynchronous tasks, it also makes sharing promises between contexts challenging.
1824+
1825+ In the example below, a promise is created in the inner context and shared with
1826+ the outer context. When the outer context `await` on the promise, the execution
1827+ flow of the outer context is disrupted in a surprising way: the log statement
1828+ is never executed.
1829+
1830+ ```mjs
1831+ import * as vm from 'node:vm';
1832+
1833+ const inner_context = vm.createContext({}, { microtaskMode: ' afterEvaluate' });
1834+
1835+ // runInContext() returns a Promise created in the inner context.
1836+ const inner_promise = vm .runInContext (
1837+ ' Promise.resolve()' ,
1838+ context,
1839+ );
1840+
1841+ // As part of performing `await`, the JavaScript runtime must enqueue a task
1842+ // on the microtask queue of the context where `inner_promise` was created.
1843+ // A task is added on the inner microtask queue, but **it will not be run
1844+ // automatically**: this task will remain pending indefinitely.
1845+ //
1846+ // Since the outer microtask queue is empty, execution in the outer module
1847+ // falls through, and the log statement below is never executed.
1848+ await inner_promise;
1849+
1850+ console .log (' this will NOT be printed' );
1851+ ```
1852+
1853+ To successfully share promises between contexts with different microtask queues,
1854+ it is necessary to ensure that tasks on the inner microtask queue will be run
1855+ ** whenever** the outer context enqueues a task on the inner microtask queue.
1856+
1857+ The tasks on the microtask queue of a given context are run whenever
1858+ ` runInContext() ` or ` SourceTextModule.evaluate() ` are invoked on a script or
1859+ module using this context. In our example, the normal execution flow can be
1860+ restored by scheduling a second call to ` runInContext() ` _ before_ `await
1861+ inner_promise`.
1862+
1863+ ``` mjs
1864+ // Schedule `runInContext()` to manually drain the inner context microtask
1865+ // queue; it will run after the `await` statement below.
1866+ setImmediate (() => {
1867+ vm .runInContext (' ' , context);
1868+ });
1869+
1870+ await inner_promise;
1871+
1872+ console .log (' OK' );
1873+ ```
1874+
1875+ ** Note:** Strictly speaking, in this mode, ` node:vm ` departs from the letter of
1876+ the ECMAScript specification for [ enqueing jobs] [ ] , by allowing asynchronous
1877+ tasks from different contexts to run in a different order than they were
1878+ enqueued.
1879+
18181880## Support of dynamic ` import() ` in compilation APIs
18191881
18201882The following APIs support an ` importModuleDynamically ` option to enable dynamic
@@ -2048,6 +2110,7 @@ const { Script, SyntheticModule } = require('node:vm');
20482110[` vm .runInContext ()` ]: #vmrunincontextcode-contextifiedobject-options
20492111[` vm .runInThisContext ()` ]: #vmruninthiscontextcode-options
20502112[contextified]: #what-does-it-mean-to-contextify-an-object
2113+ [enqueing jobs]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob
20512114[global object]: https://tc39.es/ecma262/#sec-global-object
20522115[indirect ` eval ()` call]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#direct_and_indirect_eval
20532116[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin
0 commit comments