///|
/// The storage contract every backend implements.
///
/// Async-only by design. Every real backend (S3, R2, GCS) is async, and an
/// in-memory or on-disk backend satisfies an async method with a body that
/// never suspends — so one trait keeps callers on a single code path rather
/// than forcing a sync and an async copy of everything above it.
pub(open) trait ObjectStore {
/// Read a whole object.
async fn get(Self, String) -> GetOutcome raise @bit.GitError
/// Read a byte range. Backends that cannot serve ranges must still return
/// the requested slice, reading more than they need if necessary.
async fn get_range(Self, String, ByteRange) -> GetOutcome raise @bit.GitError
/// Conditional read. Returns `NotModified` when the caller's version tag is
/// still current, which is how a replica cheaply checks the manifest.
async fn get_if_none_match(Self, String, String) -> GetOutcome raise @bit.GitError
/// Write, subject to a precondition.
async fn put(Self, String, Bytes, PutCondition) -> PutOutcome raise @bit.GitError
/// Remove a key. Deleting an absent key succeeds.
async fn delete(Self, String) -> Unit raise @bit.GitError
/// List one page of keys under a prefix, starting after the given key
/// (empty string for the first page).
async fn list(Self, String, String) -> ObjListing raise @bit.GitError
/// Whether this backend enforces write preconditions. A store that answers
/// `false` cannot host a WAL: its CAS would silently degrade to a
/// last-writer-wins overwrite.
fn supports_cas(Self) -> Bool
}
///|
/// Read every page of a listing under `prefix`.
///
/// Callers that need the whole key set (a checkpoint fold, a GC sweep) should
/// use this rather than paging by hand.
pub async fn[S : ObjectStore] list_all(
store : S,
prefix : String,
) -> Array[ObjEntry] raise @bit.GitError {
let out : Array[ObjEntry] = []
let mut after = ""
for _ in 0.. after = token
None => return out
}
}
raise @bit.GitError::IoError(
"listing \{prefix} exceeded \{MAX_LIST_PAGES} pages",
)
}
///|
/// Bound on `list_all` so a store that keeps handing back continuation tokens
/// fails loudly instead of looping forever.
const MAX_LIST_PAGES : Int = 10000
///|
/// Write a content-addressed object, treating "already there" as success.
///
/// Packs and snapshots are named by their own hash, so a create-only PUT that
/// loses the race wrote identical bytes. This is what makes the pre-CAS steps
/// of a push safe to repeat after an `Unknown`.
pub async fn[S : ObjectStore] put_immutable(
store : S,
key : String,
body : Bytes,
) -> PutOutcome raise @bit.GitError {
match store.put(key, body, PutCondition::IfNotExists) {
// Someone else wrote the same content-addressed bytes first.
NotApplied => PutOutcome::Applied("")
other => other
}
}