File
Blob: src/worker/git/pack/rewrite/selectionRetained.ts
| 1 | import type { OrderedPackSnapshot } from "@/worker/git/operations/fetch/types"; |
| 2 | import type { Logger } from "@/worker/common/logger"; |
| 3 | |
| 4 | import { clonePackHeader, type DuplicateHeaderCache } from "./ownership"; |
| 5 | import { pruneUnsafeDeadSlotRedirects } from "./selectionCompact"; |
| 6 | import { resolveDeltaBaseFromHeader } from "./selectionResolve"; |
| 7 | import { |
| 8 | ensurePackReadState, |
| 9 | readSelectedHeader, |
| 10 | selectionKey, |
| 11 | type PackReadState, |
| 12 | type RewriteOptions, |
| 13 | type SelectionTable, |
| 14 | } from "./shared"; |
| 15 | |
| 16 | export function collectRetainedRedirectsNeedingBaseResolution( |
| 17 | table: SelectionTable, |
| 18 | deadSlots: Map<number, number> |
| 19 | ): number[] { |
| 20 | const safeDeadSlots = pruneUnsafeDeadSlotRedirects(table, deadSlots); |
| 21 | const retained: number[] = []; |
| 22 | |
| 23 | for (const [deadSel] of deadSlots) { |
| 24 | // `safeDeadSlots` is the subset we are still allowed to remove. When a row |
| 25 | // is present in `deadSlots` but absent here, dead-slot pruning decided that |
| 26 | // the redirect would manufacture a cycle, so this "dead" row is actually |
| 27 | // staying live in the output pack. |
| 28 | if (safeDeadSlots.has(deadSel)) continue; |
| 29 | const typeCode = table.typeCodes[deadSel]; |
| 30 | if ((typeCode === 6 || typeCode === 7) && table.baseSlots[deadSel] < 0) { |
| 31 | retained.push(deadSel); |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | return retained; |
| 36 | } |
| 37 | |
| 38 | export async function resolveRetainedRedirectBase( |
| 39 | table: SelectionTable, |
| 40 | sel: number, |
| 41 | snapshot: OrderedPackSnapshot, |
| 42 | readerStates: Map<number, PackReadState>, |
| 43 | dedupMap: Map<number, number>, |
| 44 | duplicateHeaderCache: DuplicateHeaderCache, |
| 45 | secondaryQueue: number[], |
| 46 | env: Env, |
| 47 | log: Logger, |
| 48 | warnedFlags: Set<string>, |
| 49 | options?: RewriteOptions |
| 50 | ): Promise<boolean> { |
| 51 | if (table.baseSlots[sel] >= 0) return true; |
| 52 | |
| 53 | const packSlot = table.packSlots[sel]; |
| 54 | const pack = snapshot.packs[packSlot]; |
| 55 | const cacheKey = selectionKey(packSlot, table.entryIndices[sel]); |
| 56 | let header = duplicateHeaderCache.get(cacheKey); |
| 57 | |
| 58 | if (header === undefined) { |
| 59 | // The common path already memoized this header before issuing the redirect. |
| 60 | // Falling back to a reread is correct here, but it should stay rare and |
| 61 | // bounded to the retained-redirect edge case. |
| 62 | const readState = await ensurePackReadState( |
| 63 | env, |
| 64 | pack, |
| 65 | packSlot, |
| 66 | readerStates, |
| 67 | log, |
| 68 | warnedFlags, |
| 69 | options |
| 70 | ); |
| 71 | header = await readSelectedHeader(readState, table.offsets[sel]); |
| 72 | duplicateHeaderCache.set(cacheKey, header ? clonePackHeader(header) : null); |
| 73 | } |
| 74 | |
| 75 | if (!header) { |
| 76 | log.warn("rewrite:header-read-failed", { packKey: pack.packKey, offset: table.offsets[sel] }); |
| 77 | return false; |
| 78 | } |
| 79 | |
| 80 | log.debug("rewrite:retained-redirect-base-resolve", { |
| 81 | sel, |
| 82 | packKey: pack.packKey, |
| 83 | entryIndex: table.entryIndices[sel], |
| 84 | typeCode: table.typeCodes[sel], |
| 85 | }); |
| 86 | |
| 87 | // This intentionally resolves only the retained row's own base edge. Any |
| 88 | // newly discovered base rows still flow through the normal secondary queue so |
| 89 | // they get the same header-read and duplicate-owner handling as the primary |
| 90 | // selection pass. |
| 91 | return await resolveDeltaBaseFromHeader( |
| 92 | table, |
| 93 | sel, |
| 94 | snapshot, |
| 95 | dedupMap, |
| 96 | secondaryQueue, |
| 97 | env, |
| 98 | log, |
| 99 | header, |
| 100 | options |
| 101 | ); |
| 102 | } |