import { env, waitUntil } from "cloudflare:workers"; import { openArtifactsRepo } from "./artifacts"; import type { CommitSummary, TreeEntry } from "~/lib/types"; import { artifactsCall } from "./artifacts-call"; const OBJECT_CACHE_TTL_SECONDS = 60 * 60 * 24 * 30; const SHORT_SHA_LENGTH = 7; const WALK_CONCURRENCY = 16; const MAX_WALK_TREE_READS = 2000; export const ZERO_SHA = "0000000000000000000000000000000000000000"; const SHA_PATTERN = /^[0-9a-f]{40}$/; export function isSha(value: string): boolean { return SHA_PATTERN.test(value); } export function shortSha(sha: string): string { return sha.slice(0, SHORT_SHA_LENGTH); } export function toCommitSummary(meta: ArtifactsCommitMetadata): CommitSummary { return { sha: meta.hash, shortSha: shortSha(meta.hash), subject: meta.message.split("\n", 1)[0] ?? "", message: meta.message, authorName: meta.author.name, authorEmail: meta.author.email, committerName: meta.committer.name, authoredAt: meta.authoredAt * 1000, committedAt: meta.committedAt * 1000, parents: meta.parents, treeSha: meta.treeHash, }; } function joinPath(parent: string, name: string): string { return parent ? `${parent}/${name}` : name; } /** Read-side access to a single Artifacts repo with request memoization and KV caching of immutable objects. */ export class GitStore { private readonly commitMemo = new Map>(); private readonly treeMemo = new Map>(); private constructor( private readonly handle: ArtifactsRepo, readonly artifactsName: string, ) {} static async open(artifactsName: string): Promise { return new GitStore(await openArtifactsRepo(artifactsName), artifactsName); } private label(object: string): string { return `${this.artifactsName}@${object.slice(0, 12)}`; } close(): void { this.handle[Symbol.dispose](); } private async cached(key: string, load: () => Promise): Promise { const hit = await env.KV.get(key, "json"); if (hit !== null) return hit; const value = await load(); if (value !== null) { waitUntil(env.KV.put(key, JSON.stringify(value), { expirationTtl: OBJECT_CACHE_TTL_SECONDS })); } return value; } commit(sha: string): Promise { const existing = this.commitMemo.get(sha); if (existing) return existing; const promise = this.cached(`commit:${this.artifactsName}:${sha}`, async () => { const meta = await artifactsCall("read commit", this.label(sha), () => this.handle.readCommit(sha)); return meta ? toCommitSummary(meta) : null; }); this.commitMemo.set(sha, promise); return promise; } tree(sha: string): Promise { const existing = this.treeMemo.get(sha); if (existing) return existing; const promise = this.cached(`tree:${this.artifactsName}:${sha}`, () => artifactsCall("read tree", this.label(sha), () => this.handle.readTree(sha)), ); this.treeMemo.set(sha, promise); return promise; } async blob(sha: string): Promise | null> { const blob = await artifactsCall("read blob", this.label(sha), () => this.handle.readBlob(sha)); return blob ? new Uint8Array(await blob.arrayBuffer()) : null; } async blobStream(sha: string): Promise { return artifactsCall("read blob", this.label(sha), () => this.handle.readBlob(sha)); } async log(ref: string, limit: number, offset: number): Promise { const entries = await artifactsCall("read history", this.label(ref), () => this.handle.log({ ref, limit, offset })); return entries.map(toCommitSummary); } async listTree(treeSha: string, basePath: string): Promise { const entries = (await this.tree(treeSha)) ?? []; return entries.map((entry) => ({ name: entry.name, path: joinPath(basePath, entry.name), mode: entry.mode, sha: entry.hash, type: entry.type, })); } /** Resolves a slash-separated path under a root tree. An empty path resolves to the root tree itself. */ async entryAtPath(rootTreeSha: string, path: string): Promise { const parts = path.split("/").filter(Boolean); let current: TreeEntry = { name: "", path: "", mode: "40000", sha: rootTreeSha, type: "tree" }; for (const part of parts) { if (current.type !== "tree") return null; const entries = await this.tree(current.sha); const match = entries?.find((entry) => entry.name === part); if (!match) return null; current = { name: match.name, path: joinPath(current.path, match.name), mode: match.mode, sha: match.hash, type: match.type, }; } return current; } /** Recursively lists every non-tree entry beneath a tree, bounded by `limit`. */ async walk(treeSha: string, limit: number): Promise<{ entries: TreeEntry[]; truncated: boolean }> { const out: TreeEntry[] = []; const queue: Array<{ sha: string; path: string }> = [{ sha: treeSha, path: "" }]; let treeReads = 0; while (queue.length > 0) { const level = queue.splice(0, WALK_CONCURRENCY); treeReads += level.length; if (treeReads > MAX_WALK_TREE_READS) return { entries: out, truncated: true }; const listings = await Promise.all(level.map((item) => this.listTree(item.sha, item.path))); for (const listing of listings) { for (const entry of listing) { if (entry.type === "tree") { queue.push({ sha: entry.sha, path: entry.path }); } else { out.push(entry); if (out.length >= limit) return { entries: out, truncated: true }; } } } } return { entries: out, truncated: false }; } } export async function withStore(artifactsName: string, fn: (store: GitStore) => Promise): Promise { const store = await GitStore.open(artifactsName); try { return await fn(store); } finally { store.close(); } }