| 150 | * Drop-in replacement for fetch() for /api/files/* endpoints |
| 151 | */ |
| 152 | export async function cachedFetch(url, options = {}) { |
| 153 | // Parse URL |
| 154 | const urlObj = new URL(url, window.location.origin); |
| 155 | |
| 156 | // Only cache GET requests to /api/files/* |
| 157 | const shouldCache = |
| 158 | (!options.method || options.method === 'GET') && |
| 159 | urlObj.pathname.startsWith('/api/files/') && |
| 160 | !urlObj.searchParams.has('name'); // Don't cache downloads |
| 161 | |
| 162 | if (!shouldCache) { |
| 163 | return fetch(url, options); |
| 164 | } |
| 165 | |
| 166 | const cacheKey = fileCache.getCacheKey(url); |
| 167 | |
| 168 | // Check cache first |
| 169 | const cachedResponse = await fileCache.get(url); |
| 170 | if (cachedResponse) { |
| 171 | return cachedResponse; |
| 172 | } |
| 173 | |
| 174 | // Check if there's already a pending request for this URL |
| 175 | if (fileCache.pendingRequests.has(cacheKey)) { |
| 176 | log('[FileCache] Deduplicating request:', cacheKey); |
| 177 | // Wait for the pending request to complete |
| 178 | await fileCache.pendingRequests.get(cacheKey); |
| 179 | // Get a fresh copy from cache (each caller gets their own Response object) |
| 180 | const cached = await fileCache.get(url); |
| 181 | if (cached) { |
| 182 | return cached; |
| 183 | } |
| 184 | // If not in cache (caching may have failed), fallback to direct fetch |
| 185 | warn('[FileCache] Deduplicated request cache miss, falling back to direct fetch:', cacheKey); |
| 186 | return fetch(url, options); |
| 187 | } |
| 188 | |
| 189 | // Fetch from network |
| 190 | log('[FileCache] Fetching from network:', cacheKey); |
| 191 | |
| 192 | const fetchPromise = (async () => { |
| 193 | const response = await fetch(url, options); |
| 194 | |
| 195 | // Cache successful responses |
| 196 | if (response.ok) { |
| 197 | await fileCache.set(url, response); |
| 198 | } |
| 199 | |
| 200 | // Signal completion (don't return the response itself to avoid stream issues) |
| 201 | return response.ok; |
| 202 | })(); |
| 203 | |
| 204 | // Store the promise so concurrent requests can reuse it |
| 205 | fileCache.pendingRequests.set(cacheKey, fetchPromise); |
| 206 | |
| 207 | try { |
| 208 | const success = await fetchPromise; |
| 209 | if (success) { |