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

Configure Feed

Select the types of activity you want to include in your feed.

1/** 2 * Firehose Service - Ingests AT Protocol firehose events and caches sites to S3 3 * 4 * Modes: 5 * - Normal: Watch firehose for place.wisp.fs events 6 * - Backfill: Process existing sites from database 7 */ 8 9import { Hono } from 'hono'; 10import { serve } from '@hono/node-server'; 11import { config } from './config'; 12import { startFirehose, stopFirehose, getFirehoseHealth } from './lib/firehose'; 13import { closeDatabase, listAllSiteCaches, listAllSites, getSiteCache } from './lib/db'; 14import { storage } from './lib/storage'; 15import { handleSiteCreateOrUpdate, fetchSiteRecord } from './lib/cache-writer'; 16import { startRevalidateWorker, stopRevalidateWorker } from './lib/revalidate-worker'; 17import { closeCacheInvalidationPublisher } from './lib/cache-invalidation'; 18 19const app = new Hono(); 20 21// Health endpoint 22app.get('/health', async (c) => { 23 const firehoseHealth = getFirehoseHealth(); 24 const storageStats = await storage.getStats(); 25 26 return c.json({ 27 status: firehoseHealth.healthy ? 'healthy' : 'degraded', 28 mode: config.isBackfill ? 'backfill' : 'firehose', 29 firehose: firehoseHealth, 30 storage: storageStats, 31 }); 32}); 33 34// Graceful shutdown 35let isShuttingDown = false; 36 37async function shutdown(signal: string) { 38 if (isShuttingDown) return; 39 isShuttingDown = true; 40 41 console.log(`\n[Service] Received ${signal}, shutting down...`); 42 43 stopFirehose(); 44 await stopRevalidateWorker(); 45 await closeCacheInvalidationPublisher(); 46 await closeDatabase(); 47 48 console.log('[Service] Shutdown complete'); 49 process.exit(0); 50} 51 52process.on('SIGINT', () => shutdown('SIGINT')); 53process.on('SIGTERM', () => shutdown('SIGTERM')); 54 55/** 56 * Backfill mode - process existing sites from database 57 */ 58async function runBackfill(): Promise<void> { 59 console.log('[Backfill] Starting backfill mode'); 60 const startTime = Date.now(); 61 const forceRewriteHtml = process.env.BACKFILL_FORCE_REWRITE_HTML === 'true'; 62 63 if (forceRewriteHtml) { 64 console.log('[Backfill] Forcing HTML rewrite for all sites'); 65 } 66 67 let sites = await listAllSites(); 68 if (sites.length === 0) { 69 const cachedSites = await listAllSiteCaches(); 70 sites = cachedSites.map(site => ({ did: site.did, rkey: site.rkey })); 71 console.log('[Backfill] Sites table empty; falling back to site_cache entries'); 72 } 73 74 console.log(`[Backfill] Found ${sites.length} sites in database`); 75 76 let processed = 0; 77 let skipped = 0; 78 let failed = 0; 79 80 for (const site of sites) { 81 try { 82 // Fetch current record from PDS 83 const result = await fetchSiteRecord(site.did, site.rkey); 84 85 if (!result) { 86 console.log(`[Backfill] Site not found on PDS: ${site.did}/${site.rkey}`); 87 skipped++; 88 continue; 89 } 90 91 const existingCache = await getSiteCache(site.did, site.rkey); 92 // Check if CID matches (already up to date) 93 if (!forceRewriteHtml && existingCache && result.cid === existingCache.record_cid) { 94 console.log(`[Backfill] Site already up to date: ${site.did}/${site.rkey}`); 95 skipped++; 96 continue; 97 } 98 99 // Process the site 100 await handleSiteCreateOrUpdate(site.did, site.rkey, result.record, result.cid, { 101 forceRewriteHtml, 102 }); 103 processed++; 104 105 console.log(`[Backfill] Progress: ${processed + skipped + failed}/${sites.length}`); 106 } catch (err) { 107 console.error(`[Backfill] Failed to process ${site.did}/${site.rkey}:`, err); 108 failed++; 109 } 110 } 111 112 const elapsedMs = Date.now() - startTime; 113 const elapsedSec = Math.round(elapsedMs / 1000); 114 const elapsedMin = Math.floor(elapsedSec / 60); 115 const elapsedRemSec = elapsedSec % 60; 116 const elapsedLabel = elapsedMin > 0 ? `${elapsedMin}m ${elapsedRemSec}s` : `${elapsedSec}s`; 117 118 console.log(`[Backfill] Complete: ${processed} processed, ${skipped} skipped, ${failed} failed (${elapsedLabel} elapsed)`); 119} 120 121// Main entry point 122async function main() { 123 console.log('[Service] Starting firehose-service'); 124 console.log(`[Service] Mode: ${config.isBackfill ? 'backfill' : 'firehose'}`); 125 console.log(`[Service] S3 Bucket: ${config.s3Bucket || '(disk fallback)'}`); 126 127 // Start health server 128 const server = serve({ 129 fetch: app.fetch, 130 port: config.healthPort, 131 }); 132 133 console.log(`[Service] Health endpoint: http://localhost:${config.healthPort}/health`); 134 135 if (config.isBackfill) { 136 // Run backfill and exit 137 await runBackfill(); 138 await closeDatabase(); 139 process.exit(0); 140 } else { 141 // Start firehose 142 startFirehose(); 143 await startRevalidateWorker(); 144 } 145} 146 147main().catch((err) => { 148 console.error('[Service] Fatal error:', err); 149 process.exit(1); 150});