Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aggregate updates using addStatusHandler and Promise.resolve instead of setTimeout #42350

Merged
merged 10 commits into from Nov 7, 2022
Expand Up @@ -14,13 +14,17 @@ import { errorOverlayReducer } from './internal/error-overlay-reducer'
import {
ACTION_BUILD_OK,
ACTION_BUILD_ERROR,
ACTION_BEFORE_REFRESH,
ACTION_REFRESH,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
} from './internal/error-overlay-reducer'
import { parseStack } from './internal/helpers/parseStack'
import ReactDevOverlay from './internal/ReactDevOverlay'
import { useErrorHandler } from './internal/helpers/use-error-handler'
import {
RuntimeErrorHandler,
useErrorHandler,
} from './internal/helpers/use-error-handler'
import {
useSendMessage,
useWebsocket,
Expand All @@ -30,6 +34,7 @@ import {
interface Dispatcher {
onBuildOk(): void
onBuildError(message: string): void
onBeforeRefresh(): void
onRefresh(): void
}

Expand All @@ -38,10 +43,15 @@ type PongEvent = any

let mostRecentCompilationHash: any = null
let __nextDevClientId = Math.round(Math.random() * 100 + Date.now())
let hadRuntimeError = false

// let startLatency = undefined

function onBeforeFastRefresh(dispatcher: Dispatcher, hasUpdates: boolean) {
if (hasUpdates) {
dispatcher.onBeforeRefresh()
}
}

function onFastRefresh(dispatcher: Dispatcher, hasUpdates: boolean) {
dispatcher.onBuildOk()
if (hasUpdates) {
Expand Down Expand Up @@ -104,6 +114,7 @@ function performFullReload(err: any, sendMessage: any) {

// Attempt to update code on the fly, fall back to a hard reload.
function tryApplyUpdates(
onBeforeUpdate: (hasUpdates: boolean) => void,
onHotUpdateSuccess: (hasUpdates: boolean) => void,
sendMessage: any,
dispatcher: Dispatcher
Expand All @@ -114,7 +125,7 @@ function tryApplyUpdates(
}

function handleApplyUpdates(err: any, updatedModules: any) {
if (err || hadRuntimeError || !updatedModules) {
if (err || RuntimeErrorHandler.hadRuntimeError || !updatedModules) {
if (err) {
console.warn(
'[Fast Refresh] performing full reload\n\n' +
Expand All @@ -124,7 +135,7 @@ function tryApplyUpdates(
'It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.\n' +
'Fast Refresh requires at least one parent function component in your React tree.'
)
} else if (hadRuntimeError) {
} else if (RuntimeErrorHandler.hadRuntimeError) {
console.warn(
'[Fast Refresh] performing full reload because your application had an unrecoverable error'
)
Expand All @@ -142,6 +153,7 @@ function tryApplyUpdates(
if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates(
hasUpdates ? () => {} : onBeforeUpdate,
hasUpdates ? () => dispatcher.onBuildOk() : onHotUpdateSuccess,
sendMessage,
dispatcher
Expand All @@ -161,14 +173,25 @@ function tryApplyUpdates(

// https://webpack.js.org/api/hot-module-replacement/#check
// @ts-expect-error module.hot exists
module.hot.check(/* autoApply */ true).then(
(updatedModules: any) => {
handleApplyUpdates(null, updatedModules)
},
(err: any) => {
handleApplyUpdates(err, null)
}
)
module.hot
.check(/* autoApply */ false)
.then((updatedModules: any) => {
const hasUpdates = Boolean(updatedModules.length)
if (typeof onBeforeUpdate === 'function') {
onBeforeUpdate(hasUpdates)
}
// https://webpack.js.org/api/hot-module-replacement/#apply
// @ts-expect-error module.hot exists
return module.hot.apply()
})
.then(
(updatedModules: any) => {
handleApplyUpdates(null, updatedModules)
},
(err: any) => {
handleApplyUpdates(err, null)
}
)
}

function processMessage(
Expand Down Expand Up @@ -260,6 +283,9 @@ function processMessage(
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(
function onBeforeHotUpdate(hasUpdates: boolean) {
onBeforeFastRefresh(dispatcher, hasUpdates)
},
function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
Expand Down Expand Up @@ -287,6 +313,9 @@ function processMessage(
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(
function onBeforeHotUpdate(hasUpdates: boolean) {
onBeforeFastRefresh(dispatcher, hasUpdates)
},
function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
Expand All @@ -306,7 +335,7 @@ function processMessage(
clientId: __nextDevClientId,
})
)
if (hadRuntimeError) {
if (RuntimeErrorHandler.hadRuntimeError) {
return window.location.reload()
}
startTransition(() => {
Expand Down Expand Up @@ -361,6 +390,7 @@ export default function HotReload({
nextId: 1,
buildError: null,
errors: [],
refreshState: { type: 'idle' },
})
const dispatcher = useMemo((): Dispatcher => {
return {
Expand All @@ -370,72 +400,29 @@ export default function HotReload({
onBuildError(message: string): void {
dispatch({ type: ACTION_BUILD_ERROR, message })
},
onBeforeRefresh(): void {
dispatch({ type: ACTION_BEFORE_REFRESH })
},
onRefresh(): void {
dispatch({ type: ACTION_REFRESH })
},
}
}, [dispatch])

const handleOnUnhandledError = useCallback(
(ev: WindowEventMap['error']): void => {
if (
ev.error &&
ev.error.digest &&
(ev.error.digest.startsWith('NEXT_REDIRECT') ||
ev.error.digest === 'NEXT_NOT_FOUND')
) {
ev.preventDefault()
return
}

hadRuntimeError = true
const error = ev?.error
if (
!error ||
!(error instanceof Error) ||
typeof error.stack !== 'string'
) {
// A non-error was thrown, we don't have anything to show. :-(
return
}

if (
error.message.match(/(hydration|content does not match|did not match)/i)
) {
error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`
}

const e = error
dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(e.stack!),
})
},
[]
)
const handleOnUnhandledRejection = useCallback(
(ev: WindowEventMap['unhandledrejection']): void => {
hadRuntimeError = true
const reason = ev?.reason
if (
!reason ||
!(reason instanceof Error) ||
typeof reason.stack !== 'string'
) {
// A non-error was thrown, we don't have anything to show. :-(
return
}

const e = reason
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(e.stack!),
})
},
[]
)
const handleOnUnhandledError = useCallback((error: Error): void => {
dispatch({
type: ACTION_UNHANDLED_ERROR,
reason: error,
frames: parseStack(error.stack!),
})
}, [])
const handleOnUnhandledRejection = useCallback((reason: Error): void => {
dispatch({
type: ACTION_UNHANDLED_REJECTION,
reason: reason,
frames: parseStack(reason.stack!),
})
}, [])
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)

const webSocketRef = useWebsocket(assetPrefix)
Expand Down
Expand Up @@ -3,6 +3,7 @@ import { SupportedErrorEvent } from './container/Errors'

export const ACTION_BUILD_OK = 'build-ok'
export const ACTION_BUILD_ERROR = 'build-error'
export const ACTION_BEFORE_REFRESH = 'before-fast-refresh'
export const ACTION_REFRESH = 'fast-refresh'
export const ACTION_UNHANDLED_ERROR = 'unhandled-error'
export const ACTION_UNHANDLED_REJECTION = 'unhandled-rejection'
Expand All @@ -14,6 +15,9 @@ interface BuildErrorAction {
type: typeof ACTION_BUILD_ERROR
message: string
}
interface BeforeFastRefreshAction {
type: typeof ACTION_BEFORE_REFRESH
}
interface FastRefreshAction {
type: typeof ACTION_REFRESH
}
Expand All @@ -28,20 +32,44 @@ export interface UnhandledRejectionAction {
frames: StackFrame[]
}

export type FastRefreshState =
| {
type: 'idle'
}
| {
type: 'pending'
errors: SupportedErrorEvent[]
}

export interface OverlayState {
nextId: number
buildError: string | null
errors: SupportedErrorEvent[]
rootLayoutMissingTagsError?: {
missingTags: string[]
}
refreshState: FastRefreshState
}

function pushErrorFilterDuplicates(
errors: SupportedErrorEvent[],
err: SupportedErrorEvent
): SupportedErrorEvent[] {
return [
...errors.filter((e) => {
// Filter out duplicate errors
return e.event.reason !== err.event.reason
}),
err,
]
}

export function errorOverlayReducer(
state: Readonly<OverlayState>,
action: Readonly<
| BuildOkAction
| BuildErrorAction
| BeforeFastRefreshAction
| FastRefreshAction
| UnhandledErrorAction
| UnhandledRejectionAction
Expand All @@ -54,21 +82,56 @@ export function errorOverlayReducer(
case ACTION_BUILD_ERROR: {
return { ...state, buildError: action.message }
}
case ACTION_BEFORE_REFRESH: {
return { ...state, refreshState: { type: 'pending', errors: [] } }
}
case ACTION_REFRESH: {
return { ...state, buildError: null, errors: [] }
return {
...state,
buildError: null,
errors:
// Errors can come in during updates. In this case, UNHANDLED_ERROR
// and UNHANDLED_REJECTION events might be dispatched between the
// BEFORE_REFRESH and the REFRESH event. We want to keep those errors
// around until the next refresh. Otherwise we run into a race
// condition where those errors would be cleared on refresh completion
// before they can be displayed.
state.refreshState.type === 'pending'
? state.refreshState.errors
: [],
refreshState: { type: 'idle' },
}
}
case ACTION_UNHANDLED_ERROR:
case ACTION_UNHANDLED_REJECTION: {
return {
...state,
nextId: state.nextId + 1,
errors: [
...state.errors.filter((err) => {
// Filter out duplicate errors
return err.event.reason !== action.reason
}),
{ id: state.nextId, event: action },
],
switch (state.refreshState.type) {
case 'idle': {
return {
...state,
nextId: state.nextId + 1,
errors: pushErrorFilterDuplicates(state.errors, {
id: state.nextId,
event: action,
}),
}
}
case 'pending': {
return {
...state,
nextId: state.nextId + 1,
refreshState: {
...state.refreshState,
errors: pushErrorFilterDuplicates(state.refreshState.errors, {
id: state.nextId,
event: action,
}),
},
}
}
default:
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = state.refreshState
return state
}
}
default: {
Expand Down