package main import ( "context" "errors" "fmt" ) // Mutation — typed intent to mutate state. Every write to the store goes through // this. Validate() runs WITHOUT holding any application lock; it inspects the // current state for preconditions. Apply() is called with an acquired LockRegion // and is the only path that mutates state. // // AffectedAuthors() is used for read_state bookkeeping and mention targeting. type Mutation interface { Validate(s *Store) error Apply(s *Store, lock LockRegion) error AffectedAuthors() []string Kind() string // for logging and VOPR transition recording } // LockScope — the granularity the controller chose for this mutation. type LockScope int const ( ScopeCoarse LockScope = iota // per-author queue ScopeMedium // per-author queue + per-post sub-queue ScopeFine // per-mutation lock, no queueing ) func (s LockScope) String() string { switch s { case ScopeCoarse: return "coarse" case ScopeMedium: return "medium" case ScopeFine: return "fine" default: return "unknown" } } // LockRegion — opaque handle to an acquired lock. The mutation's Apply method // receives this so it can hold the lock for the duration of the apply and // release it explicitly if needed. type LockRegion interface { Scope() LockScope Release() } // ErrValidation — sentinel for precondition failures. Distinct from runtime // errors so callers can map to HTTP 400 vs 500. var ErrValidation = errors.New("validation failed") // ValidationError — typed validation failure with a message for the caller. type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation: %s: %s", e.Field, e.Message) } func (e *ValidationError) Unwrap() error { return ErrValidation } // Submit — the canonical entry point. The lock controller decides the scope // based on observed contention; we acquire the lock, then call Validate, then // Apply. (Validate can also be called standalone for dry-runs.) // // On Validate failure, the lock is released and no state changes. // On Apply failure, the lock is released and state is left consistent (Apply // is expected to be transactional; if it can't be, that's a bug). func Submit(ctx context.Context, s *Store, m Mutation) error { lock := s.lockCtrl.Acquire(s, m) defer lock.Release() if err := m.Validate(s); err != nil { return err } if err := m.Apply(s, lock); err != nil { return err } s.bumpFlush() return nil } // ValidateOnly — dry-run. No lock, no state change. Useful for "can I post?" // preflight checks from agents. func ValidateOnly(s *Store, m Mutation) error { return m.Validate(s) } func (s *Store) bumpFlush() { select { case s.flushCh <- struct{}{}: default: } }