diff --git a/.changeset/shaky-dolls-cheer.md b/.changeset/shaky-dolls-cheer.md new file mode 100644 index 0000000..cb09551 --- /dev/null +++ b/.changeset/shaky-dolls-cheer.md @@ -0,0 +1,5 @@ +--- +'@dittolive/ditto-chat-ui': patch +--- + +Added retry and exponential back off mechanism in the fetchAttachment to resolve error during initial render. diff --git a/sdks/js/ditto-chat-ui/src/hooks/__tests__/useImageAttachment.test.ts b/sdks/js/ditto-chat-ui/src/hooks/__tests__/useImageAttachment.test.ts index da2680b..b4cb08c 100644 --- a/sdks/js/ditto-chat-ui/src/hooks/__tests__/useImageAttachment.test.ts +++ b/sdks/js/ditto-chat-ui/src/hooks/__tests__/useImageAttachment.test.ts @@ -152,7 +152,7 @@ describe('useImageAttachment', () => { }) it('prevents duplicate fetch when already loading', async () => { - mockFetchAttachment.mockImplementation(() => {}) + mockFetchAttachment.mockImplementation(() => { }) const { result } = renderHook(() => useImageAttachment({ @@ -172,4 +172,34 @@ describe('useImageAttachment', () => { expect(result.current.isLoading).toBe(true) expect(mockFetchAttachment).toHaveBeenCalledTimes(1) }) + it('retries on failure and eventually succeeds', async () => { + let attempts = 0 + mockFetchAttachment.mockImplementation( + (_token, _onProgress, onComplete) => { + attempts++ + if (attempts < 3) { + onComplete({ success: false, error: new Error('Transient error') }) + } else { + onComplete({ success: true, data: new Uint8Array([1, 2, 3]) }) + } + }, + ) + + const { result } = renderHook(() => + useImageAttachment({ + token: mockToken, + fetchAttachment: mockFetchAttachment, + retryDelay: 10, // Fast retries for testing + }), + ) + + // Wait for the final successful result + await waitFor(() => { + expect(result.current.imageUrl).toBe('blob:mock-url') + expect(result.current.isLoading).toBe(false) + expect(result.current.error).toBeNull() + }, { timeout: 2000 }) + + expect(mockFetchAttachment).toHaveBeenCalledTimes(3) + }) }) diff --git a/sdks/js/ditto-chat-ui/src/hooks/useImageAttachment.ts b/sdks/js/ditto-chat-ui/src/hooks/useImageAttachment.ts index 506f3f6..6258406 100644 --- a/sdks/js/ditto-chat-ui/src/hooks/useImageAttachment.ts +++ b/sdks/js/ditto-chat-ui/src/hooks/useImageAttachment.ts @@ -18,6 +18,7 @@ interface UseImageAttachmentOptions { token: AttachmentToken | null fetchAttachment?: FetchAttachmentFn autoFetch?: boolean + retryDelay?: number } interface UseImageAttachmentReturn { @@ -43,6 +44,7 @@ export function useImageAttachment({ token, fetchAttachment, autoFetch = true, + retryDelay = 500, }: UseImageAttachmentOptions): UseImageAttachmentReturn { const [imageUrl, setImageUrl] = useState(null) const [progress, setProgress] = useState(0) @@ -61,8 +63,11 @@ export function useImageAttachment({ } }, [imageUrl]) + const MAX_RETRIES = 3 + const INITIAL_RETRY_DELAY = retryDelay + // Fetch function - const fetchImage = () => { + const fetchImage = (currentRetry = 0) => { if (!token) { setError('No token provided') return null @@ -74,20 +79,22 @@ export function useImageAttachment({ return null } - if (isLoading) { + if (isLoading && currentRetry === 0) { return null } setIsLoading(true) setError(null) - setProgress(0) + if (currentRetry === 0) { + setProgress(0) + } const fetcher = fetchAttachment( token, (progressValue: number) => setProgress(progressValue), (result: FetchAttachmentResult) => { - setIsLoading(false) if (result.success && result.data) { + setIsLoading(false) try { const blob = toBlobFromUint8(result.data, 'image/jpeg') const url = URL.createObjectURL(blob) @@ -98,6 +105,30 @@ export function useImageAttachment({ } } else { setError('Failed to load image') + + // Check if we should retry + if (currentRetry < MAX_RETRIES) { + const nextRetry = currentRetry + 1 + const delay = INITIAL_RETRY_DELAY * Math.pow(2, currentRetry) + + console.warn( + `Attachment fetch failed, retrying in ${delay}ms (attempt ${nextRetry}/${MAX_RETRIES})...`, + result.error, + ) + + // Clear loading while waiting for retry + setIsLoading(false) + + setTimeout(() => { + fetchImage(nextRetry) + }, delay) + } else { + setIsLoading(false) + console.error( + `Failed to fetch attachment after ${MAX_RETRIES} retries:`, + result.error, + ) + } } }, )