This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { db } from '../db/index.js';
|
||||
import type { MetricPoint, MetricsScope, PlayerMetrics } from '../../metrics.js';
|
||||
|
||||
/** Populate the shared periods from retained participation, independently of occupancy samples. */
|
||||
export function readPlayerMetrics(
|
||||
scope: MetricsScope,
|
||||
since: number,
|
||||
now: number,
|
||||
points: MetricPoint[]
|
||||
): PlayerMetrics {
|
||||
const handle = db();
|
||||
const nowIso = new Date(now).toISOString();
|
||||
const sinceIso = new Date(since).toISOString();
|
||||
const { first } = handle
|
||||
.prepare('SELECT MIN(started_at) AS first FROM leaderboard_matches WHERE started_at <= ?')
|
||||
.get(nowIso) as { first: string | null };
|
||||
const historyStartAt = first === null ? null : Date.parse(first);
|
||||
const periods = points.map((point) => {
|
||||
const available =
|
||||
historyStartAt !== null &&
|
||||
(historyStartAt < point.endAt || (historyStartAt === now && point.endAt === now));
|
||||
point.uniquePlayers = available ? 0 : null;
|
||||
point.newPlayers = available ? 0 : null;
|
||||
point.playerHistoryPartial = available && historyStartAt! > point.startAt;
|
||||
return { unique: new Set<string>(), fresh: new Set<string>() };
|
||||
});
|
||||
if (historyStartAt === null) return { historyStartAt, uniquePlayers: null, newPlayers: null };
|
||||
|
||||
const conditions = [
|
||||
'm.last_received_at >= ?',
|
||||
'm.started_at <= ?',
|
||||
'p.last_seen_at >= ?',
|
||||
'p.first_seen_at <= ?'
|
||||
];
|
||||
const params = [sinceIso, nowIso, sinceIso, nowIso];
|
||||
if (scope.game !== null) {
|
||||
conditions.push('m.game = ?');
|
||||
params.push(scope.game);
|
||||
}
|
||||
if (scope.map !== null) {
|
||||
conditions.push('m.map_name = ?');
|
||||
params.push(scope.map);
|
||||
}
|
||||
// The match clock and participation PK bound this scan to recent matches.
|
||||
// The GUID/first-seen index looks up the durable cross-game identity without
|
||||
// reclassifying a returning player after their older matches expire.
|
||||
const rows = handle
|
||||
.prepare(
|
||||
`SELECT p.guid, p.first_seen_at AS start, p.last_seen_at AS end,
|
||||
(SELECT MIN(first_seen_at) FROM leaderboard_players WHERE guid=p.guid) AS firstSeen
|
||||
FROM leaderboard_matches m JOIN leaderboard_match_players p ON p.match_id=m.id
|
||||
WHERE ${conditions.join(' AND ')}`
|
||||
)
|
||||
.iterate(...params) as Iterable<{
|
||||
guid: string;
|
||||
start: string;
|
||||
end: string;
|
||||
firstSeen: string | null;
|
||||
}>;
|
||||
const unique = new Set<string>();
|
||||
const fresh = new Set<string>();
|
||||
for (const row of rows) {
|
||||
const start = Date.parse(row.start);
|
||||
const end = Date.parse(row.end);
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) continue;
|
||||
unique.add(row.guid);
|
||||
// Scope the original sighting, never the first later visit to this game/map.
|
||||
const isNew =
|
||||
row.firstSeen !== null && Date.parse(row.firstSeen) === start && start >= since;
|
||||
if (isNew) fresh.add(row.guid);
|
||||
for (let index = 0; index < points.length; index++) {
|
||||
const point = points[index];
|
||||
// Internal boundaries belong to the next period. Include a sighting
|
||||
// exactly at the report's inclusive right edge in its final period.
|
||||
const startsBeforeEnd = start < point.endAt || (start === now && point.endAt === now);
|
||||
if (point.uniquePlayers === null || end < point.startAt || !startsBeforeEnd) continue;
|
||||
periods[index].unique.add(row.guid);
|
||||
if (isNew && start >= point.startAt) periods[index].fresh.add(row.guid);
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < points.length; index++) {
|
||||
if (points[index].uniquePlayers === null) continue;
|
||||
points[index].uniquePlayers = periods[index].unique.size;
|
||||
points[index].newPlayers = periods[index].fresh.size;
|
||||
}
|
||||
return { historyStartAt, uniquePlayers: unique.size, newPlayers: fresh.size };
|
||||
}
|
||||
Reference in New Issue
Block a user