Something went wrong. Try again.
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
Something went wrong. Try again.
5.9 kB · 172 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173import type { CustomDomainLookup, DomainLookup, SiteCache, SiteSettingsCache } from '@wispplace/database'import { type HostingDbReadRetryOutcome, metricsCollector } from '@wispplace/observability'import postgres from 'postgres'import { type CacheManager, cache } from './cache-manager'import { retryTransientRead } from './read-retry'
/** The tagged-template query the lookups issue; a `postgres` pool is one. */export type HostingSql = <T extends readonly (object | undefined)[]>( strings: TemplateStringsArray, ...parameters: readonly postgres.ParameterOrFragment<never>[]) => Promise<T>
export type HostingDbDeps = { sql: HostingSql cache: Pick<CacheManager<'domains' | 'customDomains' | 'settings' | 'siteCache'>, 'getOrFetch'> onReadRetry?: (outcome: HostingDbReadRetryOutcome) => void}
// Short TTL for negative / unmapped lookups so newly-mapped domains appear quickly.const NEGATIVE_TTL_MS = 10_000
/** The hosting service's read-only lookups over one query function and one lookup cache. */export function createHostingDb({ sql, cache, onReadRetry }: HostingDbDeps) { // Every query here is an idempotent read, and the pool can hand out a connection // that a proxy or the network already closed, so `read` is `sql` plus a short retry. const read = <T extends readonly (object | undefined)[]>( strings: TemplateStringsArray, ...parameters: readonly postgres.ParameterOrFragment<never>[] ): Promise<T> => retryTransientRead(() => sql<T>(strings, ...parameters), { onOutcome: onReadRetry })
async function getWispDomain(domain: string): Promise<DomainLookup | null> { const key = domain.toLowerCase() return cache.getOrFetch( 'domains', key, async () => { const result = await read<DomainLookup[]>` SELECT did, rkey FROM domains WHERE domain = ${key} LIMIT 1 ` return result[0] || null }, { ttl: (v) => (!v?.rkey ? NEGATIVE_TTL_MS : undefined) }, ) }
async function getCustomDomain(domain: string): Promise<CustomDomainLookup | null> { const key = domain.toLowerCase() return cache.getOrFetch( 'customDomains', key, async () => { const result = await read<CustomDomainLookup[]>` SELECT cd.id, cd.domain, cd.did, cd.rkey, cd.verified FROM custom_domains cd LEFT JOIN site_cache sc ON sc.did = cd.did AND sc.rkey = cd.rkey WHERE cd.domain = ${key} AND cd.verified = true ORDER BY (cd.rkey IS NOT NULL) DESC, (sc.did IS NOT NULL) DESC, cd.last_verified_at DESC NULLS LAST, cd.created_at DESC LIMIT 1 ` return result[0] || null }, { ttl: (v) => (!v?.rkey ? NEGATIVE_TTL_MS : undefined) }, ) }
async function getCustomDomainByHash(hash: string): Promise<CustomDomainLookup | null> { return cache.getOrFetch( 'customDomains', `hash:${hash}`, async () => { const result = await read<CustomDomainLookup[]>` SELECT id, domain, did, rkey, verified FROM custom_domains WHERE id = ${hash} AND verified = true LIMIT 1 ` return result[0] || null }, { ttl: (v) => (!v?.rkey ? NEGATIVE_TTL_MS : undefined) }, ) }
async function getSiteSettingsCache(did: string, rkey: string): Promise<SiteSettingsCache | null> { return cache.getOrFetch('settings', `${did}:${rkey}`, async () => { const result = await read<SiteSettingsCache[]>` SELECT did, rkey, record_cid, directory_listing, spa_mode, custom_404, index_files, clean_urls, headers, cached_at, updated_at FROM site_settings_cache WHERE did = ${did} AND rkey = ${rkey} LIMIT 1 ` return result[0] || null }) }
async function getSiteCache(did: string, rkey: string): Promise<SiteCache | null> { return cache.getOrFetch( 'siteCache', `${did}:${rkey}`, async () => { const result = await read<SiteCache[]>` SELECT did, rkey, record_cid, file_cids, file_objects, cached_at, updated_at, absent_since FROM site_cache WHERE did = ${did} AND rkey = ${rkey} LIMIT 1 ` return result[0] || null }, { cacheIf: (v) => v !== null }, ) }
return { getWispDomain, getCustomDomain, getCustomDomainByHash, getSiteSettingsCache, getSiteCache }}
export type HostingDb = ReturnType<typeof createHostingDb>
// The hosting service only reads. Prefer the read replica, fall back to the primary.const databaseUrl = process.env.DATABASE_READ_URL || process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/wisp'
const sql = postgres(databaseUrl, { max: 10, idle_timeout: 20,})
export const { getWispDomain, getCustomDomain, getCustomDomainByHash, getSiteSettingsCache, getSiteCache } = createHostingDb({ sql, cache, onReadRetry: metricsCollector.recordHostingDbReadRetry })
export interface ClosableDatabasePool { end(options?: { timeout?: number }): Promise<void>}
/** Close a pool at most once, however many times shutdown asks. */export function createDatabasePoolCloser( pool: ClosableDatabasePool, onError: () => void = () => {},): () => Promise<void> { let closePromise: Promise<void> | undefined
return (): Promise<void> => { if (closePromise) return closePromise
closePromise = Promise.resolve() .then(() => pool.end({ timeout: 5 })) .catch(() => onError()) return closePromise }}
const closeDatabasePool = createDatabasePoolCloser(sql, () => { console.error('[DB] Database pool failed to close cleanly')})
let databaseClosePromise: Promise<void> | undefined
/** * Close the database pool during graceful shutdown. * This function is idempotent. */export function closeDatabase(): Promise<void> { if (databaseClosePromise) return databaseClosePromise
databaseClosePromise = (async () => { await closeDatabasePool() console.log('[DB] Database connections closed') })() return databaseClosePromise}