feat(ui): enhance WebSocket event handling and polling logic

- Introduced a new `useEventRecency` hook to track the recency of WebSocket events, allowing for conditional polling based on event activity.
- Updated `AgentInfoPanel` to utilize the new hook, adjusting polling intervals based on WebSocket activity.
- Implemented debounced invalidation for auto mode events to optimize query updates during rapid event streams.
- Added utility functions for managing event recency checks in various query hooks, improving overall responsiveness and reducing unnecessary polling.
- Introduced debounce and throttle utilities for better control over function execution rates.

This enhancement improves the application's performance by reducing polling when real-time updates are available, ensuring a more efficient use of resources.
This commit is contained in:
Shirone
2026-01-21 14:57:26 +01:00
parent c3e7e57968
commit aac59c2b3a
9 changed files with 1000 additions and 22 deletions

View File

@@ -0,0 +1,176 @@
/**
* Event Recency Hook
*
* Tracks the timestamp of the last WebSocket event received.
* Used to conditionally disable polling when events are flowing
* through WebSocket (indicating the connection is healthy).
*/
import { useEffect, useCallback } from 'react';
import { create } from 'zustand';
/**
* Time threshold (ms) to consider events as "recent"
* If an event was received within this time, WebSocket is considered healthy
* and polling can be safely disabled.
*/
export const EVENT_RECENCY_THRESHOLD = 5000; // 5 seconds
/**
* Store for tracking event timestamps per query key
* This allows fine-grained control over which queries have received recent events
*/
interface EventRecencyState {
/** Map of query key (stringified) -> last event timestamp */
eventTimestamps: Record<string, number>;
/** Global last event timestamp (for any event) */
lastGlobalEventTimestamp: number;
/** Record an event for a specific query key */
recordEvent: (queryKey: string) => void;
/** Record a global event (useful for general WebSocket health) */
recordGlobalEvent: () => void;
/** Check if events are recent for a specific query key */
areEventsRecent: (queryKey: string) => boolean;
/** Check if any global events are recent */
areGlobalEventsRecent: () => boolean;
}
export const useEventRecencyStore = create<EventRecencyState>((set, get) => ({
eventTimestamps: {},
lastGlobalEventTimestamp: 0,
recordEvent: (queryKey: string) => {
const now = Date.now();
set((state) => ({
eventTimestamps: {
...state.eventTimestamps,
[queryKey]: now,
},
lastGlobalEventTimestamp: now,
}));
},
recordGlobalEvent: () => {
set({ lastGlobalEventTimestamp: Date.now() });
},
areEventsRecent: (queryKey: string) => {
const { eventTimestamps } = get();
const lastEventTime = eventTimestamps[queryKey];
if (!lastEventTime) return false;
return Date.now() - lastEventTime < EVENT_RECENCY_THRESHOLD;
},
areGlobalEventsRecent: () => {
const { lastGlobalEventTimestamp } = get();
if (!lastGlobalEventTimestamp) return false;
return Date.now() - lastGlobalEventTimestamp < EVENT_RECENCY_THRESHOLD;
},
}));
/**
* Hook to record event timestamps when WebSocket events are received.
* Should be called from WebSocket event handlers.
*
* @returns Functions to record events
*
* @example
* ```tsx
* const { recordEvent, recordGlobalEvent } = useEventRecorder();
*
* // In WebSocket event handler:
* api.autoMode.onEvent((event) => {
* recordGlobalEvent();
* if (event.featureId) {
* recordEvent(`features:${event.featureId}`);
* }
* });
* ```
*/
export function useEventRecorder() {
const recordEvent = useEventRecencyStore((state) => state.recordEvent);
const recordGlobalEvent = useEventRecencyStore((state) => state.recordGlobalEvent);
return { recordEvent, recordGlobalEvent };
}
/**
* Hook to check if WebSocket events are recent, used by queries
* to decide whether to enable/disable polling.
*
* @param queryKey - Optional specific query key to check
* @returns Object with recency check result and timestamp
*
* @example
* ```tsx
* const { areEventsRecent, areGlobalEventsRecent } = useEventRecency();
*
* // In query options:
* refetchInterval: areGlobalEventsRecent() ? false : 5000,
* ```
*/
export function useEventRecency(queryKey?: string) {
const areEventsRecent = useEventRecencyStore((state) => state.areEventsRecent);
const areGlobalEventsRecent = useEventRecencyStore((state) => state.areGlobalEventsRecent);
const lastGlobalEventTimestamp = useEventRecencyStore((state) => state.lastGlobalEventTimestamp);
const checkRecency = useCallback(
(key?: string) => {
if (key) {
return areEventsRecent(key);
}
return areGlobalEventsRecent();
},
[areEventsRecent, areGlobalEventsRecent]
);
return {
areEventsRecent: queryKey ? () => areEventsRecent(queryKey) : areEventsRecent,
areGlobalEventsRecent,
checkRecency,
lastGlobalEventTimestamp,
};
}
/**
* Utility function to create a refetchInterval that respects event recency.
* Returns false (no polling) if events are recent, otherwise returns the interval.
*
* @param defaultInterval - The polling interval to use when events aren't recent
* @returns A function suitable for React Query's refetchInterval option
*
* @example
* ```tsx
* const { data } = useQuery({
* queryKey: ['features'],
* queryFn: fetchFeatures,
* refetchInterval: createSmartPollingInterval(5000),
* });
* ```
*/
export function createSmartPollingInterval(defaultInterval: number) {
return () => {
const { areGlobalEventsRecent } = useEventRecencyStore.getState();
return areGlobalEventsRecent() ? false : defaultInterval;
};
}
/**
* Helper function to get current event recency state (for use outside React)
* Useful in query configurations where hooks can't be used directly.
*
* @returns Whether global events are recent
*/
export function getGlobalEventsRecent(): boolean {
return useEventRecencyStore.getState().areGlobalEventsRecent();
}
/**
* Helper function to get event recency for a specific query key (for use outside React)
*
* @param queryKey - The query key to check
* @returns Whether events for that query key are recent
*/
export function getEventsRecent(queryKey: string): boolean {
return useEventRecencyStore.getState().areEventsRecent(queryKey);
}