| | import { useContext, useEffect } from 'react' |
| | import { GlobalLayoutRouterContext } from '../../../../shared/lib/app-router-context.shared-runtime' |
| | import { getSocketUrl } from '../get-socket-url' |
| | import { |
| | HMR_MESSAGE_SENT_TO_BROWSER, |
| | type HmrMessageSentToBrowser, |
| | type TurbopackMessageSentToBrowser, |
| | } from '../../../../server/dev/hot-reloader-types' |
| | import { reportInvalidHmrMessage } from '../shared' |
| | import { |
| | performFullReload, |
| | processMessage, |
| | type StaticIndicatorState, |
| | } from './hot-reloader-app' |
| | import { logQueue } from '../../../../next-devtools/userspace/app/forward-logs' |
| | import { InvariantError } from '../../../../shared/lib/invariant-error' |
| | import { WEB_SOCKET_MAX_RECONNECTIONS } from '../../../../lib/constants' |
| |
|
| | let reconnections = 0 |
| | let reloading = false |
| | let serverSessionId: number | null = null |
| | let mostRecentCompilationHash: string | null = null |
| |
|
| | export function createWebSocket( |
| | assetPrefix: string, |
| | staticIndicatorState: StaticIndicatorState |
| | ) { |
| | if (!self.__next_r) { |
| | throw new InvariantError( |
| | `Expected a request ID to be defined for the document via self.__next_r.` |
| | ) |
| | } |
| |
|
| | let webSocket: WebSocket |
| | let timer: ReturnType<typeof setTimeout> |
| |
|
| | const sendMessage = (data: string) => { |
| | if (webSocket && webSocket.readyState === webSocket.OPEN) { |
| | webSocket.send(data) |
| | } |
| | } |
| |
|
| | const processTurbopackMessage = createProcessTurbopackMessage(sendMessage) |
| |
|
| | function init() { |
| | if (webSocket) { |
| | webSocket.close() |
| | } |
| |
|
| | const newWebSocket = new window.WebSocket( |
| | `${getSocketUrl(assetPrefix)}/_next/webpack-hmr?id=${self.__next_r}` |
| | ) |
| |
|
| | newWebSocket.binaryType = 'arraybuffer' |
| |
|
| | function handleOnline() { |
| | logQueue.onSocketReady(newWebSocket) |
| |
|
| | reconnections = 0 |
| | window.console.log('[HMR] connected') |
| | } |
| |
|
| | function handleMessage(event: MessageEvent) { |
| | |
| | if (reloading) { |
| | return |
| | } |
| |
|
| | try { |
| | const message: HmrMessageSentToBrowser = |
| | event.data instanceof ArrayBuffer |
| | ? parseBinaryMessage(event.data) |
| | : JSON.parse(event.data) |
| |
|
| | |
| | if (message.type === HMR_MESSAGE_SENT_TO_BROWSER.TURBOPACK_CONNECTED) { |
| | if ( |
| | serverSessionId !== null && |
| | serverSessionId !== message.data.sessionId |
| | ) { |
| | |
| | |
| | window.location.reload() |
| | reloading = true |
| | return |
| | } |
| | serverSessionId = message.data.sessionId |
| | } |
| |
|
| | |
| | if ( |
| | message.type === HMR_MESSAGE_SENT_TO_BROWSER.SYNC && |
| | 'hash' in message |
| | ) { |
| | |
| | if ( |
| | mostRecentCompilationHash !== null && |
| | mostRecentCompilationHash !== message.hash |
| | ) { |
| | window.location.reload() |
| | reloading = true |
| | return |
| | } |
| | mostRecentCompilationHash = message.hash |
| | } |
| |
|
| | processMessage( |
| | message, |
| | sendMessage, |
| | processTurbopackMessage, |
| | staticIndicatorState |
| | ) |
| | } catch (err: unknown) { |
| | reportInvalidHmrMessage(event, err) |
| | } |
| | } |
| |
|
| | function handleDisconnect() { |
| | newWebSocket.onerror = null |
| | newWebSocket.onclose = null |
| | newWebSocket.close() |
| | reconnections++ |
| |
|
| | |
| | if (reconnections > WEB_SOCKET_MAX_RECONNECTIONS) { |
| | reloading = true |
| | window.location.reload() |
| | return |
| | } |
| |
|
| | clearTimeout(timer) |
| | |
| | timer = setTimeout(init, reconnections > 5 ? 5000 : 1000) |
| | } |
| |
|
| | newWebSocket.onopen = handleOnline |
| | newWebSocket.onerror = handleDisconnect |
| | newWebSocket.onclose = handleDisconnect |
| | newWebSocket.onmessage = handleMessage |
| |
|
| | webSocket = newWebSocket |
| | return newWebSocket |
| | } |
| |
|
| | return init() |
| | } |
| |
|
| | export function createProcessTurbopackMessage( |
| | sendMessage: (data: string) => void |
| | ): (msg: TurbopackMessageSentToBrowser) => void { |
| | if (!process.env.TURBOPACK) { |
| | return () => {} |
| | } |
| |
|
| | let queue: TurbopackMessageSentToBrowser[] = [] |
| | let callback: ((msg: TurbopackMessageSentToBrowser) => void) | undefined |
| |
|
| | const processTurbopackMessage = (msg: TurbopackMessageSentToBrowser) => { |
| | if (callback) { |
| | callback(msg) |
| | } else { |
| | queue.push(msg) |
| | } |
| | } |
| |
|
| | import( |
| | |
| | '@vercel/turbopack-ecmascript-runtime/browser/dev/hmr-client/hmr-client.ts' |
| | ).then(({ connect }) => { |
| | connect({ |
| | addMessageListener(cb: (msg: TurbopackMessageSentToBrowser) => void) { |
| | callback = cb |
| |
|
| | |
| | for (const msg of queue) { |
| | cb(msg) |
| | } |
| | queue.length = 0 |
| | }, |
| | sendMessage, |
| | onUpdateError: (err: unknown) => performFullReload(err, sendMessage), |
| | }) |
| | }) |
| |
|
| | return processTurbopackMessage |
| | } |
| |
|
| | export function useWebSocketPing(webSocket: WebSocket | undefined) { |
| | const { tree } = useContext(GlobalLayoutRouterContext) |
| |
|
| | useEffect(() => { |
| | if (!webSocket) { |
| | throw new InvariantError('Expected webSocket to be defined in dev mode.') |
| | } |
| |
|
| | |
| | |
| | if (process.env.TURBOPACK) { |
| | return |
| | } |
| |
|
| | |
| | const interval = setInterval(() => { |
| | if (webSocket.readyState === webSocket.OPEN) { |
| | webSocket.send( |
| | JSON.stringify({ |
| | event: 'ping', |
| | tree, |
| | appDirRoute: true, |
| | }) |
| | ) |
| | } |
| | }, 2500) |
| | return () => clearInterval(interval) |
| | }, [tree, webSocket]) |
| | } |
| |
|
| | const textDecoder = new TextDecoder() |
| |
|
| | function parseBinaryMessage(data: ArrayBuffer): HmrMessageSentToBrowser { |
| | assertByteLength(data, 1) |
| | const view = new DataView(data) |
| | const messageType = view.getUint8(0) |
| |
|
| | switch (messageType) { |
| | case HMR_MESSAGE_SENT_TO_BROWSER.ERRORS_TO_SHOW_IN_BROWSER: { |
| | const serializedErrors = new Uint8Array(data, 1) |
| |
|
| | return { |
| | type: HMR_MESSAGE_SENT_TO_BROWSER.ERRORS_TO_SHOW_IN_BROWSER, |
| | serializedErrors, |
| | } |
| | } |
| | case HMR_MESSAGE_SENT_TO_BROWSER.REACT_DEBUG_CHUNK: { |
| | assertByteLength(data, 2) |
| | const requestIdLength = view.getUint8(1) |
| | assertByteLength(data, 2 + requestIdLength) |
| |
|
| | const requestId = textDecoder.decode( |
| | new Uint8Array(data, 2, requestIdLength) |
| | ) |
| |
|
| | const chunk = |
| | data.byteLength > 2 + requestIdLength |
| | ? new Uint8Array(data, 2 + requestIdLength) |
| | : null |
| |
|
| | return { |
| | type: HMR_MESSAGE_SENT_TO_BROWSER.REACT_DEBUG_CHUNK, |
| | requestId, |
| | chunk, |
| | } |
| | } |
| | default: { |
| | throw new InvariantError( |
| | `Invalid binary HMR message of type ${messageType}` |
| | ) |
| | } |
| | } |
| | } |
| |
|
| | function assertByteLength(data: ArrayBuffer, expectedLength: number) { |
| | if (data.byteLength < expectedLength) { |
| | throw new InvariantError( |
| | `Invalid binary HMR message: insufficient data (expected ${expectedLength} bytes, got ${data.byteLength})` |
| | ) |
| | } |
| | } |
| |
|