Skip to content

Commit 611fe63

Browse files
authored
Development: Remove matchers.reload() on each request (#83829)
## What? Re-apply #83720. It was reverted because of tests flaking significantly more often. Dug into this with @mischnic and @sokra yesterday and we found that it's a race condition because of the two separate watchers. In `setup-dev-bundler.ts` there's a Watchpack instance and Turbopack has it's own watcher. The specific case where it caused flakes is when the watchpack aggregate event runs before Turbopack's watcher has emitted a change event. Since the watchpack aggregate callback emits `ADDED_PAGE` / `REMOVED_PAGE` over the websocket it would end up triggering the browser `location.reload()` too early, before the Turbopack side has the updated list of routes. The right solution is to move the `matchers.reload()` and `ADDED_PAGE` / `REMOVED_PAGE` events to where Turbopack emits that the list of routes (entrypoints) has updated.
1 parent d43e5f9 commit 611fe63

File tree

5 files changed

+88
-33
lines changed

5 files changed

+88
-33
lines changed

packages/next/src/server/base-server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,10 @@ export default abstract class Server<
606606
this.responseCache = this.getResponseCache({ dev })
607607
}
608608

609+
protected reloadMatchers() {
610+
return this.matchers.reload()
611+
}
612+
609613
private handleRSCRequest: RouteHandler<ServerRequest, ServerResponse> = (
610614
req,
611615
_res,

packages/next/src/server/dev/hot-reloader-turbopack.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,19 @@ export async function createHotReloaderTurbopack(
612612
)
613613
}
614614

615+
const existingRoutes = [
616+
...currentEntrypoints.app.keys(),
617+
...currentEntrypoints.page.keys(),
618+
]
619+
const newRoutes = [...entrypoints.routes.keys()]
620+
621+
const addedRoutes = newRoutes.filter(
622+
(route) => !existingRoutes.includes(route)
623+
)
624+
const removedRoutes = existingRoutes.filter(
625+
(route) => !newRoutes.includes(route)
626+
)
627+
615628
processTopLevelIssues(currentTopLevelIssues, entrypoints)
616629

617630
await handleEntrypoints({
@@ -647,6 +660,35 @@ export async function createHotReloaderTurbopack(
647660
},
648661
})
649662

663+
// Reload matchers when the files have been compiled
664+
await propagateServerField(opts, 'reloadMatchers', undefined)
665+
666+
if (addedRoutes.length > 0 || removedRoutes.length > 0) {
667+
// When the list of routes changes a new manifest should be fetched for Pages Router.
668+
hotReloader.send({
669+
type: HMR_MESSAGE_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE,
670+
data: [
671+
{
672+
devPagesManifest: true,
673+
},
674+
],
675+
})
676+
}
677+
678+
for (const route of addedRoutes) {
679+
hotReloader.send({
680+
type: HMR_MESSAGE_SENT_TO_BROWSER.ADDED_PAGE,
681+
data: [route],
682+
})
683+
}
684+
685+
for (const route of removedRoutes) {
686+
hotReloader.send({
687+
type: HMR_MESSAGE_SENT_TO_BROWSER.REMOVED_PAGE,
688+
data: [route],
689+
})
690+
}
691+
650692
currentEntriesHandlingResolve!()
651693
currentEntriesHandlingResolve = undefined
652694
}

packages/next/src/server/lib/router-utils/setup-dev-bundler.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,37 +1045,49 @@ async function startWatcher(
10451045
}
10461046
opts.fsChecker.dynamicRoutes.unshift(...dataRoutes)
10471047

1048-
if (!prevSortedRoutes?.every((val, idx) => val === sortedRoutes[idx])) {
1049-
const addedRoutes = sortedRoutes.filter(
1050-
(route) => !prevSortedRoutes.includes(route)
1051-
)
1052-
const removedRoutes = prevSortedRoutes.filter(
1053-
(route) => !sortedRoutes.includes(route)
1054-
)
1048+
// For Turbopack ADDED_PAGE and REMOVED_PAGE are implemented in hot-reloader-turbopack.ts
1049+
// in order to avoid a race condition where ADDED_PAGE and REMOVED_PAGE are sent before Turbopack picked up the file change.
1050+
if (!opts.turbo) {
1051+
// Reload the matchers. The filesystem would have been written to,
1052+
// and the matchers need to re-scan it to update the router.
1053+
// Reloading the matchers should happen before `ADDED_PAGE` or `REMOVED_PAGE` is sent over the websocket
1054+
// otherwise it sends the event too early.
1055+
await propagateServerField(opts, 'reloadMatchers', undefined)
10551056

1056-
// emit the change so clients fetch the update
1057-
hotReloader.send({
1058-
type: HMR_MESSAGE_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE,
1059-
data: [
1060-
{
1061-
devPagesManifest: true,
1062-
},
1063-
],
1064-
})
1057+
if (
1058+
!prevSortedRoutes?.every((val, idx) => val === sortedRoutes[idx])
1059+
) {
1060+
const addedRoutes = sortedRoutes.filter(
1061+
(route) => !prevSortedRoutes.includes(route)
1062+
)
1063+
const removedRoutes = prevSortedRoutes.filter(
1064+
(route) => !sortedRoutes.includes(route)
1065+
)
10651066

1066-
addedRoutes.forEach((route) => {
1067+
// emit the change so clients fetch the update
10671068
hotReloader.send({
1068-
type: HMR_MESSAGE_SENT_TO_BROWSER.ADDED_PAGE,
1069-
data: [route],
1069+
type: HMR_MESSAGE_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE,
1070+
data: [
1071+
{
1072+
devPagesManifest: true,
1073+
},
1074+
],
10701075
})
1071-
})
10721076

1073-
removedRoutes.forEach((route) => {
1074-
hotReloader.send({
1075-
type: HMR_MESSAGE_SENT_TO_BROWSER.REMOVED_PAGE,
1076-
data: [route],
1077+
addedRoutes.forEach((route) => {
1078+
hotReloader.send({
1079+
type: HMR_MESSAGE_SENT_TO_BROWSER.ADDED_PAGE,
1080+
data: [route],
1081+
})
10771082
})
1078-
})
1083+
1084+
removedRoutes.forEach((route) => {
1085+
hotReloader.send({
1086+
type: HMR_MESSAGE_SENT_TO_BROWSER.REMOVED_PAGE,
1087+
data: [route],
1088+
})
1089+
})
1090+
}
10791091
}
10801092
prevSortedRoutes = sortedRoutes
10811093

@@ -1114,10 +1126,6 @@ async function startWatcher(
11141126
} else {
11151127
Log.warn('Failed to reload dynamic routes:', e)
11161128
}
1117-
} finally {
1118-
// Reload the matchers. The filesystem would have been written to,
1119-
// and the matchers need to re-scan it to update the router.
1120-
await propagateServerField(opts, 'reloadMatchers', undefined)
11211129
}
11221130
})
11231131

packages/next/src/server/route-matcher-managers/dev-route-matcher-manager.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,6 @@ export class DevRouteMatcherManager extends DefaultRouteMatcherManager {
6565
pathname: string,
6666
options: MatchOptions
6767
): AsyncGenerator<RouteMatch<RouteDefinition<RouteKind>>, null, undefined> {
68-
// Compile the development routes.
69-
// TODO: we may want to only run this during testing, users won't be fast enough to require this many dir scans
70-
await super.reload()
71-
7268
// Iterate over the development matches to see if one of them match the
7369
// request path.
7470
for await (const developmentMatch of super.matchAll(pathname, options)) {

test/integration/custom-error/test/index.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
killApp,
1111
launchApp,
1212
retry,
13+
waitFor,
1314
} from 'next-test-utils'
1415

1516
const appDir = join(__dirname, '..')
@@ -47,6 +48,8 @@ describe('Custom _error', () => {
4748
})
4849
} finally {
4950
await fs.remove(page404)
51+
// Matches `next-dev.ts` patchFileDelay
52+
await waitFor(1000)
5053
}
5154
})
5255
})
@@ -76,6 +79,8 @@ describe('Custom _error', () => {
7679
})
7780
} finally {
7881
await fs.remove(page404)
82+
// Matches `next-dev.ts` patchFileDelay
83+
await waitFor(1000)
7984
}
8085
})
8186

0 commit comments

Comments
 (0)