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

Configure Feed

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

finish work in removing old sites table codepaths

+44 -108
+20 -27
apps/firehose-service/src/index.ts
··· 3 3 * 4 4 * Modes: 5 5 * - Normal: Watch firehose for place.wisp.fs events 6 - * - Backfill: Process existing sites from database 7 - * - DB Fill Only: Collect DIDs and backfill sites table (skip S3 writes) 6 + * - Backfill: Process existing sites discovered from known DIDs 7 + * - DB Fill Only: Legacy mode; no-op now that site_cache is the canonical projection 8 8 */ 9 9 10 10 import { serve } from '@hono/node-server' ··· 14 14 import { config } from './config' 15 15 import { closeCacheInvalidationPublisher } from './lib/cache-invalidation' 16 16 import { fetchSiteRecord, handleSiteCreateOrUpdate, listSiteRecordsForDid } from './lib/cache-writer' 17 - import { closeDatabase, getSiteCache, listAllKnownDids, listAllSiteCaches, listAllSites, upsertSite } from './lib/db' 17 + import { closeDatabase, getSiteCache, listAllKnownDids } from './lib/db' 18 18 import { getActiveService, getCurrentSeq, getFirehoseHealth, startFirehose, stopFirehose } from './lib/firehose' 19 19 import { closeLeaderRedis, getLeaderInfo, releaseLeadership, runLeaderElection, saveCursor } from './lib/leader' 20 20 import { startRevalidateWorker, stopRevalidateWorker } from './lib/revalidate-worker' ··· 80 80 /** 81 81 * Backfill phase 1+2: 82 82 * - Collect all known DIDs from DB 83 - * - Backfill each DID's place.wisp.fs records into the sites table 83 + * - Discover each DID's place.wisp.fs records directly from the PDS 84 84 */ 85 - async function backfillSitesTableFromKnownDids(): Promise<void> { 85 + async function collectSitesFromKnownDids(): Promise<Array<{ did: string; rkey: string }>> { 86 86 logger.info('Phase 1/3: Collecting known DIDs') 87 87 const dids = await listAllKnownDids() 88 88 logger.info(`Collected ${dids.length} known DIDs`) 89 89 90 90 if (dids.length === 0) { 91 - logger.warn('No known DIDs found; skipping sites table backfill') 92 - return 91 + logger.warn('No known DIDs found; skipping site discovery') 92 + return [] 93 93 } 94 94 95 - logger.info('Phase 2/3: Backfilling place.wisp.fs records into sites table') 95 + logger.info('Phase 2/3: Discovering place.wisp.fs records from known DIDs') 96 96 97 97 let didsProcessed = 0 98 98 let didsFailed = 0 99 - let sitesSynced = 0 99 + let sitesDiscovered = 0 100 100 let sitesFailed = 0 101 + const discoveredSites = new Map<string, { did: string; rkey: string }>() 101 102 102 103 const concurrency = config.backfillConcurrency 103 104 ··· 106 107 const records = await listSiteRecordsForDid(did) 107 108 for (const row of records) { 108 109 try { 109 - const siteName = 110 - typeof row.record.site === 'string' && row.record.site.length > 0 ? row.record.site : row.rkey 111 - await upsertSite(did, row.rkey, siteName) 112 - sitesSynced++ 110 + discoveredSites.set(`${did}:${row.rkey}`, { did, rkey: row.rkey }) 111 + sitesDiscovered++ 113 112 } catch (err) { 114 - logger.error(`[Backfill:sites] Failed to upsert site ${did}/${row.rkey}`, err) 113 + logger.error(`[Backfill:sites] Failed to register site ${did}/${row.rkey}`, err) 115 114 sitesFailed++ 116 115 } 117 116 } ··· 121 120 didsFailed++ 122 121 } 123 122 logger.info( 124 - `[Backfill:sites] Progress ${didsProcessed + didsFailed}/${dids.length} DIDs (${sitesSynced} sites synced, ${sitesFailed} sites failed)`, 123 + `[Backfill:sites] Progress ${didsProcessed + didsFailed}/${dids.length} DIDs (${sitesDiscovered} sites discovered, ${sitesFailed} sites failed)`, 125 124 ) 126 125 } 127 126 ··· 138 137 await Promise.all(inFlight) 139 138 140 139 logger.info( 141 - `Phase 2/3 complete: ${didsProcessed} DIDs processed, ${didsFailed} DIDs failed, ${sitesSynced} sites synced, ${sitesFailed} sites failed`, 140 + `Phase 2/3 complete: ${didsProcessed} DIDs processed, ${didsFailed} DIDs failed, ${discoveredSites.size} unique sites discovered, ${sitesFailed} sites failed`, 142 141 ) 142 + return [...discoveredSites.values()] 143 143 } 144 144 145 145 /** 146 146 * Backfill phase 3: 147 - * - process sites from database and backfill blobs into S3 147 + * - process discovered sites and backfill blobs into S3 148 148 */ 149 149 async function runBackfill(): Promise<void> { 150 150 logger.info('Starting backfill mode') ··· 159 159 logger.info('Forcing full file download/write for all backfilled sites') 160 160 } 161 161 162 - await backfillSitesTableFromKnownDids() 162 + const sites = await collectSitesFromKnownDids() 163 163 164 164 if (config.isDbFillOnly) { 165 - logger.info('DB fill only mode complete; skipping phase 3/3 S3 backfill') 165 + logger.info('DB fill only mode enabled; skipping phase 3/3 cache backfill') 166 166 return 167 167 } 168 168 169 169 logger.info('Phase 3/3: Backfilling site blobs into S3') 170 170 171 - let sites = await listAllSites() 172 - if (sites.length === 0) { 173 - const cachedSites = await listAllSiteCaches() 174 - sites = cachedSites.map((site) => ({ did: site.did, rkey: site.rkey })) 175 - logger.info('Sites table empty; falling back to site_cache entries') 176 - } 177 - 178 171 const concurrency = config.backfillConcurrency 179 - logger.info(`Found ${sites.length} sites in database (concurrency: ${concurrency})`) 172 + logger.info(`Found ${sites.length} sites to process (concurrency: ${concurrency})`) 180 173 181 174 let processed = 0 182 175 let skipped = 0
-4
apps/firehose-service/src/lib/cache-writer.ts
··· 16 16 import { safeFetchBlob, safeFetchJson } from '@wispplace/safe-fetch' 17 17 import { publishCacheInvalidation } from './cache-invalidation' 18 18 import { 19 - deleteSite, 20 19 deleteSiteCache, 21 20 deleteSiteSettingsCache, 22 21 getSiteCache, 23 22 isSupporter, 24 - upsertSite, 25 23 upsertSiteCache, 26 24 upsertSiteSettingsCache, 27 25 } from './db' ··· 826 824 // Update DB with new CIDs 827 825 logger.debug(`About to upsert site cache for ${did}/${rkey}`) 828 826 await upsertSiteCache(did, rkey, recordCid, newFileCids) 829 - await upsertSite(did, rkey, record.site) 830 827 logger.debug(`Updated site cache for ${did}/${rkey} with record CID ${recordCid}`) 831 828 832 829 // Backfill settings if a record exists for this rkey ··· 864 861 865 862 // Delete from DB 866 863 await deleteSiteCache(did, rkey) 867 - await deleteSite(did, rkey) 868 864 869 865 // Notify hosting-service to invalidate its local caches 870 866 await publishCacheInvalidation(did, rkey, 'delete')
+1 -32
apps/firehose-service/src/lib/db.ts
··· 1 - import type { SiteCache, SiteRecord, SiteSettingsCache } from '@wispplace/database' 1 + import type { SiteCache, SiteSettingsCache } from '@wispplace/database' 2 2 import { createLogger } from '@wispplace/observability' 3 3 import postgres from 'postgres' 4 4 import { config } from '../config' ··· 41 41 ` 42 42 } 43 43 44 - export async function listAllSites(): Promise<SiteRecord[]> { 45 - return await sql<SiteRecord[]>` 46 - SELECT did, rkey, display_name, created_at, updated_at 47 - FROM sites 48 - ORDER BY updated_at DESC 49 - ` 50 - } 51 - 52 44 /** 53 45 * List all known DIDs from all DID-bearing tables. 54 46 * Missing tables are skipped to keep bootstrapping resilient. 55 47 */ 56 48 export async function listAllKnownDids(): Promise<string[]> { 57 49 const sources: Array<{ name: string; fetch: () => Promise<Array<{ did: string }>> }> = [ 58 - { 59 - name: 'sites', 60 - fetch: () => sql<Array<{ did: string }>>` 61 - SELECT DISTINCT did 62 - FROM sites 63 - WHERE did IS NOT NULL AND did <> '' 64 - `, 65 - }, 66 50 { 67 51 name: 'site_cache', 68 52 fetch: () => sql<Array<{ did: string }>>` ··· 218 202 219 203 export async function deleteSiteSettingsCache(did: string, rkey: string): Promise<void> { 220 204 await sql`DELETE FROM site_settings_cache WHERE did = ${did} AND rkey = ${rkey}` 221 - } 222 - 223 - export async function upsertSite(did: string, rkey: string, displayName: string): Promise<void> { 224 - await sql` 225 - INSERT INTO sites (did, rkey, display_name, created_at, updated_at) 226 - VALUES (${did}, ${rkey}, ${displayName}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW())) 227 - ON CONFLICT (did, rkey) 228 - DO UPDATE SET 229 - display_name = EXCLUDED.display_name, 230 - updated_at = EXTRACT(EPOCH FROM NOW()) 231 - ` 232 - } 233 - 234 - export async function deleteSite(did: string, rkey: string): Promise<void> { 235 - await sql`DELETE FROM sites WHERE did = ${did} AND rkey = ${rkey}` 236 205 } 237 206 238 207 export async function isSupporter(did: string): Promise<boolean> {
-23
apps/hosting-service/src/lib/db.ts
··· 70 70 ) 71 71 } 72 72 73 - export async function upsertSite(did: string, rkey: string, displayName?: string) { 74 - console.log('[DB] Read-only mode: skipping upsertSite', { did, rkey, displayName }) 75 - } 76 - 77 73 /** 78 74 * Upsert site cache entry (used by on-demand caching when a site is completely missing) 79 75 */ ··· 102 98 const error = err instanceof Error ? err : new Error(String(err)) 103 99 console.error('[DB] upsertSiteCache error:', { did, rkey, error: error.message }) 104 100 throw error 105 - } 106 - } 107 - 108 - export interface SiteRecord { 109 - did: string 110 - rkey: string 111 - display_name?: string 112 - } 113 - 114 - export async function getAllSites(): Promise<SiteRecord[]> { 115 - try { 116 - const result = await sql<SiteRecord[]>` 117 - SELECT did, rkey, display_name FROM sites 118 - ORDER BY created_at DESC 119 - ` 120 - return result 121 - } catch (err) { 122 - console.error('Failed to get all sites', err) 123 - return [] 124 101 } 125 102 } 126 103
+1
apps/main-app/public/css.d.ts
··· 1 + declare module '*.css'
-12
apps/main-app/src/lib/db.ts
··· 81 81 ) 82 82 ` 83 83 84 - // Legacy sites table. Main-app now uses site_cache as the authoritative runtime projection. 85 - await db` 86 - CREATE TABLE IF NOT EXISTS sites ( 87 - did TEXT NOT NULL, 88 - rkey TEXT NOT NULL, 89 - display_name TEXT, 90 - created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()), 91 - updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()), 92 - PRIMARY KEY (did, rkey) 93 - ) 94 - ` 95 - 96 84 // Site cache table - stores CIDs for cached sites (used by firehose/hosting services) 97 85 await db` 98 86 CREATE TABLE IF NOT EXISTS site_cache (
-5
apps/main-app/src/lib/migrations.ts
··· 228 228 console.error('Failed to create idx_custom_domains_verified:', err) 229 229 } 230 230 }), 231 - db`CREATE INDEX IF NOT EXISTS idx_sites_did ON sites(did)`.catch((err) => { 232 - if (!hasAlreadyExists(err)) { 233 - console.error('Failed to create idx_sites_did:', err) 234 - } 235 - }), 236 231 db`CREATE INDEX IF NOT EXISTS idx_site_cache_did ON site_cache(did)`.catch((err) => { 237 232 if (!hasAlreadyExists(err)) { 238 233 console.error('Failed to create idx_site_cache_did:', err)
+1 -1
apps/webhook-service/src/lib/db.ts
··· 288 288 const dids = new Set<string>() 289 289 290 290 const sites = await db<Array<{ did: string }>>` 291 - SELECT DISTINCT did FROM sites WHERE did IS NOT NULL AND did <> '' 291 + SELECT DISTINCT did FROM site_cache WHERE did IS NOT NULL AND did <> '' 292 292 ` 293 293 for (const r of sites) dids.add(r.did) 294 294
+18 -1
bun.lock
··· 16 16 "@oven/bun-darwin-aarch64": "^1.3.6", 17 17 "@oven/bun-linux-x64": "^1.3.6", 18 18 "@types/bun": "^1.3.5", 19 + "@typescript/native-preview": "^7.0.0-dev.20260421.2", 19 20 }, 20 21 }, 21 22 "apps/firehose-service": { ··· 161 162 }, 162 163 "cli": { 163 164 "name": "wispctl", 164 - "version": "1.1.0", 165 + "version": "1.1.2", 165 166 "bin": { 166 167 "wispctl": "dist/index.js", 167 168 }, ··· 1135 1136 "@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="], 1136 1137 1137 1138 "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="], 1139 + 1140 + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260421.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260421.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260421.2" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CmajHI25HpVWE9R1XFoxr+cphJPxoYD3eFioQtAvXYkMFKnLdICMS9pXre9Pybizb75ejRxjKD5/CVG055rEIg=="], 1141 + 1142 + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260421.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fHv1r3ZmVo6zxuAIFmuX3w9QxbcauoG0SsWhmDwm6VmRubLlOJIcmTtlmV3JAb9oOnq8LuzZljzT7Q39fSMQDw=="], 1143 + 1144 + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260421.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWTR6xbW9t+JS7D5DQIzo75pqVXVWUxF9PMv/+S6xsnOjCVd6g0ixHcFpFMJMKSUQpGPr8Z5f7b8ks6LHW01jg=="], 1145 + 1146 + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BWLQO3nemLDSV5PoE5GPHe1dU9Dth77Kv8/cle9Ujcp4LhPo0KincdPqFH/qKeU/xvW25mgFueflZ1nc4rKuww=="], 1147 + 1148 + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLMEuml3BhUb+jaL0TXQ4xvVODxJF+RhkI+tBWvlynsJI4khTXEiwWh+wPOJrsfBRYFRMXEu28Odl/HXkYze8w=="], 1149 + 1150 + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "x64" }, "sha512-qUrJWTB5/wv4wnRG0TRXElAxc2kykNiRNyEIEqBbLmzDlrcvAW7RRy8MXoY1ZyTiKGMu14itZ3x9oW6+blFpRw=="], 1151 + 1152 + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260421.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rc6NsWlZmCs5YUKVzKgwoBOoRUGsPzct4BDMRX0csD1devLBBc4AbUXWKsJRbpwIAnqMO1ld4sNHEb+wXgfNHQ=="], 1153 + 1154 + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260421.2", "", { "os": "win32", "cpu": "x64" }, "sha512-GQv1+dya1t6EqF2Cpsb+xoozovdX10JUSf6Kl/8xNkTapzmlHd+uMr+8ku3jIASTxoRGn0Mklgjj3MDKrOTuLg=="], 1138 1155 1139 1156 "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], 1140 1157
+2 -1
package.json
··· 44 44 "@biomejs/biome": "^2.4.6", 45 45 "@oven/bun-darwin-aarch64": "^1.3.6", 46 46 "@oven/bun-linux-x64": "^1.3.6", 47 - "@types/bun": "^1.3.5" 47 + "@types/bun": "^1.3.5", 48 + "@typescript/native-preview": "^7.0.0-dev.20260421.2" 48 49 } 49 50 }
+1 -2
tsconfig.json
··· 103 103 "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 104 104 "allowImportingTsExtensions": true, 105 105 "noEmit": true, 106 - "baseUrl": ".", 107 106 "paths": { 108 107 "@server": ["./apps/main-app/src/index.ts"], 109 108 "@server/*": ["./apps/main-app/src/*"], ··· 111 110 "@wispplace/*": ["./packages/@wispplace/*/src"] 112 111 } 113 112 }, 114 - "exclude": ["docs", "node_modules"] 113 + "exclude": ["docs", "node_modules", "apps/hosting-service/cache", "**/dist", "**/node_modules"] 115 114 }