Skip to content
File

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

typescript109 lines
1import { bytesToHex } from "@/worker/common/hex";
2import { applyGitDelta } from "@/worker/git/object-store/delta";
3import { typeCodeToObjectType } from "@/worker/git/object-store/support";
4 
5import { getRefBaseOidAt } from "../types";
6import type { PackEntryTable, ResolveOptions } from "../types";
7 
8import { throwIfAborted } from "./errors";
9import { readExternalBaseObject } from "./externalBase";
10import type { CacheEntry } from "./payloadCache";
11import { PayloadLRU } from "./payloadCache";
12import { inflateFromReader, type SequentialReader } from "./reader";
13 
14/**
15 * Get a resolved base payload from the LRU cache, or rematerialize it from the
16 * pack/object store when an earlier eviction forced it out of memory.
17 */
18export async function getBasePayload(
19 opts: ResolveOptions,
20 index: number,
21 lru: PayloadLRU,
22 reader: SequentialReader,
23 table: PackEntryTable,
24 baseIndexArr: Int32Array
25): Promise<CacheEntry> {
26 const cached = lru.get(index);
27 if (cached) return cached;
28 return await materializeEntry(opts, index, lru, reader, table, baseIndexArr);
29}
30 
31/**
32 * Materialize an already-resolved entry's payload without recursive calls. Deep
33 * delta chains are valid input, and cache misses on a long chain should not be
34 * able to exhaust the worker's JS call stack.
35 */
36async function materializeEntry(
37 opts: ResolveOptions,
38 index: number,
39 lru: PayloadLRU,
40 reader: SequentialReader,
41 table: PackEntryTable,
42 baseIndexArr: Int32Array
43): Promise<CacheEntry> {
44 const pending: number[] = [];
45 let currentIndex = index;
46 let base: CacheEntry | undefined;
47 let traversalSteps = 0;
48 
49 while (!base) {
50 throwIfAborted(opts.signal, opts.log, "materialize:walk");
51 if (traversalSteps >= baseIndexArr.length) {
52 throw new Error(`materialize: base chain cycle or runaway traversal for entry ${index}`);
53 }
54 traversalSteps++;
55 
56 const cached = lru.get(currentIndex);
57 if (cached) {
58 base = cached;
59 break;
60 }
61 
62 const baseType = typeCodeToObjectType(table.types[currentIndex]);
63 if (baseType) {
64 const payload = await inflateFromReader(reader, table, currentIndex);
65 base = { type: baseType, payload };
66 lru.set(currentIndex, base);
67 break;
68 }
69 
70 pending.push(currentIndex);
71 const bi = baseIndexArr[currentIndex];
72 if (bi >= 0) {
73 currentIndex = bi;
74 continue;
75 }
76 
77 if (table.types[currentIndex] === 7) {
78 const baseOid = bytesToHex(getRefBaseOidAt(opts.scanResult.refBaseOids, currentIndex));
79 const obj = await readExternalBaseObject(opts, baseOid);
80 if (!obj) throw new Error(`materialize: external base ${baseOid} not found`);
81 base = { type: obj.type, payload: obj.payload };
82 break;
83 }
84 
85 throw new Error(`materialize: unresolved in-pack base for entry ${currentIndex}`);
86 }
87 
88 if (!base) {
89 throw new Error(`materialize: failed to resolve base for entry ${index}`);
90 }
91 
92 while (pending.length > 0) {
93 throwIfAborted(opts.signal, opts.log, "materialize:replay");
94 const deltaIndex = pending.pop()!;
95 const deltaPayload = await inflateFromReader(reader, table, deltaIndex);
96 throwIfAborted(opts.signal, opts.log, "materialize:replay");
97 const result = applyGitDelta(base.payload, deltaPayload);
98 if (result.length !== table.decompressedSizes[deltaIndex]) {
99 throw new Error(
100 `materialize: delta result size mismatch at offset ${table.offsets[deltaIndex]} (expected ${table.decompressedSizes[deltaIndex]}, got ${result.length})`
101 );
102 }
103 base = { type: base.type, payload: result };
104 lru.set(deltaIndex, base);
105 }
106 
107 return base;
108}