mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 22:13:08 +00:00
192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
/**
|
|
* 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).
|
|
*
|
|
* Mobile-aware: On mobile devices, the recency threshold is extended
|
|
* and polling intervals are multiplied to reduce battery drain and
|
|
* network usage while maintaining data freshness through WebSocket.
|
|
*/
|
|
|
|
import { useCallback } from 'react';
|
|
import { create } from 'zustand';
|
|
import { isMobileDevice, getMobilePollingMultiplier } from '@/lib/mobile-detect';
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* On mobile, the threshold is extended to 10 seconds since WebSocket
|
|
* connections on mobile may have higher latency and more jitter.
|
|
*/
|
|
export const EVENT_RECENCY_THRESHOLD = isMobileDevice ? 10000 : 5000;
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* On mobile, the interval is multiplied by getMobilePollingMultiplier() to reduce
|
|
* battery drain and network usage. This is safe because:
|
|
* - WebSocket invalidation handles real-time updates (features, agents, etc.)
|
|
* - The service worker caches API responses for instant display
|
|
* - Longer intervals mean fewer network round-trips on slow mobile connections
|
|
*
|
|
* @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) {
|
|
const mobileAwareInterval = defaultInterval * getMobilePollingMultiplier();
|
|
return () => {
|
|
const { areGlobalEventsRecent } = useEventRecencyStore.getState();
|
|
return areGlobalEventsRecent() ? false : mobileAwareInterval;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|