progress

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package progress defines the structured run-time event contract for xray's CLI output cluster (cli-ux). A Sink consumes Events emitted at phase boundaries during a run; concrete sinks include a no-op default, a slog wrapper for the non-TTY log fallback, an NDJSON emitter for --output json, and a TTY status grid for --output auto on a terminal.

The contract is the seam imported by sibling cli-ux work: rate-limit and retry visibility (#82), the post-run summary block (#84), and the run-time status display itself (#81). The package depends on the standard library only; concrete sinks may pull in additional helpers.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WithSink

func WithSink(ctx context.Context, sink Sink) context.Context

WithSink returns a context carrying sink as the ambient progress sink. Code paths that can't be threaded a Sink directly (notably the rate-limit transport, which is constructed per-connector before the run-wide sink exists) read the sink from the request context instead.

Types

type Event

type Event struct {
	Kind        EventKind
	Repo        string
	Connector   string
	Phase       string
	Done, Total int64
	Message     string
	At          time.Time
	Fields      map[string]any
}

Event is one run-time progress signal. Repo/Connector/Phase identify the (repo, connector) pair the event applies to; global events leave them empty. Done/Total advance on PhaseProgress; sinks that don't know a Total render the partial count alone.

func (Event) IsTransition

func (e Event) IsTransition() bool

IsTransition reports whether an event marks a state transition rather than a mid-walk tick. Sinks that throttle output (LogSink without verbose, the TTY header) use this to keep noise down.

type EventKind

type EventKind string

EventKind names a discrete run-time event. The string values are the wire shape used by --output json and are stable per the schema documented in docs/spec.md ("JSON event schema").

const (
	PhaseStart    EventKind = "phase_start"
	PhaseProgress EventKind = "phase_progress"
	PhaseDone     EventKind = "phase_done"
	PhaseError    EventKind = "phase_error"
	PhaseSkipped  EventKind = "phase_skipped"
	RateLimit     EventKind = "rate_limit_wait"
	Retry         EventKind = "retry"
)

type JSONSink

type JSONSink struct {
	// contains filtered or unexported fields
}

JSONSink emits one NDJSON object per Event to a writer. The wire shape matches docs/spec.md "JSON event schema" — additive changes are non-breaking; renames or removals bump output_schema_version.

func NewJSONSink

func NewJSONSink(w io.Writer) *JSONSink

NewJSONSink wraps a writer (typically stdout). Concurrent Emit calls are serialised so NDJSON lines never interleave.

func (*JSONSink) Emit

func (s *JSONSink) Emit(e Event)

Emit writes one NDJSON line per event. The mutex keeps lines atomic across concurrent goroutines.

type LogSink

type LogSink struct {
	// contains filtered or unexported fields
}

LogSink emits one slog line per transition event. PhaseProgress events are dropped unless verbose is set; this matches the issue's acceptance criterion "Non-TTY: one line per state transition… Skip PhaseProgress ticks unless --verbose."

func NewLogSink

func NewLogSink(log *slog.Logger, verbose bool) *LogSink

NewLogSink wires a sink to an existing slog logger. The logger continues to receive xray's existing extraction lines independently; LogSink's output is additive — one line per phase boundary.

func (*LogSink) Emit

func (s *LogSink) Emit(e Event)

Emit writes one slog line for transitions; PhaseError uses Warn, everything else uses Info. PhaseProgress is dropped unless verbose.

type NopSink

type NopSink struct{}

NopSink is the zero-cost default. Used when --output quiet is in effect and when run.Options.Progress is nil.

func (NopSink) Emit

func (NopSink) Emit(Event)

type RateLimitCounter

type RateLimitCounter struct {
	// contains filtered or unexported fields
}

RateLimitCounter accumulates RateLimit events emitted by ratelimit.Transport so the post-run summary can report the cumulative wait count + total seconds. It implements Sink and is intended to be tee'd alongside the run's primary Sink (e.g. via NewTeeSink).

func NewRateLimitCounter

func NewRateLimitCounter() *RateLimitCounter

NewRateLimitCounter returns a zero-value counter ready to consume events.

func (*RateLimitCounter) Emit

func (c *RateLimitCounter) Emit(e Event)

Emit ignores every event except Kind == RateLimit. The wait duration is read from Fields["wait_duration_s"] (int seconds) or Fields["wait_duration_ms"] (int ms) if present; events without either contribute to the wait count but not the duration.

func (*RateLimitCounter) Snapshot

func (c *RateLimitCounter) Snapshot() (waits int, totalSeconds int)

Snapshot returns the cumulative counters. Safe to call concurrently with Emit.

type Sink

type Sink interface {
	Emit(Event)
}

Sink consumes events emitted during a run. Implementations must be safe to call from multiple goroutines.

func FromContext

func FromContext(ctx context.Context) Sink

FromContext returns the ambient Sink, or NopSink when none is set. Callers can always Emit unconditionally.

type TTYSink

type TTYSink struct {
	// contains filtered or unexported fields
}

TTYSink renders a live (repo × connector) status grid using hand-rolled ANSI cursor-up + clear-from-cursor redraws. Refresh runs at ~5 Hz from a background goroutine started by Start; concurrent Emit calls and the ticker are serialised by mu.

The renderer never depends on terminal capabilities beyond CUU (cursor up) and ED (erase display from cursor); both are universally supported on the terminals xray targets.

func NewTTYSink

func NewTTYSink(w io.Writer) *TTYSink

NewTTYSink wraps a writer (typically os.Stdout). Start must be called before any Emit for the grid to render incrementally; without Start, Emit still records state and a single render fires on Stop.

func (*TTYSink) Emit

func (s *TTYSink) Emit(e Event)

Emit records the event and (if Start has been called) lets the ticker pick it up on the next tick. Sub-tick latency is acceptable at 5 Hz and avoids redraw thrash under high-event-rate phases.

func (*TTYSink) Plan

func (s *TTYSink) Plan(repos, connectors []string, workers int)

Plan pre-registers the full (repo × connector) grid so every cell renders as pending from the first frame. Without Plan, cells appear only as their PhaseStart events arrive — fine but less informative.

func (*TTYSink) Start

func (s *TTYSink) Start(ctx context.Context)

Start launches the redraw ticker. Idempotent — repeat calls are no-ops. Cancellation via ctx or Stop both shut the ticker down; Stop also renders one final frame with finalised state.

func (*TTYSink) Stop

func (s *TTYSink) Stop()

Stop shuts the ticker down and renders one final frame. Safe to call multiple times.

type TeeSink

type TeeSink struct {
	// contains filtered or unexported fields
}

TeeSink fans out a single Emit call to every wrapped sink. Used in the CLI to tee a RateLimitCounter alongside the user-facing TTY or log sink without coupling either to the other.

func NewTeeSink

func NewTeeSink(sinks ...Sink) *TeeSink

NewTeeSink returns a sink that forwards every Emit to each of sinks in order. Nil entries are silently skipped.

func (*TeeSink) Emit

func (t *TeeSink) Emit(e Event)

Emit forwards e to every wrapped sink. Implementations are expected to be cheap and non-blocking; TeeSink itself does not goroutinize.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL