import { FoodLogRepository } from '@/add-log/repository'; import { STORAGE_KEYS } from '@/common/constants/storageKeys'; import { getDatabase } from '@/remote/database'; import { logger, startSpan } from '@/remote/monitor'; import * as FileSystem from 'expo-file-system'; import * as ImageManipulator from 'expo-image-manipulator'; import * as SecureStore from 'expo-secure-store'; const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:4321'; let uploadTimeout: ReturnType | null = null; export function schedulePhotoUpload(): void { if (uploadTimeout) { clearTimeout(uploadTimeout); } uploadTimeout = setTimeout(async () => { uploadTimeout = null; await uploadPendingPhotos(); }, 2000); } export async function uploadPendingPhotos(): Promise { const db = getDatabase(); const repo = new FoodLogRepository(db); const sessionToken = await SecureStore.getItemAsync(STORAGE_KEYS.SESSION_TOKEN); if (!sessionToken) return; const pending = await repo.getPendingUploads(); for (const log of pending) { try { const photoPath = await uploadPhoto(sessionToken, log.id, log.photoUri!); if (photoPath) { await repo.updateRemotePhotoPath(log.id, photoPath); } } catch (error) { logger.error(error instanceof Error ? error : new Error(String(error)), { logId: log.id }); } } } async function uploadPhoto( sessionToken: string, logId: string, photoUri: string ): Promise { const info = await FileSystem.getInfoAsync(photoUri); if (!info.exists) return null; const fileSize = info.size ?? 0; const imageInfo = await ImageManipulator.manipulateAsync(photoUri, [], {}); const { width, height } = imageInfo; const ext = photoUri.split('.').pop()?.toLowerCase() ?? 'jpg'; const signedUrlResponse = await startSpan( { name: 'photo_upload_url', op: 'http.client', attributes: { log_id: logId } }, () => fetch(`${API_BASE_URL}/api/sync/upload-photo`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${sessionToken}`, }, body: JSON.stringify({ localId: logId, ext }), }) ); if (!signedUrlResponse.ok) return null; const { success, signedUrl, path } = await signedUrlResponse.json(); if (!success) return null; const uploadResult = await startSpan( { name: 'photo_upload_bucket', op: 'http.client', attributes: { log_id: logId, file_size: fileSize, width, height }, }, () => FileSystem.uploadAsync(signedUrl, photoUri, { httpMethod: 'PUT', headers: { 'Content-Type': `image/${ext === 'jpg' ? 'jpeg' : ext}` }, }) ); if (uploadResult.status !== 200) return null; return path; }