FEATURE: add Context#perform_microtask_checkpoint#418
Open
ursm wants to merge 1 commit into
Open
Conversation
Synchronously drains the V8 microtask queue, useful when a Ruby callback invoked during context.call/eval needs spec-compliant microtask ordering between synchronous `dispatchEvent` listeners (or any other sync JS sequence that schedules microtasks). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ursm
added a commit
to ursm/capybara-simulated
that referenced
this pull request
May 27, 2026
…ion click
Per HTML spec "clean up after running script", a microtask checkpoint
runs when the JS execution context stack is empty after a callback
returns. UA-dispatched events (Playwright/CDP native click) hit this
between every listener — the engine-native dispatcher leaves no JS on
the stack — so framework microtasks queued by an earlier listener
drain before the next one fires. JS-side `dispatchEvent` does not: the
caller's frame stays on the stack and the checkpoint is suppressed.
Glimmer/Ember Backburner schedules its autorun as
`Promise.resolve().then(flush)` — a microtask. Without the
mid-bubble drain, an `OnModifier`'s destructor never runs in time to
remove an ancestor's listener, and a child handler's tracked-state
mutation leaks `updateIndex(...)`-style state changes into the parent
when bubble reaches it. Discourse's
`admin_editing_objects_typed_theme_setting_spec.rb:100` was the visible
fallout: clicking "link 1" landed on link 2 because the section li's
`{{on "click" (fn updateIndex 1)}}` listener was still registered
when bubble reached it.
The fix adds `dispatchEventForUserAction(target, event)` that shares
`dispatchEvent`'s walk but invokes a host-provided `__csim_yield()`
after every listener that fires. `__csim_yield` maps to
`Context#perform_microtask_checkpoint` on V8 (rubyjs/mini_racer#418)
and `vm.drain_jobs!` on QuickJS — both wrap the underlying engine's
microtask drain. `__csimClickResolve` dispatches its `click` event via
the new path; the rest of the click chain (pointerdown / mousedown /
pointerup / mouseup) stays on plain `dispatchEvent`.
`fireListeners` / `fireWindowListeners` now return whether any
handler actually ran, so the drain is gated on actual work — most
ancestors in a deep DOM walk have no listener and don't need a
checkpoint round trip.
Falls back to a no-op `__csim_yield` when the runtime lacks the
drain API, leaving dispatch correct but losing the
listener-interleaved drain — gem suite stays at 1466/0/35 V8 and
QuickJS.
Resolves: Discourse admin_editing_objects_typed_theme_setting_spec
:100 ("displays the validation errors when an admin tries to save the
setting with an invalid value").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ursm
added a commit
to ursm/capybara-simulated-vs-world
that referenced
this pull request
May 27, 2026
The capybara-simulated user-action click dispatch (introduced in the sibling gem commit) calls a host-provided `__csim_yield()` between listener invocations to drain V8's microtask queue, matching the HTML spec "clean up after running script" checkpoint that CDP-driven native clicks rely on. The drain maps to mini_racer's `Context#perform_microtask_checkpoint`, which is the rubyjs/mini_racer#418 addition — not yet released. Bundler's "same gem, different sources" check rejects two `gem` declarations for `mini_racer`. Wrap the upstream Discourse Gemfile's eval and drop its `mini_racer` dependency before adding our pin, so the merge is a single declaration from the PR branch. Drops admin_editing_objects_typed_theme_setting_spec :100 off the Discourse failure list — the only failing test we have whose root cause is the listener-interleaved microtask-drain gap. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MiniRacer::Context#perform_microtask_checkpoint, a thin wrapper around V8'sMicrotasksScope::PerformCheckpoint(isolate).context.call/context.evalsynchronously drain the microtask queue mid-execution.Motivation
V8's default
kAutomicrotask policy drains the queue when JS script execution returns to the embedder. When JS calls a host (Ruby) function that itself calls back into JS — for example, a syntheticdispatchEventsimulating browser-style event propagation — all the JS runs inside one script execution, so microtasks queued by listener N do not drain before listener N+1. This diverges from the HTML "clean up after running script" spec, where the UA's dispatcher runs each listener via a fresh microtask checkpoint.Currently the only escape hatch is
pump_message_loop, which drags inPumpMessageLoop/RunIdleTasksand is bool-returning. A dedicated primitive is cleaner.API
Notes
pump_message_loop's structure (HandleScope + verbose TryCatch). It deliberately does not callCancelTerminateExecution— if a microtask trips OOM via the GC epilogue or watchdog termination fires, the enclosingv8_call/v8_evalframe surfaces the error to Ruby as usual.:single_threadedmodes (uses the same dispatch tag mechanism).context.eval.Test plan
test_perform_microtask_checkpoint_returns_niltest_perform_microtask_checkpoint_drains_from_callback🤖 Generated with Claude Code