import type { EnsureFunction } from '@videojs/utils/types'; import type { Task } from '../../core/queue'; import type { AnyStore, InferStoreRequests, InferStoreState } from '../../core/store'; import type { OptimisticResult } from '../../shared/types'; import { useCallback, useReducer, useRef, useSyncExternalStore } from 'react'; /** * Track a store request with optimistic updates. * * Shows the optimistic value immediately while the request is pending, * then updates to the actual value on success or reverts on error. * * Returns a discriminated union — use `status` to narrow the type. * * @param store - The store instance containing the request * @param name - The request name to track (type-safe with autocomplete) * @param selector - Function to select the value from store state * @returns A discriminated union with the current optimistic state * * @example * ```tsx * function VolumeSlider() { * const result = useOptimistic(store, 'setVolume', s => s.volume); * * return ( * result.setValue(Number(e.target.value))} * style={{ opacity: result.status === 'pending' ? 0.5 : 1 }} * /> * ); * } * ``` */ export function useOptimistic< Store extends AnyStore, Name extends keyof InferStoreRequests, Value, Request extends InferStoreRequests[Name] = InferStoreRequests[Name], >( store: Store, name: Name, selector: (state: InferStoreState) => Value, ): OptimisticResult ReturnType>> { // Force update mechanism for optimistic value changes const [, forceUpdate] = useReducer((x: number) => x + 1, 0); // Track optimistic value (null = use actual state) const optimisticRef = useRef(null); const taskRef = useRef(store.queue.tasks[name]); // Subscribe to store state for actual value const subscribeToState = useCallback( (onStoreChange: () => void) => store.subscribe(selector, onStoreChange), [store, selector], ); const getStateSnapshot = useCallback(() => selector(store.state), [store, selector]); const actualValue = useSyncExternalStore(subscribeToState, getStateSnapshot, getStateSnapshot); // Subscribe to task queue for status const subscribeToQueue = useCallback( (onStoreChange: () => void) => store.queue.subscribe((tasks) => { const newTask = tasks[name]; if (newTask !== taskRef.current) { taskRef.current = newTask; // Clear optimistic value when task settles if (optimisticRef.current !== null && newTask?.status !== 'pending') { optimisticRef.current = null; } onStoreChange(); } }), [store, name], ); const getQueueSnapshot = useCallback(() => taskRef.current, []); const task = useSyncExternalStore(subscribeToQueue, getQueueSnapshot, getQueueSnapshot); // setValue: set optimistic value and call request const setValueRef = useRef((newValue: Value): ReturnType> => { optimisticRef.current = newValue; forceUpdate(); const request = store.request[name] as (value: Value) => ReturnType>; return request(newValue); }); // reset: clear optimistic value and reset task const resetRef = useRef(() => { optimisticRef.current = null; forceUpdate(); taskRef.current = store.queue.tasks[name]; if (taskRef.current) store.queue.reset(name); }); // Build result with discriminated union const value = optimisticRef.current !== null ? optimisticRef.current : actualValue; const base = { value, setValue: setValueRef.current, reset: resetRef.current, }; if (task?.status === 'error') { return { status: 'error', ...base, error: task.error, }; } return { status: task?.status ?? 'idle', ...base, }; } export namespace useOptimistic { export type Result = OptimisticResult; }