121 lines · 5.1 KB
import { all, chunks } from "./db";
import { accountDb, repoDb, stmt } from "./scoped-db";
import type { ScopedStatement } from "./scoped-db";
import { backfillDone, backfillRepos } from "./backfill";

/**
 * Activity rows live in each repository's RepoState; the contribution graph needs "this account's activity in a
 * time range". Each actor's AccountState keeps `activity_repos` (repository, first and last counted activity) so
 * the graph visits only those repositories. RepoState feeds it from every activity insert it runs.
 */
export const ACTIVITY_INDEX = "activity-repos";

/** Kinds that never count as contributions (contributions.ts COUNTED_ACTIVITY) and so are not indexed. */
const UNCOUNTED = new Set(["view", "push"]);
const ACTIVITY_INSERT = /^\s*INSERT\s+INTO\s+activity\s*\(\s*id\s*,\s*actor_id\s*,\s*repo_id\s*,\s*kind\s*,/i;

export type ActivityTouch = { actorId: string; repoId: string; at: number; counted: boolean };

/** The activity inserts among `statements` (repoActivityStatement's shape), for the actor index. */
export function activityTouches(statements: ScopedStatement[]): ActivityTouch[] {
  const out: ActivityTouch[] = [];
  for (const { sql, params } of statements) {
    if (!ACTIVITY_INSERT.test(sql)) continue;
    const [, actorId, repoId, kind, , createdAt] = params;
    if (typeof actorId !== "string" || typeof repoId !== "string") continue;
    out.push({ actorId, repoId, at: Number(createdAt) || Date.now(), counted: !UNCOUNTED.has(String(kind)) });
  }
  return out;
}

const UPSERT = `INSERT INTO activity_repos (repo_id, first_at, last_at, touched_at) VALUES (?1, ?2, ?3, ?4)
  ON CONFLICT (repo_id) DO UPDATE SET
    first_at = CASE WHEN excluded.first_at IS NULL THEN first_at ELSE MIN(COALESCE(first_at, excluded.first_at), excluded.first_at) END,
    last_at = CASE WHEN excluded.last_at IS NULL THEN last_at ELSE MAX(COALESCE(last_at, excluded.last_at), excluded.last_at) END,
    touched_at = MAX(touched_at, excluded.touched_at)`;

export async function indexActivity(touches: ActivityTouch[]): Promise<void> {
  await Promise.all(
    touches.map((touch) =>
      accountDb(touch.actorId).run(