Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shaky-dolls-cheer.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

@aaronleopold aaronleopold Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if this is an obvious question, I am just coming back after some time off and lacking a little bit of context, but what exactly was the problem? The way this reads, it seems like we are "fixing" a React lifecycle issue by adding retries/backoff to the fetch operation? I'd like to better understand what the root issue was during initial render

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Aaron,
During the initial load, the application was encountering an error in fetchAttachment. This issue has already been resolved as part of the Cleanup PR, which is now merged into main.
I just double-checked this - would it be okay for me to go ahead and close this PR?
Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the root issue was resolved, I think that satisfies my initial concern. I'm not opposed to having retries/backoff logic here, more so I am curious what the actual cause of the error was. Do you have any context you can share, maybe from the other PR where it was corrected?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Aaron,

To investigate the Tailwind conflicts issue, I created a new project outside our workspace and added separate modules for comments and chat to verify the behavior. At the time of creating this setup, the singleton chat store initialization was not in place, and Yuvaraj was working on that part in parallel. I had also installed the npm packages via a local folder.

After the singleton initialization was introduced, I encountered the attachment fetch issue during the first initialization, which I believe was due to the local npm cache and locally linked instances. Adding a retry resolved the issue at that time.

Later, I force-refreshed the packages, cleared all caches, and performed a fresh install directly from the npm repository. With this setup, the issue no longer occurs, even with different modules and multiple initializations within the same project.

Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ describe('useImageAttachment', () => {
})

it('prevents duplicate fetch when already loading', async () => {
mockFetchAttachment.mockImplementation(() => {})
mockFetchAttachment.mockImplementation(() => { })

const { result } = renderHook(() =>
useImageAttachment({
Expand All @@ -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)
})
})
39 changes: 35 additions & 4 deletions sdks/js/ditto-chat-ui/src/hooks/useImageAttachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface UseImageAttachmentOptions {
token: AttachmentToken | null
fetchAttachment?: FetchAttachmentFn
autoFetch?: boolean
retryDelay?: number
}

interface UseImageAttachmentReturn {
Expand All @@ -43,6 +44,7 @@ export function useImageAttachment({
token,
fetchAttachment,
autoFetch = true,
retryDelay = 500,
}: UseImageAttachmentOptions): UseImageAttachmentReturn {
const [imageUrl, setImageUrl] = useState<string | null>(null)
const [progress, setProgress] = useState(0)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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,
)
}
}
},
)
Expand Down