From 5aebfda2f2c6ae53c382d6e6a4fd095f846c3562 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 4 May 2023 11:25:40 +0200 Subject: [PATCH 01/30] vm: add experimental LocalWorker implementation A LocalWorker is a Node.js environemnt that runs within the same thread. This feature is added behind a `--experimental-localworker` flag. This work is based on The synchronous-worker module as originally written by Anna Henningsen and is incorporated here with Anna's permission. Signed-off-by: Matteo Collina Co-Authored-By: James M Snell --- LICENSE | 25 +++ doc/api/cli.md | 17 ++ doc/api/vm.md | 99 +++++++++ lib/internal/process/pre_execution.js | 11 + lib/internal/vm/localworker.js | 141 ++++++++++++ lib/vm.js | 2 + src/env_properties.h | 1 + src/node_contextify.cc | 302 ++++++++++++++++++++++++++ src/node_contextify.h | 64 ++++++ src/node_options.cc | 5 + src/node_options.h | 2 + test/parallel/test-localworker.js | 96 ++++++++ tools/license-builder.sh | 3 + 13 files changed, 768 insertions(+) create mode 100644 lib/internal/vm/localworker.js create mode 100644 test/parallel/test-localworker.js diff --git a/LICENSE b/LICENSE index b2f25c4e4b8974..95ef940cf761ef 100644 --- a/LICENSE +++ b/LICENSE @@ -2186,3 +2186,28 @@ The externally maintained libraries used by Node.js are: NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ + +- synchronous-worker, located at lib/internal/vm/localworker.js, is licensed as follows: + """ + The MIT License (MIT) + + Copyright (c) 2020 Anna Henningsen + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ diff --git a/doc/api/cli.md b/doc/api/cli.md index 8e26ae16ae6233..a61162f9ee54fd 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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-localworker` + + + +Enable experimental support for `vm.LocalWorker`. + +### `--no-experimental-localworker` + + + +Disable experimental support for `vm.LocalWorker`. + ### `--experimental-network-imports` + +* Extends: {EventEmitter} + +A `LocalWorker` is effectively a Node.js environment that runs within the +same thread. + +```mjs +import { LocalWorker } from 'vm'; +import { fileURLToPath } from 'url'; +const w = new LocalWorker(); +const myAsyncFunction = w.createRequire(fileURLToPath(import.meta.url))('my-module'); +console.log(await myAsyncFunction()); +``` + +#### `new LocalWorker()` + + + +#### `synchronousWorker.runInWorkerScope(fn)` + + + +* `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). + +#### `synchronousWorker.stop()` + + + +This will render the 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. + +#### `synchronousWorker.createRequire(filename)` + + + +* `filename` {string} + +Create a `require()` function that can be used for loading CommonJS modules +inside the inner Node.js instance. + +#### `synchronousWorker.createImport(filename)` + + + +* `filename` {string} + +Create a dynamic `import()` function that can be used for loading EcmaScript +modules inside the inner Node.js instance. + +#### `synchronousWorker.globalThis` + + + +* Type: {Object} + +Returns a reference to the global object of the inner Node.js instance. + +#### `synchronousWorker.process` + + + +* 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 diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index fc6d9ccf4f517b..ae7e63ba143393 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -67,6 +67,7 @@ function prepareExecution(options) { setupInspectorHooks(); setupWarningHandler(); setupFetch(); + setupLocalWorker(); setupWebCrypto(); setupCustomEvent(); setupCodeCoverage(); @@ -267,6 +268,16 @@ function setupFetch() { }); } +function setupLocalWorker() { + // Patch the vm module when --experimental-localworker is on. + // Please update the comments in vm.js when this block changes. + if (getOptionValue('--experimental-localworker')) { + const LocalWorker = require('internal/vm/localworker'); + const vm = require('vm'); + vm.LocalWorker = LocalWorker; + } +} + // TODO(aduh95): move this to internal/bootstrap/web/* when the CLI flag is // removed. function setupWebCrypto() { diff --git a/lib/internal/vm/localworker.js b/lib/internal/vm/localworker.js new file mode 100644 index 00000000000000..1dcbc0ec93bdb9 --- /dev/null +++ b/lib/internal/vm/localworker.js @@ -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; + /** + */ + 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} + */ + 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) { + // 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; diff --git a/lib/vm.js b/lib/vm.js index 1fdea8433495b2..e01d9e23a2889f 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -343,3 +343,5 @@ module.exports = { // The vm module is patched to include vm.Module, vm.SourceTextModule // and vm.SyntheticModule in the pre-execution phase when // --experimental-vm-modules is on. +// The vm module is also patched to include vm.LocalWorker in the +// pre-execution phase when --experimental-localworker is on. diff --git a/src/env_properties.h b/src/env_properties.h index 320d08d22577c8..00bd9a98f81ac1 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -366,6 +366,7 @@ V(socketaddress_constructor_template, v8::FunctionTemplate) \ V(streambaseentry_ctor_template, v8::FunctionTemplate) \ V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ + V(localworker_constructor_template, v8::FunctionTemplate) \ V(streamentry_ctor_template, v8::FunctionTemplate) \ V(streamentry_opaque_ctor_template, v8::FunctionTemplate) \ V(qlogoutputstream_constructor_template, v8::ObjectTemplate) \ diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 7f9f71ba74223a..3906301ea7940c 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -22,6 +22,7 @@ #include "node_contextify.h" #include "base_object-inl.h" +#include "async_wrap-inl.h" #include "memory_tracker-inl.h" #include "module_wrap.h" #include "node_context_data.h" @@ -41,13 +42,16 @@ using v8::Array; using v8::ArrayBufferView; using v8::Boolean; using v8::Context; +using v8::DeserializeInternalFieldsCallback; using v8::EscapableHandleScope; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; +using v8::Global; using v8::HandleScope; using v8::IndexedPropertyHandlerConfiguration; using v8::Int32; +using v8::Integer; using v8::Isolate; using v8::Just; using v8::Local; @@ -1391,6 +1395,300 @@ void MicrotaskQueueWrap::RegisterExternalReferences( registry->Register(New); } +bool LocalWorker::HasInstance(Environment* env, Local value) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +Local LocalWorker::GetConstructorTemplate( + Environment* env) { + Local tmpl = env->localworker_constructor_template(); + if (tmpl.IsEmpty()) { + Isolate* isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, New); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount(1); + + SetProtoMethod(isolate, tmpl, "start", Start); + SetProtoMethod(isolate, tmpl, "load", Load); + SetProtoMethod(isolate, tmpl, "stop", Stop); + SetProtoMethod(isolate, tmpl, "signalStop", SignalStop); + SetProtoMethod(isolate, tmpl, "runInCallbackScope", RunInCallbackScope); + SetProtoMethod(isolate, tmpl, "tryCloseAllHandles", TryCloseAllHandles); + + env->set_localworker_constructor_template(tmpl); + } + return tmpl; +} + +void LocalWorker::Initialize(Environment* env, + v8::Local target) { + SetConstructorFunction(env->context(), + target, + "LocalWorker", + GetConstructorTemplate(env), + SetConstructorFunctionFlag::NONE); +} + +void LocalWorker::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(New); + registry->Register(Start); + registry->Register(Load); + registry->Register(SignalStop); + registry->Register(Stop); + registry->Register(RunInCallbackScope); + registry->Register(TryCloseAllHandles); +} + +LocalWorker::LocalWorkerScope::LocalWorkerScope( + LocalWorker* w) + : EscapableHandleScope(w->isolate_), + Scope(w->context()), + Isolate::SafeForTerminationScope(w->isolate_), + w_(w), + orig_can_be_terminated_(w->can_be_terminated_) { + w_->can_be_terminated_ = true; +} + +LocalWorker::LocalWorkerScope::~LocalWorkerScope() { + w_->can_be_terminated_ = orig_can_be_terminated_; +} + +Local LocalWorker::context() const { + return context_.Get(isolate_); +} + +LocalWorker::LocalWorker(Environment* env, Local object) + : isolate_(env->isolate()), wrap_(env->isolate(), object) { + AddEnvironmentCleanupHook(env->isolate(), CleanupHook, this); + object->SetAlignedPointerInInternalField(0, this); + + Local outer_context = env->context(); + outer_context_.Reset(env->isolate(), outer_context); +} + +LocalWorker* LocalWorker::Unwrap( + const FunctionCallbackInfo& args) { + Local value = args.This(); + if (!value->IsObject() || value.As()->InternalFieldCount() < 1) { + THROW_ERR_INVALID_THIS(Environment::GetCurrent(args.GetIsolate())); + return nullptr; + } + return static_cast( + value.As()->GetAlignedPointerFromInternalField(0)); +} + +void LocalWorker::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + new LocalWorker(env, args.This()); +} + +void LocalWorker::Start(const FunctionCallbackInfo& args) { + LocalWorker* self = Unwrap(args); + if (self == nullptr) return; + self->Start(); +} + +void LocalWorker::TryCloseAllHandles( + const FunctionCallbackInfo& args) { + auto count = 0; + LocalWorker* self = Unwrap(args); + self->env_->async_hooks()->clear_async_id_stack(); + if (self->env_ != nullptr) { + auto env = self->env_; + for (auto w : *env->handle_wrap_queue()) { + count++; + w->Close(); + } + + for (auto w : *env->req_wrap_queue()) { + count++; + w->Cancel(); + } + } + args.GetReturnValue().Set(v8::Number::New(self->isolate_, count)); +} + +void LocalWorker::Stop(const FunctionCallbackInfo& args) { + LocalWorker* self = Unwrap(args); + if (self == nullptr) return; + self->Stop(true); +} + +void LocalWorker::SignalStop(const FunctionCallbackInfo& args) { + LocalWorker* self = Unwrap(args); + if (self == nullptr) return; + self->SignalStop(); + args.GetIsolate()->CancelTerminateExecution(); +} + +void LocalWorker::Load(const FunctionCallbackInfo& args) { + LocalWorker* self = Unwrap(args); + if (self == nullptr) return; + if (!args[0]->IsFunction()) { + return THROW_ERR_INVALID_ARG_TYPE( + Environment::GetCurrent(args), + "The load() argument must be a function."); + } + Local result; + if (self->Load(args[0].As()).ToLocal(&result)) { + args.GetReturnValue().Set(result); + } +} + +void LocalWorker::RunInCallbackScope( + const FunctionCallbackInfo& args) { + LocalWorker* self = Unwrap(args); + if (self == nullptr) return; + if (!args[0]->IsFunction()) { + return THROW_ERR_INVALID_ARG_TYPE( + Environment::GetCurrent(args), + "The runInCallbackScope() argument must be a function"); + } + Local result; + if (self->RunInCallbackScope(args[0].As()).ToLocal(&result)) { + args.GetReturnValue().Set(result); + } +} + +MaybeLocal LocalWorker::RunInCallbackScope(Local fn) { + if (context_.IsEmpty() || signaled_stop_) { + Environment* env = Environment::GetCurrent(isolate_); + THROW_ERR_INVALID_STATE(env, "Worker has been stopped"); + return MaybeLocal(); + } + LocalWorkerScope worker_scope(this); + v8::Isolate* isolate = isolate_; + CallbackScope callback_scope(isolate, wrap_.Get(isolate_), {1, 0}); + MaybeLocal ret = fn->Call(context(), Null(isolate), 0, nullptr); + if (signaled_stop_) { + isolate->CancelTerminateExecution(); + } + return worker_scope.EscapeMaybe(ret); +} + +void LocalWorker::Start() { + signaled_stop_ = false; + Local outer_context = outer_context_.Get(isolate_); + Environment* outer_env = GetCurrentEnvironment(outer_context); + assert(outer_env != nullptr); + uv_loop_t* loop = GetCurrentEventLoop(isolate_); + assert(loop != nullptr); + + MicrotaskQueue* microtask_queue = + outer_context_.Get(isolate_)->GetMicrotaskQueue(); + + Local context = Context::New( + isolate_, + nullptr /* extensions */, + MaybeLocal() /* global_template */, + MaybeLocal() /* global_value */, + DeserializeInternalFieldsCallback() /* internal_fields_deserializer */, + microtask_queue); + context->SetSecurityToken(outer_context->GetSecurityToken()); + if (context.IsEmpty() || !InitializeContext(context).FromMaybe(false)) { + return; + } + + context_.Reset(isolate_, context); + Context::Scope context_scope(context); + isolate_data_ = CreateIsolateData( + isolate_, + loop, + GetMultiIsolatePlatform(outer_env), + GetArrayBufferAllocator(GetEnvironmentIsolateData(outer_env))); + assert(isolate_data_ != nullptr); + ThreadId thread_id = AllocateEnvironmentThreadId(); + auto inspector_parent_handle = GetInspectorParentHandle( + outer_env, thread_id, "file:///synchronous-worker.js"); + env_ = CreateEnvironment(isolate_data_, + context, + {}, + {}, + static_cast( + EnvironmentFlags::kTrackUnmanagedFds), + thread_id, + std::move(inspector_parent_handle)); + assert(env_ != nullptr); + SetProcessExitHandler(env_, + [this](Environment* env, int code) { OnExit(code); }); +} + +void LocalWorker::OnExit(int code) { + HandleScope handle_scope(isolate_); + Local self = wrap_.Get(isolate_); + Local outer_context = outer_context_.Get(isolate_); + Context::Scope context_scope(outer_context); + Isolate::SafeForTerminationScope termination_scope(isolate_); + Local onexit_v; + if (!self->Get(outer_context, String::NewFromUtf8Literal(isolate_, "onexit")) + .ToLocal(&onexit_v) || + !onexit_v->IsFunction()) { + return; + } + Local args[] = {Integer::New(isolate_, code)}; + USE(onexit_v.As()->Call(outer_context, self, 1, args)); + SignalStop(); +} + +void LocalWorker::SignalStop() { + signaled_stop_ = true; + if (env_ != nullptr && can_be_terminated_) { + node::Stop(env_); + } +} + +void LocalWorker::Stop(bool may_throw) { + if (env_ != nullptr) { + if (!signaled_stop_) { + SignalStop(); + isolate_->CancelTerminateExecution(); + } + FreeEnvironment(env_); + env_ = nullptr; + } + if (isolate_data_ != nullptr) { + FreeIsolateData(isolate_data_); + isolate_data_ = nullptr; + } + + context_.Reset(); + outer_context_.Reset(); + microtask_queue_.reset(); + + RemoveEnvironmentCleanupHook(isolate_, CleanupHook, this); + if (!wrap_.IsEmpty()) { + HandleScope handle_scope(isolate_); + wrap_.Get(isolate_)->SetAlignedPointerInInternalField(0, nullptr); + } + wrap_.Reset(); + delete this; +} + +MaybeLocal LocalWorker::Load(Local callback) { + if (env_ == nullptr || signaled_stop_) { + Environment* env = Environment::GetCurrent(isolate_); + THROW_ERR_INVALID_STATE(env, "Worker not initialized"); + return MaybeLocal(); + } + + LocalWorkerScope worker_scope(this); + return worker_scope.EscapeMaybe( + LoadEnvironment(env_, [&](const StartExecutionCallbackInfo& info) { + Local argv[] = { + info.process_object, info.native_require, context()->Global()}; + return callback->Call(context(), Null(isolate_), 3, argv); + })); +} + +void LocalWorker::CleanupHook(void* arg) { + static_cast(arg)->Stop(false); +} + +void LocalWorker::MemoryInfo(MemoryTracker* tracker) const { + // TODO(@jasnell): Implement +} + void CreatePerIsolateProperties(IsolateData* isolate_data, Local ctor) { Isolate* isolate = isolate_data->isolate(); @@ -1423,6 +1721,8 @@ static void CreatePerContextProperties(Local target, Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); + LocalWorker::Initialize(env, target); + Local constants = Object::New(env->isolate()); Local measure_memory = Object::New(env->isolate()); Local memory_execution = Object::New(env->isolate()); @@ -1458,6 +1758,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(StopSigintWatchdog); registry->Register(WatchdogHasPendingSigint); registry->Register(MeasureMemory); + + LocalWorker::RegisterExternalReferences(registry); } } // namespace contextify } // namespace node diff --git a/src/node_contextify.h b/src/node_contextify.h index 3160160521e0fe..63135f0868492c 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -6,6 +6,7 @@ #include "base_object-inl.h" #include "node_context_data.h" #include "node_errors.h" +#include "memory_tracker-inl.h" namespace node { class ExternalReferenceRegistry; @@ -210,6 +211,69 @@ v8::Maybe StoreCodeCacheResult( bool produce_cached_data, std::unique_ptr new_cached_data); +class LocalWorker final : public node::MemoryRetainer { + public: + static bool HasInstance(node::Environment* env, v8::Local value); + static v8::Local GetConstructorTemplate( + node::Environment* env); + static void Initialize(node::Environment* env, v8::Local target); + static void RegisterExternalReferences(node::ExternalReferenceRegistry* registry); + + LocalWorker(node::Environment* env, v8::Local obj); + + static void New(const v8::FunctionCallbackInfo& args); + static void Start(const v8::FunctionCallbackInfo& args); + static void Load(const v8::FunctionCallbackInfo& args); + static void SignalStop(const v8::FunctionCallbackInfo& args); + static void Stop(const v8::FunctionCallbackInfo& args); + static void TryCloseAllHandles( + const v8::FunctionCallbackInfo& args); + static void RunInCallbackScope( + const v8::FunctionCallbackInfo& args); + + struct LocalWorkerScope : public v8::EscapableHandleScope, + public v8::Context::Scope, + public v8::Isolate::SafeForTerminationScope { + public: + explicit LocalWorkerScope(LocalWorker* w); + ~LocalWorkerScope(); + + private: + LocalWorker* w_; + bool orig_can_be_terminated_; + }; + + v8::Local context() const; + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(LocalWorker) + SET_SELF_SIZE(LocalWorker) + + private: + static LocalWorker* Unwrap(const v8::FunctionCallbackInfo& arg); + static void CleanupHook(void* arg); + void OnExit(int code); + + void Start(); + v8::MaybeLocal Load(v8::Local callback); + v8::MaybeLocal RunInCallbackScope( + v8::Local callback); + void RunLoop(uv_run_mode mode); + void SignalStop(); + void Stop(bool may_throw); + + v8::Isolate* isolate_; + v8::Global wrap_; + + std::unique_ptr microtask_queue_; + v8::Global outer_context_; + v8::Global context_; + node::IsolateData* isolate_data_ = nullptr; + node::Environment* env_ = nullptr; + bool signaled_stop_ = false; + bool can_be_terminated_ = false; +}; + } // namespace contextify } // namespace node diff --git a/src/node_options.cc b/src/node_options.cc index 4e3633e5b5b8a6..ef61e4928eb2ce 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -442,6 +442,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_vm_modules, kAllowedInEnvvar); AddOption("--experimental-worker", "", NoOp{}, kAllowedInEnvvar); + AddOption("--experimental-localworker", + "experimental LocalWorker support", + &EnvironmentOptions::experimental_localworker, + kAllowedInEnvironment, + false); AddOption("--experimental-report", "", NoOp{}, kAllowedInEnvvar); AddOption( "--experimental-wasi-unstable-preview1", "", NoOp{}, kAllowedInEnvvar); diff --git a/src/node_options.h b/src/node_options.h index e0d338f203b0c4..432bf5cddc04b3 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -128,6 +128,8 @@ class EnvironmentOptions : public Options { bool experimental_repl_await = true; bool experimental_vm_modules = false; bool expose_internals = false; + bool experimental_localworker = false; + bool force_node_api_uncaught_exceptions_policy = false; bool frozen_intrinsics = false; int64_t heap_snapshot_near_heap_limit = 0; diff --git a/test/parallel/test-localworker.js b/test/parallel/test-localworker.js new file mode 100644 index 00000000000000..ecbbfc0c4f68a8 --- /dev/null +++ b/test/parallel/test-localworker.js @@ -0,0 +1,96 @@ +// Flags: --experimental-localworker +'use strict'; + +const common = require('../common'); +const { strictEqual } = require('node:assert'); + +const { + LocalWorker, +} = require('node:vm'); + +// Properly handles timers that are about to expire when FreeEnvironment() is called on +// a shared event loop +(async function() { + const w = new LocalWorker(); + + setImmediate(() => { + setTimeout(() => {}, 20); + const now = Date.now(); + while (Date.now() - now < 30); + }); + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new LocalWorker(); + + setImmediate(() => { + setImmediate(() => { + setImmediate(() => {}); + }); + }); + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new LocalWorker(); + + setImmediate(() => { + setTimeout(() => {}, 20); + const now = Date.now(); + while (Date.now() - now < 30); + }); + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new LocalWorker(); + let called = false; + + function cb() { + called = true; + } + w.runInWorkerScope(() => { + const req = w.createRequire(__filename); + const vm = req('vm'); + const fs = req('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + }); + + await w.stop(); + strictEqual(called, true); +})().then(common.mustCall()); + +(async function() { + const w = new LocalWorker(); + let called = false; + + function cb() { + called = true; + } + w.runInWorkerScope(async () => { + const _import = await w.createImport(__filename); + const vm = await _import('vm'); + const fs = await _import('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + }); + + await w.stop(); + strictEqual(called, true); +})().then(common.mustCall()); diff --git a/tools/license-builder.sh b/tools/license-builder.sh index aaedfeed5c7b1b..8c4f1ae003fc44 100755 --- a/tools/license-builder.sh +++ b/tools/license-builder.sh @@ -148,4 +148,7 @@ addlicense "node-fs-extra" "lib/internal/fs/cp" "$licenseText" addlicense "base64" "deps/base64/base64/" "$(cat "${rootdir}/deps/base64/base64/LICENSE" || true)" +licenseText="$(curl -sL https://raw.githubusercontent.com/addaleax/synchronous-worker/HEAD/LICENSE)" +addlicense "synchronous-worker" "lib/worker_threads.js" "$licenseText" + mv "$tmplicense" "$licensefile" From a624c7e6bdc3477ffa3b59f211d29a095034be27 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 4 May 2023 12:36:35 +0200 Subject: [PATCH 02/30] fixup Signed-off-by: Matteo Collina --- doc/api/vm.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index 7589636400b082..00b91fffcbcf0b 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1602,7 +1602,7 @@ console.log(await myAsyncFunction()); added: REPLACEME --> -#### `synchronousWorker.runInWorkerScope(fn)` +#### `localworker.runInWorkerScope(fn)` -This will render the Node.js instance unusable +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 From 4211750077eeceef6dfbde5161b31c55c053a6fa Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 5 May 2023 10:45:08 +0200 Subject: [PATCH 08/30] fixup Signed-off-by: Matteo Collina --- src/node_contextify.cc | 25 ++++++++++--------------- src/node_contextify.h | 6 +++--- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 76828bd1365c05..c2d4cecdebb09f 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -1395,17 +1395,13 @@ void MicrotaskQueueWrap::RegisterExternalReferences( registry->Register(New); } -bool LocalWorker::HasInstance(Environment* env, Local value) { - return GetConstructorTemplate(env)->HasInstance(value); -} - Local LocalWorker::GetConstructorTemplate( - Environment* env) { - Local tmpl = env->localworker_constructor_template(); + IsolateData* isolate_data) { + Local tmpl = isolate_data->localworker_constructor_template(); if (tmpl.IsEmpty()) { - Isolate* isolate = env->isolate(); + Isolate* isolate = isolate_data->isolate(); tmpl = NewFunctionTemplate(isolate, New); - tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data)); tmpl->InstanceTemplate()->SetInternalFieldCount(1); SetProtoMethod(isolate, tmpl, "start", Start); @@ -1415,17 +1411,17 @@ Local LocalWorker::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "runInCallbackScope", RunInCallbackScope); SetProtoMethod(isolate, tmpl, "tryCloseAllHandles", TryCloseAllHandles); - env->set_localworker_constructor_template(tmpl); + isolate_data->set_localworker_constructor_template(tmpl); } return tmpl; } -void LocalWorker::Initialize(Environment* env, - v8::Local target) { - SetConstructorFunction(env->context(), +void LocalWorker::CreatePerIsolateProperties(IsolateData* isolate_data, + v8::Local target) { + SetConstructorFunction(isolate_data->isolate(), target, "LocalWorker", - GetConstructorTemplate(env), + GetConstructorTemplate(isolate_data), SetConstructorFunctionFlag::NONE); } @@ -1685,6 +1681,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data, ContextifyContext::CreatePerIsolateProperties(isolate_data, target); ContextifyScript::CreatePerIsolateProperties(isolate_data, target); MicrotaskQueueWrap::CreatePerIsolateProperties(isolate_data, target); + LocalWorker::CreatePerIsolateProperties(isolate_data, target); SetMethod(isolate, target, "startSigintWatchdog", StartSigintWatchdog); SetMethod(isolate, target, "stopSigintWatchdog", StopSigintWatchdog); @@ -1710,8 +1707,6 @@ static void CreatePerContextProperties(Local target, Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); - LocalWorker::Initialize(env, target); - Local constants = Object::New(env->isolate()); Local measure_memory = Object::New(env->isolate()); Local memory_execution = Object::New(env->isolate()); diff --git a/src/node_contextify.h b/src/node_contextify.h index 360a32a75be2cb..3c36df9dd7bbf2 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -213,10 +213,10 @@ v8::Maybe StoreCodeCacheResult( class LocalWorker final : public node::MemoryRetainer { public: - static bool HasInstance(node::Environment* env, v8::Local value); static v8::Local GetConstructorTemplate( - node::Environment* env); - static void Initialize(node::Environment* env, v8::Local target); + IsolateData* isolate_data); + static void CreatePerIsolateProperties(IsolateData* isolate_data, + v8::Local target); static void RegisterExternalReferences( node::ExternalReferenceRegistry* registry); From 8929ecdd3617c3e1c23c57313396dc49db80ae8e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 5 May 2023 10:45:40 +0200 Subject: [PATCH 09/30] Update test/parallel/test-localworker.js Co-authored-by: Moshe Atlow --- test/parallel/test-localworker.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/parallel/test-localworker.js b/test/parallel/test-localworker.js index 9b961f476c447a..3c777b836efe1c 100644 --- a/test/parallel/test-localworker.js +++ b/test/parallel/test-localworker.js @@ -32,16 +32,6 @@ const { await w.stop(); })().then(common.mustCall()); -(async function() { - const w = new LocalWorker(); - - setImmediate(() => { - setTimeout(() => {}, 20); - const now = Date.now(); - while (Date.now() - now < 30); - }); - await w.stop(); -})().then(common.mustCall()); (async function() { const w = new LocalWorker(); From 7f56539fdcf90ea3012c2f89076f4926937c60a3 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 5 May 2023 10:52:01 +0200 Subject: [PATCH 10/30] fixup Signed-off-by: Matteo Collina --- doc/api/vm.md | 15 ----------- lib/internal/vm/localworker.js | 7 ----- src/node_contextify.cc | 33 ------------------------ src/node_contextify.h | 2 -- test/parallel/test-localworker.js | 43 ++++++++++++++----------------- 5 files changed, 20 insertions(+), 80 deletions(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index 3059471043de5b..0e6f8152f10f21 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1602,21 +1602,6 @@ console.log(await myAsyncFunction()); added: REPLACEME --> -#### `localworker.runInWorkerScope(fn)` - - - -* `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). - #### `localworker.stop()` -Enable experimental support for `vm.LocalWorker`. +Enable experimental support for `vm.NodeRealm`. -### `--no-experimental-localworker` +### `--no-experimental-noderealm` -Disable experimental support for `vm.LocalWorker`. +Disable experimental support for `vm.NodeRealm`. ### `--experimental-network-imports` @@ -2129,7 +2129,7 @@ Node.js options that are allowed are: * `--experimental-import-meta-resolve` * `--experimental-json-modules` * `--experimental-loader` -* `--experimental-localworker` +* `--experimental-noderealm` * `--experimental-modules` * `--experimental-network-imports` * `--experimental-permission` diff --git a/doc/api/vm.md b/doc/api/vm.md index 0e6f8152f10f21..418b7c3743093a 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1577,7 +1577,7 @@ are not controllable through the timeout either. > Stability: 1 - Experimental -### Class: `LocalWorker` +### Class: `NodeRealm` -#### `localworker.stop()` +#### `noderealm.stop()` @@ -1615,17 +1613,6 @@ 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.createRequire(filename)` - - - -* `filename` {string} - -Create a `require()` function that can be used for loading CommonJS modules -inside the inner Node.js instance. - #### `noderealm.createImport(filename)` +* Returns: + This will render the inner Node.js instance unusable. and is generally comparable to running `process.exit()`. diff --git a/lib/internal/vm/noderealm.js b/lib/internal/vm/noderealm.js index 3556f1baa93b09..5076b622639581 100644 --- a/lib/internal/vm/noderealm.js +++ b/lib/internal/vm/noderealm.js @@ -77,7 +77,7 @@ class NodeRealm extends EventEmitter { // 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); + setTimeout(tryClosing, 100).unref(); } else { this.#handle.stop(); diff --git a/src/env_properties.h b/src/env_properties.h index 17114b5b276e95..65bdf8ed54534d 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -366,7 +366,7 @@ V(socketaddress_constructor_template, v8::FunctionTemplate) \ V(streambaseentry_ctor_template, v8::FunctionTemplate) \ V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ - V(noderealm_constructor_template, v8::FunctionTemplate) \ + V(noderealm_constructor_template, v8::FunctionTemplate) \ V(streamentry_ctor_template, v8::FunctionTemplate) \ V(streamentry_opaque_ctor_template, v8::FunctionTemplate) \ V(qlogoutputstream_constructor_template, v8::ObjectTemplate) \ From f37140060b0b00ad569fe6002c89912b982c19e8 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 8 May 2023 15:35:50 +0200 Subject: [PATCH 23/30] fixup Signed-off-by: Matteo Collina --- src/node_contextify.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node_contextify.cc b/src/node_contextify.cc index ad8579204f08c6..9c874e0e5ebf8f 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -1490,6 +1490,7 @@ void NodeRealm::TryCloseAllHandles( const FunctionCallbackInfo& args) { auto count = 0; NodeRealm* self = Unwrap(args); + if (self == nullptr) return; self->env_->async_hooks()->clear_async_id_stack(); count += self->env_->CleanupHandlesNoUvRun(); args.GetReturnValue().Set(v8::Number::New(self->isolate_, count)); @@ -1563,7 +1564,7 @@ void NodeRealm::Start() { assert(isolate_data_ != nullptr); ThreadId thread_id = AllocateEnvironmentThreadId(); auto inspector_parent_handle = GetInspectorParentHandle( - outer_env, thread_id, "file:///synchronous-worker.js"); + outer_env, thread_id, "file:///noderealm.js"); env_ = CreateEnvironment(isolate_data_, context, {}, @@ -1631,7 +1632,7 @@ void NodeRealm::Stop(bool may_throw) { MaybeLocal NodeRealm::Load(Local callback) { if (env_ == nullptr || signaled_stop_) { Environment* env = Environment::GetCurrent(isolate_); - THROW_ERR_INVALID_STATE(env, "Worker not initialized"); + THROW_ERR_INVALID_STATE(env, "NodeRealm not initialized"); return MaybeLocal(); } From 9f1d1a5774d82e751d80711d5bc9b39a8c00e3e5 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 8 May 2023 18:37:03 +0200 Subject: [PATCH 24/30] fixup Signed-off-by: Matteo Collina --- lib/internal/vm/noderealm.js | 20 +++++++++--- test/parallel/test-noderealm.js | 54 +++++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/lib/internal/vm/noderealm.js b/lib/internal/vm/noderealm.js index 5076b622639581..432c1f7d6b13ea 100644 --- a/lib/internal/vm/noderealm.js +++ b/lib/internal/vm/noderealm.js @@ -76,8 +76,9 @@ class NodeRealm extends EventEmitter { // 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).unref(); + setTimeout(tryClosing, 100) } else { this.#handle.stop(); @@ -88,7 +89,8 @@ class NodeRealm extends EventEmitter { // 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(); + // We cannot unref() this because we need to shut this down properly. + setTimeout(tryClosing, 100) }); } @@ -101,10 +103,18 @@ class NodeRealm extends EventEmitter { } /** - * @param {string} path + * @param {string|URL} parentURL */ - createImport(path) { - const parentURL = pathToFileURL(path); + 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 Error('createImport() must be called with a string or URL'); + } return (specifiers, importAssertions) => { return this.#loader.import(specifiers, parentURL, importAssertions || {}); diff --git a/test/parallel/test-noderealm.js b/test/parallel/test-noderealm.js index 9048414bdceca0..fe8bd55b91466b 100644 --- a/test/parallel/test-noderealm.js +++ b/test/parallel/test-noderealm.js @@ -3,6 +3,7 @@ const common = require('../common'); const { strictEqual } = require('node:assert'); +const { pathToFileURL } = require('node:url'); const { NodeRealm, @@ -35,12 +36,7 @@ const { (async function() { const w = new NodeRealm(); - let called = false; - - function cb() { - called = true; - } - + const cb = common.mustCall(); const _import = w.createImport(__filename); const vm = await _import('vm'); const fs = await _import('fs'); @@ -55,16 +51,11 @@ const { }`)({ fs, cb }); await w.stop(); - strictEqual(called, true); })().then(common.mustCall()); (async function() { const w = new NodeRealm(); - let called = false; - - function cb() { - called = true; - } + const cb = common.mustCall(); const _import = await w.createImport(__filename); const vm = await _import('vm'); const fs = await _import('fs'); @@ -79,7 +70,6 @@ const { }`)({ fs, cb }); await w.stop(); - strictEqual(called, true); })().then(common.mustCall()); // Globals are isolated @@ -95,3 +85,41 @@ const { await w.stop(); })().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + const cb = common.mustCall(); + const _import = await w.createImport(pathToFileURL(__filename)); + const vm = await _import('vm'); + const fs = await _import('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + + await w.stop(); +})().then(common.mustCall()); + +(async function() { + const w = new NodeRealm(); + const cb = common.mustCall(); + const _import = await w.createImport(pathToFileURL(__filename).toString()); + const vm = await _import('vm'); + const fs = await _import('fs'); + + vm.runInThisContext(`({ fs, cb }) => { + const stream = fs.createReadStream('${__filename}'); + stream.on('open', () => { + cb() + }) + + setTimeout(() => {}, 200000); + }`)({ fs, cb }); + + await w.stop(); +})().then(common.mustCall()); From 57409e6199e412fa7793771ac07af9b83001ef27 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 8 May 2023 18:55:22 +0200 Subject: [PATCH 25/30] Update doc/api/vm.md Co-authored-by: Geoffrey Booth --- doc/api/vm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/vm.md b/doc/api/vm.md index e45d29194f0350..1b65e78798359a 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1620,7 +1620,7 @@ the event loop of the _outer_ Node.js instance. added: REPLACEME --> -* `filename` {string} +* `specifier` {string} A module specifier like './file.js' or 'my-package' Create a function that can be used for loading modules inside the inner Node.js instance. From db0c89bae0a648382b1c9b672a674e236f6111bc Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 8 May 2023 21:29:52 +0200 Subject: [PATCH 26/30] Address review comments Signed-off-by: Matteo Collina --- doc/api/cli.md | 6 +++--- doc/api/errors.md | 11 ++++++++++ doc/api/vm.md | 15 ++++++------- doc/node.1 | 3 +++ lib/internal/errors.js | 1 + lib/internal/process/pre_execution.js | 6 +++--- .../vm/{noderealm.js => node_realm.js} | 16 +++++++++++--- lib/vm.js | 2 +- src/env_properties.h | 2 +- src/node_contextify.cc | 6 +++--- src/node_options.cc | 4 ++-- src/node_options.h | 2 +- .../{test-noderealm.js => test-node-realm.js} | 21 +++++++++++++++++-- 13 files changed, 69 insertions(+), 26 deletions(-) rename lib/internal/vm/{noderealm.js => node_realm.js} (90%) rename test/parallel/{test-noderealm.js => test-node-realm.js} (86%) diff --git a/doc/api/cli.md b/doc/api/cli.md index 39f499a545ee33..de1f3401a5db3d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -530,7 +530,7 @@ changes: Specify the `module` of a custom experimental [ECMAScript module loader][]. `module` may be any string accepted as an [`import` specifier][]. -### `--experimental-noderealm` +### `--experimental-node-realm` + +The `createImport()` function was passed a valued that was neither +a string or a `URL`. + ### `ERR_WORKER_UNSUPPORTED_EXTENSION` diff --git a/doc/api/vm.md b/doc/api/vm.md index 1b65e78798359a..65064b662cf843 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1575,7 +1575,8 @@ are not controllable through the timeout either. ### Class: `NodeRealm` -> Stability: 1 - Experimental. Use `--experimental-noderealm` CLI flag to enable this feature. +> Stability: 1 - Experimental. Use `--experimental-node-realm` CLI flag to +> enable this feature. -#### `noderealm.stop()` +#### `nodeRealm.stop()` -The `createImport()` function was passed a valued that was neither -a string or a `URL`. +The `createImport()` function was passed a value that was neither +a string nor a `URL`. diff --git a/doc/api/vm.md b/doc/api/vm.md index 4b75b578596a90..b440bd946014a0 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -1587,15 +1587,15 @@ added: REPLACEME 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` allows to load both commonjs and ESM modules. +* `NodeRealm` supports loading both CommonJS and ES modules. * Full interoperability between the host realm and the `NodeRealm` instance - is allowed + is allowed. * There is a deliberate `stop()` function. ```mjs import { NodeRealm } from 'node:vm'; const nodeRealm = new NodeRealm(); -const myAsyncFunction = nodeRealm.createImport(import.meta.url)('my-module'); +const { myAsyncFunction } = await nodeRealm.createImport(import.meta.url)('my-module'); console.log(await myAsyncFunction()); ``` @@ -1620,7 +1620,7 @@ 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(filename)` +#### `nodeRealm.createImport(specifier)`