Skip to content
File

Blob: src/worker/git/pack/indexer/resolve/externalBase.ts

typescript115 lines
1import type { PackedObjectResult, PackCatalogRow } from "@/worker/git/object-store/types";
2import type {
3 IndexedPackSource,
4 PackedObjectCandidate,
5} from "@/worker/git/object-store/candidates";
6 
7import { bytesToHex } from "@/worker/common";
8import { collectPackedObjectCandidates, loadIdxView } from "@/worker/git/object-store";
9import { materializePackedObjectCandidate } from "@/worker/git/object-store/materialize";
10import { readObject } from "@/worker/git/object-store/store";
11 
12import type { ResolveOptions } from "../types";
13 
14import { throwIfAborted } from "./errors";
15 
16function normalizeOidHex(oid: string | Uint8Array): string {
17 return typeof oid === "string" ? oid.toLowerCase() : bytesToHex(oid);
18}
19 
20function getExternalBaseCatalog(opts: ResolveOptions): PackCatalogRow[] | undefined {
21 return opts.activeCatalog ?? opts.cacheCtx?.memo?.packCatalog;
22}
23 
24async function collectExternalBaseSources(
25 opts: ResolveOptions,
26 catalog: PackCatalogRow[]
27): Promise<IndexedPackSource[]> {
28 const sources: IndexedPackSource[] = [];
29 
30 for (const pack of catalog) {
31 const idx = await loadIdxView(opts.env, pack.packKey, opts.cacheCtx, pack.packBytes);
32 if (!idx) continue;
33 sources.push({
34 packKey: pack.packKey,
35 packBytes: pack.packBytes,
36 idx,
37 });
38 }
39 
40 return sources;
41}
42 
43async function materializeExternalBaseCandidate(
44 opts: ResolveOptions,
45 candidate: PackedObjectCandidate,
46 visited: Set<string>
47): Promise<PackedObjectResult | undefined> {
48 return await materializePackedObjectCandidate({
49 env: opts.env,
50 candidate,
51 limiter: opts.limiter,
52 countSubrequest: opts.countSubrequest,
53 log: opts.log,
54 cyclePolicy: "miss",
55 resolveRefBase: async (baseOid, nextVisited) => {
56 return await readExternalBaseObject(opts, baseOid, nextVisited);
57 },
58 visited,
59 signal: opts.signal,
60 checkAborted: (stage) => throwIfAborted(opts.signal, opts.log, stage),
61 });
62}
63 
64/**
65 * Resolve a thin-pack base from the caller-supplied catalog snapshot.
66 *
67 * `readObject()` deliberately returns the first catalog hit for an OID. Backfill
68 * needs a weaker contract: when duplicate OIDs exist, a newer duplicate may be
69 * an unusable REF_DELTA whose base lives in the target pack being backfilled.
70 * Try every duplicate candidate in snapshot order and accept the first one
71 * whose delta chain materializes without crossing back into the excluded target.
72 */
73export async function readExternalBaseObject(
74 opts: ResolveOptions,
75 oid: string | Uint8Array,
76 visited = new Set<string>()
77): Promise<PackedObjectResult | undefined> {
78 const oidHex = normalizeOidHex(oid);
79 const cached = opts.cacheCtx?.memo?.packedObjects?.get(oidHex);
80 if (cached) return cached;
81 
82 const catalog = getExternalBaseCatalog(opts);
83 if (!catalog) {
84 return await readObject(opts.env, opts.repoId, oidHex, opts.cacheCtx, visited);
85 }
86 
87 const sources = await collectExternalBaseSources(opts, catalog);
88 const candidates = collectPackedObjectCandidates(sources, oid, {
89 excludePackKey: opts.packKey,
90 });
91 opts.log.debug("resolve:external-base-candidates", {
92 oid: oidHex,
93 candidates: candidates.length,
94 packs: sources.length,
95 excludedPackKey: opts.packKey,
96 });
97 
98 for (const candidate of candidates) {
99 const object = await materializeExternalBaseCandidate(opts, candidate, visited);
100 if (!object) continue;
101 
102 if (opts.cacheCtx?.memo) {
103 opts.cacheCtx.memo.packedObjects = opts.cacheCtx.memo.packedObjects || new Map();
104 opts.cacheCtx.memo.packedObjects.set(oidHex, object);
105 }
106 return object;
107 }
108 
109 opts.log.debug("resolve:external-base-miss", {
110 oid: oidHex,
111 candidates: candidates.length,
112 });
113 return undefined;
114}