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

fix cache invalidation race, storage miss re-fetch, trailing slash redirect

+31 -6
+5 -1
apps/firehose-service/src/lib/cache-writer.ts
··· 439 439 recordCid: string, 440 440 options?: { 441 441 forceRewriteHtml?: boolean; 442 + skipInvalidation?: boolean; 442 443 } 443 444 ): Promise<void> { 444 445 const forceRewriteHtml = options?.forceRewriteHtml === true; ··· 551 552 } 552 553 553 554 // Notify hosting-service to invalidate its local caches 554 - await publishCacheInvalidation(did, rkey, 'update'); 555 + // (skip for revalidate/backfill since hosting-service already has the files locally) 556 + if (!options?.skipInvalidation) { 557 + await publishCacheInvalidation(did, rkey, 'update'); 558 + } 555 559 556 560 console.log(`[Cache] Successfully cached site ${did}/${rkey}`); 557 561 }
+3 -1
apps/firehose-service/src/lib/revalidate-worker.ts
··· 47 47 return; 48 48 } 49 49 50 - await handleSiteCreateOrUpdate(did, rkey, record.record, record.cid); 50 + await handleSiteCreateOrUpdate(did, rkey, record.record, record.cid, { 51 + skipInvalidation: true, 52 + }); 51 53 52 54 console.log(`[Revalidate] Completed ${id}: ${did}/${rkey}`); 53 55 await redis.xack(config.revalidateStream, config.revalidateGroup, id);
+18 -4
apps/hosting-service/src/lib/file-serving.ts
··· 159 159 async function ensureSiteCached(did: string, rkey: string): Promise<void> { 160 160 const existing = await getSiteCache(did, rkey); 161 161 if (existing) { 162 - console.log(`[FileServing] Site ${did}/${rkey} found in DB, proceeding normally`); 163 - return; 162 + // Site is in DB — check if any files actually exist in storage 163 + const prefix = `${did}/${rkey}/`; 164 + const hasFiles = await storage.exists(prefix.slice(0, -1)) || 165 + await checkAnyFileExists(did, rkey, existing.file_cids); 166 + if (hasFiles) { 167 + return; 168 + } 169 + console.log(`[FileServing] Site ${did}/${rkey} in DB but no files in storage, re-fetching`); 170 + } else { 171 + console.log(`[FileServing] Site ${did}/${rkey} not in DB, attempting on-demand cache`); 164 172 } 165 173 166 - // Site is completely unknown — try on-demand fetch 167 - console.log(`[FileServing] Site ${did}/${rkey} not in DB, attempting on-demand cache`); 168 174 const success = await fetchAndCacheSite(did, rkey); 169 175 console.log(`[FileServing] On-demand cache for ${did}/${rkey}: ${success ? 'success' : 'failed'}`); 176 + } 177 + 178 + async function checkAnyFileExists(did: string, rkey: string, fileCids: unknown): Promise<boolean> { 179 + if (!fileCids || typeof fileCids !== 'object') return false; 180 + const cids = fileCids as Record<string, string>; 181 + const firstFile = Object.keys(cids)[0]; 182 + if (!firstFile) return false; 183 + return storage.exists(`${did}/${rkey}/${firstFile}`); 170 184 } 171 185 172 186 /**
+5
apps/hosting-service/src/server.ts
··· 81 81 return c.text('Invalid identifier', 400); 82 82 } 83 83 84 + // Redirect to trailing slash when accessing site root so relative paths resolve correctly 85 + if (!filePath && !url.pathname.endsWith('/')) { 86 + return c.redirect(`${url.pathname}/${url.search}`, 301); 87 + } 88 + 84 89 console.log(`[Server] sites.wisp.place request: identifier=${identifier}, site=${site}, filePath=${filePath}`); 85 90 86 91 // Serve with HTML path rewriting to handle absolute paths