The ObjectStore interface
The five-method contract every storage backend implements, and how to write your own.
Everything in git-fs-s3 is built on one small interface. createGitFs doesn't
know or care whether the bytes underneath come from R2, S3, memory, or
something else entirely — it only calls these five methods.
interface ObjectStore {
get(key: string): Promise<Uint8Array | null>;
getRange?(key: string, start: number, end: number): Promise<Uint8Array | null>;
put(key: string, data: Uint8Array): Promise<void>;
delete(key: string): Promise<void>;
head(key: string): Promise<ObjectStat | null>;
list(prefix: string, options?: ListOptions): Promise<ListResult>;
}The five methods above are required. getRange is optional — implement it if
your backend can read a byte range without fetching the whole object.
A missing key is null, never a thrown error. Every layer in the stack —
the git-aware fs, the cache, the retry store — relies on that to
distinguish "doesn't exist" from "the backend failed."
Method contracts
getget(key: string): Promise<Uint8Array | null>| Parameter | Type | Description |
|---|---|---|
key | string | The full object key. |
Returns: Uint8Array | null
The object's bytes, or null if the key doesn't exist. Never throws for a
missing key.
getRangegetRange?(key: string, start: number, end: number): Promise<Uint8Array | null>| Parameter | Type | Description |
|---|---|---|
key | string | The full object key. |
start | number | First byte to return, inclusive. |
end | number | Last byte to return, inclusive. |
Returns: Uint8Array | null
Optional. Bytes start..end of the object, inclusive at both ends,
or null if the key doesn't exist. A store that can't serve ranges leaves
the method undefined, and getObjectSize then reports null ("size
unknown") rather than fetching a whole pack to answer. That fallback is the
point: range reads are what let a caller ask is this object too large?
without paying for the pack that holds it. Exposed on the git-aware fs as
fs.promises.getRange, again only when the backing store has it, and
forwarded by createCachedStore straight to the backing store — a few
bytes at a known offset gain nothing from the cache.
putput(key: string, data: Uint8Array): Promise<void>| Parameter | Type | Description |
|---|---|---|
key | string | The full object key. |
data | Uint8Array | The bytes to store. |
Writes (or overwrites) an object. Object storage has no atomic multi-key transactions — a caller that needs several keys to become visible together needs its own coordination (see Semantics & limitations).
deletedelete(key: string): Promise<void>| Parameter | Type | Description |
|---|---|---|
key | string | The full object key. |
Removes an object. Deleting a key that doesn't exist is not an error.
headhead(key: string): Promise<ObjectStat | null>| Parameter | Type | Description |
|---|---|---|
key | string | The full object key. |
Returns: ObjectStat | null
Existence + size, without transferring the object body — { size: number },
or null if the key doesn't exist.
listlist(prefix: string, options?: ListOptions): Promise<ListResult>| Parameter | Type | Description |
|---|---|---|
prefix | string | Only keys starting with this prefix. |
options? | ListOptions | delimiter (directory-style grouping) and limit. |
Returns: ListResult
{ objects: { key, size }[], prefixes: string[] }. When options.delimiter
is set (git-fs-s3 always uses "/"), keys are grouped: anything after the
first delimiter past prefix collapses into one entry in prefixes
instead of appearing in objects — this is what makes readdir() work
against storage that has no real directories.
Writing your own store
Implement the interface above for any backend and pass it straight to
createGitFs — every other piece (caching, retry, the git-aware layer,
/http, /ops) works unmodified on top of it.
MemoryObjectStore (exported from the root package) is the reference
implementation — read it first. It's also the store used throughout this
package's own test suite, and the one you'll reach for in yours:
import { createGitFs, MemoryObjectStore } from 'git-fs-s3';
const fs = createGitFs(new MemoryObjectStore());Get list()'s delimiter semantics right or nothing above it works —
isomorphic-git calls readdir() constantly, and readdir() is built
entirely on list(prefix, { delimiter: '/' }). Compare your
implementation's behavior against MemoryObjectStore's for the same
inputs before trusting it.
S3ObjectStore
The one storage backend shipped out of the box, over @aws-sdk/client-s3
(an optional peer dependency, loaded only via the /s3 subpath so
consumers who don't need it don't pay for it).
import { S3ObjectStore } from 'git-fs-s3/s3';
const store = new S3ObjectStore({
client, // reuse one S3Client — see the note below
bucket: 'my-git-repos',
prefix: 'repos/', // optional: a fixed prefix under every key
contentType: (key) =>
/\/(HEAD|config)$|\/refs\//.test(key) ? 'text/plain' : undefined,
});Reuse one S3Client across every store you create — a busy repo page can
mean hundreds of object reads, and a shared client pools HTTP
connections instead of reconnecting per store.
S3ObjectStore implements the optional getRange (an S3 Range header,
inclusive at both ends), which getObjectSize uses to answer a size without
fetching the object.