import { useCallback, useEffect, useRef, useState } from 'react'; import type { BulkTransferEvent, BulkTransferHandle, BulkTransferResult } from '../client/directory-types.js'; export interface BulkTransferStatus { state: 'idle' | 'running' | 'done' | 'error' | 'aborted'; filesDone: number; filesTotal: number; bytesDone: number; bytesTotal: number; error: unknown; result: BulkTransferResult | null; /** Last event emitted by the underlying handle. */ lastEvent: BulkTransferEvent | null; } export interface UseFileTransferResult extends BulkTransferStatus { start(handle: BulkTransferHandle): void; abort(reason?: string): Promise; } const INITIAL: BulkTransferStatus = { state: 'idle', filesDone: 0, filesTotal: 0, bytesDone: 0, bytesTotal: 0, error: null, result: null, lastEvent: null, }; /** * Generic React-state wrapper around a `BulkTransferHandle`. Apps wire the * handle returned from `uploadDirectory()` / `downloadDirectory()` here to * surface progress in their UI. * * `useFileUpload` and `useFileDownload` are thin presets — they call * `start(...)` with the appropriate handle automatically. */ export function useFileTransfer(): UseFileTransferResult { const [status, setStatus] = useState(INITIAL); const handleRef = useRef(null); const start = useCallback((handle: BulkTransferHandle): void => { handleRef.current = handle; setStatus({ ...INITIAL, state: 'running' }); void (async () => { try { for await (const ev of handle.events) { setStatus((prev) => { const next: BulkTransferStatus = { ...prev, lastEvent: ev }; if (ev.type === 'plan') { next.filesTotal = ev.totalFiles; next.bytesTotal = ev.totalBytes ?? 0; } else if (ev.type === 'progress') { next.filesDone = ev.filesDone; next.bytesDone = ev.bytesDone; next.bytesTotal = ev.bytesTotal ?? prev.bytesTotal; next.filesTotal = ev.filesTotal; } else if (ev.type === 'complete') { next.state = 'done'; next.filesDone = ev.filesDone; next.bytesDone = ev.bytesDone; } else if (ev.type === 'abort') { next.state = 'aborted'; next.error = new Error(ev.reason); } else if (ev.type === 'file-error') { next.error = ev.error; } return next; }); } const result = await handle.done(); setStatus((prev) => ({ ...prev, state: 'done', result })); } catch (err) { setStatus((prev) => ({ ...prev, state: 'error', error: err })); } })(); }, []); const abort = useCallback(async (reason?: string): Promise => { if (handleRef.current === null) return; await handleRef.current.abort(reason); }, []); // Cleanup on unmount: abort any in-flight transfer. useEffect(() => { return () => { if (handleRef.current !== null) { void handleRef.current.abort('unmount').catch(() => undefined); } }; }, []); return { ...status, start, abort }; } /** * Preset: `useFileUpload` — semantically identical to `useFileTransfer`, * named distinctly to mirror the `useFileDownload` pair. */ export const useFileUpload = useFileTransfer; export const useFileDownload = useFileTransfer;