| 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 | |
| 9 | import type { EventType } from "@g1t/contracts"; |
| 10 | |
| 11 | /** What happens to a project's code and work that counts as activity. */ |
| 12 | export 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. */ |
| 28 | export const HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000; |
| 29 | |
| 30 | /** The score `score` raised at `lastAt` is worth at `at` (RFC 3339 times). */ |
| 31 | export 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`. */ |
| 40 | export 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 | } |