// Package cache keys answers by immutable object hash, so it never needs invalidation. package cache import ( "crypto/sha256" "encoding/hex" "os" "path/filepath" "sync" ) // Disk is content-addressed, and deleting the directory is safe because the server rebuilds. type Disk struct { root string mu sync.Mutex hot map[string][]byte } // hotLimit bounds the memory half, where a page reads the same few entries in a row. const hotLimit = 512 func New(root string) *Disk { return &Disk{root: root, hot: map[string][]byte{}} } // Get returns a cached value, or false. func (d *Disk) Get(kind, key string) ([]byte, bool) { if d == nil { return nil, false } k := kind + ":" + key d.mu.Lock() if v, ok := d.hot[k]; ok { d.mu.Unlock() return v, true } d.mu.Unlock() body, err := os.ReadFile(d.path(kind, key)) if err != nil { return nil, false } d.remember(k, body) return body, true } // Put ignores a write failure, because the next reader recomputes, which is the point. func (d *Disk) Put(kind, key string, body []byte) { if d == nil { return } d.remember(kind+":"+key, body) path := d.path(kind, key) if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { return } tmp := path + ".tmp" if err := os.WriteFile(tmp, body, 0o640); err != nil { return } // Rename, so a reader never sees half an answer. _ = os.Rename(tmp, path) } func (d *Disk) remember(k string, body []byte) { d.mu.Lock() defer d.mu.Unlock() if len(d.hot) >= hotLimit { d.hot = map[string][]byte{} } d.hot[k] = body } // path spreads entries over two levels, so no directory holds a million files. func (d *Disk) path(kind, key string) string { sum := sha256.Sum256([]byte(key)) name := hex.EncodeToString(sum[:]) return filepath.Join(d.root, kind, name[:2], name[2:]) }