171 lines · 6.3 KB
import { DurableObject } from "cloudflare:workers";
import { unreadNotificationCount } from "./notification-store";
import type { InvolvementEntry, InvolvementQuery, InvolvementRole } from "./involvement";

/** Bounds drift from writes that bypass `invalidateUnread`, e.g. repo deletes cascading notifications. */
const CACHE_TTL_MS = 5 * 60 * 1000;
const MAX_INVOLVEMENT_ROWS = 50_000;
const MAX_LIST = 200;

type Cached = { count: number; at: number };

type InvolvementRow = {
  issue_id: string;
  repo_id: string;
  number: number;
  is_pull: number;
  state: string;
  title: string;
  updated_at: number;
  roles: string;
};

/**
 * One per user. Holds the unread-notification count so the header badge on every page view is an
 * in-memory read on a per-user object instead of a COUNT on D1. Writers call `invalidate`; a version
 * number keeps a count computed before an invalidation from being cached after it.
 *
 * Also holds the user's involvement index: every issue and pull request they authored, are assigned to or
 * were asked to review, pushed here by each repository's RepoState. Cross-repo lists ("assigned to me",
 * "review requested") read this instead of joining across every repository.
 */
export class UserInbox extends DurableObject<Env> {
  private cached: Cached | null = null;
  private version = 0;
  private involvementReady = false;

  private get sql(): SqlStorage {
    const sql = this.ctx.storage.sql;
    if (!this.involvementReady) {
      sql.exec(`CREATE TABLE IF NOT EXISTS involvement (