Skip to content

g1t/services/projects/src/activity.ts

43 lines1,682 bytesCodeBlame
1/**
2 * How active a project is lately, for sorting a workspace's projects: each
3 * push, issue or pull request opened or closed, review, comment and
4 * deployment adds one, and what came before counts for half as much every
5 * week. Kept as a score and the time it was last raised, so raising it is
6 * one row's update. Pure, so it is tested apart from the service.
7 */
8
9import type { EventType } from "@g1t/contracts";
10
11/** What happens to a project's code and work that counts as activity. */
12export const ACTIVE: ReadonlySet<EventType> = new Set<EventType>([
13 "git.push",
14 "issue.opened",
15 "issue.closed",
16 "issue.reopened",
17 "pull.opened",
18 "pull.updated",
19 "pull.merged",
20 "pull.closed",
21 "review.completed",
22 "comment.created",
23 "deployment.succeeded",
24 "deployment.failed",
25]);
26
27/** How long until activity counts for half. */
28export const HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;
29
30/** The score `score` raised at `lastAt` is worth at `at` (RFC 3339 times). */
31export function decayed(score: number, lastAt: string | null, at: string): number {
32 if (!lastAt || !(score > 0)) return 0;
33 const elapsed = Date.parse(at) - Date.parse(lastAt);
34 if (!Number.isFinite(elapsed)) return 0;
35 // Events delivered out of order count in full: never worth more than they were.
36 return score * Math.pow(0.5, Math.max(0, elapsed) / HALF_LIFE_MS);
37}
38
39/** The score and its time after one more piece of activity at `at`. */
40export function raised(score: number, lastAt: string | null, at: string): { score: number; at: string } {
41 const later = !lastAt || Date.parse(at) >= Date.parse(lastAt) ? at : lastAt;
42 return { score: decayed(score, lastAt, later) + 1, at: later };
43}