/** * SQLite-backed cache for rendered `github` issue/PR view output, plus a * generic cache-aware wrapper that the tool ops and the `issue://`/`pr://` * protocol handlers share. * * Storage: * One process-wide connection opens lazily on first hit and stays open. All * helpers swallow open/IO failures and degrade to "no cache" so a corrupt or * unreadable DB never blocks a `gh` call. * * Soft TTL → return cached row directly. * Stateful issue/PR rows past soft TTL but within hard TTL → refresh * synchronously, falling back to the cached row if the live fetch fails. * Expensive PR diff rows past soft TTL but within hard TTL → return cached * row AND schedule a background refresh (errors logged, never thrown). * Only `/review` stores the snapshot it froze, keyed by `fetched_at`. * Cache writes do not. A published generation is insert-only: a later * snapshot that collides on the same millisecond gets a new id. * Past hard TTL → treat as miss and fetch fresh. Pinned generation reads * use that same hard TTL, counted from when `/review` retained the * snapshot, and honor the cache kill switch. */ import { Database } from "bun:sqlite"; import type { Settings } from "../config/settings.js"; export type CacheKind = "issue" | "pr" | "pr-diff" | "mr-diff"; export interface CachedView { authKey: string; repo: string; kind: CacheKind; number: number; includeComments: boolean; fetchedAt: number; payload: T; rendered: string; sourceUrl: string | undefined; } export declare function openDb(): Database | null; /** * glab config file, following glab's search order: `GLAB_CONFIG_DIR` wins * outright; otherwise the legacy `~/.config/glab-cli` path, then the * XDG/platform config dir. Returns the first existing candidate, else the first. */ export declare function getGlabConfigPath(): string; /** Env vars glab reads tokens from; they apply to every host glab talks to. */ export declare const GITLAB_TOKEN_ENV_VARS: string[]; /** * Best-effort local fingerprint for the active GitHub CLI credentials. * * Cache hits must not cross account/token boundaries, but doing a `gh api user` * probe before every cached read would defeat the soft-TTL contract that cache * hits avoid a gh round-trip. Instead, key rows by credential material that the * GitHub CLI itself consumes: token environment variables and/or hosts.yml. * The DB stores only a hash, never the token or hosts.yml contents. If no * credential source is visible, callers should pass `null` to bypass caching. */ export declare function resolveGithubCacheAuthKey(host?: string): string | undefined; /** GitLab counterpart of {@link resolveGithubCacheAuthKey}: glab token env vars and/or glab's config.yml. */ export declare function resolveGitlabCacheAuthKey(host: string): string | undefined; export declare function getCached(repo: string, kind: CacheKind, number: number, includeComments: boolean, authKey?: string): CachedView | null; export interface PutCachedInput { authKey?: string; repo: string; kind: CacheKind; number: number; includeComments: boolean; payload: T; rendered: string; sourceUrl?: string; fetchedAt?: number; } /** * Keep one immutable pr-diff generation. The live row is still one-per-PR and * a background refresh replaces it; review URLs address this copy by `fetchedAt` * so that refresh cannot retarget a file index. Does not write the live row. * * A published id is never overwritten. Storing the same snapshot again returns * its existing id and restarts its retention clock. A different snapshot that * lands on the same millisecond gets the next free id, which the caller must * publish. Retention runs from `retained_at` (when `/review` saved it), not from * `fetched_at`, so a stale live row pinned by `/review` keeps its full hard TTL. * Returns that id, or null when the database is unavailable. */ export declare function retainCachedGeneration(input: PutCachedInput): number | null; /** A generation stored by {@link retainCachedGeneration}, with the time it was saved. */ export interface CachedGeneration extends CachedView { retainedAt: number; } /** Read a pr-diff generation stored by {@link retainCachedGeneration}. */ export declare function getCachedGeneration(repo: string, kind: CacheKind, number: number, includeComments: boolean, fetchedAt: number, authKey?: string): CachedGeneration | null; /** * Read one immutable generation, enforcing the cache kill switch and hard TTL * (measured from when the generation was retained). An expired generation is * deleted. Does not read or refresh the live row. */ export declare function getRetainedGeneration(repo: string, kind: CacheKind, number: number, includeComments: boolean, fetchedAt: number, authKey?: string, settings?: Settings): CachedGeneration | null; export declare function putCached(input: PutCachedInput): void; /** Drop a specific cache entry. */ export declare function invalidate(repo: string, kind: CacheKind, number: number, includeComments?: boolean, authKey?: string): void; /** * Drop every cached row for a given issue/PR number, regardless of repo, * auth key, include_comments flag, or row kind ({@link CacheKind}). Best-effort: * swallows DB failures the same way {@link invalidate} does. * * Used by the bash-side detector that reacts to `gh issue close` / `gh pr merge` * style mutations. Repo + auth-key narrowing is intentionally skipped because * the bash command often does not name the repo (defaults to cwd's `gh` * config) and resolving the *current* repo from `cwd` for every bash call would * be far more expensive than a write-amplified DELETE. */ export declare function invalidateAllForNumber(number: number, repo?: string): void; /** Drop every cached row. Test helper. */ export declare function clearAll(): void; /** * Drop every cached row for a repo, or all rows when the repo is unknown. * Fallback for current-branch `gh pr merge`/`gh pr close`-style mutations * where the bash command names no PR number or URL, so the target row cannot * be identified. Over-invalidation is deliberate (see module header). */ export declare function invalidateAllForRepo(repo?: string): void; /** Drop live PR rows after a push, leaving issues and retained snapshots alone. */ export declare function invalidatePullRequestsForRepo(repo: string): void; /** * Test/maintenance helper. Closes and forgets the cached connection so the * next access reopens against (possibly) a different DB path. */ export declare function resetForTests(): void; export interface FreshResult { rendered: string; sourceUrl: string | undefined; payload: T; } export interface CacheLookupOptions { repo: string; kind: CacheKind; number: number; includeComments: boolean; /** * Auth/credential namespace for cache rows. Omit only in storage-layer * tests; pass `null` when production code cannot determine an identity and * must bypass persistent cache reads/writes. */ authKey?: string | null; fetchFresh: () => Promise>; settings?: Settings | undefined; now?: number; } export type CacheStatus = "miss" | "fresh" | "refreshed" | "stale" | "disabled"; export interface CacheLookupResult { rendered: string; sourceUrl: string | undefined; payload: T; status: CacheStatus; fetchedAt: number; } export interface CacheTtl { softMs: number; hardMs: number; enabled: boolean; } export declare function resolveCacheTtl(settings?: Settings): CacheTtl; export declare function getOrFetchView(options: CacheLookupOptions): Promise>; /** * Human-friendly freshness note for protocol-handler `notes[]` rendering. */ export declare function formatFreshnessNote(status: CacheStatus, fetchedAtMs: number, now?: number): string;