109 lines · 4.4 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 };

/** The counted 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" || UNCOUNTED.has(String(kind))) continue;
    out.push({ actorId, repoId, at: Number(createdAt) || Date.now() });
  }
  return out;
}

export async function indexActivity(touches: ActivityTouch[]): Promise<void> {
  await Promise.all(
    touches.map((touch) =>
      accountDb(touch.actorId).run(
        `INSERT INTO activity_repos (repo_id, first_at, last_at) VALUES (?, ?, ?)
         ON CONFLICT (repo_id) DO UPDATE SET first_at = MIN(first_at, excluded.first_at),
           last_at = MAX(last_at, excluded.last_at)`,
        touch.repoId,
        touch.at,
        touch.at,