Files
gosentry/src/app/service.go
T
mixeme 9931ec1237 T3.1: Add app.Service owning state behind a mutex
Create src/app/service.go: the application-service layer that becomes
the single owner of the durable jobs slice and the transient runtime
map, guarded by a non-reentrant sync.Mutex. NewService wires a loaded
store; Open() is the convenience entry point. Read-only accessors
(Jobs/Runtime/Store) take the lock, and Jobs() returns a copy to keep
callers from mutating Service-owned state.

State-mutating intents and the event/observer machinery are deferred to
T3.2-T3.4. Adds no-Fyne unit tests for runtime construction, copy
isolation, and store wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 07:34:34 +03:00

85 lines
2.9 KiB
Go

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]
}