// SPDX-License-Identifier: AGPL-3.0-or-later export interface ChannelFrecencyEntry { readonly hitCount: number; readonly recentHits: ReadonlyArray; readonly heat: number; readonly score: number; } export type ChannelFrecencyHistory = Map; interface RankedChannelFrecency { readonly id: string; readonly heat: number; } const CHANNEL_FRECENCY_MAX_ITEMS = 100; const CHANNEL_FRECENCY_MAX_SAMPLES = 10; const CHANNEL_FRECENCY_UNCOMPUTED = -1; const DAY_MS = 86_400_000; const MINUTE_MS = 60_000; function createEntry( hitCount: number, recentHits: ReadonlyArray, heat: number, score: number, ): ChannelFrecencyEntry { return Object.freeze({hitCount, recentHits: Object.freeze(recentHits), heat, score}); } function utcOffsetMinutes(timestamp: number): number { return -new Date(timestamp).getTimezoneOffset(); } function compareAscending(a: number, b: number): number { return a - b; } export function channelFrecencyDayDiff(now: number, then: number): number { const zoneDeltaMs = (utcOffsetMinutes(then) - utcOffsetMinutes(now)) * MINUTE_MS; return Math.trunc((now - then - zoneDeltaMs) / DAY_MS) || 0; } export function channelFrecencyWeight(dayDiff: number): number { if (dayDiff === 0) return 100; if (dayDiff >= 1 && dayDiff < 2) return 70; if (dayDiff >= 2 && dayDiff < 4) return 50; if (dayDiff >= 4 && dayDiff < 7) return 30; if (dayDiff >= 7) return 10; return 1; } function scoreRecentUses(recentHits: ReadonlyArray, now: number): number { const sampleCount = Math.min(recentHits.length, CHANNEL_FRECENCY_MAX_SAMPLES); let score = 0; for (let index = 0; index < sampleCount; index++) { score += channelFrecencyWeight(channelFrecencyDayDiff(now, recentHits[index])); } return score; } export function trackChannelUse(history: ChannelFrecencyHistory, key: string, timestamp?: number): void { const use = timestamp ?? Date.now(); const entry = history.get(key); if (entry === undefined) { history.set(key, createEntry(1, [use], CHANNEL_FRECENCY_UNCOMPUTED, 0)); return; } const recentHits = [...entry.recentHits, use]; if (timestamp !== undefined) { recentHits.sort(compareAscending); } history.set( key, createEntry( entry.hitCount + 1, recentHits.slice(-CHANNEL_FRECENCY_MAX_SAMPLES), CHANNEL_FRECENCY_UNCOMPUTED, entry.score, ), ); } export function computeChannelFrecency(history: ChannelFrecencyHistory, now: number): void { for (const [key, entry] of history) { if (entry.heat !== CHANNEL_FRECENCY_UNCOMPUTED) continue; const score = scoreRecentUses(entry.recentHits, now); if (score > 0) { const heat = Math.ceil(entry.hitCount * (score / entry.recentHits.length)); history.set(key, createEntry(entry.hitCount, entry.recentHits, heat, score)); } else { history.delete(key); } } } function findOldestLastUseKey(history: ReadonlyMap): string | null { let oldestKey: string | null = null; let oldestLastUse = Number.POSITIVE_INFINITY; for (const [key, entry] of history) { const lastUse = entry.recentHits.at(-1); if (lastUse !== undefined && lastUse < oldestLastUse) { oldestKey = key; oldestLastUse = lastUse; } } return oldestKey; } export function capChannelFrecencyHistory(history: ChannelFrecencyHistory): void { while (history.size > CHANNEL_FRECENCY_MAX_ITEMS) { const oldestKey = findOldestLastUseKey(history); if (oldestKey === null) return; history.delete(oldestKey); } } export function rankFrequentChannelIds( history: ReadonlyMap, resolveRecordId: (key: string) => string | null, ): ReadonlyArray { const ranked: Array = []; for (const [key, entry] of history) { const id = resolveRecordId(key); if (id !== null) { ranked.push({id, heat: entry.heat}); } } ranked.sort((a, b) => b.heat - a.heat); return Object.freeze(ranked.slice(0, CHANNEL_FRECENCY_MAX_ITEMS).map((item) => item.id)); } interface ChannelFrecencyWireEntry { readonly totalUses: number; readonly recentUsesMs: ReadonlyArray; } export type ChannelFrecencyWireUsage = Record}>; export type ChannelFrecencyWireUsageInput = Readonly>; function entryToWire(entry: ChannelFrecencyEntry): {totalUses: number; recentUsesMs: Array} { return {totalUses: entry.hitCount, recentUsesMs: entry.recentHits.map((use) => BigInt(use))}; } export function channelFrecencyHistoryToWire( history: ReadonlyMap, ): ChannelFrecencyWireUsage { const usage: ChannelFrecencyWireUsage = {}; for (const [key, entry] of history) { usage[key] = entryToWire(entry); } return usage; } function readWireUses(entry: ChannelFrecencyWireEntry | undefined): Array { if (entry === undefined) return []; return entry.recentUsesMs.map((use) => Number(use)).filter(isUseTimestamp); } export function channelFrecencyHistoryFromWire( usage: ChannelFrecencyWireUsageInput, now: number, ): ChannelFrecencyHistory { const revived: Array = []; for (const [key, entry] of Object.entries(usage)) { revived.push([key, {hitCount: entry?.totalUses, recentHits: readWireUses(entry)}]); } return restoreChannelFrecencyHistory(revived, now); } export function mergeChannelFrecencyWireUsage( local: ChannelFrecencyWireUsageInput, incoming: ChannelFrecencyWireUsageInput, now: number, ): ChannelFrecencyWireUsage { const merged: ChannelFrecencyHistory = new Map(); const keys = new Set([...Object.keys(local), ...Object.keys(incoming)]); for (const key of keys) { const localEntry = local[key]; const incomingEntry = incoming[key]; const uses = [...new Set([...readWireUses(localEntry), ...readWireUses(incomingEntry)])]; if (uses.length === 0) continue; uses.sort(compareAscending); const totalUses = Math.max(localEntry?.totalUses ?? 0, incomingEntry?.totalUses ?? 0); if (!Number.isFinite(totalUses) || totalUses <= 0) continue; merged.set(key, createEntry(totalUses, uses.slice(-CHANNEL_FRECENCY_MAX_SAMPLES), CHANNEL_FRECENCY_UNCOMPUTED, 0)); } computeChannelFrecency(merged, now); capChannelFrecencyHistory(merged); return channelFrecencyHistoryToWire(merged); } function isUseTimestamp(value: unknown): value is number { return typeof value === 'number' && Number.isFinite(value) && value > 0; } function readPersistedEntry(value: unknown): ChannelFrecencyEntry | null { if (typeof value !== 'object' || value === null) return null; const {hitCount, recentHits} = value as {readonly hitCount?: unknown; readonly recentHits?: unknown}; if (typeof hitCount !== 'number' || !Number.isFinite(hitCount) || !Array.isArray(recentHits)) return null; return createEntry(hitCount, recentHits.filter(isUseTimestamp), CHANNEL_FRECENCY_UNCOMPUTED, 0); } export function restoreChannelFrecencyHistory( revived: Iterable, now: number, ): ChannelFrecencyHistory { const history: ChannelFrecencyHistory = new Map(); for (const [key, value] of revived) { if (typeof key !== 'string') continue; const entry = readPersistedEntry(value); if (entry !== null) { history.set(key, entry); } } computeChannelFrecency(history, now); return history; }