import type { key_val } from '$lib/stores/ae_stores'; /** * Performs a DELETE request to the Aether API. * Refactored 2026-01-08 to use standard fetch with timeout, custom fetch injection, * standardized kebab-case headers, and robust V3 response handling. */ export const delete_object = async function delete_object({ api_cfg = null, endpoint = '', headers = {}, params = {}, data = {}, timeout = 60000, return_meta = false, log_lvl = 0, retry_count = 5 }: { api_cfg: any; endpoint: string; headers?: any; params?: any; data?: any; timeout?: number; return_meta?: boolean; log_lvl?: number; retry_count?: number; }) { if (log_lvl) { console.log(`*** delete_object() *** Endpoint: ${endpoint}`); console.log('Params:', params); if (log_lvl > 1) { console.log('Data:', data); console.log(`Base URL: ${api_cfg?.['base_url']}`); } } if (!api_cfg) { console.error('No API Config was provided. Returning false.'); return false; } // Construct the URL with query parameters const url = new URL(endpoint, api_cfg['base_url']); if (params) { Object.keys(params).forEach((key) => url.searchParams.append(key, params[key]) ); } // Clean and merge headers without mutating the original api_cfg const headers_cleaned: key_val = {}; const merged_headers = { ...api_cfg['headers'], ...headers }; // Handle "Bootstrap Paradox" for unauthenticated requests if (merged_headers.hasOwnProperty('x-no-account-id')) { delete merged_headers['x-account-id']; if (merged_headers['x-no-account-id'] === null) { merged_headers['x-no-account-id'] = 'Nothing to See Here'; } } 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 const jwt = headers_cleaned['jwt'] || headers_cleaned['JWT'] || api_cfg['jwt']; if ( jwt && !headers_cleaned['Authorization'] && !headers_cleaned['authorization'] ) { headers_cleaned['Authorization'] = `Bearer ${jwt}`; } headers_cleaned['Content-Type'] = 'application/json'; if (log_lvl > 1) { console.log('Final cleaned headers:', headers_cleaned); } 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++) { // Keep timeout handle at attempt scope so catch can always clear it. let timeoutId: ReturnType | null = null; try { const controller = new AbortController(); // AbortError alone is ambiguous. Track helper-timeout aborts so // caller/navigation aborts can still fail fast with no retry. let did_timeout_abort = false; timeoutId = setTimeout(() => { did_timeout_abort = true; console.error( `API DELETE request timed out after ${timeout}ms.` ); controller.abort(); }, timeout); const fetchOptions: RequestInit = { method: 'DELETE', headers: headers_cleaned, body: Object.keys(data).length > 0 ? JSON.stringify(data) : undefined, signal: controller.signal }; const response = await fetch_method( url.toString(), fetchOptions ).catch(function (error: any) { if ( error?.name === 'AbortError' || error?.name === 'TypeError' || error?.message?.includes('aborted') ) { if (log_lvl > 1) { console.log( 'API DELETE: Request aborted or browser-terminated.', error ); } return error; } console.log( 'API DELETE Object *fetch* request was aborted or failed in an unexpected way.', error ); return error; }); if (timeoutId) clearTimeout(timeoutId); // Error object was returned from fetch catch block; decide retry class. if ( response instanceof Error || (response && (response.name === 'AbortError' || response.name === 'TypeError')) ) { if (response.name === 'AbortError') { if (did_timeout_abort) { throw new Error( `Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms` ); } return false; } throw new Error( `Network error (attempt ${attempt}): ${response.message}` ); } if (!response) { throw new Error( `HTTP fetch request was aborted or failed in an unexpected way! URL = ${url.toString()}` ); } if (log_lvl) { console.log( `Response: status=${response.status} attempt=${attempt}` ); } if (!response.ok) { if (response.status === 404) { console.warn('404 Not Found. Returning null.'); return null; } const errorBody = await response.text(); console.error( `HTTP error! status: ${response.status}`, errorBody ); // Fail fast on client/auth/validation failures. if ( response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422 ) { return false; } throw new Error( `HTTP error! status: ${response.status} - ${errorBody}` ); } const json = await response.json(); if (log_lvl > 1) { console.log('Response JSON:', json); } // Return the response data or metadata // Robustly handle V3 response envelopes return return_meta ? json : json.data !== undefined ? json.data : json; } catch (error) { // Ensure per-attempt timeout is always cleared on failure. if (timeoutId) clearTimeout(timeoutId); console.error(`API DELETE error on attempt ${attempt}:`, error); if (attempt === retry_count) { console.error('Max retry attempts reached. Returning false.'); return false; } // Backoff before retrying. Caps at 8s to match GET/POST/PATCH policy. const delay_ms = Math.min(2000 * attempt, 8000); console.log( `API DELETE: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})` ); await new Promise((resolve) => setTimeout(resolve, delay_ms)); } } };