diff --git a/src/core/reducer.ts b/src/core/reducer.ts index 4725e25..5d7df32 100644 --- a/src/core/reducer.ts +++ b/src/core/reducer.ts @@ -58,8 +58,8 @@ export function reducer(state: InternalState, action: Action): InternalState { case "SUCCESS": return { ...state, status: "success", progress: 100 }; case "ERROR": return { ...state, status: "error", error: action.error }; case "RESET": return { ...initialState, isOffline: state.isOffline }; - case "OFFLINE": return { ...state, isOffline: true }; - case "ONLINE": return { ...state, isOffline: false }; + case "OFFLINE": return state.isOffline ? state : { ...state, isOffline: true }; + case "ONLINE": return state.isOffline ? { ...state, isOffline: false } : state; default: return state; } } \ No newline at end of file diff --git a/src/core/useUploader.ts b/src/core/useUploader.ts index 1d5057f..c96756d 100644 --- a/src/core/useUploader.ts +++ b/src/core/useUploader.ts @@ -16,10 +16,14 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { onFileSelect, onFileReject, onUploadStart, onProgress, onChunkAttempt, onChunkSuccess, onChunkAttemptFailure, onPause, onResume, onAbort, onError, onSuccess, onStateChange, + onOffline, onOnline, } = props; const [state, dispatch] = useReducer(reducer, initialState); + const netCbRef = useRef({ onOnline, onOffline }); + netCbRef.current = { onOnline, onOffline }; + const stateRef = useRef(state); stateRef.current = state; @@ -75,8 +79,9 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { return; } - dispatch({ type: "SELECT_FILE", file: f }); - onFileSelect?.(f); + autoStartedFor.current = null; + dispatch({ type: "SELECT_FILE", file: f }); + onFileSelect?.(f); }, [disabled, accept, maxFileSize, onFileReject, onFileSelect]); const start = useCallback(async () => { @@ -124,12 +129,7 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { engineRef.current = engine; engine.on("progress", (e: any) => { - if (stateRef.current.status === "paused") { - if (pauseIntentRef.current) { - try { engineRef.current?.pause(); } catch { /* noop */ } - } - return; - } + if (stateRef.current.status === "paused") return; const value = Math.round(e?.detail?.progress ?? 0); dispatch({ type: "PROGRESS", value }); onProgress?.(value); @@ -162,17 +162,6 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { }); }); - engine.on("offline", () => { - dispatch({ type: "OFFLINE" }); - }); - - engine.on("online", () => { - dispatch({ type: "ONLINE" }); - if (pauseIntentRef.current) { - try { engineRef.current?.pause(); } catch { /* noop */ } - } - }); - engine.on("success", () => { clearEngine(); dispatch({ type: "SUCCESS" }); @@ -181,6 +170,7 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { engine.on("error", (e: any) => { const message = e?.detail?.message ?? "Upload failed"; + try { engine.abort(); } catch { /* detach listeners, kill session */ } clearEngine(); dispatch({ type: "ERROR", error: { message } }); onError?.({ message }); @@ -200,11 +190,8 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { const startRef = useRef(start); startRef.current = start; - const pauseIntentRef = useRef(false); - const pause = useCallback(() => { if (stateRef.current.status !== "uploading") return; - pauseIntentRef.current = true; try { engineRef.current?.pause(); } catch { /* noop */ } dispatch({ type: "PAUSE" }); onPause?.(); @@ -212,14 +199,12 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { const resume = useCallback(async () => { if (stateRef.current.status !== "paused") return; - pauseIntentRef.current = false; try { await engineRef.current?.resume(); } catch { /* noop */ } dispatch({ type: "RESUME" }); onResume?.(); }, [onResume]); const abort = useCallback(() => { - pauseIntentRef.current = false; autoStartedFor.current = null; cancelEngine(); dispatch({ type: "RESET" }); @@ -227,12 +212,38 @@ export function useUploader(props: FastPixUploaderProps): UploaderContextValue { }, [cancelEngine, onAbort]); const reset = useCallback(() => { - pauseIntentRef.current = false; autoStartedFor.current = null; cancelEngine(); dispatch({ type: "RESET" }); }, [cancelEngine]); + useEffect(() => { + if (globalThis.window === undefined) return; + + const handleOffline = () => { + dispatch({ type: "OFFLINE" }); + netCbRef.current.onOffline?.(); + }; + + const handleOnline = () => { + dispatch({ type: "ONLINE" }); + netCbRef.current.onOnline?.(); + }; + + // Sync initial connectivity without firing a transition callback. + if (globalThis.navigator !== undefined && !globalThis.navigator.onLine) { + dispatch({ type: "OFFLINE" }); + } + + globalThis.window.addEventListener("offline", handleOffline); + globalThis.window.addEventListener("online", handleOnline); + + return () => { + globalThis.window.removeEventListener("offline", handleOffline); + globalThis.window.removeEventListener("online", handleOnline); + }; +}, []); + useEffect(() => { if (fileProp) selectFile(fileProp); }, [fileProp]); diff --git a/src/types.ts b/src/types.ts index 90d43de..25a5faf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,8 @@ export interface FastPixUploaderProps { onError?: (error: UploaderError) => void; onSuccess?: () => void; onStateChange?: (state: UploaderState) => void; + onOffline?: () => void; + onOnline?: () => void; } export interface FastPixUploaderRef {