Skip to content
File

Blob: src/worker/git/operations/read/objects.ts

typescript134 lines
1import type { CacheContext } from "@/worker/cache";
2import type { TreeEntry } from "./types";
3 
4import { buildObjectCacheKey, cacheOrLoadObject } from "@/worker/cache";
5import { createBlobFromBytes, createLogger } from "@/worker/common";
6import { readObject } from "@/worker/git/object-store";
7 
8type LooseObjectRead = {
9 type: string;
10 payload: Uint8Array;
11};
12 
13function ensureMemo(cacheCtx: CacheContext | undefined, repoId: string) {
14 if (!cacheCtx) return;
15 if (!cacheCtx.memo || (cacheCtx.memo.repoId && cacheCtx.memo.repoId !== repoId)) {
16 cacheCtx.memo = { repoId };
17 return;
18 }
19 if (!cacheCtx.memo.repoId) cacheCtx.memo.repoId = repoId;
20}
21 
22export function parseTree(buf: Uint8Array): TreeEntry[] {
23 const td = new TextDecoder();
24 const out: TreeEntry[] = [];
25 let i = 0;
26 while (i < buf.length) {
27 let sp = i;
28 while (sp < buf.length && buf[sp] !== 0x20) sp++;
29 if (sp >= buf.length) break;
30 const mode = td.decode(buf.subarray(i, sp));
31 let nul = sp + 1;
32 while (nul < buf.length && buf[nul] !== 0x00) nul++;
33 if (nul + 20 > buf.length) break;
34 const name = td.decode(buf.subarray(sp + 1, nul));
35 const oidBytes = buf.subarray(nul + 1, nul + 21);
36 const oid = [...oidBytes].map((b) => b.toString(16).padStart(2, "0")).join("");
37 out.push({ mode, name, oid });
38 i = nul + 21;
39 }
40 return out;
41}
42 
43/**
44 * Pack-first object reader. Reads git objects from the active pack catalog
45 * via the worker-local object store.
46 */
47export async function readLooseObjectRaw(
48 env: Env,
49 repoId: string,
50 oid: string,
51 cacheCtx?: CacheContext
52): Promise<{ type: string; payload: Uint8Array } | undefined> {
53 const oidLc = oid.toLowerCase();
54 ensureMemo(cacheCtx, repoId);
55 
56 if (cacheCtx?.memo?.objects?.has(oidLc)) {
57 return cacheCtx.memo.objects.get(oidLc);
58 }
59 
60 const logger = createLogger(env.LOG_LEVEL, {
61 service: "readObjectRaw",
62 repoId,
63 });
64 // Visibility-aware cache bypass. Private route handlers set both
65 // `no-cache-read` and `no-cache-write` on `cacheCtx.memo.flags` so this
66 // path (and the JSON cache helpers at the route layer) skip the shared
67 // Workers Cache entirely. Either flag forces the bypass branch; there is
68 // no asymmetric "read but don't write" use case in our policy.
69 const bypassCacheRead = cacheCtx?.memo?.flags?.has("no-cache-read") === true;
70 const bypassCacheWrite = cacheCtx?.memo?.flags?.has("no-cache-write") === true;
71 const bypassCache = bypassCacheRead || bypassCacheWrite;
72 
73 const loadFromPacks = async (): Promise<LooseObjectRead | undefined> => {
74 const packed = await readObject(env, repoId, oidLc, cacheCtx);
75 if (packed) {
76 logger.debug("object-read", {
77 source: "pack-catalog",
78 oid: oidLc,
79 type: packed.type,
80 packKey: packed.packKey,
81 });
82 return { type: packed.type, payload: packed.payload };
83 }
84 return undefined;
85 };
86 
87 const storeMemoized = (value: LooseObjectRead | undefined) => {
88 if (!cacheCtx?.memo) return;
89 cacheCtx.memo.objects = cacheCtx.memo.objects || new Map();
90 cacheCtx.memo.objects.set(oidLc, value);
91 };
92 
93 if (cacheCtx) {
94 const cacheKey = buildObjectCacheKey(cacheCtx.req, repoId, oidLc);
95 const loaded = bypassCache
96 ? await loadFromPacks()
97 : await cacheOrLoadObject(cacheKey, loadFromPacks, cacheCtx.ctx);
98 
99 storeMemoized(loaded);
100 return loaded;
101 }
102 
103 const loaded = await loadFromPacks();
104 return loaded;
105}
106 
107export async function readBlob(
108 env: Env,
109 repoId: string,
110 oid: string,
111 cacheCtx?: CacheContext
112): Promise<{ content: Uint8Array | null; type: string | null }> {
113 const obj = await readLooseObjectRaw(env, repoId, oid, cacheCtx);
114 if (!obj) return { content: null, type: null };
115 return { content: obj.payload, type: obj.type };
116}
117 
118export async function readBlobStream(
119 env: Env,
120 repoId: string,
121 oid: string,
122 cacheCtx?: CacheContext
123): Promise<Response | null> {
124 const obj = await readLooseObjectRaw(env, repoId, oid, cacheCtx);
125 if (!obj || obj.type !== "blob") return null;
126 return new Response(createBlobFromBytes(obj.payload).stream(), {
127 headers: {
128 "Content-Type": "application/octet-stream",
129 "Cache-Control": "public, max-age=31536000, immutable",
130 ETag: `"${oid.toLowerCase()}"`,
131 },
132 });
133}