Repository navigation
proposal for shared microtask queues #65555
Description
Activity
- addedfeature requestIssues requesting new Node.js features.Issues requesting new Node.js features.
on Aug 27, 2026 - addedvmIssues and PRs related to the vm subsystem.Issues and PRs related to the vm subsystem.
on Aug 27, 2026 Thanks for the proposal. Are there existing projects that would benefit from the behavior shown in the Window/iframe example? That context would help explain the value of a public API.
TL;DR: Couldn't find an external issue confirming the exact problem. Narrowed the WPT search to microtask checkpoints and cross-realm ordering, but none combined the two. Contrived a cross-realm variation on an existing WPT: Chromium, Firefox, and Safari passed; Playwright WebKit failed; jsdom unexpectedly passed. Conclusion: problem is legit but niche.
I searched WPT for microtask checkpoints and cross-realm ordering. The most relevant tests I found were:
microtask_after_script.htmlmicrotask_after_raf.htmlmicrotasks-and-constructors.htmlperform-microtask-checkpoint-before-construction.html
They test checkpoints, but none combines a checkpoint with cross-realm ordering. The closest is
microtask_after_script.html, which already expects the equivalent ofA1, B1, A2.So before everyone decides I'm wasting their time, here is a contrived variation on that WPT. It moves
B1into a same-agent iframe and should expose the problem. Green iframe good; red bad.<!doctype html> <iframe></iframe> <script> const iframe = document.querySelector("iframe"); window.addEventListener("load", () => { const events = []; // Queue A1 in the Window realm. Promise.resolve().then(() => { events.push("A1"); // A1 queues A2 in the Window realm. Promise.resolve().then(() => { events.push("A2"); const passed = JSON.stringify(events) === JSON.stringify(["A1", "B1", "A2"]); iframe.contentDocument.body.style.background = passed ? "green" : "red"; }); }); // Compile and queue B1 second in the iframe realm. iframe.contentWindow.Function("events", ` Promise.resolve().then(() => { events.push("B1"); }); `)(events); }); </script>
Runtime Result Chromium Green Firefox Green WebKit 26.5 (via Playwright) Red Safari 26.5 Green jsdom 29.1.1 Green jsdom passing surprised me because it still has known parser and checkpoint problems, including #2413 and two WPT failures marked impossible. However, many of those issues look addressable with parse5's streaming parser and/or Node's
afterEvaluatemicrotask mode, so I don't think they confirm the shared-queue problem I'm proposing here.- added a commit that references this issue
on Sep 10, 2026 - added 6 commits that reference this issue
on Sep 30, 2026 I've now built and tested both #66461 and #65582 with the V8 patches and compatibility addon I use for my browser project. Both ran the relevant existing tests without new failures.
I don't have a preference between them. I like #66461's explicit manual mode contract, and #65582 makes a good case for keeping queue selection and draining policy independent. Both worked for my current requirements.
Whichever is adopted, I see two natural follow-ups: directly enqueueing a microtask onto a selected queue, and a completion callback that runs after draining but before V8 clears kept objects.
The usage I have in mind looks like this:
import vm from 'node:vm'; import assert from 'node:assert/strict'; // Proposed additions: enqueueMicrotask() and a completion callback (i.e. runMicrotasks + cb) const trace = []; // Execution order across contexts and host callbacks. const record = value => trace.push(value); // Synchronous; does not enqueue work. const queue = vm.createMicrotaskQueue(); // Shared native queue. const createRealm = () => vm.createContext( { record }, // Each context supplies its own Promise. { microtaskMode: { type: 'manual', queue } }, // #66461 // { microtaskQueue: queue }, // #65582 ); const window = createRealm(); // Models a Window realm. const iframe = createRealm(); // A same-agent iframe shares its queue. const otherQueue = vm.createMicrotaskQueue(); // Independently scheduled work. const otherTrace = []; otherQueue.enqueueMicrotask(() => otherTrace.push('other event loop')); vm.runInContext([ 'Promise.resolve().then(() => {', // Reaction callback belongs to this context. ' record("window");', // Synchronous host call. // Key case: this microtask schedules another microtask. ' Promise.resolve().then(() => record("follow-up"));', '});', ].join('\n'), window); vm.runInContext([ 'Promise.resolve().then(() => record("iframe"));', // Same FIFO, another context. ].join('\n'), iframe); // Explicitly select the queue for a callback created in Node's context. queue.enqueueMicrotask(() => record('host microtask')); assert.deepEqual(trace, []); // Both evaluations left their microtasks pending. // Proposed completion callback: after the drain, before ClearKeptObjects. queue.runMicrotasks(() => { record('host cleanup'); // Host-owned checkpoint bookkeeping. }); // V8 clears kept objects before returning. assert.deepEqual(trace, [ 'window', 'iframe', 'host microtask', 'follow-up', // The newly scheduled microtask must precede completion. 'host cleanup', ]); assert.deepEqual(otherTrace, []); // The checkpoint left the other queue untouched.
The completion callback could alternatively be registered on the queue, following V8's
AddMicrotasksCompletedCallback()model.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsAwaiting Triage
#34023 added
microtaskMode: 'afterEvaluate', which gives a context its own microtask queue and drains it after evaluation. There was discussion about giving authors broader control over the queue, but that was ultimately left out as beyond what users seemed to need.However, the HTML Standard is riddled with requirements that need this kind of control. For example, HTML assigns a microtask queue to each event loop. Its clean up after running script algorithm then requires:
A
Windowand its same-agent iframe have separate realms but share an event loop. If they are represented by separatevm.Contexts, they therefore need one shared microtask queue.Currently there's just not enough control over when queues are drained:
afterEvaluatedrains synchronously, but gives every context a separate queue.Let's try to explore this with code.
The HTML model instead requires:
With default contexts, both reactions are placed on Node's shared microtask queue in the required order, but there is no supported public API for performing the required checkpoint before
run()returns. That is why the first call returns[].With
afterEvaluate, returning from the Window evaluation immediately drains the Window's private queue to exhaustion. The Window reaction therefore runs its follow-up before the iframe's private queue can be drained:Even if the iframe's private queue could be drained before the Window's, that would merely reverse the problem:
Node's public
vmtherefore cannot perform this synchronous shared-queue checkpoint. Well, it isn't entirely impossible to make this example pass:process._tickCallback()does it. I initially thought that might be an acceptable hack for my own code, but it's deprecated and, after trying to build an actual event loop around it, looks DOA as a general solution because it’s a club when I needed a sewing needle (it drains Node’s sharednextTickandmicrotask queues, including work unrelated to the checkpoint).My proposal is to expose a queue that multiple contexts can share and let authors be responsible for the draining. Something like:
I do have a working prototype dreamed up with AI assist, but Node/V8 embedding internals are honestly outside my working knowledge.