Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,31 @@ added:

A `ModuleRequest` represents the request to import a module with given import attributes and phase.

## Class: `vm.MicrotaskQueue`

<!-- YAML
added: REPLACEME
-->

Represents an explicit microtask queue that can be shared across multiple
`vm.Context` instances and synchronously drained by the embedder.

### `new vm.MicrotaskQueue()`

<!-- YAML
added: REPLACEME
-->

Creates a new `vm.MicrotaskQueue` instance.

### `microtaskQueue.runMicrotasks()`

<!-- YAML
added: REPLACEME
-->

Synchronously runs all microtasks currently queued in this microtask queue.

## `vm.compileFunction(code[, params[, options]])`

<!-- YAML
Expand Down Expand Up @@ -1474,6 +1499,10 @@ changes:
scheduled through `Promise`s and `async function`s) will be run immediately
after a script has run through [`script.runInContext()`][].
They are included in the `timeout` and `breakOnSigint` scopes in that case.
* `microtaskQueue` {vm.MicrotaskQueue} A microtask queue created with
[`new vm.MicrotaskQueue()`][] or [`vm.createMicrotaskQueue()`][]. If
specified, microtasks scheduled inside this context will be placed on this
queue, allowing multiple contexts to share the same microtask queue.
* `importModuleDynamically`
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
Used to specify the how the modules should be loaded when `import()` is
Expand Down Expand Up @@ -1555,6 +1584,28 @@ Returns `true` if the given `object` object has been [contextified][] using
[`vm.createContext()`][], or if it's the global object of a context created
using [`vm.constants.DONT_CONTEXTIFY`][].

## `vm.createMicrotaskQueue()`

<!-- YAML
added: REPLACEME
-->

* Returns: {vm.MicrotaskQueue}

Creates a new [`vm.MicrotaskQueue`][] instance. Shortcut to
`new vm.MicrotaskQueue()`.

## `vm.isMicrotaskQueue(object)`

<!-- YAML
added: REPLACEME
-->

* `object` {any}
* Returns: {boolean}

Returns `true` if the given `object` is an instance of [`vm.MicrotaskQueue`][].

## `vm.measureMemory([options])`

<!-- YAML
Expand Down Expand Up @@ -1823,6 +1874,10 @@ changes:
scheduled through `Promise`s and `async function`s) will be run immediately
after the script has run. They are included in the `timeout` and
`breakOnSigint` scopes in that case.
* `microtaskQueue` {vm.MicrotaskQueue} A microtask queue created with
[`new vm.MicrotaskQueue()`][] or [`vm.createMicrotaskQueue()`][]. If
specified, microtasks scheduled inside the new context will be placed on
this queue.
* Returns: {any} the result of the very last statement executed in the script.

This method is a shortcut to
Expand Down Expand Up @@ -2594,3 +2649,6 @@ const { Script, SyntheticModule } = require('node:vm');
[global object]: https://tc39.es/ecma262/#sec-global-object
[indirect `eval()` call]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#direct_and_indirect_eval
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin
[`new vm.MicrotaskQueue()`]: #new-vmmicrotaskqueue
[`vm.MicrotaskQueue`]: #class-vmmicrotaskqueue
[`vm.createMicrotaskQueue()`]: #vmcreatemicrotaskqueue
36 changes: 34 additions & 2 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const {

const {
ContextifyScript,
MicrotaskQueue: ContextifyMicrotaskQueue,
isMicrotaskQueue: _isMicrotaskQueue,
makeContext,
constants,
measureMemory: _measureMemory,
Expand Down Expand Up @@ -82,6 +84,16 @@ function isContext(object) {
return _isContext(object);
}

class MicrotaskQueue extends ContextifyMicrotaskQueue {}

function createMicrotaskQueue() {
return new MicrotaskQueue();
}

function isMicrotaskQueue(object) {
return typeof object === 'object' && object !== null && _isMicrotaskQueue(object);
}

class Script extends ContextifyScript {
constructor(code, options = kEmptyObject) {
code = `${code}`;
Expand Down Expand Up @@ -202,6 +214,7 @@ function getContextOptions(options) {
origin: options.contextOrigin,
codeGeneration: undefined,
microtaskMode: options.microtaskMode,
microtaskQueue: options.microtaskQueue ?? options.contextMicrotaskQueue,
};
if (contextOptions.name !== undefined)
validateString(contextOptions.name, 'options.contextName');
Expand Down Expand Up @@ -235,6 +248,7 @@ function createContext(contextObject = { __proto__: ObjectPrototype }, options =
origin,
codeGeneration,
microtaskMode,
microtaskQueue,
importModuleDynamically,
} = options;

Expand All @@ -255,12 +269,27 @@ function createContext(contextObject = { __proto__: ObjectPrototype }, options =
validateOneOf(microtaskMode,
'options.microtaskMode',
['afterEvaluate', undefined]);
const microtaskQueue = (microtaskMode === 'afterEvaluate');
if (microtaskQueue !== undefined && !isMicrotaskQueue(microtaskQueue)) {
throw new ERR_INVALID_ARG_TYPE(
'options.microtaskQueue',
'MicrotaskQueue',
microtaskQueue,
);
}

const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, name);

const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
const result = makeContext(
contextObject,
name,
origin,
strings,
wasm,
microtaskQueue ?? (microtaskMode === 'afterEvaluate'),
hostDefinedOptionId,
microtaskMode === 'afterEvaluate',
);
// Register the context scope callback after the context was initialized.
registerImportModuleDynamically(result, importModuleDynamically);
return result;
Expand Down Expand Up @@ -413,6 +442,9 @@ module.exports = {
compileFunction,
measureMemory,
constants: vmConstants,
MicrotaskQueue,
createMicrotaskQueue,
isMicrotaskQueue,
};

// The vm module is patched to include vm.Module, vm.SourceTextModule
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@
V(lock_info_template, v8::DictionaryTemplate) \
V(lock_query_template, v8::DictionaryTemplate) \
V(message_port_constructor_template, v8::FunctionTemplate) \
V(microtask_queue_constructor_template, v8::FunctionTemplate) \
V(module_wrap_constructor_template, v8::FunctionTemplate) \
V(mx_record_template, v8::DictionaryTemplate) \
V(naptr_record_template, v8::DictionaryTemplate) \
Expand Down
100 changes: 94 additions & 6 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ ContextifyContext* ContextifyContext::New(Environment* env,

const SnapshotData* snapshot_data = env->isolate_data()->snapshot_data();

MicrotaskQueue* queue =
options->own_microtask_queue
? options->own_microtask_queue.get()
: env->isolate()->GetCurrentContext()->GetMicrotaskQueue();
MicrotaskQueue* queue = options->own_microtask_queue.get();
if (queue == nullptr) queue = options->shared_microtask_queue.get();
if (queue == nullptr)
queue = env->isolate()->GetCurrentContext()->GetMicrotaskQueue();

Local<Context> v8_context;
if (!(CreateV8Context(env->isolate(), object_template, snapshot_data, queue)
Expand All @@ -178,7 +178,9 @@ ContextifyContext::ContextifyContext(Environment* env,
ContextOptions* options)
: microtask_queue_(options->own_microtask_queue
? options->own_microtask_queue.release()
: nullptr) {
: nullptr),
shared_microtask_queue_(std::move(options->shared_microtask_queue)),
drain_after_evaluate_(options->drain_after_evaluate) {
CppgcMixin::Wrap(this, env, wrapper);

context_.Reset(env->isolate(), v8_context);
Expand Down Expand Up @@ -414,7 +416,7 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
ContextOptions options;

CHECK_EQ(args.Length(), 7);
CHECK_GE(args.Length(), 7);
Local<Object> sandbox;
if (args[0]->IsObject()) {
sandbox = args[0].As<Object>();
Expand Down Expand Up @@ -445,11 +447,20 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
if (args[5]->IsBoolean() && args[5]->BooleanValue(env->isolate())) {
options.own_microtask_queue =
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit);
options.drain_after_evaluate = true;
} else if (args[5]->IsObject()) {
ContextifyMicrotaskQueue* queue;
ASSIGN_OR_RETURN_UNWRAP_CPPGC(&queue, args[5].As<Object>());
options.shared_microtask_queue = queue->microtask_queue();
}

CHECK(args[6]->IsSymbol());
options.host_defined_options_id = args[6].As<Symbol>();

if (args.Length() > 7 && args[7]->IsBoolean()) {
options.drain_after_evaluate = args[7]->BooleanValue(env->isolate());
}

TryCatchScope try_catch(env);
ContextifyContext* context_ptr =
ContextifyContext::New(env, sandbox, &options);
Expand Down Expand Up @@ -2027,13 +2038,89 @@ static void MeasureMemory(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(promise);
}

void ContextifyMicrotaskQueue::CreatePerIsolateProperties(
IsolateData* isolate_data, Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
Local<String> class_name = FIXED_ONE_BYTE_STRING(isolate, "MicrotaskQueue");

Local<FunctionTemplate> tmpl = NewFunctionTemplate(isolate, New);
tmpl->InstanceTemplate()->SetInternalFieldCount(
ContextifyMicrotaskQueue::kInternalFieldCount);
tmpl->SetClassName(class_name);
SetProtoMethod(isolate, tmpl, "runMicrotasks", RunMicrotasks);

target->Set(isolate, "MicrotaskQueue", tmpl);
SetMethod(isolate, target, "isMicrotaskQueue", IsMicrotaskQueue);
isolate_data->set_microtask_queue_constructor_template(tmpl);
}

void ContextifyMicrotaskQueue::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(New);
registry->Register(RunMicrotasks);
registry->Register(IsMicrotaskQueue);
}

ContextifyMicrotaskQueue* ContextifyMicrotaskQueue::New(
Environment* env, Local<Object> object) {
DCHECK_NOT_NULL(env->isolate()->GetCppHeap());
return cppgc::MakeGarbageCollected<ContextifyMicrotaskQueue>(
env->cppgc_allocation_handle(), env, object);
}

void ContextifyMicrotaskQueue::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args.IsConstructCall());
New(env, args.This());
}

ContextifyMicrotaskQueue::ContextifyMicrotaskQueue(
Environment* env, Local<Object> object)
: microtask_queue_(
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit)) {
CppgcMixin::Wrap(this, env, object);
}

bool ContextifyMicrotaskQueue::InstanceOf(Environment* env,
const Local<Value>& value) {
return !value.IsEmpty() &&
value->IsObject() &&
env->microtask_queue_constructor_template()->HasInstance(value);
}

void ContextifyMicrotaskQueue::RunMicrotasks(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!ContextifyMicrotaskQueue::InstanceOf(env, args.This())) {
THROW_ERR_INVALID_THIS(
env,
"MicrotaskQueue methods can only be called on "
"MicrotaskQueue instances.");
return;
}
ContextifyMicrotaskQueue* queue;
ASSIGN_OR_RETURN_UNWRAP_CPPGC(&queue, args.This());
queue->microtask_queue_->PerformCheckpoint(args.GetIsolate());
}

void ContextifyMicrotaskQueue::IsMicrotaskQueue(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
bool is_queue =
args.Length() > 0 && args[0]->IsObject() && InstanceOf(env, args[0]) &&
CppgcMixin::Unwrap<ContextifyMicrotaskQueue>(args[0].As<Object>()) !=
nullptr;
args.GetReturnValue().Set(is_queue);
}

void CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();

ContextifyContext::CreatePerIsolateProperties(isolate_data, target);
ContextifyScript::CreatePerIsolateProperties(isolate_data, target);
ContextifyFunction::CreatePerIsolateProperties(isolate_data, target);
ContextifyMicrotaskQueue::CreatePerIsolateProperties(isolate_data, target);

SetMethod(isolate, target, "runInterruptible", RunInterruptible);

Expand Down Expand Up @@ -2083,6 +2170,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
ContextifyContext::RegisterExternalReferences(registry);
ContextifyScript::RegisterExternalReferences(registry);
ContextifyFunction::RegisterExternalReferences(registry);
ContextifyMicrotaskQueue::RegisterExternalReferences(registry);

registry->Register(CompileFunctionForCJSLoader);
registry->Register(RunInterruptible);
Expand Down
36 changes: 35 additions & 1 deletion src/node_contextify.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ struct ContextOptions {
v8::Local<v8::Boolean> allow_code_gen_strings;
v8::Local<v8::Boolean> allow_code_gen_wasm;
std::unique_ptr<v8::MicrotaskQueue> own_microtask_queue;
std::shared_ptr<v8::MicrotaskQueue> shared_microtask_queue;
bool drain_after_evaluate = false;
v8::Local<v8::Symbol> host_defined_options_id;
bool vanilla = false;
};
Expand Down Expand Up @@ -122,7 +124,9 @@ class ContextifyContext final : CPPGC_MIXIN(ContextifyContext) {
}

inline v8::MicrotaskQueue* microtask_queue() const {
return microtask_queue_.get();
if (!drain_after_evaluate_) return nullptr;
if (microtask_queue_) return microtask_queue_.get();
return shared_microtask_queue_.get();
}

template <typename T>
Expand Down Expand Up @@ -187,6 +191,36 @@ class ContextifyContext final : CPPGC_MIXIN(ContextifyContext) {

v8::TracedReference<v8::Context> context_;
std::unique_ptr<v8::MicrotaskQueue> microtask_queue_;
std::shared_ptr<v8::MicrotaskQueue> shared_microtask_queue_;
bool drain_after_evaluate_ = false;
};

class ContextifyMicrotaskQueue final : CPPGC_MIXIN(ContextifyMicrotaskQueue) {
public:
SET_CPPGC_NAME(ContextifyMicrotaskQueue)
DEFAULT_CPPGC_TRACE()
SET_NO_MEMORY_INFO()

ContextifyMicrotaskQueue(Environment* env, v8::Local<v8::Object> object);
~ContextifyMicrotaskQueue() override = default;

static ContextifyMicrotaskQueue* New(Environment* env,
v8::Local<v8::Object> object);
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static bool InstanceOf(Environment* env, const v8::Local<v8::Value>& value);
static void RunMicrotasks(const v8::FunctionCallbackInfo<v8::Value>& args);
static void IsMicrotaskQueue(const v8::FunctionCallbackInfo<v8::Value>& args);

static void CreatePerIsolateProperties(IsolateData* isolate_data,
v8::Local<v8::ObjectTemplate> target);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);

inline std::shared_ptr<v8::MicrotaskQueue> microtask_queue() const {
return microtask_queue_;
}

private:
std::shared_ptr<v8::MicrotaskQueue> microtask_queue_;
};

class ContextifyScript final : CPPGC_MIXIN(ContextifyScript) {
Expand Down
Loading
Loading