Mining/pkg/mining/repository.go
Claude 93a825c7da
ax(batch): expand abbreviated receiver names (AX Principle 1)
Rename short receivers to predictable full names across all packages:
- lb -> logBuffer (LogBuffer methods in miner.go)
- l -> logger (Logger methods in logging/logger.go)
- l -> level (Level.String() in logging/logger.go)
- h -> histogram (LatencyHistogram methods in metrics.go)
- r -> repository (FileRepository methods in repository.go)

Agents can now grep for the receiver name and get the type without
context. Single-letter 'c', 'm', 's', 'e' kept where idiomatic for
Container, Miner, Store, and Error types per Go convention.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-02 18:05:42 +01:00

159 lines
4 KiB
Go

package mining
import (
"encoding/json"
"os"
"path/filepath"
"sync"
)
// var repo Repository[MinersConfig] = NewFileRepository[MinersConfig](path)
// data, err := repo.Load()
type Repository[T any] interface {
// data, err := repo.Load()
Load() (T, error)
// repo.Save(updated)
Save(data T) error
// repo.Update(func(d *T) error { d.Field = value; return nil })
Update(fn func(*T) error) error
}
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig))
// data, err := repo.Load()
type FileRepository[T any] struct {
mutex sync.RWMutex
path string
defaults func() T
}
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig), myOption)
type FileRepositoryOption[T any] func(*FileRepository[T])
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig))
func WithDefaults[T any](fn func() T) FileRepositoryOption[T] {
return func(repo *FileRepository[T]) {
repo.defaults = fn
}
}
// repo := NewFileRepository[MinersConfig](path, WithDefaults(defaultMinersConfig))
func NewFileRepository[T any](path string, options ...FileRepositoryOption[T]) *FileRepository[T] {
repo := &FileRepository[T]{
path: path,
}
for _, option := range options {
option(repo)
}
return repo
}
// data, err := repo.Load()
// if err != nil { return defaults, err }
func (repository *FileRepository[T]) Load() (T, error) {
repository.mutex.RLock()
defer repository.mutex.RUnlock()
var result T
data, err := os.ReadFile(repository.path)
if err != nil {
if os.IsNotExist(err) {
if repository.defaults != nil {
return repository.defaults(), nil
}
return result, nil
}
return result, ErrInternal("failed to read file").WithCause(err)
}
if err := json.Unmarshal(data, &result); err != nil {
return result, ErrInternal("failed to unmarshal data").WithCause(err)
}
return result, nil
}
// if err := repo.Save(updated); err != nil { return ErrInternal("save").WithCause(err) }
func (repository *FileRepository[T]) Save(data T) error {
repository.mutex.Lock()
defer repository.mutex.Unlock()
return repository.saveUnlocked(data)
}
// return repository.saveUnlocked(data) // called by Save and Update while mutex is held
func (repository *FileRepository[T]) saveUnlocked(data T) error {
dir := filepath.Dir(repository.path)
if err := os.MkdirAll(dir, 0755); err != nil {
return ErrInternal("failed to create directory").WithCause(err)
}
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return ErrInternal("failed to marshal data").WithCause(err)
}
return AtomicWriteFile(repository.path, jsonData, 0600)
}
// repo.Update(func(configuration *MinersConfig) error {
// configuration.Miners = append(configuration.Miners, entry)
// return nil
// })
func (repository *FileRepository[T]) Update(fn func(*T) error) error {
repository.mutex.Lock()
defer repository.mutex.Unlock()
// Load current data
var data T
fileData, err := os.ReadFile(repository.path)
if err != nil {
if os.IsNotExist(err) {
if repository.defaults != nil {
data = repository.defaults()
}
} else {
return ErrInternal("failed to read file").WithCause(err)
}
} else {
if err := json.Unmarshal(fileData, &data); err != nil {
return ErrInternal("failed to unmarshal data").WithCause(err)
}
}
// Apply modification
if err := fn(&data); err != nil {
return err
}
// Save atomically
return repository.saveUnlocked(data)
}
// path := repo.Path() // => "/home/user/.config/lethean-desktop/miners.json"
func (repository *FileRepository[T]) Path() string {
return repository.path
}
// if !repo.Exists() { return defaults, nil }
func (repository *FileRepository[T]) Exists() bool {
repository.mutex.RLock()
defer repository.mutex.RUnlock()
_, err := os.Stat(repository.path)
return err == nil
}
// if err := repo.Delete(); err != nil { return err }
func (repository *FileRepository[T]) Delete() error {
repository.mutex.Lock()
defer repository.mutex.Unlock()
err := os.Remove(repository.path)
if os.IsNotExist(err) {
return nil
}
return err
}