Fix dev overlay hydration error ordering (#94555)
### What?
Preserve FIFO ordering for events received while the dev overlay
connects its React dispatcher.
- Replace the separate global dispatch pointer and backlog with an
`EventQueue`.
- Keep new events queued until the existing backlog has finished
replaying.
- Add focused unit coverage for normal ordering, events enqueued during
replay, and replay failures.
### Why?
The React 18 Pages Router hydration test for an extra node inside
Suspense reports three events in this order:
1. The runtime hydration mismatch captured from the error/console path.
2. The recoverable hydration mismatch.
3. The recoverable Suspense boundary client-render fallback.
The overlay previously set `maybeDispatch` from `useInsertionEffect`,
but deferred replaying its existing queue with `setTimeout`. If the
first event arrived before the overlay connected, then the remaining
recoverable events arrived after `maybeDispatch` was exposed but before
the timeout, those newer events dispatched immediately and overtook the
queued event. The resulting order was 2, 3, 1, which caused the inline
snapshot failure.
Investigation also found that enabling Node.js streams was correlated
with the failure but was not its direct cause:
- The failing test uses the Pages Router renderer, which still calls
`ReactDOMServerPages.renderToReadableStream` and does not select the App
Router Node Fizz stream implementation.
- The Node streams default change did not modify the Pages client error
bridge or dev overlay queue.
- The queue race predates the Node streams rollout. Timing changes
around the rollout, including adjacent lazy loading of the overlay UX,
made the existing race reproducible.
The correct fix is therefore to enforce the overlay queue ordering
invariant rather than update the snapshot or special-case Node streams.
### How?
`EventQueue.connect()` leaves direct dispatch disabled while draining
the backlog. Events received during replay are appended to the same
queue and drained before the dispatcher becomes directly available. A
`finally` block preserves the previous cleanup and connection behavior
when a queued event throws, while `disconnect()` only clears the
currently connected dispatcher.
### Verification
- `pnpm prettier --with-node-modules --ignore-path .prettierignore
--write packages/next/src/next-devtools/dev-overlay.browser.tsx
packages/next/src/next-devtools/dev-overlay/event-queue.ts
packages/next/src/next-devtools/dev-overlay/event-queue.test.ts`
- `npx eslint --config eslint.config.mjs --fix
packages/next/src/next-devtools/dev-overlay.browser.tsx
packages/next/src/next-devtools/dev-overlay/event-queue.ts
packages/next/src/next-devtools/dev-overlay/event-queue.test.ts`
- `pnpm --filter=next build`
- `pnpm exec jest
packages/next/src/next-devtools/dev-overlay/event-queue.test.ts
--runInBand`
- `HEADLESS=true NEXT_TEST_REACT_VERSION=18.3.1
NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-webpack
test/development/acceptance/hydration-error.test.ts -t "should show
correct hydration error when client renders an extra node inside
Suspense content"`
- `HEADLESS=true NEXT_TEST_REACT_VERSION=18.3.1
NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo
test/development/acceptance/hydration-error.test.ts -t "should show
correct hydration error when client renders an extra node inside
Suspense content"`
<!-- NEXT_JS_LLM_PR -->