105 lines · 4.0 KB
import { recordArtifactsCall } from "./artifacts-health";
import { basicAuthHeader, repoTokenSecret } from "./artifacts";
import { parsePack } from "./pack";
import { concatBytes, pktLine } from "./pktline";
import { limitStream, PackTooLargeError } from "./ref-sync-protocol";
import { packFromResponse, remoteFor } from "./snapshot";

const DELIM_PKT = new TextEncoder().encode("0001");
const FLUSH = new TextEncoder().encode("0000");
/** Trees come too (Artifacts only supports `blob:none`), so caps stay well inside the 128 MB isolate. */
const MAX_GRAPH_BYTES = 24 * 1024 * 1024;
const MAX_GRAPH_INFLATED_BYTES = 40 * 1024 * 1024;
const MAX_GRAPH_OBJECTS = 200_000;
const MAX_GRAPH_OBJECT_BYTES = 8 * 1024 * 1024;
const GRAPH_TIMEOUT_MS = 25_000;
const PARENT_LINE = /^parent ([0-9a-f]{40})$/gm;
const decoder = new TextDecoder();

function parentsOf(content: Uint8Array): string[] {
  const header = decoder.decode(content.subarray(0, Math.min(content.byteLength, 64 * 1024))).split("\n\n")[0];
  return [...header.matchAll(PARENT_LINE)].map((match) => match[1]);
}

/**
 * Whether `target` is an ancestor of any of `tips`, in ONE protocol v2 fetch: wants every tip, has the
 * target, `filter blob:none`. The pack then holds exactly the commits reachable from the tips but not from
 * the target, so the target is reachable iff one of those commits names it as a parent.
 * Artifacts' `ready`/`ACK` only report that it has the object (it answers `ready` for unrelated commits),
 * so they cannot be used. Returns null when the answer is unknown (no remote, error, or over budget).
 */
export async function reachableViaFetch(
  artifactsName: string,
  tips: string[],
  target: string,
): Promise<boolean | null> {
  if (tips.includes(target)) return true;
  const wants = [...new Set(tips)];
  if (wants.length === 0) return false;
  const remote = await remoteFor(artifactsName);
  if (!remote) return null;