Skip to content

Flush BatchingRemoteConnection on a microtask by default#604

Merged
justinhenricks merged 1 commit intomainfrom
jhen__flush-batch-on-microtask
Apr 27, 2026
Merged

Flush BatchingRemoteConnection on a microtask by default#604
justinhenricks merged 1 commit intomainfrom
jhen__flush-batch-on-microtask

Conversation

@justinhenricks
Copy link
Copy Markdown
Contributor

@justinhenricks justinhenricks commented Apr 23, 2026

Summary

Change the default batch function used by BatchingRemoteConnection to flush queued mutations on a microtask (via queueMicrotask, falling back to Promise.resolve().then(...)) instead of a macrotask via MessageChannel/setTimeout.

Why

When the remote side runs logic like:

// Remote side (extension sandbox)
async perform() {
  fieldApi.setError('Invalid');   // queues a remote-dom mutation
}

...and the host runs logic like:

// Host side
await extension.perform();         // RPC awaits a response
focusFirstError(document);         // scans for [data-error] targets

...over an @remote-ui/rpc-style channel, the timeline under the old macrotask default is:

  1. setError queues a remote-dom mutation (scheduled on a macrotask via MessageChannel)
  2. RPC await resolves via the microtask queue
  3. focusFirstError runs against stale DOM and finds 0 targets
  4. The mutation arrives on the next macrotask, too late

Awaiting yields to the microtask queue but not the macrotask queue, so the RPC response always beats the mutation to the host.

In production, this is why checkout wouldn't auto-scroll to a buyer's invalid field when a blocking extension set an error: the auto-scroll ran before the error mutation landed, and there was nothing in the DOM to scroll to. We shipped the fix downstream in checkout-web (shop/world#604053, internal) by passing a custom batch option, but the macrotask default is surprising and easy to miss, so this patches it upstream.

Scheduling the flush on a microtask guarantees mutations land on the host before any awaited RPC response resolves.

Changes

  • BatchingRemoteConnection's default batch now uses queueMicrotask, with a Promise.resolve().then(...) fallback.
  • Updated the README section describing batching behavior, and noted how to restore macrotask batching via a custom batch option.
  • Updated / added tests:
    • Renamed waitForNextTaskwaitForNextMicrotask.
    • New test verifying flush happens before an awaited promise resolves.
    • New test verifying queueMicrotask is used.
    • New test verifying the Promise.resolve().then(...) fallback when queueMicrotask is unavailable.
  • Added a minor changeset.

Backwards compatibility

This is a behavioral change to the default, hence the minor bump. Consumers who depended on macrotask timing can restore it with a custom batch option (documented in both the changeset and the README).

@justinhenricks justinhenricks marked this pull request as ready for review April 23, 2026 15:52
Copy link
Copy Markdown
Member

@robin-drexler robin-drexler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would really love to get @developit or @henrytao-me's 👀 on this

@@ -0,0 +1,21 @@
---
'@remote-dom/core': minor
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost wonder if that should be a breaking change. I'm leaning more towards no, but the thought has crossed my mind.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels more like a bug fix to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I think if something were to break with this change that would be unexpected

Comment thread packages/core/source/elements/tests/connection.test.ts
Copy link
Copy Markdown
Contributor

@kumar303 kumar303 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor request: In the PR description, I suggest adding a concrete example of how focusing the DOM after an error won't work since the DOM is stale. This example will concisely demonstrate the bug you're fixing.

An AI review also caught a minor issue worth looking into.

Otherwise the fix looks good ✨

AI review

Reviewed with Claude Opus 4.7 via pi.

Explicit per-test setup
  • Consider splitting "flushes mutations" into three focused tests: (a) .flush() drains synchronously, (b) .flush() cancels the pending microtask (no double-fire), (c) a subsequent mutate() schedules a fresh batch. Scenario (b) currently has no dedicated coverage.

Comment thread packages/core/source/elements/tests/connection.test.ts Outdated
Comment thread packages/core/README.md Outdated
Copy link
Copy Markdown
Contributor

@kumar303 kumar303 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ whoops, meant to approve

@justinhenricks justinhenricks force-pushed the jhen__flush-batch-on-microtask branch from 00a5ce8 to 7177edb Compare April 24, 2026 14:09
@justinhenricks
Copy link
Copy Markdown
Contributor Author

Thanks @kumar303 updated the PR description and addressed the feedback, as well as the AI note on splitting up the tests.

@justinhenricks justinhenricks merged commit aa74fc8 into main Apr 27, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants