Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { getAllSites } from './db'; 2import { fetchSiteRecord, getPdsForDid, downloadAndCacheSite, isCached } from './utils'; 3import { logger } from './observability'; 4import { markSiteAsBeingCached, unmarkSiteAsBeingCached } from './cache'; 5 6export interface BackfillOptions { 7 skipExisting?: boolean; // Skip sites already in cache 8 concurrency?: number; // Number of sites to cache concurrently 9 maxSites?: number; // Maximum number of sites to backfill (for testing) 10} 11 12export interface BackfillStats { 13 total: number; 14 cached: number; 15 skipped: number; 16 failed: number; 17 duration: number; 18} 19 20/** 21 * Backfill all sites from the database into the local cache 22 */ 23export async function backfillCache(options: BackfillOptions = {}): Promise<BackfillStats> { 24 const { 25 skipExisting = true, 26 concurrency = 3, 27 maxSites, 28 } = options; 29 30 const startTime = Date.now(); 31 const stats: BackfillStats = { 32 total: 0, 33 cached: 0, 34 skipped: 0, 35 failed: 0, 36 duration: 0, 37 }; 38 39 logger.info('Starting cache backfill', { skipExisting, concurrency, maxSites }); 40 console.log(` 41╔══════════════════════════════════════════╗ 42║ CACHE BACKFILL STARTING ║ 43╚══════════════════════════════════════════╝ 44 `); 45 46 try { 47 // Get all sites from database 48 let sites = await getAllSites(); 49 stats.total = sites.length; 50 51 logger.info(`Found ${sites.length} sites in database`); 52 console.log(`📊 Found ${sites.length} sites in database`); 53 54 // Limit if specified 55 if (maxSites && maxSites > 0) { 56 sites = sites.slice(0, maxSites); 57 console.log(`⚙️ Limited to ${maxSites} sites for backfill`); 58 } 59 60 // Process sites in batches 61 const batches: typeof sites[] = []; 62 for (let i = 0; i < sites.length; i += concurrency) { 63 batches.push(sites.slice(i, i + concurrency)); 64 } 65 66 let processed = 0; 67 for (const batch of batches) { 68 await Promise.all( 69 batch.map(async (site) => { 70 try { 71 // Check if already cached 72 if (skipExisting && isCached(site.did, site.rkey)) { 73 stats.skipped++; 74 processed++; 75 logger.debug(`Skipping already cached site`, { did: site.did, rkey: site.rkey }); 76 console.log(`⏭️ [${processed}/${sites.length}] Skipped (cached): ${site.display_name || site.rkey}`); 77 return; 78 } 79 80 // Fetch site record 81 const siteData = await fetchSiteRecord(site.did, site.rkey); 82 if (!siteData) { 83 stats.failed++; 84 processed++; 85 logger.error('Site record not found during backfill', null, { did: site.did, rkey: site.rkey }); 86 console.log(`❌ [${processed}/${sites.length}] Failed (not found): ${site.display_name || site.rkey}`); 87 return; 88 } 89 90 // Get PDS endpoint 91 const pdsEndpoint = await getPdsForDid(site.did); 92 if (!pdsEndpoint) { 93 stats.failed++; 94 processed++; 95 logger.error('PDS not found during backfill', null, { did: site.did }); 96 console.log(`❌ [${processed}/${sites.length}] Failed (no PDS): ${site.display_name || site.rkey}`); 97 return; 98 } 99 100 // Mark site as being cached to prevent serving stale content during update 101 markSiteAsBeingCached(site.did, site.rkey); 102 103 try { 104 // Download and cache site 105 await downloadAndCacheSite(site.did, site.rkey, siteData.record, pdsEndpoint, siteData.cid); 106 stats.cached++; 107 processed++; 108 logger.info('Successfully cached site during backfill', { did: site.did, rkey: site.rkey }); 109 console.log(`✅ [${processed}/${sites.length}] Cached: ${site.display_name || site.rkey}`); 110 } finally { 111 // Always unmark, even if caching fails 112 unmarkSiteAsBeingCached(site.did, site.rkey); 113 } 114 } catch (err) { 115 stats.failed++; 116 processed++; 117 logger.error('Failed to cache site during backfill', err, { did: site.did, rkey: site.rkey }); 118 console.log(`❌ [${processed}/${sites.length}] Failed: ${site.display_name || site.rkey}`); 119 } 120 }) 121 ); 122 } 123 124 stats.duration = Date.now() - startTime; 125 126 console.log(` 127╔══════════════════════════════════════════╗ 128║ CACHE BACKFILL COMPLETED ║ 129╚══════════════════════════════════════════╝ 130 131📊 Total Sites: ${stats.total} 132✅ Cached: ${stats.cached} 133⏭️ Skipped: ${stats.skipped} 134❌ Failed: ${stats.failed} 135⏱️ Duration: ${(stats.duration / 1000).toFixed(2)}s 136 `); 137 138 logger.info('Cache backfill completed', stats); 139 } catch (err) { 140 logger.error('Cache backfill failed', err); 141 console.error('❌ Cache backfill failed:', err); 142 } 143 144 return stats; 145}