AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

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

fix: reduce memory footprint to prevent OOM on constrained containers

- DuckDB threads 2→1 (saves ~125MB native), memory_limit 512→256MB
- FTS rebuild interval 500→5000 (reduces frequency of expensive shadow table materialization)
- CHECKPOINT after FTS rebuild to compact WAL and free DuckDB memory
- Startup phase memory logging to diagnose where memory is consumed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+20 -3
+1 -1
packages/hatk/src/config.ts
··· 82 82 fetchTimeout: parseInt(env.BACKFILL_FETCH_TIMEOUT || '') || backfillRaw.fetchTimeout || 300, 83 83 maxRetries: parseInt(env.BACKFILL_MAX_RETRIES || '') || backfillRaw.maxRetries || 5, 84 84 }, 85 - ftsRebuildInterval: parseInt(env.FTS_REBUILD_INTERVAL || '') || parsed.ftsRebuildInterval || 500, 85 + ftsRebuildInterval: parseInt(env.FTS_REBUILD_INTERVAL || '') || parsed.ftsRebuildInterval || 5000, 86 86 oauth: null, 87 87 admins: env.ADMINS ? env.ADMINS.split(',').map((s) => s.trim()) : parsed.admins || [], 88 88 }
+2 -2
packages/hatk/src/db.ts
··· 124 124 ddlStatements: string[], 125 125 ): Promise<void> { 126 126 instance = await DuckDBInstance.create(dbPath === ':memory:' ? undefined : dbPath, { 127 - memory_limit: '512MB', 128 - threads: '2', 127 + memory_limit: '256MB', 128 + threads: '1', 129 129 }) 130 130 con = await instance.connect() 131 131 readCon = await instance.connect()
+5
packages/hatk/src/fts.ts
··· 785 785 } 786 786 } 787 787 788 + // Compact WAL to free DuckDB memory after heavy FTS operations 789 + try { 790 + await runSQL('CHECKPOINT') 791 + } catch {} 792 + 788 793 emit('fts', 'rebuild', { 789 794 collections_total: collections.length, 790 795 collections_rebuilt: rebuilt,
+12
packages/hatk/src/main.ts
··· 26 26 import { loadOnLoginHook } from './oauth/hooks.ts' 27 27 import { initSetup } from './setup.ts' 28 28 29 + function logMemory(phase: string): void { 30 + const mem = process.memoryUsage() 31 + log(`[mem] ${phase}: heap=${Math.round(mem.heapUsed / 1024 / 1024)}MB rss=${Math.round(mem.rss / 1024 / 1024)}MB external=${Math.round(mem.external / 1024 / 1024)}MB arrayBuffers=${Math.round(mem.arrayBuffers / 1024 / 1024)}MB`) 32 + } 33 + 29 34 const configPath = process.argv[2] || 'config.yaml' 30 35 const configDir = dirname(resolve(configPath)) 31 36 37 + logMemory('startup') 38 + 32 39 // 1. Load config 33 40 const config = loadConfig(configPath) 34 41 configureRelay(config.relay) ··· 91 98 mkdirSync(dirname(config.database), { recursive: true }) 92 99 } 93 100 await initDatabase(config.database, schemas, ddlStatements) 101 + logMemory('after-db-init') 94 102 log(`[main] DuckDB initialized (${config.database === ':memory:' ? 'in-memory' : config.database})`) 95 103 96 104 ··· 137 145 log(`[main] OAuth initialized (issuer: ${config.oauth.issuer})`) 138 146 } 139 147 148 + logMemory('before-server') 149 + 140 150 // 5. Start server immediately (don't wait for backfill) 141 151 const collectionSet = new Set(collections) 142 152 startServer(config.port, collections, config.publicDir, config.oauth, config.admins) ··· 151 161 .map((f) => f.name) 152 162 .join(', ')}`, 153 163 ) 164 + 165 + logMemory('after-server') 154 166 155 167 // 6. Start indexer with cursor 156 168 const cursor = await getCursor('relay')