Skip to content
File

Blob: src/worker/git/pack/refIndex.ts

typescript686 lines
1import type { CacheContext } from "@/worker/cache";
2import type { GitObjectType } from "@/worker/git/core";
3import type { IdxView } from "@/worker/git/object-store/types";
4 
5import { bytesEqual, bytesToHex, createLogger } from "@/worker/common";
6import { typeCodeToObjectType } from "@/worker/git/object-store/support";
7import { countSubrequest, getLimiter } from "@/worker/git/operations/limits";
8import { packRefsKey } from "@/worker/keys";
9import { buildOidSortedEntryIndices } from "./indexer/writeIdx";
10import type { PackEntryTable } from "./indexer/types";
11 
12const PACK_REF_MAGIC = 0x50524546; // "PREF"
13const PACK_REF_VERSION = 1;
14const PACK_REF_HEADER_BYTES = 4 + 4 + 4 + 8 + 20 + 20;
15const OID_BYTES = 20;
16const OID_HEX_BYTES = OID_BYTES * 2;
17const PACK_REF_VIEW_CACHE_MAX_BYTES = 16 * 1024 * 1024;
18const UINT32_SPAN = 0x1_0000_0000;
19const TREE_LINE_PREFIX = [0x74, 0x72, 0x65, 0x65, 0x20]; // "tree "
20const PARENT_LINE_PREFIX = [0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x20]; // "parent "
21const OBJECT_LINE_PREFIX = [0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x20]; // "object "
22const TYPE_LINE_PREFIX = [0x74, 0x79, 0x70, 0x65, 0x20]; // "type "
23 
24type PackRefValidationKind = "corrupt" | "stale";
25export type { PackRefIndexStatus } from "@/shared/git/types";
26 
27type PackRefInvalidResult = {
28 type: "Invalid";
29 kind: PackRefValidationKind;
30 reason: string;
31};
32 
33export type PackRefView = {
34 packKey: string;
35 objectCount: number;
36 packBytes: number;
37 packChecksum: Uint8Array;
38 idxChecksum: Uint8Array;
39 /** Logical object type codes in idx OID order. */
40 typeCodes: Uint8Array;
41 /** Big-endian uint32 starts into `rawRefs`, measured in 20-byte OID slots. */
42 refStartsBytes: Uint8Array;
43 /** View over `refStartsBytes`; avoids decoding a second typed-array copy. */
44 refStartsView: DataView;
45 /** Flat raw 20-byte referenced OIDs in idx OID order. */
46 rawRefs: Uint8Array;
47};
48 
49export type PackRefBuildResult = {
50 bytes: Uint8Array;
51 objectCount: number;
52 refCount: number;
53 refIndexBytes: number;
54};
55 
56export type PackRefSnapshotEntry = {
57 packKey: string;
58 packBytes: number;
59 idx: IdxView;
60 refs: PackRefView;
61};
62 
63export type PackRefSnapshotLoadResult =
64 | {
65 type: "Ready";
66 packs: PackRefSnapshotEntry[];
67 }
68 | {
69 type: "Missing";
70 packs: Array<{
71 packKey: string;
72 packBytes: number;
73 reason: "missing" | PackRefValidationKind;
74 detail?: string;
75 }>;
76 };
77 
78export type PackRefViewLoadResult =
79 | {
80 type: "Ready";
81 view: PackRefView;
82 }
83 | {
84 type: "Missing";
85 reason: "missing";
86 }
87 | PackRefInvalidResult;
88 
89type CachedPackRefView = {
90 view: PackRefView;
91 bytes: number;
92};
93 
94const packRefViewCache = new Map<string, CachedPackRefView>();
95let packRefViewCacheBytes = 0;
96 
97function getPackRefViewCacheKey(packKey: string, idxChecksum: Uint8Array): string {
98 return `${packKey}\0${bytesToHex(idxChecksum)}`;
99}
100 
101function estimatePackRefViewBytes(view: PackRefView): number {
102 return (
103 view.typeCodes.byteLength +
104 view.refStartsBytes.byteLength +
105 view.rawRefs.byteLength +
106 view.packChecksum.byteLength +
107 view.idxChecksum.byteLength
108 );
109}
110 
111function touchPackRefViewCache(cacheKey: string, view: PackRefView): void {
112 const existing = packRefViewCache.get(cacheKey);
113 if (existing) {
114 packRefViewCache.delete(cacheKey);
115 packRefViewCacheBytes -= existing.bytes;
116 }
117 
118 const bytes = estimatePackRefViewBytes(view);
119 if (bytes > PACK_REF_VIEW_CACHE_MAX_BYTES) return;
120 
121 packRefViewCache.set(cacheKey, { view, bytes });
122 packRefViewCacheBytes += bytes;
123 
124 while (packRefViewCacheBytes > PACK_REF_VIEW_CACHE_MAX_BYTES) {
125 const firstKey = packRefViewCache.keys().next().value;
126 if (!firstKey) break;
127 const first = packRefViewCache.get(firstKey);
128 packRefViewCache.delete(firstKey);
129 packRefViewCacheBytes -= first?.bytes ?? 0;
130 }
131}
132 
133function writeUint64(dv: DataView, pos: number, value: number): void {
134 if (!Number.isSafeInteger(value) || value < 0) {
135 throw new Error(`ref-index: unsupported 64-bit value ${value}`);
136 }
137 const hi = Math.floor(value / UINT32_SPAN);
138 const lo = value >>> 0;
139 dv.setUint32(pos, hi, false);
140 dv.setUint32(pos + 4, lo, false);
141}
142 
143function readUint64(dv: DataView, pos: number): number {
144 const hi = dv.getUint32(pos, false);
145 const lo = dv.getUint32(pos + 4, false);
146 const value = hi * UINT32_SPAN + lo;
147 if (!Number.isSafeInteger(value)) {
148 throw new Error("ref-index: pack byte count exceeds safe integer support");
149 }
150 return value;
151}
152 
153function invalid(kind: PackRefValidationKind, reason: string): PackRefInvalidResult {
154 return { type: "Invalid", kind, reason };
155}
156 
157function isValidObjectTypeCode(typeCode: number): boolean {
158 return typeCodeToObjectType(typeCode) !== null;
159}
160 
161// Sidecar generation runs while receive/backfill already holds inflated object
162// payloads. Parse ASCII Git headers directly from bytes so large packs do not
163// allocate one hex string per recorded edge before writing the flat sidecar.
164function hexNibble(value: number): number {
165 if (value >= 0x30 && value <= 0x39) return value - 0x30;
166 if (value >= 0x61 && value <= 0x66) return value - 0x61 + 10;
167 if (value >= 0x41 && value <= 0x46) return value - 0x41 + 10;
168 return -1;
169}
170 
171function hasHexOidAt(payload: Uint8Array, hexStart: number, lineEnd: number): boolean {
172 if (hexStart + OID_HEX_BYTES > lineEnd) return false;
173 for (let index = 0; index < OID_HEX_BYTES; index++) {
174 if (hexNibble(payload[hexStart + index]) < 0) return false;
175 }
176 return true;
177}
178 
179function writeHexOidAt(
180 payload: Uint8Array,
181 hexStart: number,
182 out: Uint8Array,
183 outStart: number
184): void {
185 for (let index = 0; index < OID_BYTES; index++) {
186 const hi = hexNibble(payload[hexStart + index * 2]);
187 const lo = hexNibble(payload[hexStart + index * 2 + 1]);
188 if (hi < 0 || lo < 0) {
189 throw new Error("ref-index: invalid hex object id");
190 }
191 out[outStart + index] = (hi << 4) | lo;
192 }
193}
194 
195function lineHasPrefix(
196 payload: Uint8Array,
197 lineStart: number,
198 lineEnd: number,
199 prefix: readonly number[]
200): boolean {
201 if (lineStart + prefix.length > lineEnd) return false;
202 for (let index = 0; index < prefix.length; index++) {
203 if (payload[lineStart + index] !== prefix[index]) return false;
204 }
205 return true;
206}
207 
208function valueStartsWithAscii(
209 payload: Uint8Array,
210 start: number,
211 lineEnd: number,
212 value: string
213): boolean {
214 if (start + value.length > lineEnd) return false;
215 for (let index = 0; index < value.length; index++) {
216 if (payload[start + index] !== value.charCodeAt(index)) return false;
217 }
218 return true;
219}
220 
221function forEachHeaderLine(
222 payload: Uint8Array,
223 visit: (lineStart: number, lineEnd: number) => void
224): void {
225 let cursor = 0;
226 while (cursor < payload.byteLength) {
227 const lineStart = cursor;
228 while (cursor < payload.byteLength && payload[cursor] !== 0x0a) cursor++;
229 const lineEnd = cursor > lineStart && payload[cursor - 1] === 0x0d ? cursor - 1 : cursor;
230 if (lineEnd === lineStart) break;
231 visit(lineStart, lineEnd);
232 cursor++;
233 }
234}
235 
236function parseCommitRefBytes(payload: Uint8Array): Uint8Array {
237 let treeHexStart = -1;
238 let parentCount = 0;
239 
240 forEachHeaderLine(payload, (lineStart, lineEnd) => {
241 if (treeHexStart < 0 && lineHasPrefix(payload, lineStart, lineEnd, TREE_LINE_PREFIX)) {
242 const hexStart = lineStart + TREE_LINE_PREFIX.length;
243 if (hasHexOidAt(payload, hexStart, lineEnd)) treeHexStart = hexStart;
244 return;
245 }
246 
247 if (lineHasPrefix(payload, lineStart, lineEnd, PARENT_LINE_PREFIX)) {
248 const hexStart = lineStart + PARENT_LINE_PREFIX.length;
249 if (hasHexOidAt(payload, hexStart, lineEnd)) parentCount++;
250 }
251 });
252 
253 const treeCount = treeHexStart >= 0 ? 1 : 0;
254 const refs = new Uint8Array((treeCount + parentCount) * OID_BYTES);
255 let refsOffset = 0;
256 if (treeHexStart >= 0) {
257 writeHexOidAt(payload, treeHexStart, refs, refsOffset);
258 refsOffset += OID_BYTES;
259 }
260 
261 forEachHeaderLine(payload, (lineStart, lineEnd) => {
262 if (!lineHasPrefix(payload, lineStart, lineEnd, PARENT_LINE_PREFIX)) return;
263 const hexStart = lineStart + PARENT_LINE_PREFIX.length;
264 if (!hasHexOidAt(payload, hexStart, lineEnd)) return;
265 writeHexOidAt(payload, hexStart, refs, refsOffset);
266 refsOffset += OID_BYTES;
267 });
268 
269 return refs;
270}
271 
272function isValidTagTypeLine(payload: Uint8Array, lineStart: number, lineEnd: number): boolean {
273 if (!lineHasPrefix(payload, lineStart, lineEnd, TYPE_LINE_PREFIX)) return false;
274 const valueStart = lineStart + TYPE_LINE_PREFIX.length;
275 return (
276 valueStartsWithAscii(payload, valueStart, lineEnd, "commit") ||
277 valueStartsWithAscii(payload, valueStart, lineEnd, "tree") ||
278 valueStartsWithAscii(payload, valueStart, lineEnd, "blob") ||
279 valueStartsWithAscii(payload, valueStart, lineEnd, "tag")
280 );
281}
282 
283function parseTagRefBytes(payload: Uint8Array): Uint8Array {
284 let objectHexStart = -1;
285 let hasTargetType = false;
286 
287 forEachHeaderLine(payload, (lineStart, lineEnd) => {
288 if (objectHexStart < 0 && lineHasPrefix(payload, lineStart, lineEnd, OBJECT_LINE_PREFIX)) {
289 const hexStart = lineStart + OBJECT_LINE_PREFIX.length;
290 if (hasHexOidAt(payload, hexStart, lineEnd)) objectHexStart = hexStart;
291 return;
292 }
293 
294 if (isValidTagTypeLine(payload, lineStart, lineEnd)) {
295 hasTargetType = true;
296 }
297 });
298 
299 if (objectHexStart < 0 || !hasTargetType) return new Uint8Array(0);
300 const refs = new Uint8Array(OID_BYTES);
301 writeHexOidAt(payload, objectHexStart, refs, 0);
302 return refs;
303}
304 
305function visitTreeClosureRawRefs(
306 payload: Uint8Array,
307 visit: (rawRefs: Uint8Array, oidStart: number) => void
308): void {
309 let cursor = 0;
310 
311 while (cursor < payload.length) {
312 const modeStart = cursor;
313 while (cursor < payload.length && payload[cursor] !== 0x20) cursor++;
314 if (cursor >= payload.length) break;
315 
316 const modeEnd = cursor;
317 cursor++;
318 while (cursor < payload.length && payload[cursor] !== 0x00) cursor++;
319 
320 const oidStart = cursor + 1;
321 if (oidStart + OID_BYTES > payload.length) break;
322 
323 if (!isGitlinkMode(payload, modeStart, modeEnd)) {
324 visit(payload, oidStart);
325 }
326 cursor = oidStart + OID_BYTES;
327 }
328}
329 
330function parseTreeClosureRefBytes(payload: Uint8Array): Uint8Array {
331 let refCount = 0;
332 visitTreeClosureRawRefs(payload, () => {
333 refCount++;
334 });
335 
336 const refs = new Uint8Array(refCount * OID_BYTES);
337 let refsOffset = 0;
338 visitTreeClosureRawRefs(payload, (rawRefs, oidStart) => {
339 refs.set(rawRefs.subarray(oidStart, oidStart + OID_BYTES), refsOffset);
340 refsOffset += OID_BYTES;
341 });
342 return refs;
343}
344 
345function objectRefBytes(type: GitObjectType, payload: Uint8Array): Uint8Array {
346 if (type === "commit") {
347 return parseCommitRefBytes(payload);
348 }
349 
350 if (type === "tree") {
351 return parseTreeClosureRefBytes(payload);
352 }
353 
354 if (type === "tag") {
355 return parseTagRefBytes(payload);
356 }
357 
358 return new Uint8Array(0);
359}
360 
361/**
362 * Parse tree child references used by fetch closure planning. Gitlinks are
363 * mode 160000 entries and point at commits in another repository, so they must
364 * not become required objects in the superproject's pack closure.
365 */
366export function parseTreeClosureRefs(payload: Uint8Array): string[] {
367 const rawRefs = parseTreeClosureRefBytes(payload);
368 const out: string[] = [];
369 for (let offset = 0; offset < rawRefs.byteLength; offset += OID_BYTES) {
370 out.push(bytesToHex(rawRefs.subarray(offset, offset + OID_BYTES)));
371 }
372 return out;
373}
374 
375function isGitlinkMode(payload: Uint8Array, modeStart: number, modeEnd: number): boolean {
376 const gitlinkMode = "160000";
377 if (modeEnd - modeStart !== gitlinkMode.length) return false;
378 for (let index = 0; index < gitlinkMode.length; index++) {
379 if (payload[modeStart + index] !== gitlinkMode.charCodeAt(index)) return false;
380 }
381 return true;
382}
383 
384export class PackRefsBuilder {
385 private readonly rawRefsByEntry: Array<Uint8Array | undefined>;
386 
387 constructor(objectCount: number) {
388 this.rawRefsByEntry = new Array<Uint8Array | undefined>(objectCount);
389 }
390 
391 recordObject(index: number, type: GitObjectType, payload: Uint8Array): void {
392 this.rawRefsByEntry[index] = objectRefBytes(type, payload);
393 }
394 
395 recordBlob(index: number): void {
396 this.rawRefsByEntry[index] = new Uint8Array(0);
397 }
398 
399 build(args: {
400 table: PackEntryTable;
401 objectCount: number;
402 packBytes: number;
403 packChecksum: Uint8Array;
404 idxChecksum: Uint8Array;
405 }): PackRefBuildResult {
406 const sortedIndices = buildOidSortedEntryIndices(args.table, args.objectCount);
407 const refStarts = new Uint32Array(args.objectCount + 1);
408 let refCount = 0;
409 
410 for (let sortedIndex = 0; sortedIndex < sortedIndices.length; sortedIndex++) {
411 const entryIndex = sortedIndices[sortedIndex];
412 const typeCode = args.table.objectTypes[entryIndex];
413 if (!isValidObjectTypeCode(typeCode)) {
414 throw new Error(`ref-index: unresolved object type for entry ${entryIndex}`);
415 }
416 const refs = this.rawRefsByEntry[entryIndex];
417 if (refs === undefined) {
418 throw new Error(`ref-index: missing logical refs for entry ${entryIndex}`);
419 }
420 if (refs.byteLength % OID_BYTES !== 0) {
421 throw new Error(`ref-index: invalid logical refs for entry ${entryIndex}`);
422 }
423 
424 refStarts[sortedIndex] = refCount;
425 refCount += refs.byteLength / OID_BYTES;
426 }
427 refStarts[args.objectCount] = refCount;
428 
429 const totalBytes =
430 PACK_REF_HEADER_BYTES + args.objectCount + (args.objectCount + 1) * 4 + refCount * OID_BYTES;
431 const out = new Uint8Array(totalBytes);
432 const dv = new DataView(out.buffer, out.byteOffset, out.byteLength);
433 let pos = 0;
434 
435 dv.setUint32(pos, PACK_REF_MAGIC, false);
436 pos += 4;
437 dv.setUint32(pos, PACK_REF_VERSION, false);
438 pos += 4;
439 dv.setUint32(pos, args.objectCount, false);
440 pos += 4;
441 writeUint64(dv, pos, args.packBytes);
442 pos += 8;
443 out.set(args.packChecksum, pos);
444 pos += OID_BYTES;
445 out.set(args.idxChecksum, pos);
446 pos += OID_BYTES;
447 
448 for (let sortedIndex = 0; sortedIndex < sortedIndices.length; sortedIndex++) {
449 out[pos++] = args.table.objectTypes[sortedIndices[sortedIndex]];
450 }
451 
452 for (let index = 0; index < refStarts.length; index++) {
453 dv.setUint32(pos, refStarts[index], false);
454 pos += 4;
455 }
456 
457 for (let sortedIndex = 0; sortedIndex < sortedIndices.length; sortedIndex++) {
458 const entryIndex = sortedIndices[sortedIndex];
459 const refs = this.rawRefsByEntry[entryIndex];
460 if (refs === undefined) {
461 throw new Error(`ref-index: missing logical refs for entry ${entryIndex}`);
462 }
463 out.set(refs, pos);
464 pos += refs.byteLength;
465 }
466 
467 return {
468 bytes: out,
469 objectCount: args.objectCount,
470 refCount,
471 refIndexBytes: out.byteLength,
472 };
473 }
474}
475 
476export function parsePackRefView(
477 packKey: string,
478 bytes: Uint8Array,
479 idx: IdxView
480): PackRefViewLoadResult {
481 if (bytes.byteLength < PACK_REF_HEADER_BYTES) {
482 return invalid("corrupt", "truncated-header");
483 }
484 
485 const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
486 let pos = 0;
487 if (dv.getUint32(pos, false) !== PACK_REF_MAGIC) return invalid("corrupt", "bad-magic");
488 pos += 4;
489 if (dv.getUint32(pos, false) !== PACK_REF_VERSION) {
490 return invalid("corrupt", "bad-version");
491 }
492 pos += 4;
493 
494 const objectCount = dv.getUint32(pos, false);
495 pos += 4;
496 if (objectCount !== idx.count) return invalid("stale", "object-count-mismatch");
497 
498 let packBytes: number;
499 try {
500 packBytes = readUint64(dv, pos);
501 } catch {
502 return invalid("corrupt", "pack-bytes-overflow");
503 }
504 pos += 8;
505 if (packBytes !== idx.packSize) return invalid("stale", "pack-bytes-mismatch");
506 
507 const packChecksum = bytes.subarray(pos, pos + OID_BYTES);
508 pos += OID_BYTES;
509 if (!bytesEqual(packChecksum, idx.packChecksum)) {
510 return invalid("stale", "pack-checksum-mismatch");
511 }
512 
513 const idxChecksum = bytes.subarray(pos, pos + OID_BYTES);
514 pos += OID_BYTES;
515 if (!bytesEqual(idxChecksum, idx.idxChecksum)) {
516 return invalid("stale", "idx-checksum-mismatch");
517 }
518 
519 const typeCodesEnd = pos + objectCount;
520 if (typeCodesEnd > bytes.byteLength) return invalid("corrupt", "truncated-type-codes");
521 const typeCodes = bytes.subarray(pos, typeCodesEnd);
522 pos = typeCodesEnd;
523 for (let index = 0; index < typeCodes.length; index++) {
524 if (!isValidObjectTypeCode(typeCodes[index])) {
525 return invalid("corrupt", "invalid-type-code");
526 }
527 }
528 
529 const startsBytes = (objectCount + 1) * 4;
530 if (pos + startsBytes > bytes.byteLength) return invalid("corrupt", "truncated-ref-starts");
531 const refStartsBytes = bytes.subarray(pos, pos + startsBytes);
532 const refStartsView = new DataView(
533 refStartsBytes.buffer,
534 refStartsBytes.byteOffset,
535 refStartsBytes.byteLength
536 );
537 let previous = 0;
538 for (let index = 0; index < objectCount + 1; index++) {
539 const value = dv.getUint32(pos + index * 4, false);
540 if (index > 0 && value < previous) {
541 return invalid("corrupt", "non-monotonic-ref-starts");
542 }
543 previous = value;
544 }
545 pos += startsBytes;
546 
547 const rawRefs = bytes.subarray(pos);
548 const finalRefOffset = refStartsView.getUint32(objectCount * 4, false);
549 if (finalRefOffset * OID_BYTES !== rawRefs.byteLength) {
550 return invalid("corrupt", "invalid-final-ref-offset");
551 }
552 
553 return {
554 type: "Ready",
555 view: {
556 packKey,
557 objectCount,
558 packBytes,
559 packChecksum,
560 idxChecksum,
561 typeCodes,
562 refStartsBytes,
563 refStartsView,
564 rawRefs,
565 },
566 };
567}
568 
569export function getPackRefTypeCode(view: PackRefView, oidIndex: number): number | undefined {
570 if (oidIndex < 0 || oidIndex >= view.objectCount) return undefined;
571 return view.typeCodes[oidIndex];
572}
573 
574export function getPackRefObjectType(
575 view: PackRefView,
576 oidIndex: number
577): GitObjectType | undefined {
578 const typeCode = getPackRefTypeCode(view, oidIndex);
579 if (typeCode === undefined) return undefined;
580 return typeCodeToObjectType(typeCode) ?? undefined;
581}
582 
583function getPackRefStart(view: PackRefView, oidIndex: number): number {
584 return view.refStartsView.getUint32(oidIndex * 4, false);
585}
586 
587export function getPackRefRawRefAt(
588 view: PackRefView,
589 oidIndex: number,
590 refOffset: number
591): Uint8Array | undefined {
592 if (oidIndex < 0 || oidIndex >= view.objectCount || refOffset < 0) return undefined;
593 const refStart = getPackRefStart(view, oidIndex);
594 const refEnd = getPackRefStart(view, oidIndex + 1);
595 const refIndex = refStart + refOffset;
596 if (refIndex >= refEnd) return undefined;
597 const byteStart = refIndex * OID_BYTES;
598 return view.rawRefs.subarray(byteStart, byteStart + OID_BYTES);
599}
600 
601export function visitPackRefRawRefsAt(
602 view: PackRefView,
603 oidIndex: number,
604 visit: (rawRefs: Uint8Array, oidStart: number) => void
605): void {
606 if (oidIndex < 0 || oidIndex >= view.objectCount) return;
607 const refStart = getPackRefStart(view, oidIndex);
608 const refEnd = getPackRefStart(view, oidIndex + 1);
609 
610 for (let refIndex = refStart; refIndex < refEnd; refIndex++) {
611 visit(view.rawRefs, refIndex * OID_BYTES);
612 }
613}
614 
615export function getPackRefRefsAt(view: PackRefView, oidIndex: number): string[] {
616 if (oidIndex < 0 || oidIndex >= view.objectCount) return [];
617 const refs: string[] = [];
618 
619 visitPackRefRawRefsAt(view, oidIndex, (rawRefs, byteStart) => {
620 refs.push(bytesToHex(rawRefs.subarray(byteStart, byteStart + OID_BYTES)));
621 });
622 
623 return refs;
624}
625 
626export async function loadPackRefView(
627 env: Env,
628 packKey: string,
629 idx: IdxView,
630 cacheCtx?: CacheContext
631): Promise<PackRefViewLoadResult> {
632 if (cacheCtx && !cacheCtx.memo) {
633 cacheCtx.memo = {};
634 }
635 
636 const cacheKey = getPackRefViewCacheKey(packKey, idx.idxChecksum);
637 const memoView = cacheCtx?.memo?.packRefViews?.get(cacheKey);
638 if (memoView) return { type: "Ready", view: memoView };
639 
640 const cached = packRefViewCache.get(cacheKey);
641 if (cached) {
642 touchPackRefViewCache(cacheKey, cached.view);
643 if (cacheCtx?.memo) {
644 cacheCtx.memo.packRefViews = cacheCtx.memo.packRefViews || new Map();
645 cacheCtx.memo.packRefViews.set(cacheKey, cached.view);
646 }
647 return { type: "Ready", view: cached.view };
648 }
649 
650 const inflight = cacheCtx?.memo?.packRefViewPromises?.get(cacheKey);
651 if (inflight) return await inflight;
652 
653 const log = createLogger(env.LOG_LEVEL, { service: "PackRefIndex" });
654 const limiter = getLimiter(cacheCtx);
655 const promise = (async (): Promise<PackRefViewLoadResult> => {
656 const obj = await limiter.run("r2:get-pack-refs", async () => {
657 if (!countSubrequest(cacheCtx)) {
658 log.warn("soft-budget-exhausted", { op: "r2:get-pack-refs" });
659 }
660 return await env.REPO_BUCKET.get(packRefsKey(packKey));
661 });
662 if (!obj) return { type: "Missing", reason: "missing" };
663 
664 const parsed = parsePackRefView(packKey, new Uint8Array(await obj.arrayBuffer()), idx);
665 if (parsed.type !== "Ready") return parsed;
666 
667 touchPackRefViewCache(cacheKey, parsed.view);
668 if (cacheCtx?.memo) {
669 cacheCtx.memo.packRefViews = cacheCtx.memo.packRefViews || new Map();
670 cacheCtx.memo.packRefViews.set(cacheKey, parsed.view);
671 }
672 return parsed;
673 })();
674 
675 if (cacheCtx?.memo) {
676 cacheCtx.memo.packRefViewPromises = cacheCtx.memo.packRefViewPromises || new Map();
677 cacheCtx.memo.packRefViewPromises.set(cacheKey, promise);
678 }
679 
680 try {
681 return await promise;
682 } finally {
683 cacheCtx?.memo?.packRefViewPromises?.delete(cacheKey);
684 }
685}