|
1 | 1 | import { createLogger } from '@sim/logger' |
| 2 | +import { sleep } from '@sim/utils/helpers' |
2 | 3 | import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' |
3 | 4 | import { toast } from '@/components/emcn' |
4 | | -import { isApiClientError } from '@/lib/api/client/errors' |
| 5 | +import { ApiClientError, isApiClientError } from '@/lib/api/client/errors' |
5 | 6 | import { requestJson } from '@/lib/api/client/request' |
6 | 7 | import { getUsageLimitsContract } from '@/lib/api/contracts/usage-limits' |
7 | 8 | import { |
@@ -267,23 +268,52 @@ async function uploadWorkspaceFile( |
267 | 268 | throw error |
268 | 269 | } |
269 | 270 |
|
270 | | - const data = await requestJson(registerWorkspaceFileContract, { |
271 | | - params: { id: workspaceId }, |
272 | | - body: { |
273 | | - key: result.key, |
274 | | - name: result.name, |
275 | | - size: result.size, |
276 | | - contentType: result.contentType, |
277 | | - }, |
278 | | - signal, |
279 | | - }) |
| 271 | + const data = await registerWithRetry(workspaceId, result, signal) |
280 | 272 |
|
281 | 273 | if (!data.success || !data.file) { |
282 | 274 | throw new Error(data.error || 'Failed to register file') |
283 | 275 | } |
284 | 276 | return { success: true, file: data.file } |
285 | 277 | } |
286 | 278 |
|
| 279 | +const REGISTER_MAX_ATTEMPTS = 3 |
| 280 | +const REGISTER_RETRY_DELAY_MS = 500 |
| 281 | + |
| 282 | +/** |
| 283 | + * Register the uploaded object with bounded retries. The server-side handler |
| 284 | + * is idempotent (existing-record short-circuit), so safely retrying handles |
| 285 | + * dropped responses that would otherwise orphan the object in storage. |
| 286 | + */ |
| 287 | +async function registerWithRetry( |
| 288 | + workspaceId: string, |
| 289 | + result: { key: string; name: string; size: number; contentType: string }, |
| 290 | + signal?: AbortSignal |
| 291 | +) { |
| 292 | + let lastError: unknown |
| 293 | + for (let attempt = 1; attempt <= REGISTER_MAX_ATTEMPTS; attempt++) { |
| 294 | + try { |
| 295 | + return await requestJson(registerWorkspaceFileContract, { |
| 296 | + params: { id: workspaceId }, |
| 297 | + body: { |
| 298 | + key: result.key, |
| 299 | + name: result.name, |
| 300 | + size: result.size, |
| 301 | + contentType: result.contentType, |
| 302 | + }, |
| 303 | + signal, |
| 304 | + }) |
| 305 | + } catch (error) { |
| 306 | + lastError = error |
| 307 | + if (signal?.aborted) throw error |
| 308 | + const isTransient = |
| 309 | + !(error instanceof ApiClientError) || (error.status >= 500 && error.status < 600) |
| 310 | + if (!isTransient || attempt === REGISTER_MAX_ATTEMPTS) throw error |
| 311 | + await sleep(REGISTER_RETRY_DELAY_MS * attempt) |
| 312 | + } |
| 313 | + } |
| 314 | + throw lastError |
| 315 | +} |
| 316 | + |
287 | 317 | export function useUploadWorkspaceFile() { |
288 | 318 | const queryClient = useQueryClient() |
289 | 319 |
|
|
0 commit comments