-
Notifications
You must be signed in to change notification settings - Fork 30k
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
Changes from 2 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 |
---|---|---|
|
@@ -1573,6 +1573,105 @@ 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. | ||
|
||
## Local Worker | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
> Stability: 1 - Experimental | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Class: `LocalWorker` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* Extends: {EventEmitter} | ||
|
||
A `LocalWorker` is effectively a Node.js environment that runs within the | ||
same thread. | ||
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 need a bit more detail than this. A Node.js environment . . . with its own global scope? That can have separate To others’ points, how does this differ from Realm? 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. A Realm (in specs terms) does not support ESM, |
||
|
||
```mjs | ||
import { LocalWorker } from 'vm'; | ||
import { fileURLToPath } from 'url'; | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const w = new LocalWorker(); | ||
const myAsyncFunction = w.createRequire(fileURLToPath(import.meta.url))('my-module'); | ||
console.log(await myAsyncFunction()); | ||
``` | ||
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 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 LocalWorker()` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
#### `localworker.runInWorkerScope(fn)` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `fn` {Function} | ||
|
||
Wrap `fn` and run it as if it were run on the event loop of the inner Node.js | ||
instance. In particular, this ensures that Promises created by the function | ||
itself are resolved correctly. You should generally use this to run any code | ||
inside the inner Node.js instance that performs asynchronous activity and that | ||
is not already running in an asynchronous context (you can compare this to | ||
the code that runs synchronously from the main file of a Node.js application). | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#### `localworker.stop()` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This will render the Node.js instance unusable | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#### `localworker.createRequire(filename)` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `filename` {string} | ||
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. This is really any module specifier that can be passed to 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. this is just require, yes. 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. Since this is the same as |
||
|
||
Create a `require()` function that can be used for loading CommonJS modules | ||
inside the inner Node.js instance. | ||
|
||
#### `localworker.createImport(filename)` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `filename` {string} | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Create a dynamic `import()` function that can be used for loading EcmaScript | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
modules inside 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. What is the signature of the returned function? Does it return a promise? Does it match the signature of |
||
|
||
#### `localworker.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 |
||
|
||
#### `localworker.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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
'use strict'; | ||
|
||
// LocalWorker 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 { | ||
LocalWorker: LocalWorkerImpl, | ||
} = internalBinding('contextify'); | ||
|
||
const EventEmitter = require('events'); | ||
const { setTimeout } = require('timers'); | ||
const { dirname, join } = require('path'); | ||
|
||
let debug = require('internal/util/debuglog').debuglog('localworker', (fn) => { | ||
debug = fn; | ||
}); | ||
|
||
class LocalWorker extends EventEmitter { | ||
#handle = undefined; | ||
#process = undefined; | ||
#global = undefined; | ||
#module = undefined; | ||
#stoppedPromise = undefined; | ||
/** | ||
*/ | ||
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. Nit: empty comment? |
||
constructor() { | ||
super(); | ||
this.#handle = new LocalWorkerImpl(); | ||
this.#handle.onexit = (code) => { | ||
this.stop(); | ||
this.emit('exit', code); | ||
}; | ||
try { | ||
this.#handle.start(); | ||
this.#handle.load((process, nativeRequire, globalThis) => { | ||
this.#process = process; | ||
this.#module = nativeRequire('module'); | ||
this.#global = globalThis; | ||
process.on('uncaughtException', (err) => { | ||
if (process.listenerCount('uncaughtException') === 1) { | ||
this.emit('error', err); | ||
process.exit(1); | ||
} | ||
}); | ||
}); | ||
} 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 onExit = () => { | ||
debug('stopping localworker'); | ||
this.#handle.stop(); | ||
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); | ||
} else { | ||
this.#handle.signalStop(); | ||
|
||
setTimeout(onExit, 100); | ||
} | ||
}; | ||
|
||
// 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); | ||
}); | ||
} | ||
|
||
get process() { | ||
return this.#process; | ||
} | ||
|
||
get globalThis() { | ||
return this.#global; | ||
} | ||
|
||
createRequire(...args) { | ||
return this.#module.createRequire(...args); | ||
} | ||
|
||
/** | ||
* @param {() => any} method | ||
*/ | ||
runInWorkerScope(method) { | ||
return this.#handle.runInCallbackScope(method); | ||
} | ||
|
||
/** | ||
* @param {string} filename | ||
*/ | ||
async createImport(filename) { | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// This is a hack to get around creating a dynamic import function | ||
// from code. We create a temporary file that exports the import | ||
// function, and then delete it. | ||
// TODO(@mcollina): figure out how to do this using internal APIs. | ||
|
||
const req = this.createRequire(filename); | ||
const fs = req('fs/promises'); | ||
|
||
const sourceText = ` | ||
module.exports = (file) => import(file); | ||
`; | ||
|
||
const dest = join(dirname(filename), `_import_jump_${process.pid}.js`); | ||
await fs.writeFile(dest, sourceText); | ||
|
||
const ownImport = req(dest); | ||
|
||
await fs.unlink(dest); | ||
|
||
return ownImport; | ||
} | ||
} | ||
|
||
module.exports = LocalWorker; |
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.js
where it currently sayslib/worker_threads.js
, rerun the license builder, and this should be updated.