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