package budget import ( "context" "sync" "time" ) // Memory is a zero-dependency in-process BudgetStorage: per-user rolling-window // usage held in memory (lost on restart). The default behind DBBudget for a // light host or tests; mort uses its GORM Storage, contrib/store adds SQLite. type Memory struct { mu sync.Mutex rows map[string]*SkillBudget } // NewMemory returns an empty in-memory BudgetStorage. func NewMemory() *Memory { return &Memory{rows: map[string]*SkillBudget{}} } var _ BudgetStorage = (*Memory)(nil) func (m *Memory) Initialize(context.Context) error { return nil } func (m *Memory) Get(_ context.Context, userID string) (*SkillBudget, error) { m.mu.Lock() defer m.mu.Unlock() r, ok := m.rows[userID] if !ok { return nil, nil } cp := *r // copy out so callers can't mutate our row return &cp, nil } func (m *Memory) Add(_ context.Context, userID string, secondsUsed float64, now time.Time) error { m.mu.Lock() defer m.mu.Unlock() r, ok := m.rows[userID] if !ok { m.rows[userID] = &SkillBudget{ UserID: userID, WindowStart: now, SecondsUsed: secondsUsed, RunsCount: 1, UpdatedAt: now, } return nil } // Roll the window over if it's older than the window length. if now.Sub(r.WindowStart) >= budgetWindow { r.WindowStart = now r.SecondsUsed = 0 r.RunsCount = 0 } r.SecondsUsed += secondsUsed r.RunsCount++ r.UpdatedAt = now return nil }