Handle unhandled rejections more gracefully. (#4417)

Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
Jacob Richman
2025-07-25 17:35:26 -07:00
committed by GitHub
parent fb751c542b
commit 21fef1620d
7 changed files with 321 additions and 214 deletions

View File

@@ -4,7 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import {
useCallback,
useEffect,
useReducer,
useRef,
useTransition,
} from 'react';
import { ConsoleMessageItem } from '../types.js';
export interface UseConsoleMessagesReturn {
@@ -13,75 +19,90 @@ export interface UseConsoleMessagesReturn {
clearConsoleMessages: () => void;
}
export function useConsoleMessages(): UseConsoleMessagesReturn {
const [consoleMessages, setConsoleMessages] = useState<ConsoleMessageItem[]>(
[],
);
const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
const messageQueueTimeoutRef = useRef<number | null>(null);
type Action =
| { type: 'ADD_MESSAGES'; payload: ConsoleMessageItem[] }
| { type: 'CLEAR' };
const processMessageQueue = useCallback(() => {
if (messageQueueRef.current.length === 0) {
return;
}
const newMessagesToAdd = messageQueueRef.current;
messageQueueRef.current = [];
setConsoleMessages((prevMessages) => {
const newMessages = [...prevMessages];
newMessagesToAdd.forEach((queuedMessage) => {
function consoleMessagesReducer(
state: ConsoleMessageItem[],
action: Action,
): ConsoleMessageItem[] {
switch (action.type) {
case 'ADD_MESSAGES': {
const newMessages = [...state];
for (const queuedMessage of action.payload) {
const lastMessage = newMessages[newMessages.length - 1];
if (
newMessages.length > 0 &&
newMessages[newMessages.length - 1].type === queuedMessage.type &&
newMessages[newMessages.length - 1].content === queuedMessage.content
lastMessage &&
lastMessage.type === queuedMessage.type &&
lastMessage.content === queuedMessage.content
) {
newMessages[newMessages.length - 1].count =
(newMessages[newMessages.length - 1].count || 1) + 1;
// Create a new object for the last message to ensure React detects
// the change, preventing mutation of the existing state object.
newMessages[newMessages.length - 1] = {
...lastMessage,
count: lastMessage.count + 1,
};
} else {
newMessages.push({ ...queuedMessage, count: 1 });
}
});
}
return newMessages;
});
messageQueueTimeoutRef.current = null; // Allow next scheduling
}, []);
const scheduleQueueProcessing = useCallback(() => {
if (messageQueueTimeoutRef.current === null) {
messageQueueTimeoutRef.current = setTimeout(
processMessageQueue,
0,
) as unknown as number;
}
}, [processMessageQueue]);
case 'CLEAR':
return [];
default:
return state;
}
}
export function useConsoleMessages(): UseConsoleMessagesReturn {
const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []);
const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [, startTransition] = useTransition();
const processQueue = useCallback(() => {
if (messageQueueRef.current.length > 0) {
const messagesToProcess = messageQueueRef.current;
messageQueueRef.current = [];
startTransition(() => {
dispatch({ type: 'ADD_MESSAGES', payload: messagesToProcess });
});
}
timeoutRef.current = null;
}, []);
const handleNewMessage = useCallback(
(message: ConsoleMessageItem) => {
messageQueueRef.current.push(message);
scheduleQueueProcessing();
if (!timeoutRef.current) {
// Batch updates using a timeout. 16ms is a reasonable delay to batch
// rapid-fire messages without noticeable lag.
timeoutRef.current = setTimeout(processQueue, 16);
}
},
[scheduleQueueProcessing],
[processQueue],
);
const clearConsoleMessages = useCallback(() => {
setConsoleMessages([]);
if (messageQueueTimeoutRef.current !== null) {
clearTimeout(messageQueueTimeoutRef.current);
messageQueueTimeoutRef.current = null;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
messageQueueRef.current = [];
startTransition(() => {
dispatch({ type: 'CLEAR' });
});
}, []);
// Cleanup on unmount
useEffect(
() =>
// Cleanup on unmount
() => {
if (messageQueueTimeoutRef.current !== null) {
clearTimeout(messageQueueTimeoutRef.current);
}
},
() => () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
},
[],
);