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,

19461946

which are shared by all contexts. Therefore, callbacks passed to those functions

19471947

are 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

1950201219512013

The 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