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
97 changes: 95 additions & 2 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,19 @@ changes:
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
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.
`breakOnSigint` scopes in that case. If `microtaskQueue` (or
`contextMicrotaskQueue`) is also specified, evaluating the script will
drain that shared microtask queue (including any microtasks queued from
other contexts sharing the queue). If `microtaskQueue` is not specified, a
private microtask queue is created exclusively for this context.
* `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. By default, microtasks placed on this queue are not
automatically drained when script evaluation finishes; they remain queued
until explicitly drained using [`microtaskQueue.runMicrotasks()`][], unless
`microtaskMode: 'afterEvaluate'` is also specified. An alias for this
option is `contextMicrotaskQueue`.
* Returns: {any} the result of the very last statement executed in the script.

This method is a shortcut to `script.runInContext(vm.createContext(options), options)`.
Expand Down Expand Up @@ -1318,6 +1330,37 @@ 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.

By default, passing a `vm.MicrotaskQueue` to [`vm.createContext()`][] attaches the
context to that queue without automatically draining it after script evaluation;
microtasks remain queued until explicitly drained using
[`microtaskQueue.runMicrotasks()`][]. If automatic draining upon script completion
is also desired, pass `microtaskMode: 'afterEvaluate'` alongside `microtaskQueue`.

### `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 +1517,19 @@ 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.
If `microtaskQueue` is also specified, evaluating a script in this context
will drain that shared microtask queue (including any pending microtasks
scheduled by other contexts sharing the queue). If `microtaskQueue` is not
specified, a private microtask queue is created exclusively for this
context.
* `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.
By default, microtasks placed on this queue are not automatically drained
when script evaluation finishes; they remain queued until explicitly drained
using [`microtaskQueue.runMicrotasks()`][], unless `microtaskMode: 'afterEvaluate'`
is also specified.
* `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 +1611,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 @@ -1822,7 +1900,18 @@ changes:
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
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.
`breakOnSigint` scopes in that case. If `microtaskQueue` is also specified,
evaluating the script will drain that shared microtask queue (including any
pending microtasks scheduled by other contexts sharing the queue). If
`microtaskQueue` is not specified, a private microtask queue is created
exclusively for this context.
* `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. By default, microtasks placed on this queue are not
automatically drained when script evaluation finishes; they must be
explicitly drained using [`microtaskQueue.runMicrotasks()`][], unless
`microtaskMode: 'afterEvaluate'` is also specified.
* Returns: {any} the result of the very last statement executed in the script.

This method is a shortcut to
Expand Down Expand Up @@ -2577,16 +2666,20 @@ const { Script, SyntheticModule } = require('node:vm');
[`Error`]: errors.md#class-error
[`URL`]: url.md#class-url
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
[`microtaskQueue.runMicrotasks()`]: #microtaskqueuerunmicrotasks
[`new vm.MicrotaskQueue()`]: #new-vmmicrotaskqueue
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
[`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate
[`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
[`url.origin`]: url.md#urlorigin
[`vm.MicrotaskQueue`]: #class-vmmicrotaskqueue
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify
[`vm.createContext()`]: #vmcreatecontextcontextobject-options
[`vm.createMicrotaskQueue()`]: #vmcreatemicrotaskqueue
[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
[`vm.runInThisContext()`]: #vmruninthiscontextcode-options
[contextified]: #what-does-it-mean-to-contextify-an-object
Expand Down
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
Loading