Files
blacktop e209828c31 feat(diff): add cacheable task-based reports
Refactor diff jobs into task-owned renderers with persistent cache support, root-confined walkers, and quieter Mach-O/Firmware output.
2026-06-04 12:02:46 -06:00

124 lines
3.6 KiB
Go

package storage
import (
"bytes"
"encoding/gob"
"fmt"
"maps"
"sync"
)
// MemoryStore is the in-memory Store used for OTA/Directory diff modes, tests,
// and any run that does not need cross-invocation persistence. Values are gob
// encoded on Put and decoded on Get so the observable behavior matches
// SQLiteStore: Tasks always round-trip through the same serialization, and a
// MemoryStore cache hit cannot accidentally hand back a live struct that a
// SQLite hit would have decoded into a fresh allocation.
type MemoryStore struct {
mu sync.RWMutex
// rows is keyed by Scope, then by row key. The inner map is non-nil for
// any Scope that has ever been written to.
rows map[Scope]map[string][]byte
// complete records which Scopes have been marked complete by their
// owning Task.
complete map[Scope]bool
}
// NewMemoryStore returns an empty in-memory Store. The zero value is not
// usable; callers must go through this constructor so the internal maps are
// allocated up front.
func NewMemoryStore() *MemoryStore {
return &MemoryStore{
rows: make(map[Scope]map[string][]byte),
complete: make(map[Scope]bool),
}
}
// Put gob-encodes v and stores it under (scope, key), replacing any prior
// value.
func (s *MemoryStore) Put(scope Scope, key string, v any) error {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(v); err != nil {
return fmt.Errorf("memory store: encode %s/%s: %w", scope.Task, key, err)
}
s.mu.Lock()
defer s.mu.Unlock()
bucket, ok := s.rows[scope]
if !ok {
bucket = make(map[string][]byte)
s.rows[scope] = bucket
}
// Copy the encoded bytes so callers may reuse the input buffer freely.
enc := make([]byte, buf.Len())
copy(enc, buf.Bytes())
bucket[key] = enc
return nil
}
// Get loads the value at (scope, key) into v. found is false when no row
// exists; err is non-nil only on decode failures.
func (s *MemoryStore) Get(scope Scope, key string, v any) (bool, error) {
s.mu.RLock()
bucket, ok := s.rows[scope]
if !ok {
s.mu.RUnlock()
return false, nil
}
enc, ok := bucket[key]
s.mu.RUnlock()
if !ok {
return false, nil
}
if err := gob.NewDecoder(bytes.NewReader(enc)).Decode(v); err != nil {
return true, fmt.Errorf("memory store: decode %s/%s: %w", scope.Task, key, err)
}
return true, nil
}
// Iter calls fn for every row in scope. Rows are visited under a snapshot
// taken with the read lock held, so fn may freely call back into the store.
func (s *MemoryStore) Iter(scope Scope, fn func(key string, decode func(v any) error) error) error {
s.mu.RLock()
bucket, ok := s.rows[scope]
if !ok {
s.mu.RUnlock()
return nil
}
// Snapshot keys + payload pointers so fn may mutate the store via other
// Scopes without deadlocking. Values are immutable byte slices.
snapshot := make(map[string][]byte, len(bucket))
maps.Copy(snapshot, bucket)
s.mu.RUnlock()
for k, enc := range snapshot {
decode := func(v any) error {
if err := gob.NewDecoder(bytes.NewReader(enc)).Decode(v); err != nil {
return fmt.Errorf("memory store: decode %s/%s: %w", scope.Task, k, err)
}
return nil
}
if err := fn(k, decode); err != nil {
return err
}
}
return nil
}
// MarkComplete records that every required row for scope has been written.
func (s *MemoryStore) MarkComplete(scope Scope) error {
s.mu.Lock()
s.complete[scope] = true
s.mu.Unlock()
return nil
}
// Complete reports whether MarkComplete was previously called for scope.
func (s *MemoryStore) Complete(scope Scope) (bool, error) {
s.mu.RLock()
done := s.complete[scope]
s.mu.RUnlock()
return done, nil
}
// Close is a no-op for MemoryStore.
func (s *MemoryStore) Close() error { return nil }