feat: unify interactive operation coordination
This commit is contained in:
+176
-138
@@ -1,21 +1,5 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// OperationPhase describes coordination state independently of any terminal UI.
|
||||
type OperationPhase uint8
|
||||
|
||||
const (
|
||||
OperationRunning OperationPhase = iota + 1
|
||||
OperationAwaitingConfirmation
|
||||
OperationSucceeded
|
||||
OperationFailed
|
||||
OperationBlocked
|
||||
)
|
||||
|
||||
// StatusKind gives presentation layers semantics without requiring them to
|
||||
// inspect user-facing status text.
|
||||
type StatusKind uint8
|
||||
@@ -25,15 +9,12 @@ const (
|
||||
StatusSuccess
|
||||
StatusError
|
||||
StatusBlocked
|
||||
StatusConfirmation
|
||||
)
|
||||
|
||||
// OperationStatus is the current concise status plus optional expandable
|
||||
// detail. TargetID relates a result to the Target it can explicitly resolve.
|
||||
// OperationStatus is concise status text related to an optional Target.
|
||||
type OperationStatus struct {
|
||||
Kind StatusKind
|
||||
Summary string
|
||||
Detail string
|
||||
TargetID string
|
||||
}
|
||||
|
||||
@@ -57,21 +38,161 @@ const (
|
||||
TargetProgressFailed
|
||||
)
|
||||
|
||||
// OperationUpdate is the common progressive contract used by interactive
|
||||
// detection, Target mutation, and Active Profile transition operations.
|
||||
type OperationUpdate struct {
|
||||
Snapshot Snapshot
|
||||
Phase OperationPhase
|
||||
Status OperationStatus
|
||||
Progress []TargetProgress
|
||||
Confirmation *Confirmation
|
||||
FirstFailedTargetID string
|
||||
Err error
|
||||
// OperationState is a closed tagged set. Its concrete value makes an update
|
||||
// exactly one of running, confirmation, succeeded, failed, or closed.
|
||||
type OperationState interface {
|
||||
operationState()
|
||||
}
|
||||
|
||||
type operationRunning struct {
|
||||
status OperationStatus
|
||||
progress []TargetProgress
|
||||
}
|
||||
|
||||
func (operationRunning) operationState() {}
|
||||
|
||||
type operationConfirmation struct{ request Confirmation }
|
||||
|
||||
func (operationConfirmation) operationState() {}
|
||||
|
||||
type operationSucceeded struct {
|
||||
status OperationStatus
|
||||
progress []TargetProgress
|
||||
}
|
||||
|
||||
func (operationSucceeded) operationState() {}
|
||||
|
||||
type operationFailed struct {
|
||||
status OperationStatus
|
||||
progress []TargetProgress
|
||||
firstFailedTargetID string
|
||||
err error
|
||||
}
|
||||
|
||||
func (operationFailed) operationState() {}
|
||||
|
||||
type operationClosed struct{ status OperationStatus }
|
||||
|
||||
func (operationClosed) operationState() {}
|
||||
|
||||
// OperationUpdate is the common progressive contract used by interactive
|
||||
// detection, Target mutation, and Active Profile transition operations. Build
|
||||
// values with the constructors below so contradictory states are unrepresentable.
|
||||
type OperationUpdate struct {
|
||||
Snapshot Snapshot
|
||||
State OperationState
|
||||
}
|
||||
|
||||
func RunningOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress) OperationUpdate {
|
||||
status.Kind = StatusProgress
|
||||
return OperationUpdate{Snapshot: snapshot, State: operationRunning{status: status, progress: cloneProgress(progress)}}
|
||||
}
|
||||
|
||||
func ConfirmationOperationUpdate(snapshot Snapshot, request Confirmation) OperationUpdate {
|
||||
return OperationUpdate{Snapshot: snapshot, State: operationConfirmation{request: request}}
|
||||
}
|
||||
|
||||
func SucceededOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress) OperationUpdate {
|
||||
status.Kind = StatusSuccess
|
||||
return OperationUpdate{Snapshot: snapshot, State: operationSucceeded{status: status, progress: cloneProgress(progress)}}
|
||||
}
|
||||
|
||||
func FailedOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress, firstFailedTargetID string, err error) OperationUpdate {
|
||||
status.Kind = StatusError
|
||||
return OperationUpdate{Snapshot: snapshot, State: operationFailed{
|
||||
status: status, progress: cloneProgress(progress), firstFailedTargetID: firstFailedTargetID, err: err,
|
||||
}}
|
||||
}
|
||||
|
||||
// ClosedOperationUpdate reports a request that cannot start, such as an
|
||||
// overlapping interactive operation. It is terminal and has no progress or
|
||||
// error payload.
|
||||
func ClosedOperationUpdate(snapshot Snapshot, status OperationStatus) OperationUpdate {
|
||||
status.Kind = StatusBlocked
|
||||
return OperationUpdate{Snapshot: snapshot, State: operationClosed{status: status}}
|
||||
}
|
||||
|
||||
func cloneProgress(progress []TargetProgress) []TargetProgress {
|
||||
return append([]TargetProgress(nil), progress...)
|
||||
}
|
||||
|
||||
func (u OperationUpdate) Status() OperationStatus {
|
||||
switch state := u.State.(type) {
|
||||
case operationRunning:
|
||||
return state.status
|
||||
case operationSucceeded:
|
||||
return state.status
|
||||
case operationFailed:
|
||||
return state.status
|
||||
case operationClosed:
|
||||
return state.status
|
||||
default:
|
||||
return OperationStatus{}
|
||||
}
|
||||
}
|
||||
|
||||
func (u OperationUpdate) Progress() []TargetProgress {
|
||||
switch state := u.State.(type) {
|
||||
case operationRunning:
|
||||
return cloneProgress(state.progress)
|
||||
case operationSucceeded:
|
||||
return cloneProgress(state.progress)
|
||||
case operationFailed:
|
||||
return cloneProgress(state.progress)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (u OperationUpdate) Confirmation() *Confirmation {
|
||||
state, ok := u.State.(operationConfirmation)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
request := state.request
|
||||
request.Details = append([]string(nil), request.Details...)
|
||||
return &request
|
||||
}
|
||||
|
||||
func (u OperationUpdate) Err() error {
|
||||
if state, ok := u.State.(operationFailed); ok {
|
||||
return state.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u OperationUpdate) FirstFailedTargetID() string {
|
||||
if state, ok := u.State.(operationFailed); ok {
|
||||
return state.firstFailedTargetID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u OperationUpdate) IsRunning() bool {
|
||||
_, ok := u.State.(operationRunning)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (u OperationUpdate) IsConfirmation() bool {
|
||||
_, ok := u.State.(operationConfirmation)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (u OperationUpdate) IsSucceeded() bool {
|
||||
_, ok := u.State.(operationSucceeded)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (u OperationUpdate) IsFailed() bool {
|
||||
_, ok := u.State.(operationFailed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (u OperationUpdate) IsTerminal() bool { return !u.IsRunning() }
|
||||
|
||||
// OperationStream exposes the useful initial frame synchronously and all
|
||||
// subsequent structured changes through Updates. Updates closes after a
|
||||
// terminal phase.
|
||||
// terminal update.
|
||||
type OperationStream struct {
|
||||
Snapshot Snapshot
|
||||
Updates <-chan OperationUpdate
|
||||
@@ -84,84 +205,32 @@ type InteractiveModule interface {
|
||||
StartOperation(Intent) OperationStream
|
||||
}
|
||||
|
||||
// StartOperation rejects overlap instead of queuing hidden interactive work.
|
||||
// Synchronous Dispatch callers retain the established serialized behavior.
|
||||
// StartOperation returns before dispatch serialization is acquired. This keeps
|
||||
// Bubble Tea Update non-blocking even when a synchronous caller currently owns
|
||||
// the serialized Application entry.
|
||||
func (a *Application) StartOperation(intent Intent) OperationStream {
|
||||
initial := a.Snapshot()
|
||||
a.interactiveMu.Lock()
|
||||
if a.interactiveOperation {
|
||||
a.interactiveMu.Unlock()
|
||||
updates := make(chan OperationUpdate, 1)
|
||||
updates <- OperationUpdate{
|
||||
Snapshot: a.Snapshot(),
|
||||
Phase: OperationBlocked,
|
||||
Status: OperationStatus{Kind: StatusBlocked,
|
||||
Summary: "Another system operation is already in progress",
|
||||
Detail: "Wait for the current operation to finish before starting another modification."},
|
||||
}
|
||||
updates <- ClosedOperationUpdate(initial, OperationStatus{
|
||||
Summary: "Another system operation is already in progress",
|
||||
})
|
||||
close(updates)
|
||||
return OperationStream{Snapshot: a.Snapshot(), Updates: updates}
|
||||
return OperationStream{Snapshot: initial, Updates: updates}
|
||||
}
|
||||
a.interactiveOperation = true
|
||||
a.interactiveMu.Unlock()
|
||||
|
||||
if intent.Kind == RefreshTargets {
|
||||
return a.startDetectionOperation()
|
||||
}
|
||||
|
||||
initial := a.Snapshot()
|
||||
updates := make(chan OperationUpdate, 2*len(a.targets)+2)
|
||||
go func() {
|
||||
defer close(updates)
|
||||
defer a.finishInteractiveOperation()
|
||||
final := a.dispatch(intent, func(result Result) {
|
||||
updates <- operationUpdate(result, false)
|
||||
updates <- runningOperationUpdate(result)
|
||||
})
|
||||
updates <- operationUpdate(final, true)
|
||||
}()
|
||||
return OperationStream{Snapshot: initial, Updates: updates}
|
||||
}
|
||||
|
||||
func (a *Application) startDetectionOperation() OperationStream {
|
||||
initial, detection := a.BeginTargetDetection()
|
||||
updates := make(chan OperationUpdate, len(a.targets)+1)
|
||||
go func() {
|
||||
defer close(updates)
|
||||
defer a.finishInteractiveOperation()
|
||||
var failures []error
|
||||
firstFailed := ""
|
||||
for item := range detection {
|
||||
if item.Complete {
|
||||
update := OperationUpdate{
|
||||
Snapshot: item.Snapshot,
|
||||
Phase: OperationSucceeded,
|
||||
Status: OperationStatus{Kind: StatusSuccess, Summary: "Target availability and state checks completed"},
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
update.Phase = OperationFailed
|
||||
update.Err = errors.Join(failures...)
|
||||
update.FirstFailedTargetID = firstFailed
|
||||
update.Status = OperationStatus{Kind: StatusError, Summary: "Some Target checks failed", Detail: update.Err.Error(), TargetID: firstFailed}
|
||||
}
|
||||
updates <- update
|
||||
continue
|
||||
}
|
||||
targetSnapshot, name := targetByID(item.Snapshot, item.TargetID)
|
||||
progress := TargetProgress{TargetID: item.TargetID, State: targetSnapshot.State, Evidence: targetSnapshot.Evidence, Phase: TargetProgressSucceeded}
|
||||
status := OperationStatus{Kind: StatusProgress, Summary: "Checked " + name + " Target", TargetID: item.TargetID}
|
||||
if targetSnapshot.DetectionPhase == TargetDetectionFailed {
|
||||
progress.Phase = TargetProgressFailed
|
||||
progress.Err = targetSnapshot.DetectionErr
|
||||
status = OperationStatus{Kind: StatusError, Summary: "Could not check " + name + " Target", TargetID: item.TargetID}
|
||||
if targetSnapshot.DetectionErr != nil {
|
||||
status.Detail = targetSnapshot.DetectionErr.Error()
|
||||
failures = append(failures, fmt.Errorf("%s Target: %w", name, targetSnapshot.DetectionErr))
|
||||
}
|
||||
if firstFailed == "" {
|
||||
firstFailed = item.TargetID
|
||||
}
|
||||
}
|
||||
updates <- OperationUpdate{Snapshot: item.Snapshot, Phase: OperationRunning, Status: status, Progress: []TargetProgress{progress}, Err: progress.Err}
|
||||
}
|
||||
updates <- terminalOperationUpdate(final)
|
||||
}()
|
||||
return OperationStream{Snapshot: initial, Updates: updates}
|
||||
}
|
||||
@@ -172,57 +241,26 @@ func (a *Application) finishInteractiveOperation() {
|
||||
a.interactiveMu.Unlock()
|
||||
}
|
||||
|
||||
func targetByID(snapshot Snapshot, id string) (TargetSnapshot, string) {
|
||||
for _, item := range snapshot.Targets {
|
||||
if item.ID == id {
|
||||
name := item.Description.Name
|
||||
if name == "" {
|
||||
name = id
|
||||
}
|
||||
return item, name
|
||||
}
|
||||
func runningOperationUpdate(result Result) OperationUpdate {
|
||||
if result.ConfirmationRequest != nil {
|
||||
return ConfirmationOperationUpdate(result.Snapshot, *result.ConfirmationRequest)
|
||||
}
|
||||
return TargetSnapshot{ID: id}, id
|
||||
return RunningOperationUpdate(result.Snapshot, result.Status, result.Progress)
|
||||
}
|
||||
|
||||
func operationUpdate(result Result, terminal bool) OperationUpdate {
|
||||
update := OperationUpdate{
|
||||
Snapshot: result.Snapshot,
|
||||
Phase: OperationRunning,
|
||||
Status: result.Status,
|
||||
Progress: result.Progress,
|
||||
Confirmation: result.ConfirmationRequest,
|
||||
FirstFailedTargetID: result.FirstFailedTargetID,
|
||||
Err: result.Err,
|
||||
}
|
||||
func terminalOperationUpdate(result Result) OperationUpdate {
|
||||
if result.ConfirmationRequest != nil {
|
||||
update.Phase = OperationAwaitingConfirmation
|
||||
if update.Status.Kind == 0 {
|
||||
update.Status = OperationStatus{Kind: StatusConfirmation, Summary: result.ConfirmationRequest.Title, Detail: result.ConfirmationRequest.Summary}
|
||||
}
|
||||
return update
|
||||
}
|
||||
if update.Status.Kind == 0 {
|
||||
switch {
|
||||
case result.Err != nil:
|
||||
update.Status = OperationStatus{Kind: StatusError, Summary: "Operation failed", Detail: result.Err.Error()}
|
||||
case result.Notice != "":
|
||||
update.Status = OperationStatus{Kind: StatusSuccess, Summary: result.Notice}
|
||||
}
|
||||
}
|
||||
if !terminal {
|
||||
return update
|
||||
return ConfirmationOperationUpdate(result.Snapshot, *result.ConfirmationRequest)
|
||||
}
|
||||
status := result.Status
|
||||
if result.Err != nil {
|
||||
update.Phase = OperationFailed
|
||||
if update.Status.Kind != StatusError {
|
||||
update.Status = OperationStatus{Kind: StatusError, Summary: "Operation failed", Detail: result.Err.Error(), TargetID: update.Status.TargetID}
|
||||
}
|
||||
} else {
|
||||
update.Phase = OperationSucceeded
|
||||
if update.Status.Kind == StatusProgress || update.Status.Kind == 0 {
|
||||
update.Status.Kind = StatusSuccess
|
||||
if status.Summary == "" {
|
||||
status.Summary = "Operation failed"
|
||||
}
|
||||
return FailedOperationUpdate(result.Snapshot, status, result.Progress, result.FirstFailedTargetID, result.Err)
|
||||
}
|
||||
return update
|
||||
if status.Summary == "" {
|
||||
status.Summary = result.Notice
|
||||
}
|
||||
return SucceededOperationUpdate(result.Snapshot, status, result.Progress)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user