Skip to content

[next] Suppress duplicate dev-watcher notifications instead of re-invalidating - #3592

Open
pranaygp wants to merge 2 commits into
codex/fix-next-hmr-build-racefrom
pgp/hmr-duplicate-suppression
Open

[next] Suppress duplicate dev-watcher notifications instead of re-invalidating#3592
pranaygp wants to merge 2 commits into
codex/fix-next-hmr-build-racefrom
pgp/hmr-duplicate-suppression

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Stacked on #3333 (base branch is codex/fix-next-hmr-build-race); GitHub will retarget this to main when #3333 merges. The two PRs together are what makes the E2E Local Dev lanes deterministic.

Summary & Motivation

The E2E Local Dev lanes (nextjs-webpack/nextjs-turbopack × stable/canary × node/quickjs) are the reason main has been red on most runs for days: dev.test.ts's HMR log-count assertions fail with expected 2/3/5 to be 1 (spurious extra rebuilds) or expected +0 to be 1 (a dropped edit). #3529 fixed the mid-rebuild absorption race and #3333 fixes convergence, but the duplicate watcher event mode survives both: watchers routinely emit several events per write (chokidar 4 has no fsevents; fs.watch double-fires), and the classifier treats every notification as a fresh invalidation.

Reproduced on #3333's own head in 2 local runs: the body-only changes test failed with expected 2 to be 1 — the server log shows full rediscovery → build → full rediscovery for a single fs.writeFile, because the duplicate event either lands mid-build (the overlap handler schedules an unconditional follow-up full) or after it (byte-identical content classifies as a conservative full). The same double-full signature is visible in the failing CI runs (e.g. nextjs-webpack stable node, two skip lines 1.2s apart for one edit).

The fix

SourceSnapshot now records the file mtime alongside the content hash, which identifies a specific write:

  • hash and mtime both match the baseline → a duplicate notification for a write that was already consumed → no-op, logged as workflow dev hmr: duplicate (not counted by the e2e assertions).
  • hash matches but mtime is newer → a distinct rewrite whose interim states an in-flight build may have consumed → stays a conservative full rediscovery for build-relevant files (this keeps the torn-consumption safety fix(next): converge workflow HMR rebuilds #3333's byte-identical rule existed for, scoped to events that are actually suspicious).

Two supporting changes make the suppression cover every path:

  1. Full decisions carry the classifier's reads (whole batch) and the full rebuild seeds baselines from them for files it discovers for the first time — a freshly created file's duplicate events now diff equal instead of forcing a second rediscovery. This also closes the create-then-edit window [next] Fix dev HMR dropping edits that race a full rediscovery rebuild #3529 documented as "narrowed rather than closed": a real mid-build edit diffs as changed (rebuild), a same-content rewrite as a rewrite (conservative rebuild).
  2. The schedule-a-full-on-any-event-during-a-build overlap handling is removed. Overlapping events already re-classify after the build against the baseline it consumed, and the mtime rule covers the torn-consumption case the overlap full existed for. This eliminates the guaranteed double rediscovery whenever a duplicate landed mid-build.

Irrelevant files are now tracked on first sight so their duplicates suppress too instead of re-logging skip (the { skip: 1 } exact assertions).

Validation

  • @workflow/next unit suite: 44/44 (new coverage: duplicate suppression, same-content rewrite stays full, full-decision snapshot seeding, batch mixing duplicates with real changes, irrelevant-file tracking).
  • nextjs-webpack stable node, staged tarball workbench (CI's exact recipe): 5 consecutive full dev.test.ts runs green (85–120s each). Baseline on this machine: fix(next): converge workflow HMR rebuilds #3333's head failed 1 of 2 runs (expected 2 to be 1); CI on main fails these lanes in the majority of runs.
  • nextjs-turbopack stable node: 2 consecutive full runs green (7 tests each).
  • Server logs across the runs show the mechanism firing: full rediscovery → duplicate → idle where the same edit previously produced full → full.

One local-harness caveat discovered on the way, for anyone reproducing: a .env.local in the workbench (e.g. from vercel env pull) makes next dev --turbopack evaluate the config twice and spawn two builder instances — every HMR log line doubles and all exact-count tests fail. CI never has one; delete it locally.

🤖 Generated with Claude Code

NathanColosimo and others added 2 commits August 14, 2026 23:02
…alidating

Watchers routinely emit several events per write (chokidar 4 has no
fsevents; node fs.watch double-fires), and the classifier treated every
one as a fresh invalidation: a duplicate notification for an
already-consumed write became another hot rebuild or full rediscovery.
The dev e2e HMR log-count assertions count those, which is the dominant
remaining source of E2E Local Dev lane failures on main (observed as
"expected 2/3/5 to be 1" across webpack/turbopack, stable and canary).

Source snapshots now record the file mtime alongside the content hash,
which identifies a specific write: an event whose content AND mtime both
match the baseline is a duplicate and classifies as a no-op (logged as
"workflow dev hmr: duplicate", which the tests do not count), while
identical content with a newer mtime is a distinct rewrite whose interim
states an in-flight build may have consumed, and stays a conservative
full rediscovery for build-relevant files.

Two supporting changes make the suppression airtight:

- Full-rediscovery decisions carry every snapshot the classifier read
  (the whole batch, not just up to the deciding file), and the full
  rebuild seeds baselines from them for files it discovers for the first
  time — a freshly created file's duplicate events now diff equal
  instead of forcing a second rediscovery (the create-then-edit window
  #3529 documented as narrowed is closed by the mtime rule: a real
  mid-build edit diffs as changed, a same-content rewrite as a rewrite).
- The unconditional schedule-a-full-on-any-event-during-a-build overlap
  handling is removed: overlapping events already re-classify after the
  build against the baseline it consumed, and the mtime rule covers the
  torn-consumption case the overlap full existed for. This eliminates
  the guaranteed double rediscovery when a duplicate landed mid-build.

Irrelevant files that cannot affect the build are now tracked on first
sight so their duplicates suppress too instead of re-logging skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pranaygp
pranaygp requested a review from a team as a code owner August 17, 2026 19:11
Copilot AI lite review requested due to automatic review settings August 17, 2026 19:11
@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e93fe3a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@workflow/next Patch
workflow Patch
@workflow/world-testing Patch
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Aug 17, 2026 7:15pm
example-nextjs-workflow-webpack Ready Ready Preview Aug 17, 2026 7:15pm
example-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-astro-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-express-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-fastify-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-hono-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-nestjs-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-nitro-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-nuxt-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-python-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-sveltekit-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-tanstack-start-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workbench-vite-workflow Ready Ready Preview Aug 17, 2026 7:15pm
workflow-docs Ready Ready Preview, v0 Aug 17, 2026 7:15pm
workflow-swc-playground Ready Ready Preview Aug 17, 2026 7:15pm
workflow-tarballs Ready Ready Preview Aug 17, 2026 7:15pm
workflow-web Ready Ready Preview Aug 17, 2026 7:15pm

Copilot AI left a comment

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.

Pull request overview

This PR updates the Next.js dev watcher rebuild pipeline in @workflow/next to suppress duplicate filesystem notifications (rather than treating them as fresh invalidations), improving determinism of HMR behavior and stabilizing local-dev E2E lanes.

Changes:

  • Extend SourceSnapshot with a content hash + mtimeMs to distinguish true duplicates from same-content rewrites, and introduce a duplicate rebuild decision.
  • Rework rebuild scheduling/processing to batch file events and remove “always schedule a follow-up full rebuild on overlap” behavior.
  • Update unit and E2E tests to cover duplicate suppression and to wait for an explicit “idle” marker before asserting log counts.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/next/src/watch-rebuild.ts Adds hash+mtime snapshotting, duplicate decision, and a new rebuild scheduler + batch-based classifier.
packages/next/src/watch-rebuild.test.ts Adds/updates unit coverage for scheduling and duplicate/no-op semantics.
packages/next/src/builder-eager.ts Integrates new scheduler + classifier behavior and adds idle/duplicate dev HMR markers.
packages/core/e2e/dev.test.ts Adjusts HMR log assertions to wait for idle and relaxes expectations in select scenarios.
.changeset/hmr-duplicate-event-suppression.md Changeset describing duplicate watcher suppression behavior.
.changeset/fix-next-hmr-build-race.md Changeset for the stacked/base fix that preserves edits during rebuilds.
Suppressed comments (1)

packages/core/e2e/dev.test.ts:250

  • In the 'any' log-count mode, the “at least one marker happened” check ignores the new duplicate marker. If an edit is absorbed by an in-flight rebuild and only emits duplicate + idle, this poll will never succeed even though the system is converged. Include the duplicate marker in this list.
              [
                hmrLogMessages.skip,
                hmrLogMessages.hot,
                hmrLogMessages.full,
              ].reduce(

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +355 to 359
if (pending && !timer) {
void flush();
} else if (!pending) {
onIdle();
}
Comment on lines 465 to +468
const canonicalPath =
knownFileAliases.get(normalizedPath) ?? normalizedPath;
const fileChanges = classifyFileChanges({
changedFiles: [],
knownFiles,
removedFiles: [canonicalPath],
});
knownFileAliases.delete(normalizedPath);
scheduleFileChanges(fileChanges);
scheduleFileChange(canonicalPath);
Comment on lines 153 to +157
const hmrLogMessages = {
skip: 'workflow dev hmr: skip',
hot: 'workflow dev hmr: hot rebuild',
full: 'workflow dev hmr: full rediscovery',
idle: 'workflow dev hmr: idle',
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