-
Notifications
You must be signed in to change notification settings - Fork 30.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
vm: add experimental NodeRealm implementation #47855
Closed
Closed
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
5aebfda
vm: add experimental LocalWorker implementation
mcollina a624c7e
fixup
mcollina b04d52c
fixup
mcollina 156fad5
fixup
mcollina 8ff9d0a
Update doc/api/vm.md
mcollina 1050cb2
Update src/node_contextify.cc
mcollina 1b5978b
Update doc/api/vm.md
mcollina 4211750
fixup
mcollina 8929ecd
Update test/parallel/test-localworker.js
mcollina 7f56539
fixup
mcollina db8bf72
add test aboug globalThis
mcollina 62e534b
fix linting
mcollina bb0a04a
renamed to NodeRealm
mcollina 75618e6
Avoid the file trick for loading ESM
mcollina 1b8059f
fixup
mcollina 6184855
Update doc/api/vm.md
mcollina 0ffaba0
Update doc/api/vm.md
mcollina aa46778
Update doc/api/vm.md
mcollina c3f4321
Update lib/vm.js
mcollina 39e58b3
Update lib/internal/vm/noderealm.js
mcollina 8474e51
Update doc/api/vm.md
mcollina 8240d61
Apply suggestions from code review
mcollina f371400
fixup
mcollina 9f1d1a5
fixup
mcollina 57409e6
Update doc/api/vm.md
mcollina db0c89b
Address review comments
mcollina 3988738
fixup
mcollina db395ac
fixup
mcollina 7ba9bbd
fixup
mcollina 4209518
Apply suggestions from code review
mcollina File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1573,6 +1573,84 @@ inside a `vm.Context`, functions passed to them will be added to global queues, | |
which are shared by all contexts. Therefore, callbacks passed to those functions | ||
are not controllable through the timeout either. | ||
|
||
### Class: `NodeRealm` | ||
|
||
> Stability: 1 - Experimental. Use `--experimental-node-realm` CLI flag to | ||
> enable this feature. | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* Extends: {EventEmitter} | ||
|
||
A `NodeRealm` is effectively a Node.js environment that runs within the | ||
same thread. It similar to a [ShadowRealm][], but with a few main differences: | ||
|
||
* `NodeRealm` supports loading both CommonJS and ES modules. | ||
* Full interoperability between the host realm and the `NodeRealm` instance | ||
is allowed. | ||
* There is a deliberate `stop()` function. | ||
|
||
```mjs | ||
import { NodeRealm } from 'node:vm'; | ||
const nodeRealm = new NodeRealm(); | ||
const { myAsyncFunction } = await nodeRealm.createImport(import.meta.url)('my-module'); | ||
console.log(await myAsyncFunction()); | ||
``` | ||
|
||
#### `new NodeRealm()` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
#### `nodeRealm.stop()` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* Returns: <Promise> | ||
|
||
This will render the inner Node.js instance unusable. | ||
and is generally comparable to running `process.exit()`. | ||
|
||
This method returns a promise that will be resolved when all resources | ||
associated with this Node.js instance are released. This promise resolves on | ||
the event loop of the _outer_ Node.js instance. | ||
|
||
#### `nodeRealm.createImport(specifier)` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `specifier` {string} A module specifier like './file.js' or 'my-package' | ||
|
||
Creates a function that can be used for loading | ||
modules inside the inner Node.js instance. | ||
|
||
#### `nodeRealm.globalThis` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* Type: {Object} | ||
|
||
Returns a reference to the global object of the inner Node.js instance. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be clarified whether this value is mutable. e.g. is it possible to |
||
|
||
#### `nodeRealm.process` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* Type: {Object} | ||
|
||
Returns a reference to the `process` object of the inner Node.js instance. | ||
|
||
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records | ||
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules | ||
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation | ||
|
@@ -1599,3 +1677,4 @@ are not controllable through the timeout either. | |
[global object]: https://es5.github.io/#x15.1 | ||
[indirect `eval()` call]: https://es5.github.io/#x10.4.2 | ||
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin | ||
[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
'use strict'; | ||
|
||
// NodeRealm was originally a separate module developed by | ||
// Anna Henningsen and published separately on npm as the | ||
// synchronous-worker module under the MIT license. It has been | ||
// incorporated into Node.js with Anna's permission. | ||
// See the LICENSE file for LICENSE and copyright attribution. | ||
|
||
const { | ||
Promise, | ||
} = primordials; | ||
|
||
const { | ||
emitExperimentalWarning, | ||
} = require('internal/util'); | ||
|
||
const { | ||
ERR_VM_NODE_REALM_INVALID_PARENT, | ||
} = require('internal/errors').codes; | ||
|
||
const { | ||
NodeRealm: NodeRealmImpl, | ||
} = internalBinding('contextify'); | ||
|
||
const { URL } = require('internal/url'); | ||
const EventEmitter = require('events'); | ||
const { setTimeout } = require('timers'); | ||
const { pathToFileURL } = require('url'); | ||
|
||
let debug = require('internal/util/debuglog').debuglog('noderealm', (fn) => { | ||
debug = fn; | ||
}); | ||
|
||
class NodeRealm extends EventEmitter { | ||
#handle = undefined; | ||
#process = undefined; | ||
#global = undefined; | ||
#stoppedPromise = undefined; | ||
#loader = undefined; | ||
|
||
constructor() { | ||
super(); | ||
emitExperimentalWarning('NodeRealm'); | ||
this.#handle = new NodeRealmImpl(); | ||
this.#handle.onexit = (code) => { | ||
this.stop(); | ||
this.emit('exit', code); | ||
}; | ||
try { | ||
this.#handle.start(); | ||
this.#handle.load((process, nativeRequire, globalThis) => { | ||
this.#process = process; | ||
this.#global = globalThis; | ||
process.on('uncaughtException', (err) => { | ||
if (process.listenerCount('uncaughtException') === 1) { | ||
// If we are stopping, silence all errors | ||
if (!this.#stoppedPromise) { | ||
this.emit('error', err); | ||
} | ||
process.exit(1); | ||
} | ||
}); | ||
}); | ||
|
||
const req = this.#handle.internalRequire(); | ||
this.#loader = req('internal/process/esm_loader').esmLoader; | ||
} catch (err) { | ||
this.#handle.stop(); | ||
throw err; | ||
} | ||
} | ||
|
||
/** | ||
* @returns {Promise<void>} | ||
*/ | ||
async stop() { | ||
// TODO(@mcollina): add support for AbortController, we want to abort this, | ||
// or add a timeout. | ||
return this.#stoppedPromise ??= new Promise((resolve) => { | ||
const tryClosing = () => { | ||
const closed = this.#handle.tryCloseAllHandles(); | ||
debug('closed %d handles', closed); | ||
if (closed > 0) { | ||
// This is an active wait for the handles to close. | ||
// We might want to change this in the future to use a callback, | ||
// but at this point it seems like a premature optimization. | ||
// We cannot unref() this because we need to shut this down properly. | ||
// TODO(@mcollina): refactor to use a close callback | ||
setTimeout(tryClosing, 100); | ||
} else { | ||
|
||
this.#handle.stop(); | ||
resolve(); | ||
} | ||
}; | ||
|
||
// We use setTimeout instead of setImmediate because it runs in a different | ||
// phase of the event loop. This is important because the immediate queue | ||
// would crash if the environment it refers to has been already closed. | ||
// We cannot unref() this because we need to shut this down properly. | ||
setTimeout(tryClosing, 100); | ||
}); | ||
} | ||
|
||
get process() { | ||
return this.#process; | ||
} | ||
|
||
get globalThis() { | ||
return this.#global; | ||
} | ||
|
||
/** | ||
* @param {string|URL} parentURL | ||
*/ | ||
createImport(parentURL) { | ||
if (typeof parentURL === 'string') { | ||
if (parentURL.indexOf('file://') === 0) { | ||
parentURL = new URL(parentURL); | ||
} else { | ||
parentURL = pathToFileURL(parentURL); | ||
} | ||
} else if (!(parentURL instanceof URL)) { | ||
throw new ERR_VM_NODE_REALM_INVALID_PARENT(parentURL); | ||
} | ||
|
||
return (specifiers, importAssertions) => { | ||
return this.#loader.import(specifiers, parentURL, importAssertions || {}); | ||
}; | ||
} | ||
} | ||
|
||
module.exports = NodeRealm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think the docs should clarify the difference between this and a
ShadowRealm
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also like to understand the differences (and similarities) between this and a worker. Because they look very similar. For example, does a realm have an event loop? Does it share globals? (I'm assuming yes and no?)