94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
/**
|
|
* 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<DigestCadence, CommunityStats>();
|
|
|
|
/** 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(),
|
|
};
|