next.js/packages/next/src/client/dev/hot-reloader/pages/websocket.ts
websocket.ts122 lines3.4 KB
import { logQueue } from '../../../../next-devtools/userspace/app/forward-logs'
import {
  HMR_MESSAGE_SENT_TO_BROWSER,
  type HmrMessageSentToBrowser,
} from '../../../../server/dev/hot-reloader-types'
import { getSocketUrl } from '../get-socket-url'
import { WEB_SOCKET_MAX_RECONNECTIONS } from '../../../../lib/constants'

let source: WebSocket

type MessageCallback = (message: HmrMessageSentToBrowser) => void

const messageCallbacks: Array<MessageCallback> = []

export function addMessageListener(callback: MessageCallback) {
  messageCallbacks.push(callback)
}

export function sendMessage(data: string) {
  if (!source || source.readyState !== source.OPEN) return
  return source.send(data)
}

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

export function connectHMR(options: { path: string; assetPrefix: string }) {
  let timer: ReturnType<typeof setTimeout>

  function init() {
    if (source) source.close()

    function handleOnline() {
      logQueue.onSocketReady(source)
      reconnections = 0
      window.console.log('[HMR] connected')
    }

    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
      }

      const message: HmrMessageSentToBrowser = JSON.parse(event.data)

      if (message.type === HMR_MESSAGE_SENT_TO_BROWSER.TURBOPACK_CONNECTED) {
        if (
          serverSessionId !== null &&
          serverSessionId !== message.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 = message.data.sessionId
      }

      for (const messageCallback of messageCallbacks) {
        messageCallback(message)
      }
    }

    function handleDisconnect() {
      source.onerror = null
      source.onclose = null
      source.close()
      reconnections++
      // After WEB_SOCKET_MAX_RECONNECTIONS reconnects we'll want to reload the page as it indicates the dev server is no longer running.
      if (reconnections > WEB_SOCKET_MAX_RECONNECTIONS) {
        reloading = true
        window.location.reload()
        return
      }

      clearTimeout(timer)
      // Try again after 5 seconds
      timer = setTimeout(init, reconnections > 5 ? 5000 : 1000)
    }

    const url = getSocketUrl(options.assetPrefix)

    source = new window.WebSocket(`${url}${options.path}`)
    source.onopen = handleOnline
    source.onerror = handleDisconnect
    source.onclose = handleDisconnect
    source.onmessage = handleMessage
  }

  function handleVisibilityChange() {
    if (
      document.visibilityState === 'visible' &&
      source.readyState !== WebSocket.OPEN
    ) {
      reconnections = 0
      clearTimeout(timer)
      init()
    }
  }

  function handleOnlineEvent() {
    if (source.readyState !== WebSocket.OPEN) {
      reconnections = 0
      clearTimeout(timer)
      init()
    }
  }

  document.addEventListener('visibilitychange', handleVisibilityChange)
  window.addEventListener('online', handleOnlineEvent)

  init()
}
Quest for Codev2.0.0
/
SIGN IN