Caching
createCachedStore wraps any ObjectStore with an in-process LRU. Git
objects are content-addressed, so a hit is always correct data forever —
refs and other mutable paths need a much shorter TTL instead, via
ttlForKey (below).
createCachedStorecreateCachedStore(store: ObjectStore, options?: CacheOptions): CachedObjectStore| Parameter | Type | Description |
|---|---|---|
store | ObjectStore | The store to wrap. |
options? | CacheOptions | See below. |
Returns: CachedObjectStore
A CachedObjectStore is a normal ObjectStore plus one extra method:
invalidate(prefix).
Options
Section titled “Options”| Option | Default | What it does |
|---|---|---|
maxBytes |
50 MiB | Total cache budget. |
maxEntryBytes |
maxBytes / 10 |
Largest single entry admitted — keeps one huge packfile from evicting the whole working set. |
ttlMs |
60 000 | Entry time-to-live. |
ttlForKey |
— | Per-key/prefix TTL override — see below. |
cacheMisses |
false |
Also cache “not found” results. |
cacheLists |
false |
Also cache list() results (directory listings, existence probes). |
coalesce |
true |
Collapse concurrent identical requests into one backend call. |
onHit / onMiss |
— | Instrumentation hooks, called with the key. |
ttlForKey
Section titled “ttlForKey”(key: string) => number | undefined — called for every get/head
admission and every list() result, keyed by the object’s key (for
get/head) or the listed prefix (for list()). Return a number to
override ttlMs for that specific entry; return undefined to fall back
to the default.
A single ttlMs tuned for object reads is right for object reads — the
whole point of content addressing is that a hit is correct forever — but it’s
wrong for the handful of keys in a gitdir that are mutable:
- Refs (
refs/heads/<branch>,HEAD) — the same key’s value changes on every push. - Directory listings under
objects/(particularlyobjects/pack/) — the set of packs grows on every push, even though no individual pack’s bytes ever change.
A ref is one small object and a pack listing is one bounded list() call,
so re-checking either far more often than ttlMs costs little. Everything
downstream of a fresh ref or listing — the tree, commit, and blob reads
keyed by the sha it resolves to, or the specific pack name a fresh listing
named — still gets the full ttlMs cache benefit once the structure itself
is current:
const cached = createCachedStore(store, { ttlMs: 3_600_000, // 1h — safe for object reads ttlForKey: (key) => /\/(HEAD|refs(\/|$)|objects\/(pack\/)?$)/.test(key) ? 5_000 : undefined,});cacheMisses
Section titled “cacheMisses”Loose-object probes on a packed repository are always misses — every one of them is otherwise a wasted round trip on every single request. Turning this on is close to free performance, with one condition:
cacheLists
Section titled “cacheLists”Caches list() results — which backs both readdir() and existence
probes. Writes made through this same cached store keep listings
consistent automatically. There’s one deliberate asymmetry worth knowing:
A non-empty limit: 1 probe (the shape of “does this directory have
anything in it”) survives writes underneath it — a write can add entries to
a directory, but it can’t make a directory that already has something in it
stop existing, so that specific cached answer stays valid regardless.
After a write that happens outside this cached store (another process, a
bulk sync), call invalidate(prefix) — see
the production stack guide.
coalesce
Section titled “coalesce”Multiple concurrent callers requesting the same key collapse into one backend request, sharing the result. On by default — a page render that fans out N requests for the same object (e.g. N callers all resolving the same ref) pays for one network round trip, not N.
The staleness contract, plainly
Section titled “The staleness contract, plainly”cacheMisses and cacheLists trade read traffic for a window — bounded by
ttlMs — in which another process’s writes are invisible here. This is
fine in exactly two situations: each repo is served by one process at a
time, or every external write path calls invalidate(). Outside those two,
don’t turn them on.