/** * Gathers what the whole site did over a period, for the `/community` page * and the digests' shared sections. The digests ask {@link communityStatsOf} * for each reader's own period. The page reads the figures of the week and * the month ending at the last refresh, which {@link communityJob} redoes * every {@link COMMUNITY_REFRESH_MINUTES}, so a visit runs no query. * * @module */ import { db } from "../db/connection.ts"; import { COMMUNITY_REFRESH_MINUTES } from "../config.ts"; import type { CommunityStats } from "../../shared/community.ts"; import { DIGEST_CADENCES, type DigestCadence, PERIOD_MS, } from "../../shared/settings.ts"; import type { Job } from "../lib/scheduler.ts"; import { listTopDumps } from "./dump-service.ts"; import { listNewPublicPlaylists } from "./playlist-service.ts"; import { listNewUsers } from "./user-service.ts"; /** How many top dumps a period keeps, as many as the monthly recap shows. */ const TOP_MAX = 10; /** Returns how many rows of `table` were created from `from` to `to`. */ function countOf( table: string, from: string, to: string, where = "1", ): number { return (db.prepare( `SELECT COUNT(*) AS n FROM ${table} WHERE ${where} AND created_at >= ? AND created_at < ?;`, ).get(from, to) as { n: number }).n; } /** * Returns what the site did from `from` to `to`: its most upvoted public * dumps, who joined, the new public playlists, the chat's messages and the * totals. */ export function communityStatsOf(from: Date, to: Date): CommunityStats { const [f, t] = [from.toISOString(), to.toISOString()]; return { from, to, top: listTopDumps(from, to, TOP_MAX), members: listNewUsers(from, to), playlists: listNewPublicPlaylists(from, to), chatMessages: countOf("chat_messages", f, t), totals: { dumps: countOf("dumps", f, t, "is_private = 0"), upvotes: countOf("votes", f, t), comments: countOf("comments", f, t, "deleted = 0"), }, }; } /** The figures {@link getCommunityStats} serves, by period. */ const latest = new Map(); /** Recomputes the figures of each period, ending at `now`. */ export function refreshCommunityStats(now: Date = new Date()): void { for (const period of DIGEST_CADENCES) { latest.set( period, communityStatsOf(new Date(now.getTime() - PERIOD_MS[period]), now), ); } } /** * Returns the figures of `period` as of the last refresh, for * `GET /api/community`, refreshing them first if there are none yet. */ export function getCommunityStats(period: DigestCadence): CommunityStats { if (!latest.has(period)) refreshCommunityStats(); return latest.get(period)!; } /** * Refreshes the figures at startup, then every * {@link COMMUNITY_REFRESH_MINUTES}. `main.ts` starts it. */ export const communityJob: Job = { name: "community", everyMs: COMMUNITY_REFRESH_MINUTES * 60_000, firstAfterMs: 0, run: () => refreshCommunityStats(), };