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

pub/sub site caching status and show site upading page properly

+53 -3
+1 -1
apps/firehose-service/src/lib/cache-invalidation.ts
··· 47 47 export async function publishCacheInvalidation( 48 48 did: string, 49 49 rkey: string, 50 - action: 'update' | 'delete' | 'settings' 50 + action: 'updating' | 'update' | 'delete' | 'settings' 51 51 ): Promise<void> { 52 52 const redis = getPublisher(); 53 53 if (!redis) return;
+6
apps/firehose-service/src/lib/cache-writer.ts
··· 637 637 }); 638 638 } 639 639 640 + // Notify hosting-service that this site is about to be updated so it can 641 + // show the "updating" page instead of serving stale or partially-updated files. 642 + if (!options?.skipInvalidation) { 643 + await publishCacheInvalidation(did, rkey, 'updating'); 644 + } 645 + 640 646 // Compare CIDs to determine what to download/delete 641 647 const newFiles = collectFileInfo(expandedRoot.entries); 642 648 const filesToDownload: FileInfo[] = [];
+33 -2
apps/hosting-service/src/lib/cache-invalidation.ts
··· 4 4 * Listens to Redis pub/sub for cache invalidation messages from the firehose-service. 5 5 * When a site is updated/deleted, clears the hosting-service's local caches 6 6 * (tiered storage hot+warm tiers, redirect rules) so stale data isn't served. 7 + * 8 + * Also tracks sites that are actively being downloaded ('updating' action) so 9 + * the serving layer can show a "site updating" page instead of stale/partial content. 7 10 */ 8 11 9 12 import Redis from 'ioredis'; ··· 12 15 import { cache } from './cache-manager'; 13 16 14 17 const CHANNEL = 'wisp:cache-invalidate'; 18 + 19 + // Sites currently being downloaded by the firehose-service. 20 + // Maps `${did}/${rkey}` → timestamp when the update started. 21 + // Used to show an "updating" page instead of serving stale files. 22 + const UPDATING_TTL_MS = 10 * 60 * 1000; // 10 minutes safety timeout 23 + const updatingSites = new Map<string, number>(); 24 + 25 + export function isSiteUpdating(did: string, rkey: string): boolean { 26 + const key = `${did}/${rkey}`; 27 + const since = updatingSites.get(key); 28 + if (since === undefined) return false; 29 + if (Date.now() - since > UPDATING_TTL_MS) { 30 + // Firehose must have crashed; remove the stale entry 31 + updatingSites.delete(key); 32 + return false; 33 + } 34 + return true; 35 + } 15 36 16 37 let subscriber: Redis | null = null; 17 38 ··· 73 94 const { did, rkey, action } = JSON.parse(message) as { 74 95 did: string; 75 96 rkey: string; 76 - action: 'update' | 'delete' | 'settings'; 97 + action: 'updating' | 'update' | 'delete' | 'settings'; 77 98 }; 78 99 79 100 if (!did || !rkey) { ··· 81 102 return; 82 103 } 83 104 84 - console.log(`[CacheInvalidation] Invalidating ${did}/${rkey} (${action})`); 105 + console.log(`[CacheInvalidation] Received ${action} for ${did}/${rkey}`); 106 + 107 + if (action === 'updating') { 108 + // Firehose is about to download new files — mark site as updating 109 + updatingSites.set(`${did}/${rkey}`, Date.now()); 110 + console.log(`[CacheInvalidation] Marked ${did}/${rkey} as updating`); 111 + return; 112 + } 113 + 114 + // For update/delete/settings: clear the updating flag and invalidate caches 115 + updatingSites.delete(`${did}/${rkey}`); 85 116 86 117 const prefix = `${did}/${rkey}/`; 87 118
+13
apps/hosting-service/src/lib/file-serving.ts
··· 11 11 import { loadRedirectRules, matchRedirectRule, parseCookies, parseQueryString } from './redirects'; 12 12 import { isHtmlContent, rewriteHtmlPaths } from './html-rewriter'; 13 13 import { generate404Page, generateDirectoryListing, siteUpdatingResponse } from './page-generators'; 14 + import { isSiteUpdating } from './cache-invalidation'; 14 15 import { getIndexFiles, applyCustomHeaders } from './request-utils'; 15 16 import { cache } from './cache-manager'; 16 17 import { storage } from './storage'; ··· 318 319 fullUrl?: string, 319 320 headers?: Record<string, string> 320 321 ): Promise<Response> { 322 + if (isSiteUpdating(did, rkey)) { 323 + return shouldServeUpdatingPage(headers) 324 + ? siteUpdatingResponse() 325 + : new Response('Site is updating', { status: 503, headers: { 'Cache-Control': 'no-store', 'Retry-After': '5' } }); 326 + } 327 + 321 328 const trace = createTrace(); 322 329 323 330 // Load settings for this site ··· 622 629 fullUrl?: string, 623 630 headers?: Record<string, string> 624 631 ): Promise<Response> { 632 + if (isSiteUpdating(did, rkey)) { 633 + return shouldServeUpdatingPage(headers) 634 + ? siteUpdatingResponse() 635 + : new Response('Site is updating', { status: 503, headers: { 'Cache-Control': 'no-store', 'Retry-After': '5' } }); 636 + } 637 + 625 638 const trace = createTrace(); 626 639 627 640 // Load settings for this site