01fd572a89
## Summary Completed Phase 5 refactoring and reached the target architecture. **Architectural milestone achieved:** - Service layer owns all state and is the sole writer - UI is a thin Fyne view, all widget updates marshaled via `fyne.Do` - Core engines are stateless and injectable - Domain types are pure (no `yaml:"-"` fields) - Full module builds and `go vet ./...` clean ## Changes - Bump version: 0.3.6 → 0.4.0 - Update CHANGELOG with Phase 5 summary - Add ROADMAP "Refactoring Follow-Ups" section ## Known follow-up work 1. **Linux test build broken** — `runner_test.go` needs `//go:build windows` tag 2. **File-size limits exceeded** — `operations.go` (486 lines), `jobs_view.go` (415 lines) See ROADMAP.md for details. --------- Co-authored-by: mixeme <mix.public@ya.ru> Reviewed-on: #1
101 lines
4.0 KiB
Go
101 lines
4.0 KiB
Go
package app
|
|
|
|
import "gitea.mixdep.ru/mix/gosentry/src/domain"
|
|
|
|
// Event is something the Service did to its state that observers may want to
|
|
// react to. It is a sealed interface: the concrete types in this file are the
|
|
// only implementations (enforced by the unexported isEvent marker), so a UI
|
|
// listener can exhaustively type-switch over them and the compiler will flag a
|
|
// new event type that a switch forgot to handle.
|
|
//
|
|
// Events replace the old single onChange callback. Instead of the scheduler
|
|
// reaching into the GUI, the Service emits typed events and the UI subscribes —
|
|
// the UI's listener becomes the one place that touches widgets.
|
|
type Event interface {
|
|
isEvent()
|
|
}
|
|
|
|
// JobChanged signals that a job's durable config or transient runtime changed:
|
|
// created, edited, deleted, enabled/disabled, or a status transition such as a
|
|
// run starting. Observers should re-read the affected state through the Service
|
|
// (Jobs/Runtime) rather than expect a payload snapshot — that keeps the event
|
|
// small and avoids handing out stale copies.
|
|
//
|
|
// JobID identifies the affected job. A zero JobID means a broad change (for
|
|
// example a delete, or a global pause that touched every job) and observers
|
|
// should refresh their whole view.
|
|
type JobChanged struct {
|
|
JobID int
|
|
}
|
|
|
|
// RunRecorded signals that a job run finished and produced a RunRecord. It
|
|
// carries the record by value because the record is an immutable result that
|
|
// observers append to history; there is nothing for them to re-read.
|
|
type RunRecorded struct {
|
|
Record domain.RunRecord
|
|
}
|
|
|
|
// SchedulerStateChanged signals that the global scheduler pause state flipped.
|
|
// The UI uses it to update the pause/resume control and status text.
|
|
type SchedulerStateChanged struct {
|
|
Paused bool
|
|
}
|
|
|
|
// ErrorOccurred signals a background error that could not be returned to a
|
|
// caller — typically a failed save or cleanup after an async run. The UI
|
|
// surfaces it in the History tab so the user is not silently left with
|
|
// un-persisted state.
|
|
type ErrorOccurred struct {
|
|
Err error
|
|
}
|
|
|
|
func (JobChanged) isEvent() {}
|
|
func (RunRecorded) isEvent() {}
|
|
func (SchedulerStateChanged) isEvent() {}
|
|
func (ErrorOccurred) isEvent() {}
|
|
|
|
// Observer receives events emitted by the Service. OnEvent is the single
|
|
// reaction point; the UI implements it and marshals any widget work onto the
|
|
// main thread (fyne.Do) itself — the Service knows nothing about Fyne.
|
|
type Observer interface {
|
|
OnEvent(Event)
|
|
}
|
|
|
|
// ObserverFunc adapts a plain function to the Observer interface, so callers can
|
|
// subscribe a closure without declaring a type.
|
|
type ObserverFunc func(Event)
|
|
|
|
// OnEvent calls the wrapped function.
|
|
func (f ObserverFunc) OnEvent(event Event) { f(event) }
|
|
|
|
// Subscribe registers an observer to receive every subsequently emitted event.
|
|
// Registration is expected during setup, before the scheduler starts, but is
|
|
// guarded so it is safe at any time.
|
|
func (s *Service) Subscribe(observer Observer) {
|
|
s.dispatchMu.Lock()
|
|
defer s.dispatchMu.Unlock()
|
|
s.observers = append(s.observers, observer)
|
|
}
|
|
|
|
// emit delivers an event to every registered observer.
|
|
//
|
|
// Single-threaded dispatch contract:
|
|
// - emit holds dispatchMu for the whole dispatch, so observers are never
|
|
// invoked concurrently and never overlap with each other or with Subscribe.
|
|
// Each observer sees events one at a time, in emit order.
|
|
// - emit must be called WITHOUT holding s.mu. The Service computes a state
|
|
// change under mu, releases it, then emits — so an observer is free to call
|
|
// back into read methods (Jobs/Runtime) without deadlocking on the state
|
|
// lock.
|
|
// - An observer must NOT call back into a Service method that emits (directly
|
|
// or indirectly): dispatchMu is non-reentrant, so re-entrant emission would
|
|
// deadlock. Observers react and return quickly; long or UI work is the
|
|
// observer's own responsibility to defer (e.g. fyne.Do).
|
|
func (s *Service) emit(event Event) {
|
|
s.dispatchMu.Lock()
|
|
defer s.dispatchMu.Unlock()
|
|
for _, observer := range s.observers {
|
|
observer.OnEvent(event)
|
|
}
|
|
}
|