mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 15:11:12 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
210
src-old/lib/imageUploadHelper.ts
Normal file
210
src-old/lib/imageUploadHelper.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import { invokeWithTracking } from './edgeFunctionTracking';
|
||||
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
|
||||
import { handleError, handleNonCriticalError } from './errorHandler';
|
||||
|
||||
export interface CloudflareUploadResponse {
|
||||
result: {
|
||||
id: string;
|
||||
variants: string[];
|
||||
};
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// Internal type to track upload status
|
||||
interface UploadedImageWithFlag extends UploadedImage {
|
||||
wasNewlyUploaded?: boolean;
|
||||
}
|
||||
|
||||
// Upload timeout in milliseconds (30 seconds)
|
||||
const UPLOAD_TIMEOUT_MS = 30000;
|
||||
|
||||
/**
|
||||
* Creates a promise that rejects after a timeout
|
||||
*/
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, operation: string): Promise<T> {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${operation} timed out after ${timeoutMs}ms`)), timeoutMs)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads pending local images to Cloudflare via Supabase Edge Function
|
||||
* @param images Array of UploadedImage objects (mix of local and already uploaded)
|
||||
* @returns Array of UploadedImage objects with all images uploaded
|
||||
*/
|
||||
export async function uploadPendingImages(images: UploadedImage[]): Promise<UploadedImage[]> {
|
||||
// Process all images in parallel for better performance using allSettled
|
||||
const uploadPromises = images.map(async (image, index): Promise<UploadedImageWithFlag> => {
|
||||
if (image.isLocal && image.file) {
|
||||
const fileName = image.file.name;
|
||||
|
||||
// Step 1: Get upload URL from our Supabase Edge Function (with tracking and timeout)
|
||||
const { data: uploadUrlData, error: urlError, requestId } = await withTimeout(
|
||||
invokeWithTracking(
|
||||
'upload-image',
|
||||
{ action: 'get-upload-url' }
|
||||
),
|
||||
UPLOAD_TIMEOUT_MS,
|
||||
'Get upload URL'
|
||||
);
|
||||
|
||||
if (urlError || !uploadUrlData?.uploadURL) {
|
||||
const error = new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
|
||||
handleError(error, {
|
||||
action: 'Get Upload URL',
|
||||
metadata: { fileName, requestId }
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
// Step 2: Upload file directly to Cloudflare with retry on transient failures
|
||||
const formData = new FormData();
|
||||
formData.append('file', image.file);
|
||||
|
||||
const { withRetry } = await import('./retryHelpers');
|
||||
const uploadResponse = await withRetry(
|
||||
() => withTimeout(
|
||||
fetch(uploadUrlData.uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}),
|
||||
UPLOAD_TIMEOUT_MS,
|
||||
'Cloudflare upload'
|
||||
),
|
||||
{
|
||||
maxAttempts: 3,
|
||||
baseDelay: 500,
|
||||
shouldRetry: (error) => {
|
||||
// Retry on network errors, timeouts, or 5xx errors
|
||||
if (error instanceof Error) {
|
||||
const msg = error.message.toLowerCase();
|
||||
if (msg.includes('timeout')) return true;
|
||||
if (msg.includes('network')) return true;
|
||||
if (msg.includes('failed to fetch')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const errorText = await uploadResponse.text();
|
||||
const error = new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
|
||||
handleError(error, {
|
||||
action: 'Cloudflare Upload',
|
||||
metadata: { fileName, status: uploadResponse.status, timeout_ms: UPLOAD_TIMEOUT_MS }
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
const result: CloudflareUploadResponse = await uploadResponse.json();
|
||||
|
||||
if (!result.success || !result.result) {
|
||||
const error = new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
|
||||
handleError(error, {
|
||||
action: 'Cloudflare Upload',
|
||||
metadata: { fileName }
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
// Clean up object URL
|
||||
URL.revokeObjectURL(image.url);
|
||||
|
||||
// Step 3: Return uploaded image metadata with wasNewlyUploaded flag
|
||||
return {
|
||||
url: `https://cdn.thrillwiki.com/images/${result.result.id}/public`,
|
||||
cloudflare_id: result.result.id,
|
||||
caption: image.caption,
|
||||
isLocal: false,
|
||||
wasNewlyUploaded: true // Flag to track newly uploaded images
|
||||
};
|
||||
} else {
|
||||
// Already uploaded, keep as is
|
||||
return {
|
||||
url: image.url,
|
||||
cloudflare_id: image.cloudflare_id,
|
||||
caption: image.caption,
|
||||
isLocal: false,
|
||||
wasNewlyUploaded: false // Pre-existing image
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all uploads to settle (succeed or fail)
|
||||
const results = await Promise.allSettled(uploadPromises);
|
||||
|
||||
// Separate successful and failed uploads
|
||||
const successfulUploads: UploadedImageWithFlag[] = [];
|
||||
const newlyUploadedImageIds: string[] = []; // Track ONLY newly uploaded images for cleanup
|
||||
const errors: string[] = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const uploadedImage = result.value;
|
||||
successfulUploads.push(uploadedImage);
|
||||
// Only track newly uploaded images for potential cleanup
|
||||
if (uploadedImage.wasNewlyUploaded && uploadedImage.cloudflare_id) {
|
||||
newlyUploadedImageIds.push(uploadedImage.cloudflare_id);
|
||||
}
|
||||
} else {
|
||||
errors.push(result.reason?.message || `Upload ${index + 1} failed`);
|
||||
}
|
||||
});
|
||||
|
||||
// If any uploads failed, clean up ONLY newly uploaded images and throw error
|
||||
if (errors.length > 0) {
|
||||
if (newlyUploadedImageIds.length > 0) {
|
||||
const cleanupError = new Error(`Some uploads failed, cleaning up ${newlyUploadedImageIds.length} newly uploaded images`);
|
||||
handleError(cleanupError, {
|
||||
action: 'Upload Cleanup',
|
||||
metadata: {
|
||||
newlyUploadedCount: newlyUploadedImageIds.length,
|
||||
failureCount: errors.length
|
||||
}
|
||||
});
|
||||
|
||||
// Attempt cleanup in parallel with detailed error tracking
|
||||
const cleanupResults = await Promise.allSettled(
|
||||
newlyUploadedImageIds.map(imageId =>
|
||||
invokeWithTracking('upload-image', {
|
||||
action: 'delete',
|
||||
imageId,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Track cleanup failures silently (non-critical)
|
||||
const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected');
|
||||
if (cleanupFailures.length > 0) {
|
||||
handleNonCriticalError(
|
||||
new Error(`Failed to cleanup ${cleanupFailures.length} of ${newlyUploadedImageIds.length} images`),
|
||||
{
|
||||
action: 'Image Cleanup',
|
||||
metadata: {
|
||||
cleanupFailures: cleanupFailures.length,
|
||||
totalCleanup: newlyUploadedImageIds.length,
|
||||
orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected')
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const finalError = new Error(`Failed to upload ${errors.length} of ${images.length} images: ${errors.join('; ')}`);
|
||||
handleError(finalError, {
|
||||
action: 'Image Upload',
|
||||
metadata: { failureCount: errors.length, totalCount: images.length }
|
||||
});
|
||||
throw finalError;
|
||||
}
|
||||
|
||||
// Remove the wasNewlyUploaded flag before returning
|
||||
return successfulUploads.map(({ wasNewlyUploaded, ...image }) => image as UploadedImage);
|
||||
}
|
||||
Reference in New Issue
Block a user