-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
vm: add experimental NodeRealm implementation #47855
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
Changes from 23 commits
5aebfda
a624c7e
b04d52c
156fad5
8ff9d0a
1050cb2
1b5978b
4211750
8929ecd
7f56539
db8bf72
62e534b
bb0a04a
75618e6
1b8059f
6184855
0ffaba0
aa46778
c3f4321
39e58b3
8474e51
8240d61
f371400
9f1d1a5
57409e6
db0c89b
3988738
db395ac
7ba9bbd
4209518
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -530,6 +530,22 @@ changes: | |
| Specify the `module` of a custom experimental [ECMAScript module loader][]. | ||
| `module` may be any string accepted as an [`import` specifier][]. | ||
|
|
||
| ### `--experimental-noderealm` | ||
mcollina marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
mcollina marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
|
|
||
| Enable experimental support for `vm.NodeRealm`. | ||
|
|
||
| ### `--no-experimental-noderealm` | ||
|
||
|
|
||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
|
|
||
| Disable experimental support for `vm.NodeRealm`. | ||
|
|
||
| ### `--experimental-network-imports` | ||
|
|
||
| <!-- YAML | ||
|
|
@@ -2113,6 +2129,7 @@ Node.js options that are allowed are: | |
| * `--experimental-import-meta-resolve` | ||
| * `--experimental-json-modules` | ||
| * `--experimental-loader` | ||
| * `--experimental-noderealm` | ||
|
||
| * `--experimental-modules` | ||
| * `--experimental-network-imports` | ||
| * `--experimental-permission` | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1573,6 +1573,78 @@ 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-noderealm` 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. | ||||||
|
|
||||||
| ```mjs | ||||||
| import { NodeRealm } from 'node:vm'; | ||||||
| const noderealm = new NodeRealm(); | ||||||
| const myAsyncFunction = noderealm.createImport(import.meta.url)('my-module'); | ||||||
|
||||||
| console.log(await myAsyncFunction()); | ||||||
| ``` | ||||||
|
Member
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. I do think the docs should clarify the difference between this and a
Contributor
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. 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?) |
||||||
|
|
||||||
| #### `new NodeRealm()` | ||||||
|
|
||||||
| <!-- YAML | ||||||
| added: REPLACEME | ||||||
| --> | ||||||
|
|
||||||
| #### `noderealm.stop()` | ||||||
|
||||||
| #### `noderealm.stop()` | |
| #### `nodeRealm.stop()` |
And for all following.
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
mcollina marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
mcollina marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
mcollina marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
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.
It should be clarified whether this value is mutable. e.g. is it possible to localworker.globalThis.foo = 1 and have that value reflected within the local worker.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| '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 { | ||
| NodeRealm: NodeRealmImpl, | ||
| } = internalBinding('contextify'); | ||
|
|
||
| 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(); | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.#handle = new NodeRealmImpl(); | ||
| this.#handle.onexit = (code) => { | ||
| this.stop(); | ||
|
Contributor
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. Should we wait for the |
||
| 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. | ||
| // TODO(@mcollina): refactor to use a close callback | ||
| setTimeout(tryClosing, 100).unref(); | ||
|
||
| } else { | ||
|
|
||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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. | ||
| setTimeout(tryClosing, 100).unref(); | ||
| }); | ||
| } | ||
|
|
||
| get process() { | ||
| return this.#process; | ||
| } | ||
|
|
||
| get globalThis() { | ||
| return this.#global; | ||
| } | ||
|
|
||
| /** | ||
| * @param {string} path | ||
| */ | ||
| createImport(path) { | ||
| const parentURL = pathToFileURL(path); | ||
mcollina marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return (specifiers, importAssertions) => { | ||
| return this.#loader.import(specifiers, parentURL, importAssertions || {}); | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| module.exports = NodeRealm; | ||
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.
This needs to be updated to point to the correct location via the license builder.
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.
Could you point me at the docs for this?
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 don't know if there are docs for it, but I think you can change this line to
lib/internal/vm/localworker.jswhere it currently sayslib/worker_threads.js, rerun the license builder, and this should be updated.