Skip to content

Commit

Permalink
Turbopack HMR: Reload the page when server session changes (#68630)
Browse files Browse the repository at this point in the history
It’s possible to the following scenario to occur:

- Browser loads a page and establishes HMR connection
- User quits the server, makes changes, and restarts the server
- The browser reloads, the user makes more changes, and the two reach an
inconsistent state

To avoid this, reload the browser when the server changes. This already
happened with webpack, so implement it for Turbopack.

Test Plan: `TURBOPACK=1 pnpm test-dev
test/development/basic/hmr.test.ts`

---------

Co-authored-by: Tobias Koppers <[email protected]>
  • Loading branch information
wbinnssmith and sokra authored Aug 8, 2024
1 parent cc92654 commit be10d2a
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,9 @@ function processMessage(
case HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED: {
processTurbopackMessage({
type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED,
data: {
sessionId: obj.data.sessionId,
},
})
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ function processMessage(obj: HMR_ACTION_TYPES) {
for (const listener of turbopackMessageListeners) {
listener({
type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED,
data: obj.data,
})
}
break
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { HMR_ACTION_TYPES } from '../../../../server/dev/hot-reloader-types'
import {
HMR_ACTIONS_SENT_TO_BROWSER,
type HMR_ACTION_TYPES,
} from '../../../../server/dev/hot-reloader-types'
import { getSocketUrl } from '../internal/helpers/get-socket-url'

let source: WebSocket
Expand All @@ -17,6 +20,8 @@ export function sendMessage(data: string) {
}

let reconnections = 0
let reloading = false
let serverSessionId: number | null = null

export function connectHMR(options: { path: string; assetPrefix: string }) {
function init() {
Expand All @@ -28,8 +33,36 @@ export function connectHMR(options: { path: string; assetPrefix: string }) {
}

function handleMessage(event: MessageEvent<string>) {
// While the page is reloading, don't respond to any more messages.
// On reconnect, the server may send an empty list of changes if it was restarted.
if (reloading) {
return
}

// Coerce into HMR_ACTION_TYPES as that is the format.
const msg: HMR_ACTION_TYPES = JSON.parse(event.data)

if (
'action' in msg &&
msg.action === HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED
) {
if (
serverSessionId !== null &&
serverSessionId !== msg.data.sessionId
) {
// Either the server's session id has changed and it's a new server, or
// it's been too long since we disconnected and we should reload the page.
// There could be 1) unhandled server errors and/or 2) stale content.
// Perform a hard reload of the page.
window.location.reload()

reloading = true
return
}

serverSessionId = msg.data.sessionId
}

for (const eventCallback of eventCallbacks) {
eventCallback(msg)
}
Expand All @@ -43,6 +76,7 @@ export function connectHMR(options: { path: string; assetPrefix: string }) {
reconnections++
// After 25 reconnects we'll want to reload the page as it indicates the dev server is no longer running.
if (reconnections > 25) {
reloading = true
window.location.reload()
return
}
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ const isTestMode = !!(
process.env.DEBUG
)

const sessionId = Math.floor(Number.MAX_SAFE_INTEGER * Math.random())

export async function createHotReloaderTurbopack(
opts: SetupOpts,
serverFields: ServerFields,
Expand Down Expand Up @@ -667,6 +669,7 @@ export async function createHotReloaderTurbopack(

const turbopackConnected: TurbopackConnectedAction = {
action: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED,
data: { sessionId },
}
sendToClient(client, turbopackConnected)

Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/server/dev/hot-reloader-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ interface DevPagesManifestUpdateAction {

export interface TurbopackConnectedAction {
action: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED
data: { sessionId: number }
}

export interface AppIsrManifestAction {
Expand All @@ -130,7 +131,10 @@ export type HMR_ACTION_TYPES =

export type TurbopackMsgToBrowser =
| { type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_MESSAGE; data: any }
| { type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED }
| {
type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED
data: { sessionId: number }
}

export interface NextJsHotReloaderInterface {
turbopackProject?: Project
Expand Down
25 changes: 25 additions & 0 deletions test/development/basic/hmr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1206,4 +1206,29 @@ describe.each([
await next.patchFile(pageName, originalContent)
}
})

it('should reload the page when the server restarts', async () => {
const browser = await webdriver(next.url, basePath + '/hmr/about', {
headless: false,
})
await check(() => getBrowserBodyText(browser), /This is the about page/)

await next.destroy()

let reloadPromise = new Promise((resolve) => {
browser.on('request', (req) => {
if (req.url().endsWith('/hmr/about')) {
resolve(req.url())
}
})
})

next = await createNext({
files: join(__dirname, 'hmr'),
nextConfig,
forcedPort: next.appPort,
})

await reloadPromise
})
})

0 comments on commit be10d2a

Please sign in to comment.