package app import ( "sync" "gitea.mixdep.ru/mix/gosentry/src/domain" "gitea.mixdep.ru/mix/gosentry/src/storage" ) // Service is the application-service layer: the single owner of GoSentry's // in-memory state. It holds the durable jobs slice, the transient runtime map // keyed by Job.ID, and a reference to the store that persists them. All access // to that state goes through a mutex so the GUI and the scheduler can no longer // race on a shared *[]Job. // // This is the first slice of the layer (T3.1): it establishes ownership and the // locking contract. State-mutating intents (CreateJob, RunNow, SetGlobalPause, // ...) and the event/observer machinery are added in later tasks; for now the // Service only owns state and exposes read snapshots. // // Locking contract: mu is a plain, non-reentrant mutex. Exported methods take // it; unexported helpers ending in "Locked" assume the caller already holds it. // The Service must never call back into the UI (or any code that might re-enter // the Service) while holding mu. type Service struct { mu sync.Mutex store *storage.Store jobs []domain.Job runtimes map[int]*domain.JobRuntime } // NewService wires the Service to a loaded store and its jobs. It builds the // initial runtime map from the durable jobs so every job has transient state // from the moment the Service exists. The store is the Service's sole channel // to persistence. func NewService(store *storage.Store, jobs []domain.Job) *Service { return &Service{ store: store, jobs: jobs, runtimes: domain.NewRuntimes(jobs), } } // Open loads the store and constructs a Service from it in one step. It is the // convenience entry point for the application; tests inject a pre-built store // via NewService instead. func Open() (*Service, error) { store, jobs, err := storage.OpenStore() if err != nil { return nil, err } return NewService(store, jobs), nil } // Store returns the underlying store. It is exposed so callers that still need // resolved paths and config (the GUI, during the transition) can reach them; // later phases narrow this surface. func (s *Service) Store() *storage.Store { return s.store } // Jobs returns a copy of the durable jobs slice. Returning a copy keeps callers // from mutating Service-owned state behind its back: the Service stays the sole // writer. func (s *Service) Jobs() []domain.Job { s.mu.Lock() defer s.mu.Unlock() jobs := make([]domain.Job, len(s.jobs)) copy(jobs, s.jobs) return jobs } // Runtime returns the transient runtime state for a job ID, or nil if no job // with that ID is loaded. The returned pointer is the live runtime; reads of it // are only safe while no concurrent mutation is in flight, which holds during // the current single-threaded transition and is tightened as the scheduler // moves behind the Service in T3.4. func (s *Service) Runtime(id int) *domain.JobRuntime { s.mu.Lock() defer s.mu.Unlock() return s.runtimes[id] }