import "server-only"; import { count, eq, sql } from "drizzle-orm"; import { CameraWeb, db, Rooms, User } from "@/lib/db"; /** * Row counters for the public homepage. * * These live in one place on purpose: the homepage and the boot warm-up both * populate the same cache keys, so two different implementations would race to * write different values into the same entry. */ /** Physical table names, needed because `information_schema` speaks in strings. */ const TABLE = { users: "users", rooms: "rooms", photos: "camera_web", } as const; /** * Ask the storage engine for its own row estimates. * * An exact `COUNT(*)` on InnoDB walks an index, so it gets slower as the table * grows. For a homepage counter that a visitor sees as a rounded "member since" * number, an approximate count is the right trade: it is a single indexed * lookup on `information_schema` instead of a scan of the whole table. * * InnoDB's estimate can lag behind reality and, right after a bulk write, can be * badly off. It is therefore never used as a reason to show zero: a missing * estimate falls back to the exact count, so the worst case is the old * behaviour. * * Returns null for any table the engine has no estimate for. */ export async function estimatedRowCounts( tables: string[], ): Promise> { const result: Record = {}; for (const table of tables) result[table] = null; if (tables.length === 0) return result; try { const names = tables.map((table) => sql`${table}`); const rows = await db.execute<{ TABLE_NAME: string; TABLE_ROWS: number | string | null; }>(sql` SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (${sql.join(names, sql`, `)}) `); for (const row of rows as unknown as { TABLE_NAME: string; TABLE_ROWS: number | string | null; }[]) { const value = Number(row.TABLE_ROWS); // TABLE_ROWS is a BIGINT that node-mysql hands back as a string or a // double; anything non-finite means "no estimate", not zero rows. if (row.TABLE_NAME in result && Number.isFinite(value) && value >= 0) result[row.TABLE_NAME] = Math.floor(value); } } catch { // No estimate available: every caller falls back to the exact count. } return result; } /** * Count rows in a table, preferring the cheap estimate. * * `estimate` names the table to ask the engine about, `exact` is the original * query, kept as the fallback. */ async function countTable( estimate: string, exact: () => Promise, ): Promise { const estimates = await estimatedRowCounts([estimate]); const value = estimates[estimate]; if (value !== null && value > 0) return value; return exact(); } export function countUsers(): Promise { return countTable(TABLE.users, async () => { const [row] = await db.select({ total: count() }).from(User); return row?.total ?? 0; }); } export function countRooms(): Promise { return countTable(TABLE.rooms, async () => { const [row] = await db.select({ total: count() }).from(Rooms); return row?.total ?? 0; }); } export function countPhotos(): Promise { return countTable(TABLE.photos, async () => { const [row] = await db.select({ total: count() }).from(CameraWeb); return row?.total ?? 0; }); } /** * Online users is deliberately *not* estimated: it is read from an index over a * small subset of rows, it is the one counter people watch closely, and being a * few seconds behind looks broken rather than approximate. */ export async function countOnline(): Promise { const [row] = await db .select({ total: count() }) .from(User) .where(eq(User.online, "1")); return row?.total ?? 0; }