File
Blob: src/worker/git/pack/indexer/resolve/materialize.ts
| 1 | import { bytesToHex } from "@/worker/common/hex"; |
| 2 | import { applyGitDelta } from "@/worker/git/object-store/delta"; |
| 3 | import { typeCodeToObjectType } from "@/worker/git/object-store/support"; |
| 4 | |
| 5 | import { getRefBaseOidAt } from "../types"; |
| 6 | import type { PackEntryTable, ResolveOptions } from "../types"; |
| 7 | |
| 8 | import { throwIfAborted } from "./errors"; |
| 9 | import { readExternalBaseObject } from "./externalBase"; |
| 10 | import type { CacheEntry } from "./payloadCache"; |
| 11 | import { PayloadLRU } from "./payloadCache"; |
| 12 | import { 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 | */ |
| 18 | export 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 | */ |
| 36 | async 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 | } |