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
103 changes: 91 additions & 12 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,18 @@ changes:
`EvalError`. **Default:** `true`.
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
module will throw a `WebAssembly.CompileError`. **Default:** `true`.
* `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.
* `microtaskMode` {string|Object}
* If set to the string `'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.
* If set to an object of the form `{ type: 'manual', queue }`, where
`queue` is a [`vm.MicrotaskQueue`][] created via
[`vm.createMicrotaskQueue()`][], microtasks scheduled while running the
script are placed on `queue` instead of being drained automatically.
`queue` may be shared with other contexts so that microtasks from all
of them can be drained together, in the order they were scheduled, via
a single explicit [`microtaskQueue.runMicrotasks()`][] call.
* 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 +1326,35 @@ added:

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

## Class: `vm.MicrotaskQueue`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

An explicit microtask queue that can be attached to multiple contexts (via
the `microtaskMode` option of [`vm.createContext()`][],
[`vm.runInNewContext()`][], and [`script.runInNewContext()`][]) so that they
share where their microtasks (`Promise` reactions and `async function`
continuations) are placed, and so that those microtasks are drained together,
explicitly, by the embedder, instead of automatically by Node.js.

Instances are created with [`vm.createMicrotaskQueue()`][]; the constructor is
not exported by the `node:vm` module.

### `microtaskQueue.runMicrotasks()`

<!-- YAML
added: REPLACEME
-->

Synchronously runs every microtask currently queued on `microtaskQueue`, in
the order in which they were scheduled. If running a microtask schedules
further microtasks on the same queue, those are run as well before this
method returns.

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

<!-- YAML
Expand Down Expand Up @@ -1470,10 +1507,19 @@ changes:
`EvalError`. **Default:** `true`.
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
module will throw a `WebAssembly.CompileError`. **Default:** `true`.
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
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.
* `microtaskMode` {string|Object}
* If set to the string `'afterEvaluate'`, microtasks (tasks
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 set to an object of the form `{ type: 'manual', queue }`, where
`queue` is a [`vm.MicrotaskQueue`][] created via
[`vm.createMicrotaskQueue()`][], microtasks scheduled inside the new
context are placed on `queue` instead of being drained automatically.
`queue` may be shared with other contexts so that microtasks from all
of them can be drained together, in the order they were scheduled, via
a single explicit [`microtaskQueue.runMicrotasks()`][] call.
* `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 @@ -1542,6 +1588,19 @@ context.
The provided `name` and `origin` of the context are made visible through the
Inspector API.

## `vm.createMicrotaskQueue()`

<!-- YAML
added: REPLACEME
-->

* Returns: {vm.MicrotaskQueue}

Creates a new [`vm.MicrotaskQueue`][] that can be passed as the `queue` of a
`{ type: 'manual', queue }` value for the `microtaskMode` option of
[`vm.createContext()`][], so that multiple contexts can share where their
microtasks are placed. See [`vm.MicrotaskQueue`][] for details.

## `vm.isContext(object)`

<!-- YAML
Expand Down Expand Up @@ -1819,10 +1878,18 @@ changes:
experimental modules API. We do not recommend using it in a production
environment. For detailed information, see
[Support of dynamic `import()` in compilation APIs][].
* `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.
* `microtaskMode` {string|Object}
* If set to the string `'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.
* If set to an object of the form `{ type: 'manual', queue }`, where
`queue` is a [`vm.MicrotaskQueue`][] created via
[`vm.createMicrotaskQueue()`][], microtasks scheduled while running the
script are placed on `queue` instead of being drained automatically.
`queue` may be shared with other contexts so that microtasks from all
of them can be drained together, in the order they were scheduled, via
a single explicit [`microtaskQueue.runMicrotasks()`][] call.
* Returns: {any} the result of the very last statement executed in the script.

This method is a shortcut to
Expand Down Expand Up @@ -2352,6 +2419,13 @@ the ECMAScript specification for [enqueuing jobs][], by allowing asynchronous
tasks from different contexts to run in a different order than they were
enqueued.

Contexts that need to share promises without departing from the
specification's ordering guarantees can instead attach the same
[`vm.MicrotaskQueue`][] to each of them (`microtaskMode: { type: 'manual',
queue }`) and call [`microtaskQueue.runMicrotasks()`][] explicitly, once,
whenever a checkpoint is due; microtasks from every context sharing `queue`
then run together, in the order they were scheduled.

## Support of dynamic `import()` in compilation APIs

The following APIs support an `importModuleDynamically` option to enable dynamic
Expand Down Expand Up @@ -2577,17 +2651,22 @@ 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
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
[`script.runInNewContext()`]: #scriptruninnewcontextcontextobject-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.runInNewContext()`]: #vmruninnewcontextcode-contextobject-options
[`vm.runInThisContext()`]: #vmruninthiscontextcode-options
[contextified]: #what-does-it-mean-to-contextify-an-object
[enqueuing jobs]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob
Expand Down
58 changes: 51 additions & 7 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,
makeContext,
constants,
measureMemory: _measureMemory,
Expand Down Expand Up @@ -194,6 +196,50 @@ function getRunInContextArgs(contextifiedObject, options = kEmptyObject) {
};
}

class MicrotaskQueue extends ContextifyMicrotaskQueue {}

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

/**
* Validates and normalizes the `microtaskMode` option shared by
* `vm.createContext()`, `vm.runInNewContext()`, and `script.runInNewContext()`.
*
* `microtaskMode` accepts either:
* - the string `'afterEvaluate'` (unchanged legacy behavior: the context gets
* its own private microtask queue that is drained automatically after
* every evaluation), or
* - an object `{ type: 'manual', queue }`, where `queue` is a
* `vm.MicrotaskQueue` created via `vm.createMicrotaskQueue()`, that the
* context should share with other contexts. Draining only happens when the
* caller explicitly calls `queue.runMicrotasks()`.
* These two forms are mutually exclusive by construction: a context is either
* drained automatically (with a private queue) or shared-and-manual, never
* both, so there is no "shared queue that also auto-drains" combination to
* reason about.
* @param {undefined | 'afterEvaluate' | { type: string, queue: unknown }}
* microtaskMode
* @returns {{ afterEvaluate: boolean, queueHandle: MicrotaskQueue|undefined }}
*/
function getMicrotaskModeOptions(microtaskMode) {
if (microtaskMode === undefined) {
return { afterEvaluate: false, queueHandle: undefined };
}
if (typeof microtaskMode === 'string') {
validateOneOf(microtaskMode, 'options.microtaskMode', ['afterEvaluate']);
return { afterEvaluate: true, queueHandle: undefined };
}
validateObject(microtaskMode, 'options.microtaskMode');
const { type, queue } = microtaskMode;
validateOneOf(type, 'options.microtaskMode.type', ['manual']);
if (!isMicrotaskQueue(queue)) {
throw new ERR_INVALID_ARG_TYPE(
'options.microtaskMode.queue', 'vm.MicrotaskQueue', queue);
}
return { afterEvaluate: false, queueHandle: queue };
}

function getContextOptions(options) {
if (!options)
return {};
Expand All @@ -217,8 +263,6 @@ function getContextOptions(options) {
validateBoolean(wasm, 'options.contextCodeGeneration.wasm');
contextOptions.codeGeneration = { strings, wasm };
}
if (options.microtaskMode !== undefined)
validateString(options.microtaskMode, 'options.microtaskMode');
return contextOptions;
}

Expand Down Expand Up @@ -252,15 +296,14 @@ function createContext(contextObject = { __proto__: ObjectPrototype }, options =
validateBoolean(wasm, 'options.codeGeneration.wasm');
}

validateOneOf(microtaskMode,
'options.microtaskMode',
['afterEvaluate', undefined]);
const microtaskQueue = (microtaskMode === 'afterEvaluate');
const { afterEvaluate, queueHandle } = getMicrotaskModeOptions(microtaskMode);

const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, name);

const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
const result = makeContext(
contextObject, name, origin, strings, wasm,
queueHandle ?? afterEvaluate, hostDefinedOptionId);
// Register the context scope callback after the context was initialized.
registerImportModuleDynamically(result, importModuleDynamically);
return result;
Expand Down Expand Up @@ -411,6 +454,7 @@ module.exports = {
runInThisContext,
isContext,
compileFunction,
createMicrotaskQueue,
measureMemory,
constants: vmConstants,
};
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,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
78 changes: 73 additions & 5 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,8 @@ 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)) {
CppgcMixin::Wrap(this, env, wrapper);

context_.Reset(env->isolate(), v8_context);
Expand Down Expand Up @@ -445,6 +446,15 @@ 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);
} else if (args[5]->IsObject()) {
CHECK(env->microtask_queue_constructor_template()->HasInstance(args[5]));
// A vm.MicrotaskQueue (options.microtaskMode = { type: 'manual', queue })
// is not exclusively owned by this context, so it's stored separately
// from own_microtask_queue: see ContextOptions::shared_microtask_queue
// and ContextifyContext::microtask_queue().
ContextifyMicrotaskQueue* queue;
ASSIGN_OR_RETURN_UNWRAP(&queue, args[5].As<Object>());
options.shared_microtask_queue = queue->microtask_queue();
}

CHECK(args[6]->IsSymbol());
Expand Down Expand Up @@ -2027,13 +2037,70 @@ 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<FunctionTemplate> tmpl = NewFunctionTemplate(isolate, New);
tmpl->InstanceTemplate()->SetInternalFieldCount(
BaseObject::kInternalFieldCount);
SetProtoMethod(isolate, tmpl, "runMicrotasks", RunMicrotasks);

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

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

ContextifyMicrotaskQueue::ContextifyMicrotaskQueue(Environment* env,
Local<Object> wrap)
: BaseObject(env, wrap),
microtask_queue_(
MicrotaskQueue::New(env->isolate(), MicrotasksPolicy::kExplicit)) {
// Nothing outside this wrapper needs to keep it alive: once no JS
// reference to it remains, it's fine for it to be collected. The
// underlying v8::MicrotaskQueue stays alive independently, for as long as
// any ContextifyContext still holds a shared_ptr copy of it (see
// ContextOptions::shared_microtask_queue).
MakeWeak();
}

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

void ContextifyMicrotaskQueue::IsMicrotaskQueue(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
args.GetReturnValue().Set(
env->microtask_queue_constructor_template()->HasInstance(args[0]));
}

// queue.runMicrotasks()
void ContextifyMicrotaskQueue::RunMicrotasks(
const FunctionCallbackInfo<Value>& args) {
ContextifyMicrotaskQueue* queue;
ASSIGN_OR_RETURN_UNWRAP(&queue, args.This());
queue->microtask_queue_->PerformCheckpoint(args.GetIsolate());
}

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 +2150,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
Loading