A Go-based unified package cache supporting NPM packages and Go modules, with content-addressable storage using BLAKE3, multiple storage backends, and combined TTL/LRU expiration.
- Files stored by SHA-512 hash at
~/.pnpm-store/v3/files/{hash[0:2]}/{hash} - File-level deduplication across all packages
- Hardlinks/reflinks to project's
node_modules
- Structure:
{module}/@v/{version}.{info|mod|zip|ziphash} - Protocol endpoints:
/@v/list,/@v/{version}.info,/@v/{version}.mod,/@v/{version}.zip .ziphashcontainsh1:base64(sha256)for integrity
We can unify these: store all content in CAFS, then provide protocol adapters that expose the appropriate interface for each package type.
content-cache/
├── cache.go # Main Cache interface
├── hash.go # BLAKE3 hashing
├── store/
│ ├── store.go # Content-addressable store interface
│ └── cafs.go # CAFS implementation
├── backend/
│ ├── backend.go # Storage backend interface
│ ├── filesystem.go # Local filesystem
│ └── s3.go # S3/Minio
├── protocol/
│ ├── npm/
│ │ ├── registry.go # NPM registry protocol handler
│ │ └── tarball.go # Tarball extraction/storage
│ └── goproxy/
│ ├── proxy.go # GOPROXY protocol handler
│ └── module.go # Module metadata handling
├── expiry/
│ └── expiry.go # TTL + LRU expiration
├── compress/
│ └── zstd.go # Compression
└── server/
└── http.go # HTTP server exposing all protocols
All package content ultimately stored as content-addressed blobs:
// Hash is a BLAKE3 256-bit digest
type Hash [32]byte
func (h Hash) String() string // hex encoding
func (h Hash) Dir() string // first 2 chars for sharding
// Store provides content-addressable storage
type Store interface {
// Put stores content, returns its hash
Put(ctx context.Context, r io.Reader) (Hash, error)
// Get retrieves content by hash
Get(ctx context.Context, h Hash) (io.ReadCloser, error)
// Has checks if content exists
Has(ctx context.Context, h Hash) (bool, error)
// Delete removes content
Delete(ctx context.Context, h Hash) error
}Storage layout:
blobs/
├── ab/
│ └── abcd1234... # full hash as filename
├── cd/
│ └── cdef5678...
└── ...
Each package type gets a protocol adapter that translates to/from CAFS:
type NPMRegistry interface {
// GetPackageMetadata returns package.json metadata
GetPackageMetadata(ctx context.Context, name string) (*PackageMetadata, error)
// GetTarball retrieves a package tarball
GetTarball(ctx context.Context, name, version string) (io.ReadCloser, error)
// PutTarball stores a package tarball (from upstream)
PutTarball(ctx context.Context, name, version string, r io.Reader) error
}NPM index storage (maps package@version → content hash):
npm/
├── lodash/
│ ├── metadata.json # combined package metadata
│ └── versions/
│ ├── 4.17.21.json # version-specific metadata + content hash
│ └── 4.17.20.json
type GoProxy interface {
// List returns available versions
List(ctx context.Context, module string) ([]string, error)
// Info returns version metadata
Info(ctx context.Context, module, version string) (*VersionInfo, error)
// Mod returns go.mod content
Mod(ctx context.Context, module, version string) ([]byte, error)
// Zip returns module zip
Zip(ctx context.Context, module, version string) (io.ReadCloser, error)
// Store caches a module from upstream
Store(ctx context.Context, module, version string, info, mod, zip io.Reader) error
}Go module index storage:
goproxy/
├── github.com/
│ └── pkg/
│ └── errors/
│ └── @v/
│ ├── list # newline-separated versions
│ ├── v0.9.1.info # {"Version":"...","Time":"...","ZipHash":"blake3:..."}
│ ├── v0.9.1.mod # go.mod content (or hash reference)
│ └── v0.9.1.ziphash # hash of zip in CAFS
Single server exposing multiple protocols:
GET /npm/{package} → Package metadata
GET /npm/{package}/-/{tarball} → Tarball download
GET /goproxy/{module}/@v/list → Version list
GET /goproxy/{module}/@v/{ver}.info → Version info
GET /goproxy/{module}/@v/{ver}.mod → go.mod
GET /goproxy/{module}/@v/{ver}.zip → Module zip
GET /health → Health check
GET /metrics → Prometheus metrics
Cache acts as a pull-through cache:
- Request comes in for package/module
- Check local cache (CAFS + index)
- If miss: fetch from upstream, store in CAFS, update index
- Return content
type Upstream interface {
Fetch(ctx context.Context, req Request) (Response, error)
}
// NPM upstream: registry.npmjs.org
// Go upstream: proxy.golang.org (or direct VCS)Combined TTL + LRU with reference counting:
type ExpiryManager interface {
// Touch updates access time for content
Touch(ctx context.Context, h Hash) error
// Run starts background expiration
Run(ctx context.Context) error
}- TTL: Content expires N days after last access
- LRU: When cache exceeds max size, evict least-recently-used
- Reference counting: Index entries reference content hashes; content only deleted when unreferenced
- NPM storage: Store whole tarballs (simpler, faster serving, deduplicated at tarball level)
- First protocol: GOPROXY (simpler protocol, validates CAFS before NPM complexity)
hash.go- BLAKE3 Hash type with streaming computationbackend/backend.go- Backend interfacebackend/filesystem.go- Filesystem backend with atomic writesstore/cafs.go- Content-addressable store on top of backend
protocol/goproxy/proxy.go- GOPROXY HTTP handlerprotocol/goproxy/module.go- Module index managementserver/http.go- HTTP server with GOPROXY routes- Upstream proxy to proxy.golang.org
- Integration test:
GOPROXY=http://localhost:8080 go mod download
expiry/expiry.go- TTL + LRU manager- Metadata storage for access times
- Background garbage collection
protocol/npm/registry.go- NPM registry HTTP handlerprotocol/npm/tarball.go- Tarball handling (store whole tarballs in CAFS)- Upstream proxy to registry.npmjs.org
- Package metadata caching
- Integration test:
npm config set registry http://localhost:8080/npm/ && npm install
backend/s3.go- S3/Minio backend- Multipart upload support for large files
compress/zstd.go- zstd compression- Transparent compression in CAFS layer
- OpenTelemetry tracing
- Prometheus metrics
- Structured logging (slog)
| File | Purpose |
|---|---|
cache.go |
Main entry point, configuration |
hash.go |
BLAKE3 Hash type |
store/store.go |
Store interface |
store/cafs.go |
CAFS implementation |
backend/backend.go |
Backend interface |
backend/filesystem.go |
Filesystem backend |
backend/s3.go |
S3 backend |
protocol/goproxy/proxy.go |
GOPROXY handler |
protocol/goproxy/module.go |
Module index |
protocol/npm/registry.go |
NPM handler |
protocol/npm/tarball.go |
Tarball handling |
server/http.go |
HTTP server |
expiry/expiry.go |
Expiration manager |
compress/zstd.go |
Compression |
- Unit tests for CAFS operations (put/get/delete)
- Integration tests with
gocommand against GOPROXY endpoints - Integration tests with
npm/pnpmagainst NPM endpoints - Benchmark tests for throughput and latency
- Example: Set up as local proxy, run
go mod downloadandnpm install
# Start the cache server
$ content-cache serve --listen :8080 --storage ./cache
# Configure Go to use it
$ export GOPROXY=http://localhost:8080/goproxy,direct
# Configure npm to use it
$ npm config set registry http://localhost:8080/npm/
# Now installs are cached locally
$ go mod download
$ npm install