1/**
2 * Abstract storage interfaces the protocol choreography is written against.
3 *
4 * The concrete implementations live in `src/store/` (DO SQLite + R2); the
5 * protocol layer (`UploadPack`, `ReceivePack`, `PackParser`, `PackWriter`,
6 * `Advertise`) depends only on these data-shaped contracts, which keeps the
7 * entire `src/Git/Protocol/` layer unit-testable in plain bun with in-memory fakes.
8 */
9import type * as Effect from "effect/Effect";
10import * as Schema from "effect/Schema";
11import type * as Stream from "effect/Stream";
12import type { ObjectType, Oid } from "./ObjectCodec.ts";
13
14/**
15 * Error produced by any of the abstract store interfaces. Concrete
16 * implementations wrap their own failures (SQL, R2) into this tag so the
17 * protocol layer's error unions stay closed.
18 */
19export class StoreError extends Schema.TaggedError<StoreError>()("StoreError", {
20 reason: Schema.String,
21}) {}
22
23/**
24 * Where an object's compressed bytes live (mirrors the `objects.location`
25 * column): `row` = SQLite BLOB, `r2` = oversize loose object in R2,
26 * `pack` = v1.1 compacted pack in R2.
27 */
28export type ObjectLocation = "row" | "r2" | "pack";
29
30/**
31 * Metadata for one stored object — also the shape of a pack manifest entry
32 * (the fetch closure materializes a list of these, and `PackWriter` streams
33 * from it).
34 */
35export interface ObjectMeta {
36 readonly oid: Oid;
37 readonly type: ObjectType;
38 /** Uncompressed size in bytes. */
39 readonly size: number;
40 /** Stored (zlib-compressed) size in bytes. */
41 readonly zsize: number;
42 /** Storage tier the zdata lives in. */
43 readonly location: ObjectLocation;
44}
45
46/**
47 * One entry of a fetch-pack manifest. Alias of {@link ObjectMeta} — the
48 * closure walk produces exactly the metadata the pack emitter needs.
49 */
50export type ManifestEntry = ObjectMeta;
51
52/**
53 * Read access to the object store, as needed by pack parsing (thin-pack base
54 * resolution), pack writing (zdata streaming), negotiation (have existence),
55 * and the push connectivity check.
56 *
57 * All byte payloads are `zlib(content)` **without** the loose header, so pack
58 * emission is `varint(type,size) + zdata` verbatim.
59 */
60export interface ObjectSource {
61 /** Whether an object exists (live objects only, not staged). */
62 readonly has: (oid: Oid) => Effect.Effect<boolean, StoreError>;
63 /**
64 * Filters the given oids down to the subset that exists in the store.
65 * Implementations batch with chunked `IN` lists; order is not guaranteed.
66 */
67 readonly filterExisting: (
68 oids: ReadonlyArray<Oid>,
69 ) => Effect.Effect<ReadonlyArray<Oid>, StoreError>;
70 /** Object metadata, or `undefined` when the object does not exist. */
71 readonly getMeta: (
72 oid: Oid,
73 ) => Effect.Effect<ObjectMeta | undefined, StoreError>;
74 /**
75 * Streams the stored compressed bytes (`zlib(content)`, no loose header)
76 * regardless of location (row BLOB, R2 get, v1.1 ranged pack read).
77 */
78 readonly readZData: (oid: Oid) => Stream.Stream<Uint8Array, StoreError>;
79 /**
80 * Reads and inflates the full object content. Used for thin-pack base
81 * resolution and tree/commit walks; callers must respect the per-object
82 * size cap before requesting.
83 */
84 readonly readContent: (oid: Oid) => Effect.Effect<Uint8Array, StoreError>;
85 /**
86 * Emits `varint(type,size) + zdata` for every entry as a stream of LARGE
87 * chunks: objects are read in batches (one statement per ~256) and
88 * concatenated, and pack-resident objects are visited in pack order so
89 * the window cache is sequential. Optional — sources without batching
90 * fall back to one `readZData` per entry (DESIGN §22).
91 */
92 readonly packEntries?:
93 | ((
94 entries: ReadonlyArray<ManifestEntry>,
95 ) => Stream.Stream<Uint8Array, StoreError>)
96 | undefined;
97}
98
99/**
100 * One advertised ref: full name, tip oid, and — for annotated tags — the
101 * peeled target commit oid (advertised as a `^{}` line).
102 */
103export interface RefRecord {
104 /** Full ref name, e.g. `refs/heads/main`, `refs/tags/v1`. */
105 readonly name: string;
106 readonly oid: Oid;
107 /** Peeled target for annotated tags (`refs/tags/x^{}` advertisement). */
108 readonly peeled?: Oid | undefined;
109}
110
111/**
112 * A point-in-time snapshot of the ref namespace used to build the v0
113 * advertisement. `HEAD` is virtual: it resolves to
114 * `refs/heads/<defaultBranch>` when that ref exists.
115 */
116export interface RefsSnapshot {
117 /** The repo's default branch short name, e.g. `main`. */
118 readonly defaultBranch: string;
119 /** All refs, sorted by name (the advertisement preserves this order). */
120 readonly refs: ReadonlyArray<RefRecord>;
121}
122
123/**
124 * Inputs to the fetch closure computation (implemented over the commit graph
125 * in `src/Git/Store/Closure.ts`).
126 */
127export interface ClosureRequest {
128 /** Requested tips (from `want` lines). */
129 readonly wants: ReadonlyArray<Oid>;
130 /**
131 * Common commits negotiated with the client (the ACKed haves). The walk
132 * stops at these boundaries.
133 */
134 readonly haves: ReadonlyArray<Oid>;
135 /** Depth bound from `deepen <n>`, when the client requested a shallow fetch. */
136 readonly depth?: number | undefined;
137 /** The client's existing shallow tips (additional walk boundaries). */
138 readonly clientShallow: ReadonlyArray<Oid>;
139 /**
140 * The REPO's own shallow boundary (commits whose parents were never
141 * ingested — a depth-limited import). These never expand in any walk and
142 * always surface as `shallow <oid>` lines when included in a pack, so
143 * clones of a shallow repo are themselves shallow (and fsck-clean).
144 */
145 readonly repoShallow?: ReadonlyArray<Oid> | undefined;
146}
147
148/**
149 * The result of a closure computation: the pack manifest plus the shallow
150 * boundary bookkeeping for the response's shallow section.
151 */
152export interface ClosureResult {
153 /**
154 * Every object to pack (commits + trees + blobs + tags), each exactly once.
155 * Materialized before streaming so the pack header count is known upfront.
156 */
157 readonly entries: ReadonlyArray<ManifestEntry>;
158 /** Commits that became shallow boundaries (`shallow <oid>` lines). */
159 readonly shallow: ReadonlyArray<Oid>;
160 /**
161 * Previously-shallow client tips that are now complete
162 * (`unshallow <oid>` lines).
163 */
164 readonly unshallow: ReadonlyArray<Oid>;
165}
166
167/**
168 * Computes the object closure for a fetch: BFS from wants over the commit
169 * graph stopping at haves (generation-bounded), depth-bounded when shallow,
170 * plus the full tree/blob closure of the frontier commits.
171 */
172export interface ClosureSource {
173 readonly commitClosure: (
174 request: ClosureRequest,
175 ) => Effect.Effect<ClosureResult, StoreError>;
176}