| 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 | */ |
| 9 | import type * as Effect from "effect/Effect"; |
| 10 | import * as Schema from "effect/Schema"; |
| 11 | import type * as Stream from "effect/Stream"; |
| 12 | import 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 | */ |
| 19 | export 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 | */ |
| 28 | export 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 | */ |
| 35 | export 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 | */ |
| 50 | export 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 | */ |
| 60 | export 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 | */ |
| 103 | export 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 | */ |
| 116 | export 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 | */ |
| 127 | export 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 | */ |
| 152 | export 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 | */ |
| 172 | export interface ClosureSource { |
| 173 | readonly commitClosure: ( |
| 174 | request: ClosureRequest, |
| 175 | ) => Effect.Effect<ClosureResult, StoreError>; |
| 176 | } |