import type { key_val } from '$lib/stores/ae_stores'; export let temp_get_blob_percent_completed = 0; export const get_blob_percent_completed = temp_get_blob_percent_completed; export const temp_get_object_percent_completed = 0; export const get_object_percent_completed = temp_get_object_percent_completed; export const get_object = async function get_object({ api_cfg = null, endpoint = '', headers = {}, params = {}, data = {}, timeout = 90000, return_meta = false, return_blob = false, filename = '', auto_download = false, as_list = false, // Is this still really needed? // The task_id value should be a random string that is unique to the task. This is used to identify the task in the message event. task_id = crypto.randomUUID(), log_lvl = 0, retry_count = 5 // Number of retry attempts }: { api_cfg: any; endpoint: string; headers?: any; params?: any; data?: any; timeout?: number; return_meta?: boolean; return_blob?: boolean; filename?: null | string; auto_download?: boolean; as_list?: boolean; task_id?: string; log_lvl?: number; retry_count?: number; }) { if (log_lvl) { console.log(`*** get_object() *** Endpoint: ${endpoint} AE Task ID: ${task_id}`); console.log('Params:', params); if (log_lvl > 1) { console.log('Data:', data); } } if (!api_cfg) { console.log('No API Config was provided. Returning false.'); return false; } // FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts if (typeof navigator !== 'undefined' && !navigator.onLine) { if (log_lvl) console.log('get_object: Browser is offline. Failing fast to allow cache fallback.'); return false; } const url = new URL(endpoint, api_cfg['base_url']); Object.keys(params).forEach((key) => url.searchParams.append(key, params[key])); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); // Clean and merge headers without mutating the original api_cfg const headers_cleaned: key_val = {}; const merged_headers = { ...api_cfg['headers'], ...headers }; // Auto-promote account_id from api_cfg to header if missing if (!merged_headers['x-account-id'] && api_cfg['account_id']) { merged_headers['x-account-id'] = api_cfg['account_id']; } // Handle "Bootstrap Paradox" for unauthenticated requests const bypass_val = merged_headers['x-no-account-id'] || merged_headers['x_no_account_id']; const is_valid_bypass = bypass_val === 'bypass' || bypass_val === 'Nothing to See Here' || params['key'] || bypass_val === 'direct-download'; if (is_valid_bypass) { if (log_lvl > 1) console.log('api_get_object: Valid bypass detected. Stripping account ID context.'); delete merged_headers['x-account-id']; delete merged_headers['x_account_id']; } else { // If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header // but DO NOT strip the valid Account ID. delete merged_headers['x-no-account-id']; delete merged_headers['x_no_account_id']; } // Standardize all headers to kebab-case and ensure string values for (const prop in merged_headers) { const prop_cleaned = prop.replaceAll('_', '-'); let value = merged_headers[prop]; if (value === null || value === undefined) continue; if (typeof value !== 'string') { value = JSON.stringify(value); } headers_cleaned[prop_cleaned] = value; } // Auto-inject Authorization header if JWT is present but header is missing let jwt = headers_cleaned['jwt'] || headers_cleaned['JWT'] || api_cfg['jwt'] || api_cfg['headers']?.['jwt'] || api_cfg['headers']?.['JWT']; // Final Fallback: Direct check of primary ae_loc key if (!jwt && typeof localStorage !== 'undefined') { try { const raw = localStorage.getItem('ae_loc'); if (raw) { const json = JSON.parse(raw); if (json.jwt) jwt = json.jwt; } } catch (e) { // Silently fail on storage read } } if (jwt && !headers_cleaned['Authorization'] && !headers_cleaned['authorization']) { headers_cleaned['Authorization'] = `Bearer ${jwt}`; } if (log_lvl > 1) { console.log('Final cleaned headers:', headers_cleaned); } const fetchOptions: RequestInit = { method: 'GET', headers: headers_cleaned, signal: controller.signal }; if (log_lvl > 1) { console.log('Fetch options:', fetchOptions); } let fetch_method: any = fetch; if (api_cfg.fetch) { if (log_lvl > 1) { console.log('Using custom fetch function from api_cfg!!!'); } fetch_method = api_cfg.fetch; } for (let attempt = 1; attempt <= retry_count; attempt++) { // FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts if (typeof navigator !== 'undefined' && !navigator.onLine) { if (log_lvl) console.log(`get_object: Browser is offline (attempt ${attempt}). Failing fast to allow cache fallback.`); return false; } try { const response = await fetch_method(url.toString(), fetchOptions).catch(function ( error: any ) { // SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs if (error.name === 'AbortError' || error.message?.includes('aborted') || error.name === 'TypeError') { if (log_lvl > 1) { console.log('API GET: Request was aborted or terminated by browser. This is expected during navigation.', error); } return error; // Return error to be handled below } console.log( 'API GET Object *fetch* request failed in an unexpected way.', error ); return error; }); clearTimeout(timeoutId); // Check if we should stop due to abort or network failure if (response instanceof Error || (response && (response.name === 'TypeError' || response.name === 'AbortError'))) { // If it was an explicit abort, definitely stop if (response.name === 'AbortError') return false; if (log_lvl > 1) console.log('API GET Object: Detected NetworkError or TypeError. Failing fast.'); return false; } if (!response) { if (log_lvl > 1) { console.log( 'API GET Object: Something went wrong with *fetch* request. Returning false? Throwing an error!' ); } throw new Error( `HTTP fetch request was aborted or failed in an unexpected way! URL = ${url.toString()}` ); // This will allow it to retry // return false; // This will stop the retries } if (log_lvl) { console.log( `Response: status=${response.status} statusText=${response.statusText} url=${response.url} attempt=${attempt}` ); } if (log_lvl > 1) { console.log('Response:', response); } if (!response.ok) { if (response.status === 404) { if (log_lvl) { console.log('The response was a 404 not found "error". Returning null.'); } return null; } // FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422) if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) { if (log_lvl) console.error(`API Client Failure (${response.status}). Failing fast.`); if (response.status === 401 || response.status === 403) { console.warn(`AUTH DIAGNOSTICS: Headers sent for ${endpoint}:`, { has_auth: !!headers_cleaned['Authorization'], has_api_key: !!headers_cleaned['x-aether-api-key'], has_account_id: !!headers_cleaned['x-account-id'], jwt_preview: jwt ? `${jwt.slice(0, 8)}...` : 'MISSING' }); } // Structured Error Handling (V3): Attempt to get rich error metadata let error_json: any = null; try { error_json = await response.json(); } catch (e) { // Not JSON } if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json); if (error_json?.meta?.details) { return error_json; } // Fallback for standard FastAPI "detail" errors if (error_json?.detail) { return { meta: { success: false, status_code: response.status, details: { category: 'validation', message: typeof error_json.detail === 'string' ? error_json.detail : JSON.stringify(error_json.detail), raw: error_json.detail } } }; } return false; } throw new Error(`HTTP error! status: ${response.status}`); } if (!return_blob) { const json = await response.json(); if (log_lvl > 1) { console.log('Response JSON:', json); } if (!Array.isArray(json.data) && as_list) { return [json.data]; } return json.data || json; } else { const reader = response.body?.getReader(); const contentLength = +response.headers.get('Content-Length')!; let receivedLength = 0; const chunks = []; while (true) { const { done, value } = await reader!.read(); if (done) break; chunks.push(value); receivedLength += value.length; const percent_completed = Math.round((receivedLength * 100) / contentLength); if (log_lvl > 1) { console.log( 'GET Blob Progress:', percent_completed, 'Total:', contentLength, 'Loaded:', receivedLength, 'Percent Completed', percent_completed ); } temp_get_blob_percent_completed = percent_completed; try { if (typeof window !== 'undefined') { window.postMessage( { type: 'api_download_blob', status: 'downloading', task_id: task_id, endpoint: endpoint, filename: filename, size_total: contentLength, size_loaded: receivedLength, percent_completed: percent_completed }, '*' ); } } catch (e) { console.error('Error posting message:', e); } } const blob = new Blob(chunks); if (auto_download) { const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.setAttribute('download', filename || 'download'); document.body.appendChild(link); link.click(); link.remove(); return true; } else { return blob; } } } catch (error) { console.log(`API GET object request *fetch* error on attempt ${attempt}:`, error); if (attempt === retry_count) { console.log('Max retry attempts reached. Returning false.'); return false; } // Log retry information if (log_lvl) { console.log(`Retrying... (${attempt}/${retry_count})`); } } } };