PushStack Docs
git-fs-s3

Caching

createCachedStore's in-process LRU read cache — options, staleness guarantees, and when to reach for each.

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).

createCachedStore(store: ObjectStore, options?: CacheOptions): CachedObjectStore
ParameterTypeDescription
storeObjectStoreThe store to wrap.
options?CacheOptionsSee below.

Returns: CachedObjectStore

A CachedObjectStore is a normal ObjectStore plus one extra method: invalidate(prefix).

Options

OptionDefaultWhat it does
maxBytes50 MiBTotal cache budget.
maxEntryBytesmaxBytes / 10Largest single entry admitted — keeps one huge packfile from evicting the whole working set.
ttlMs60 000Entry time-to-live.
ttlForKey—Per-key/prefix TTL override — see below.
cacheMissesfalseAlso cache "not found" results.
cacheListsfalseAlso cache list() results (directory listings, existence probes).
coalescetrueCollapse concurrent identical requests into one backend call.
copytrueReturn a fresh copy per hit so callers can't mutate cached bytes. Set false when readers never mutate (isomorphic-git doesn't) — otherwise every hit copies the whole entry, megabytes for a packfile.
onHit / onMiss—Instrumentation hooks, called with the key.

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/ (particularly objects/pack/) — the set of packs grows on every push, even though no individual pack's bytes ever change.

This cache is in-process with no cross-instance invalidation broadcast — invalidate() only clears the instance that calls it (typically the one that just handled a push). A different warm instance that had already cached a ref, or a pack directory's listing, keeps serving that pre-push view for the rest of ttlMs — not because anything is wrong with its own invalidation logic, but because it was never told to re-check. For a pack listing specifically, this doesn't just mean a stale ref: it means a prefetch/pack-lookup pass can fail to discover a freshly pushed pack exists at all, and misreport its objects as missing from storage rather than simply not-yet-listed.

classifyGitdirPath turns a gitdir-relative path into a typed entry (head, refs, pack-dir, pack-file, loose-object, …) and isMutableGitdirEntry is true exactly for the mutable ones above.

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:

import { classifyGitdirPath, isMutableGitdirEntry } from 'git-fs-s3';

// Keys here are `repos/<owner>/<repo>/git/<path inside the gitdir>`.
const pathInGitdir = (key: string) => key.split('/').slice(4).join('/');

const cached = createCachedStore(store, {
  ttlMs: 3_600_000, // 1h — safe for object reads
  ttlForKey: (key) =>
    isMutableGitdirEntry(classifyGitdirPath(pathInGitdir(key)))
      ? 5_000
      : undefined,
});

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:

Only safe when this process is the sole writer to the backend. If another process (or another instance) writes a key this process just cached as "missing," that write is invisible here until ttlMs expires or someone calls invalidate().

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

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

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.

On this page