Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

cache control and trigger full re-download if incremental sync fails

+92 -32
+81 -18
apps/firehose-service/src/lib/cache-writer.ts
··· 521 521 522 522 logger.info(`Files unchanged: ${newFiles.length - filesToDownload.length}, to download: ${filesToDownload.length}, to delete: ${pathsToDelete.length}`); 523 523 524 - // Download new/changed files (with concurrency limit) 525 524 const DOWNLOAD_CONCURRENCY = 20; 526 - for (let i = 0; i < filesToDownload.length; i += DOWNLOAD_CONCURRENCY) { 527 - const batch = filesToDownload.slice(i, i + DOWNLOAD_CONCURRENCY); 528 - await Promise.allSettled( 529 - batch.map(file => downloadAndWriteBlob(did, rkey, file, pdsEndpoint)) 530 - ); 525 + const DELETE_CONCURRENCY = 50; 526 + 527 + const downloadFiles = async (files: FileInfo[]) => { 528 + const failures: Array<{ path: string; error: unknown }> = []; 529 + for (let i = 0; i < files.length; i += DOWNLOAD_CONCURRENCY) { 530 + const batch = files.slice(i, i + DOWNLOAD_CONCURRENCY); 531 + const results = await Promise.allSettled( 532 + batch.map(file => downloadAndWriteBlob(did, rkey, file, pdsEndpoint)) 533 + ); 534 + for (let j = 0; j < results.length; j++) { 535 + const result = results[j]; 536 + if (result?.status === 'rejected') { 537 + const file = batch[j]; 538 + if (file) failures.push({ path: file.path, error: result.reason }); 539 + } 540 + } 541 + } 542 + return failures; 543 + }; 544 + 545 + const deleteKeys = async (keys: string[]) => { 546 + const failures: Array<{ key: string; error: unknown }> = []; 547 + for (let i = 0; i < keys.length; i += DELETE_CONCURRENCY) { 548 + const batch = keys.slice(i, i + DELETE_CONCURRENCY); 549 + const results = await Promise.allSettled(batch.map(key => deleteFile(key))); 550 + for (let j = 0; j < results.length; j++) { 551 + const result = results[j]; 552 + if (result?.status === 'rejected') { 553 + const key = batch[j]; 554 + if (key) failures.push({ key, error: result.reason }); 555 + } 556 + } 557 + } 558 + return failures; 559 + }; 560 + 561 + const keysToDelete: string[] = []; 562 + for (const path of pathsToDelete) { 563 + keysToDelete.push(`${did}/${rkey}/${path}`); 564 + if (isHtmlFile(path)) { 565 + keysToDelete.push(`${did}/${rkey}/.rewritten/${path}`); 566 + } 531 567 } 532 568 533 - // Delete removed files (both original and rewritten) with batching 534 - if (pathsToDelete.length > 0) { 535 - const keysToDelete: string[] = []; 536 - for (const path of pathsToDelete) { 537 - keysToDelete.push(`${did}/${rkey}/${path}`); 538 - if (isHtmlFile(path)) { 539 - keysToDelete.push(`${did}/${rkey}/.rewritten/${path}`); 540 - } 569 + // Incremental sync first 570 + const downloadFailures = await downloadFiles(filesToDownload); 571 + const deleteFailures = await deleteKeys(keysToDelete); 572 + 573 + // Recovery path: wipe site prefix and perform full rebuild if incremental had failures 574 + if (downloadFailures.length > 0 || deleteFailures.length > 0) { 575 + logger.warn(`Incremental sync failed for ${did}/${rkey}; falling back to full rebuild`, { 576 + did, 577 + rkey, 578 + downloadFailures: downloadFailures.length, 579 + deleteFailures: deleteFailures.length, 580 + }); 581 + 582 + const prefix = `${did}/${rkey}/`; 583 + const existingKeys = await listFiles(prefix); 584 + const wipeFailures = await deleteKeys(existingKeys); 585 + if (wipeFailures.length > 0) { 586 + logger.error(`Failed to wipe site prefix before full rebuild for ${did}/${rkey}`, undefined, { 587 + did, 588 + rkey, 589 + wipeFailures: wipeFailures.length, 590 + sampleFailures: wipeFailures.slice(0, 5).map((f) => ({ 591 + key: f.key, 592 + error: f.error instanceof Error ? f.error.message : String(f.error), 593 + })), 594 + }); 595 + throw new Error(`Failed to wipe site prefix for ${did}/${rkey}`); 541 596 } 542 597 543 - const DELETE_CONCURRENCY = 50; 544 - for (let i = 0; i < keysToDelete.length; i += DELETE_CONCURRENCY) { 545 - const batch = keysToDelete.slice(i, i + DELETE_CONCURRENCY); 546 - await Promise.allSettled(batch.map(key => deleteFile(key))); 598 + const fullDownloadFailures = await downloadFiles(newFiles); 599 + if (fullDownloadFailures.length > 0) { 600 + logger.error(`Full rebuild failed for ${did}/${rkey}`, undefined, { 601 + did, 602 + rkey, 603 + fullDownloadFailures: fullDownloadFailures.length, 604 + sampleFailures: fullDownloadFailures.slice(0, 5).map((f) => ({ 605 + path: f.path, 606 + error: f.error instanceof Error ? f.error.message : String(f.error), 607 + })), 608 + }); 609 + throw new Error(`Full rebuild failed for ${did}/${rkey}`); 547 610 } 548 611 } 549 612
+11 -14
apps/hosting-service/src/lib/file-serving.ts
··· 24 24 import { createTrace, span, logTrace, type RequestTrace } from './trace'; 25 25 26 26 const logger = createLogger('file-serving'); 27 + const STANDARD_CACHE_CONTROL = 'public, max-age=600'; 27 28 28 29 type FileStorageResult = StorageResult<Uint8Array>; 29 30 type FileForRequestResult = { result: FileStorageResult; filePath: string; wasRewritten: boolean }; ··· 144 145 const content = Buffer.from(result.data); 145 146 const meta = result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 146 147 const mimeType = meta?.mimeType || lookup(filePath) || 'application/octet-stream'; 147 - const cacheControl = mimeType.startsWith('text/html') 148 - ? 'public, max-age=300' 149 - : 'public, max-age=31536000, immutable'; 148 + const cacheControl = STANDARD_CACHE_CONTROL; 150 149 const etag = result.metadata.checksum ? `"${result.metadata.checksum}"` : undefined; 151 150 152 151 // Handle conditional requests (If-None-Match → 304 Not Modified) ··· 206 205 const content = Buffer.from(result.data); 207 206 const meta = result.metadata.customMetadata as { encoding?: string; mimeType?: string } | undefined; 208 207 const mimeType = meta?.mimeType || lookup(filePath) || 'application/octet-stream'; 209 - const cacheControl = mimeType.startsWith('text/html') 210 - ? 'public, max-age=300' 211 - : 'public, max-age=31536000, immutable'; 208 + const cacheControl = STANDARD_CACHE_CONTROL; 212 209 213 210 const headers: Record<string, string> = { 214 211 'Content-Type': mimeType, ··· 320 317 status, 321 318 headers: { 322 319 'Location': targetPath, 323 - 'Cache-Control': status === 301 ? 'public, max-age=31536000' : 'public, max-age=0', 320 + 'Cache-Control': STANDARD_CACHE_CONTROL, 324 321 }, 325 322 }); 326 323 } else if (status === 404) { ··· 423 420 return new Response(html, { 424 421 headers: { 425 422 'Content-Type': 'text/html; charset=utf-8', 426 - 'Cache-Control': 'public, max-age=300', 423 + 'Cache-Control': STANDARD_CACHE_CONTROL, 427 424 }, 428 425 }); 429 426 } ··· 520 517 status: 404, 521 518 headers: { 522 519 'Content-Type': 'text/html; charset=utf-8', 523 - 'Cache-Control': 'public, max-age=300', 520 + 'Cache-Control': STANDARD_CACHE_CONTROL, 524 521 }, 525 522 }); 526 523 } ··· 550 547 status: 404, 551 548 headers: { 552 549 'Content-Type': 'text/html; charset=utf-8', 553 - 'Cache-Control': 'public, max-age=300', 550 + 'Cache-Control': STANDARD_CACHE_CONTROL, 554 551 }, 555 552 }); 556 553 } ··· 630 627 status, 631 628 headers: { 632 629 'Location': redirectTarget, 633 - 'Cache-Control': status === 301 ? 'public, max-age=31536000' : 'public, max-age=0', 630 + 'Cache-Control': STANDARD_CACHE_CONTROL, 634 631 }, 635 632 }); 636 633 } else if (status === 404) { ··· 746 743 return new Response(html, { 747 744 headers: { 748 745 'Content-Type': 'text/html; charset=utf-8', 749 - 'Cache-Control': 'public, max-age=300', 746 + 'Cache-Control': STANDARD_CACHE_CONTROL, 750 747 }, 751 748 }); 752 749 } ··· 841 838 status: 404, 842 839 headers: { 843 840 'Content-Type': 'text/html; charset=utf-8', 844 - 'Cache-Control': 'public, max-age=300', 841 + 'Cache-Control': STANDARD_CACHE_CONTROL, 845 842 }, 846 843 }); 847 844 } ··· 871 868 status: 404, 872 869 headers: { 873 870 'Content-Type': 'text/html; charset=utf-8', 874 - 'Cache-Control': 'public, max-age=300', 871 + 'Cache-Control': STANDARD_CACHE_CONTROL, 875 872 }, 876 873 }); 877 874 }