117 lines
2.8 KiB
TypeScript
117 lines
2.8 KiB
TypeScript
|
|
import { useCallback, useRef, useEffect } from 'react';
|
||
|
|
import { useTabVisibility } from './useTabVisibility';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Edge 3.3: Hook to manage long-running operations that should continue when tab is hidden
|
||
|
|
* Ensures operations continue correctly when tab is switched or becomes hidden
|
||
|
|
*
|
||
|
|
* @param operation - The long-running operation function
|
||
|
|
* @param options - Configuration options
|
||
|
|
* @returns Object with operation control functions
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* const { start, stop, isRunning } = useLongRunningOperation(
|
||
|
|
* async () => {
|
||
|
|
* await processLargeFile();
|
||
|
|
* },
|
||
|
|
* {
|
||
|
|
* continueWhenHidden: true,
|
||
|
|
* onComplete: () => console.log('Done'),
|
||
|
|
* onError: (error) => console.error(error),
|
||
|
|
* }
|
||
|
|
* );
|
||
|
|
*
|
||
|
|
* // Start operation
|
||
|
|
* start();
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function useLongRunningOperation<T = void>(
|
||
|
|
operation: () => Promise<T>,
|
||
|
|
options: {
|
||
|
|
continueWhenHidden?: boolean; // Continue operation when tab is hidden (default: true)
|
||
|
|
onComplete?: (result: T) => void;
|
||
|
|
onError?: (error: Error) => void;
|
||
|
|
onStart?: () => void;
|
||
|
|
onStop?: () => void;
|
||
|
|
} = {},
|
||
|
|
) {
|
||
|
|
const {
|
||
|
|
continueWhenHidden = true,
|
||
|
|
onComplete,
|
||
|
|
onError,
|
||
|
|
onStart,
|
||
|
|
onStop,
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
const isRunningRef = useRef(false);
|
||
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||
|
|
const { isVisible } = useTabVisibility();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start the long-running operation
|
||
|
|
*/
|
||
|
|
const start = useCallback(async () => {
|
||
|
|
// Edge 3.3: Don't start if already running
|
||
|
|
if (isRunningRef.current) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Edge 3.3: Don't start if tab is hidden and continueWhenHidden is false
|
||
|
|
if (!isVisible && !continueWhenHidden) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
isRunningRef.current = true;
|
||
|
|
abortControllerRef.current = new AbortController();
|
||
|
|
onStart?.();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await operation();
|
||
|
|
onComplete?.(result);
|
||
|
|
return result;
|
||
|
|
} catch (error) {
|
||
|
|
// Edge 3.3: Only handle error if not aborted
|
||
|
|
if (error instanceof Error && error.name !== 'AbortError') {
|
||
|
|
onError?.(error);
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
isRunningRef.current = false;
|
||
|
|
abortControllerRef.current = null;
|
||
|
|
}
|
||
|
|
}, [operation, isVisible, continueWhenHidden, onComplete, onError, onStart]);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stop the long-running operation
|
||
|
|
*/
|
||
|
|
const stop = useCallback(() => {
|
||
|
|
if (abortControllerRef.current) {
|
||
|
|
abortControllerRef.current.abort();
|
||
|
|
abortControllerRef.current = null;
|
||
|
|
}
|
||
|
|
isRunningRef.current = false;
|
||
|
|
onStop?.();
|
||
|
|
}, [onStop]);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if operation is currently running
|
||
|
|
*/
|
||
|
|
const isRunning = useCallback(() => {
|
||
|
|
return isRunningRef.current;
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Edge 3.3: Cleanup on unmount
|
||
|
|
useEffect(() => {
|
||
|
|
return () => {
|
||
|
|
stop();
|
||
|
|
};
|
||
|
|
}, [stop]);
|
||
|
|
|
||
|
|
return {
|
||
|
|
start,
|
||
|
|
stop,
|
||
|
|
isRunning,
|
||
|
|
};
|
||
|
|
}
|