summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/useConsoleMessages.ts
diff options
context:
space:
mode:
authorJacob Richman <[email protected]>2025-07-25 17:35:26 -0700
committerGitHub <[email protected]>2025-07-26 00:35:26 +0000
commit21fef1620d78f07af01a75b8bbbeeb15798e73ef (patch)
tree751591161eb9d65f6d776152dadf8183a39a8179 /packages/cli/src/ui/hooks/useConsoleMessages.ts
parentfb751c542bc935158aaa0d01c0694eb3bb6b2919 (diff)
Handle unhandled rejections more gracefully. (#4417)
Co-authored-by: Tommaso Sciortino <[email protected]>
Diffstat (limited to 'packages/cli/src/ui/hooks/useConsoleMessages.ts')
-rw-r--r--packages/cli/src/ui/hooks/useConsoleMessages.ts115
1 files changed, 68 insertions, 47 deletions
diff --git a/packages/cli/src/ui/hooks/useConsoleMessages.ts b/packages/cli/src/ui/hooks/useConsoleMessages.ts
index 52ffbd39..3b71560e 100644
--- a/packages/cli/src/ui/hooks/useConsoleMessages.ts
+++ b/packages/cli/src/ui/hooks/useConsoleMessages.ts
@@ -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);
-
- const processMessageQueue = useCallback(() => {
- if (messageQueueRef.current.length === 0) {
- return;
- }
-
- const newMessagesToAdd = messageQueueRef.current;
- messageQueueRef.current = [];
+type Action =
+ | { type: 'ADD_MESSAGES'; payload: ConsoleMessageItem[] }
+ | { type: 'CLEAR' };
- 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;
- });
+ }
+ case 'CLEAR':
+ return [];
+ default:
+ return state;
+ }
+}
- messageQueueTimeoutRef.current = null; // Allow next scheduling
- }, []);
+export function useConsoleMessages(): UseConsoleMessagesReturn {
+ const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []);
+ const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
+ const [, startTransition] = useTransition();
- const scheduleQueueProcessing = useCallback(() => {
- if (messageQueueTimeoutRef.current === null) {
- messageQueueTimeoutRef.current = setTimeout(
- processMessageQueue,
- 0,
- ) as unknown as number;
+ const processQueue = useCallback(() => {
+ if (messageQueueRef.current.length > 0) {
+ const messagesToProcess = messageQueueRef.current;
+ messageQueueRef.current = [];
+ startTransition(() => {
+ dispatch({ type: 'ADD_MESSAGES', payload: messagesToProcess });
+ });
}
- }, [processMessageQueue]);
+ 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);
+ }
+ },
[],
);