// Package opstate provides a mutex-guarded registry for long-running // operations (e.g. async VM create, async image build). A registry stores // operations by ID and can prune completed ones after a retention window. package opstate import ( "sync" "time" ) // AsyncOp is the protocol each operation type must satisfy. Implementations // own their own concurrency for the returned values — the registry treats // them as opaque. type AsyncOp interface { ID() string IsDone() bool UpdatedAt() time.Time Cancel() } // Registry is a mutex-guarded map of in-flight operations keyed by op ID. // One registry per operation kind; each owns its own lock. type Registry[T AsyncOp] struct { mu sync.Mutex byID map[string]T } // Insert adds op keyed by its ID. func (r *Registry[T]) Insert(op T) { r.mu.Lock() defer r.mu.Unlock() if r.byID == nil { r.byID = map[string]T{} } r.byID[op.ID()] = op } // Get returns the operation with the given ID, if present. func (r *Registry[T]) Get(id string) (T, bool) { r.mu.Lock() defer r.mu.Unlock() op, ok := r.byID[id] return op, ok } // List returns a snapshot of every operation currently in the // registry — both pending and (un-pruned) completed. Callers filter // by IsDone() if they care about state. The slice is freshly // allocated; mutating it doesn't affect the registry. // // Used by `banger update`'s preflight to detect in-flight operations // before swapping binaries. func (r *Registry[T]) List() []T { r.mu.Lock() defer r.mu.Unlock() out := make([]T, 0, len(r.byID)) for _, op := range r.byID { out = append(out, op) } return out } // Prune drops completed operations last updated before the cutoff. func (r *Registry[T]) Prune(before time.Time) { r.mu.Lock() defer r.mu.Unlock() for id, op := range r.byID { if !op.IsDone() { continue } if op.UpdatedAt().Before(before) { delete(r.byID, id) } } }