| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 44ce971 commit 9347ddd
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1946,6 +1946,68 @@ inside a `vm.Context`, functions passed to them will be added to global queues, | |||
| 1946 | 1946 | which are shared by all contexts. Therefore, callbacks passed to those functions | |
| 1947 | 1947 | are not controllable through the timeout either. | |
| 1948 | 1948 | ||
| 1949 | + ### 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 | + | ||
| 1949 | 2011 | ## Support of dynamic `import()` in compilation APIs | |
| 1950 | 2012 | ||
| 1951 | 2013 | The following APIs support an `importModuleDynamically` option to enable dynamic | |
@@ -2183,6 +2245,7 @@ const { Script, SyntheticModule } = require('node:vm'); | |||
| 2183 | 2245 | [`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options | |
| 2184 | 2246 | [`vm.runInThisContext()`]: #vmruninthiscontextcode-options | |
| 2185 | 2247 | [contextified]: #what-does-it-mean-to-contextify-an-object | |
| 2248 | + [enqueing jobs]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob | ||
| 2186 | 2249 | [global object]: https://tc39.es/ecma262/#sec-global-object | |
| 2187 | 2250 | [indirect `eval()` call]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#direct_and_indirect_eval | |
| 2188 | 2251 | [origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,7 @@ const microtaskMode = 'afterEvaluate'; | |||
| 14 | 14 | ||
| 15 | 15 | (async () => { | |
| 16 | 16 | const mustNotCall1 = common.mustNotCall(); | |
| 17 | + const mustNotCall2 = common.mustNotCall(); | ||
| 17 | 18 | const mustCall1 = common.mustCall(); | |
| 18 | 19 | ||
| 19 | 20 | const inner = {}; | |
@@ -30,4 +31,41 @@ const microtaskMode = 'afterEvaluate'; | |||
| 30 | 31 | ||
| 31 | 32 | // Prior to the fix for Issue 59541, the next statement was never executed. | |
| 32 | 33 | mustCall1(); | |
| 34 | + | ||
| 35 | + await inner.promise; | ||
| 36 | + | ||
| 37 | + // This is expected: the await statement above enqueues a (thenable job) task | ||
| 38 | + // onto the inner context microtask queue, but it will not be checkpointed, | ||
| 39 | + // therefore we never make progress. | ||
| 40 | + mustNotCall2(); | ||
| 41 | + })().then(common.mustNotCall()); | ||
| 42 | + | ||
| 43 | + (async () => { | ||
| 44 | + const mustNotCall1 = common.mustNotCall(); | ||
| 45 | + const mustCall1 = common.mustCall(); | ||
| 46 | + const mustCall2 = common.mustCall(); | ||
| 47 | + const mustCall3 = common.mustCall(); | ||
| 48 | + | ||
| 49 | + const inner = {}; | ||
| 50 | + | ||
| 51 | + const context = vm.createContext({ inner }, { microtaskMode }); | ||
| 52 | + | ||
| 53 | + const module = new vm.SourceTextModule( | ||
| 54 | + 'inner.promise = Promise.resolve();', | ||
| 55 | + { context }, | ||
| 56 | + ); | ||
| 57 | + | ||
| 58 | + await module.link(mustNotCall1); | ||
| 59 | + await module.evaluate(); | ||
| 60 | + mustCall1(); | ||
| 61 | + | ||
| 62 | + setImmediate(() => { | ||
| 63 | + mustCall2(); | ||
| 64 | + // This will checkpoint the inner context microtask queue, and allow the | ||
| 65 | + // promise from the inner context to be resolved in the outer context. | ||
| 66 | + module.evaluate(); | ||
| 67 | + }); | ||
| 68 | + | ||
| 69 | + await inner.promise; | ||
| 70 | + mustCall3(); | ||
| 33 | 71 | })().then(common.mustCall()); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,66 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // https://github.com/nodejs/node/issues/59541 | ||
| 4 | + // | ||
| 5 | + // Promises created in a context using microtaskMode: "aferEvaluate" (meaning | ||
| 6 | + // it has its own microtask queue), when resolved in the surrounding context, | ||
| 7 | + // will schedule a task back onto the inner context queue. This test checks that | ||
| 8 | + // the async execution progresses normally. | ||
| 9 | + | ||
| 10 | + const common = require('../common'); | ||
| 11 | + const vm = require('vm'); | ||
| 12 | + | ||
| 13 | + const microtaskMode = 'afterEvaluate'; | ||
| 14 | + | ||
| 15 | + (async () => { | ||
| 16 | + const mustNotCall1 = common.mustNotCall(); | ||
| 17 | + | ||
| 18 | + await vm.runInNewContext( | ||
| 19 | + `Promise.resolve()`, | ||
| 20 | + {}, { microtaskMode }); | ||
| 21 | + | ||
| 22 | + // Expected behavior: resolving an promise created in the inner context, from | ||
| 23 | + // the outer context results in the execution flow falling through, unless the | ||
| 24 | + // inner context microtask queue is manually drained, which we don't do here. | ||
| 25 | + mustNotCall1(); | ||
| 26 | + })().then(common.mustNotCall()); | ||
| 27 | + | ||
| 28 | + (async () => { | ||
| 29 | + const mustCall1 = common.mustCall(); | ||
| 30 | + const mustCall2 = common.mustCall(); | ||
| 31 | + const mustCall3 = common.mustCall(); | ||
| 32 | + | ||
| 33 | + // Create a new context. | ||
| 34 | + const context = vm.createContext({}, { microtaskMode }); | ||
| 35 | + | ||
| 36 | + setImmediate(() => { | ||
| 37 | + // This will drain the context microtask queue, after the `await` statement | ||
| 38 | + // below, and allow the promise from the inner context, created below, to be | ||
| 39 | + // resolved in the outer context. | ||
| 40 | + vm.runInContext('', context); | ||
| 41 | + mustCall2(); | ||
| 42 | + }); | ||
| 43 | + | ||
| 44 | + const inner_promise = vm.runInContext( | ||
| 45 | + `Promise.resolve()`, | ||
| 46 | + context); | ||
| 47 | + mustCall1(); | ||
| 48 | + | ||
| 49 | + await inner_promise; | ||
| 50 | + mustCall3(); | ||
| 51 | + })().then(common.mustCall()); | ||
| 52 | + | ||
| 53 | + { | ||
| 54 | + const mustNotCall1 = common.mustNotCall(); | ||
| 55 | + const mustCall1 = common.mustCall(); | ||
| 56 | + | ||
| 57 | + const context = vm.createContext({ setImmediate, mustNotCall1 }, { microtaskMode }); | ||
| 58 | + | ||
| 59 | + // setImmediate() will be run after runInContext() returns, and since the | ||
| 60 | + // anonymous function passed to `then` is defined in the inner context, the | ||
| 61 | + // thenable job task will be enqueued on the inner context microtask queue, | ||
| 62 | + // but at this point, it will not be drained automatically. | ||
| 63 | + vm.runInContext(`new Promise(setImmediate).then(() => mustNotCall1())`, context); | ||
| 64 | + | ||
| 65 | + mustCall1(); | ||
| 66 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments