Refactoring complete: v0.4.0 architectural milestone (#1)
## 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
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user