···637637 });
638638 }
639639640640+ // Notify hosting-service that this site is about to be updated so it can
641641+ // show the "updating" page instead of serving stale or partially-updated files.
642642+ if (!options?.skipInvalidation) {
643643+ await publishCacheInvalidation(did, rkey, 'updating');
644644+ }
645645+640646 // Compare CIDs to determine what to download/delete
641647 const newFiles = collectFileInfo(expandedRoot.entries);
642648 const filesToDownload: FileInfo[] = [];
···44 * Listens to Redis pub/sub for cache invalidation messages from the firehose-service.
55 * When a site is updated/deleted, clears the hosting-service's local caches
66 * (tiered storage hot+warm tiers, redirect rules) so stale data isn't served.
77+ *
88+ * Also tracks sites that are actively being downloaded ('updating' action) so
99+ * the serving layer can show a "site updating" page instead of stale/partial content.
710 */
811912import Redis from 'ioredis';
···1215import { cache } from './cache-manager';
13161417const CHANNEL = 'wisp:cache-invalidate';
1818+1919+// Sites currently being downloaded by the firehose-service.
2020+// Maps `${did}/${rkey}` → timestamp when the update started.
2121+// Used to show an "updating" page instead of serving stale files.
2222+const UPDATING_TTL_MS = 10 * 60 * 1000; // 10 minutes safety timeout
2323+const updatingSites = new Map<string, number>();
2424+2525+export function isSiteUpdating(did: string, rkey: string): boolean {
2626+ const key = `${did}/${rkey}`;
2727+ const since = updatingSites.get(key);
2828+ if (since === undefined) return false;
2929+ if (Date.now() - since > UPDATING_TTL_MS) {
3030+ // Firehose must have crashed; remove the stale entry
3131+ updatingSites.delete(key);
3232+ return false;
3333+ }
3434+ return true;
3535+}
15361637let subscriber: Redis | null = null;
1738···7394 const { did, rkey, action } = JSON.parse(message) as {
7495 did: string;
7596 rkey: string;
7676- action: 'update' | 'delete' | 'settings';
9797+ action: 'updating' | 'update' | 'delete' | 'settings';
7798 };
789979100 if (!did || !rkey) {
···81102 return;
82103 }
831048484- console.log(`[CacheInvalidation] Invalidating ${did}/${rkey} (${action})`);
105105+ console.log(`[CacheInvalidation] Received ${action} for ${did}/${rkey}`);
106106+107107+ if (action === 'updating') {
108108+ // Firehose is about to download new files — mark site as updating
109109+ updatingSites.set(`${did}/${rkey}`, Date.now());
110110+ console.log(`[CacheInvalidation] Marked ${did}/${rkey} as updating`);
111111+ return;
112112+ }
113113+114114+ // For update/delete/settings: clear the updating flag and invalidate caches
115115+ updatingSites.delete(`${did}/${rkey}`);
8511686117 const prefix = `${did}/${rkey}/`;
87118
+13
apps/hosting-service/src/lib/file-serving.ts
···1111import { loadRedirectRules, matchRedirectRule, parseCookies, parseQueryString } from './redirects';
1212import { isHtmlContent, rewriteHtmlPaths } from './html-rewriter';
1313import { generate404Page, generateDirectoryListing, siteUpdatingResponse } from './page-generators';
1414+import { isSiteUpdating } from './cache-invalidation';
1415import { getIndexFiles, applyCustomHeaders } from './request-utils';
1516import { cache } from './cache-manager';
1617import { storage } from './storage';
···318319 fullUrl?: string,
319320 headers?: Record<string, string>
320321): Promise<Response> {
322322+ if (isSiteUpdating(did, rkey)) {
323323+ return shouldServeUpdatingPage(headers)
324324+ ? siteUpdatingResponse()
325325+ : new Response('Site is updating', { status: 503, headers: { 'Cache-Control': 'no-store', 'Retry-After': '5' } });
326326+ }
327327+321328 const trace = createTrace();
322329323330 // Load settings for this site
···622629 fullUrl?: string,
623630 headers?: Record<string, string>
624631): Promise<Response> {
632632+ if (isSiteUpdating(did, rkey)) {
633633+ return shouldServeUpdatingPage(headers)
634634+ ? siteUpdatingResponse()
635635+ : new Response('Site is updating', { status: 503, headers: { 'Cache-Control': 'no-store', 'Retry-After': '5' } });
636636+ }
637637+625638 const trace = createTrace();
626639627640 // Load settings for this site