| 1 | /** |
| 2 | * The one place commits get their people (lib/commit-people.ts): every |
| 3 | * page that shows a commit's author asks here, once for all its commits. |
| 4 | * One identity call per page for the addresses not already known, and |
| 5 | * each answer kept a minute in the isolate, so a page of fifty commits by |
| 6 | * three people asks about three addresses, and the next page asks about |
| 7 | * none. A failure matches nobody: the names on the commits are shown. |
| 8 | */ |
| 9 | import type { Commit, EmailOwner } from "@g1t/contracts"; |
| 10 | |
| 11 | import { type ShownCommit, addressesToMatch, showCommit } from "./commit-people"; |
| 12 | import { accounts } from "./services.server"; |
| 13 | |
| 14 | /** How long an answer is kept: an address can be confirmed or removed. */ |
| 15 | const KEEP_MS = 60_000; |
| 16 | /** Addresses kept at most; the oldest go first. */ |
| 17 | const MAX_KEPT = 5_000; |
| 18 | /** `email_owners` takes at most this many at once. */ |
| 19 | const BATCH = 200; |
| 20 | |
| 21 | /** Each address's owner, or null for nobody, until `until`. */ |
| 22 | const kept = new Map<string, { until: number; owner: EmailOwner | null }>(); |
| 23 | |
| 24 | function keep(email: string, owner: EmailOwner | null) { |
| 25 | kept.delete(email); |
| 26 | kept.set(email, { until: Date.now() + KEEP_MS, owner }); |
| 27 | if (kept.size > MAX_KEPT) kept.delete(kept.keys().next().value as string); |
| 28 | } |
| 29 | |
| 30 | /** The accounts behind these (lowercased) addresses; unknown ones are left out. */ |
| 31 | export async function emailOwners(emails: string[]): Promise<Record<string, EmailOwner>> { |
| 32 | const owners: Record<string, EmailOwner> = {}; |
| 33 | const ask: string[] = []; |
| 34 | const now = Date.now(); |
| 35 | for (const email of new Set(emails)) { |
| 36 | const hit = kept.get(email); |
| 37 | if (hit && hit.until > now) { |
| 38 | if (hit.owner) owners[email] = hit.owner; |
| 39 | } else { |
| 40 | ask.push(email); |
| 41 | } |
| 42 | } |
| 43 | for (let start = 0; start < ask.length; start += BATCH) { |
| 44 | const chunk = ask.slice(start, start + BATCH); |
| 45 | const found = await accounts.emailOwners(chunk).catch(() => null); |
| 46 | if (!found) continue; |
| 47 | for (const email of chunk) { |
| 48 | const owner = found[email] ?? null; |
| 49 | keep(email, owner); |
| 50 | if (owner) owners[email] = owner; |
| 51 | } |
| 52 | } |
| 53 | return owners; |
| 54 | } |
| 55 | |
| 56 | /** Commits with their people, in one identity call at most. */ |
| 57 | export async function showCommits(commits: Commit[]): Promise<ShownCommit[]> { |
| 58 | if (commits.length === 0) return []; |
| 59 | const owners = await emailOwners(addressesToMatch(commits)); |
| 60 | return commits.map((commit) => showCommit(commit, owners)); |
| 61 | } |
| 62 | |
| 63 | /** One commit, or null, with its people. */ |
| 64 | export async function showOneCommit(commit: Commit | null | undefined): Promise<ShownCommit | null> { |
| 65 | return commit ? (await showCommits([commit]))[0]! : null; |
| 66 | } |