import { serve } from '@hono/node-server'; import { announceLnurlMints } from './announce.ts'; import { config } from './config.ts'; import { closeDb, getDb, pruneProbes } from './db.ts'; import { closePool, runDiscovery } from './discovery.ts'; import { log } from './log.ts'; import { probeAll, probeUrls } from './probe.ts'; import { createApp } from './server.ts'; const DAY_MS = 24 * 60 * 60 * 1000; let shuttingDown = false; /** Set while a probe or discovery cycle is in flight, so shutdown can wait for it. */ let inFlight: Promise = Promise.resolve(); function track(work: Promise): Promise { inFlight = inFlight.then(() => work).catch(() => undefined); return work; } /* * One cycle of each kind at a time. A cycle that outlives its interval (a relay socket * that never closes, a wedged fetch) must not have the next timer tick start a second * copy beside it — that doubles the relay load and interleaves state writes for no * new information. The late cycle finishes; the ticks it swallowed are simply skipped. */ let probeRunning = false; let discoveryRunning = false; async function probeCycle(): Promise { if (shuttingDown || probeRunning) return; probeRunning = true; try { await track(probeAll()); /* * Announcing runs after probing, in the same cycle, because it publishes only what * the probe just confirmed — running it on its own timer would mean signing for a * state that could be an interval old. Off by default; see `announceConfig`. * * Its own failures are caught here rather than allowed to mark the probe cycle * failed: whether relays accepted an event says nothing about whether this site * successfully checked its mints. */ await track(announceLnurlMints()).catch((err: unknown) => { log.error('announce cycle failed', { reason: err instanceof Error ? err.message : String(err), }); }); } catch (err) { log.error('probe cycle failed', { reason: err instanceof Error ? err.message : String(err) }); } finally { probeRunning = false; } } async function discoveryCycle(backfill: boolean): Promise { if (shuttingDown || discoveryRunning) return; discoveryRunning = true; try { const result = await track(runDiscovery(backfill)); // New mints are probed straight away, not on the next cycle, so their page has // real metadata as soon as it is reachable. if (result.newMints.length > 0) await track(probeUrls(result.newMints)); } catch (err) { log.error('discovery cycle failed', { reason: err instanceof Error ? err.message : String(err), }); } finally { discoveryRunning = false; } } async function main(): Promise { // Fail loudly here rather than on the first request: a bad DATABASE_URL, an // unreachable Postgres or an unwritable SQLite directory should stop the boot. await getDb(); const server = serve({ fetch: createApp().fetch, port: config.port }, (info) => { log.info('api listening', { port: info.port, db: config.db.label, relays: config.relays.length, }); }); const probeTimer = setInterval(() => void probeCycle(), config.probeIntervalMin * 60 * 1000); const discoveryTimer = setInterval( () => void discoveryCycle(false), config.discoveryIntervalMin * 60 * 1000, ); const pruneTimer = setInterval(() => { void pruneProbes() .then((removed) => { if (removed > 0) log.info('pruned probes', { rows: removed }); }) .catch((err: unknown) => { log.error('prune failed', { reason: err instanceof Error ? err.message : String(err) }); }); }, DAY_MS); // Startup: probe what we already know, then a full discovery backfill so a fresh // database converges to complete review counts (BACKEND.md). void probeCycle().then(() => discoveryCycle(true)); const shutdown = (signal: string) => { if (shuttingDown) return; shuttingDown = true; log.info('shutting down', { signal }); clearInterval(probeTimer); clearInterval(discoveryTimer); clearInterval(pruneTimer); const stop = (): void => { // closeDb drains the Postgres pool, so it is awaited before the process leaves. void closeDb().finally(() => process.exit(0)); }; void inFlight.finally(() => { closePool(); server.close(stop); // Do not hang forever on a relay socket that refuses to close. setTimeout(stop, 8000).unref(); }); }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); } void main().catch((err: unknown) => { log.error('startup failed', { reason: err instanceof Error ? err.message : String(err) }); process.exit(1); });