Help us shape the future of Go! Take the 2026 Go Developer Survey

scope

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package scope stamps an async command's result with the screen instance that issued it, so Update can tell a request its own current instance is still waiting on from one an earlier instance of the same kind of screen never got to see land.

A screen is a value (AGENTS.md §4.8): esc, then reopening the same key, builds a brand new Model rather than mutating the old one. That does not by itself stop the old instance's outstanding tea.Cmd — a real network or cluster call fired from Init or a keypress keeps running, and nothing in bubbletea reaches into a Cmd already in flight to cancel it on its own; Scope (Open/Ctx/DoCtx, below) is this package's answer for a caller that wants a real cancellable context.Context tied to the screen's own lifetime, not just a stale-result guard. Its result message still arrives, is still routed to whichever screen is now on top by concrete type (app.go's own default forward-to-top-screen case), and — because a new instance of the same screen kind has since been pushed in the old one's place — can land on a Model that never asked for it. plan.Model's loadedMsg racing a second p press and watch.Model's snapshotMsg racing w on a different family are the same shape: an Update that trusts whatever tea.Msg arrives shows the earlier instance's stale answer as if it were the current one's.

ID is a lightweight scope: New allocates one, process-wide unique, once at construction — and again wherever a Model reloads itself in place rather than being replaced by a fresh instance (plan's own override rebuild is today's one example), since a stale in-flight result for the request that reload superseded is exactly the same hazard, one instance re-asking a newer question of itself. Do and After build a tea.Cmd whose result carries that ID via Stamped; Result[T] is the carrier for a message with no field of its own to hold one — every adopting screen still owns its own named message types, just returned as scope.Result[thatType] from Do or After instead of a bare `func() tea.Msg`.

Every adopting screen's Update starts with `if scope.Foreign(m.id, msg) { return m, nil }`: drop it and read nothing else out of it, and let whatever request the current instance itself issued answer in its own time. Never stamp a spinner or cursor-blink tick — internal/ui/uitest's own Drain already drops those unconditionally wherever a test unwinds a batch, and a screen that read its own current id from inside a long recurring tick chain would only ever see the id frozen into that chain's first tea.Tick, not the instance's actual current one.

Index

Constants

View Source
const (
	// Resolve bounds one plan's digest resolution + BuildPlan call (internal/app/plan's own
	// loadCmd): a cluster read plus a registry HEAD per repo, chained through pkg/resolve's
	// priority order (AGENTS.md §4.10). Also reused by internal/app's own openDeploy, whose
	// call is synchronous and pure (no cluster/registry involved) but budgeted the same way for
	// consistency rather than left unbounded. Raised from 20s to 120s:
	// the registry credential chain's `op` link can be an interactive 1Password approval
	// (§4.6/§4.10 — the same reason git's own commit step gets a "waiting for signing approval"
	// notice rather than a short deadline), and 20s was tight enough to fail resolution outright
	// on a slow approval even though the underlying call was otherwise healthy.
	Resolve = 120 * time.Second
	// Drift bounds one env's cluster read in the matrix's own fan-out (F5/Init) and the watch
	// screen's own poll — one read at a time, so a single slow env or family never delays
	// another already answered.
	Drift = 10 * time.Second
	// RefreshRepo bounds the matrix's F5 re-read of the repo (fetch + discover against a cached
	// origin/<base> view, AGENTS.md §6's own `plan --dry-run` reasoning).
	RefreshRepo = 30 * time.Second
	// History bounds one repo's commit-delta/migration fetch (internal/app/plan's historyCmds,
	// internal/app/tags' own commit history) — a forge call, potentially several pages of it.
	History = 15 * time.Second
	// RestartRead bounds reading what a restart would touch (internal/restart, live pod specs).
	RestartRead = 10 * time.Second
	// RestartDo bounds actually stamping the restart annotation on every target Deployment.
	RestartDo = 15 * time.Second
	// RestartObserve bounds one rollout-progress poll.
	RestartObserve = 10 * time.Second
	// List bounds one session.Controller listing (internal/app/session) — every promotion state
	// file re-observed against the forge and cluster. Mirrored here for documentation; the
	// controller's own Config.ListTimeout already defaults to this exact value (§4.1 — nothing
	// consults this constant directly, so the two cannot silently drift without a reviewer
	// noticing the two literals disagree). Raised from 30s to 60s: every
	// in-flight promotion's own forge/cluster re-observation happens in this one call, and 30s
	// was tight for an operator with several promotions in flight against a forge under load.
	List = 60 * time.Second
	// Abandon bounds waiting for a busy Step to notice its ctx was cancelled before
	// session.Controller.Abandon gives up and calls Backend.Abandon anyway. Mirrored here the
	// same way List is; the controller's own Config.AbandonTimeout is the value actually
	// consulted. It does NOT bound the Backend.Abandon call itself — see AbandonCall.
	Abandon = time.Second
	// AbandonCall bounds one Backend.Abandon call — a real re-observation (git ls-remote plus a
	// forge PR/check lookup) followed by closing the PR and deleting the remote branch, none of
	// which is instant. Mirrored here the same way List is; the controller's own
	// Config.AbandonCallTimeout is the value actually consulted. Deliberately generous and
	// independent of Abandon above: conflating the two (a single 1s timeout used for both the
	// busy-Step wait and the call) made a real abandon regularly fail with "context deadline
	// exceeded".
	AbandonCall = 60 * time.Second
)

Timeout budgets for the calls internal/app's screens make through DoCtx or Timeout. Each is a conservative ceiling on how long an operator waits before a screen says a call did not answer — not a target latency; the underlying call is expected to finish well inside it under normal conditions. Constants for now, not config keys: every one of these calls is already re-observed on retry (R, F5, resuming the screen), so a conservative constant costs an extra keypress on the rare slow call rather than a wrong answer, and a config key would need its own validation, docs and default before it earned its keep.

Variables

This section is empty.

Functions

func After

func After[T any](id ID, d time.Duration, v T) tea.Cmd

After schedules v to arrive after d, stamped with id — the scope-aware analogue of tea.Tick for a fixed value rather than a func of the fire time.

func Do

func Do[T any](id ID, f func() T) tea.Cmd

Do runs f and stamps its result with id, wrapped as a Result[T] — the scope-aware analogue of `func() tea.Msg { return f() }` for an async call whose result message has no field of its own to carry the id in.

func DoCtx

func DoCtx[T any](s Scope, timeout time.Duration, f func(context.Context) T) tea.Cmd

DoCtx runs f with a context derived from s — bounded by both s's own cancellation and by timeout, whichever comes first — and stamps the result with s.ID exactly as Do does: the ctx-bound cousin of Do, for a call that must also die the moment its screen is closed.

func Foreign

func Foreign(id ID, msg tea.Msg) bool

Foreign reports whether msg is a Stamped result whose ID differs from id — the guard every adopting screen's Update starts with. A msg that isn't Stamped at all (a key press, a window resize, a navigation message this screen itself just emitted) is never foreign; Foreign only ever says yes about a message this package's own convention applies to.

func Timeout

Timeout derives a bounded, standalone context (context.Background() wrapped by context.WithTimeout) for a caller with no Scope of its own to derive from — the matrix screen's per-env cluster reads and repo refresh, in practice: the matrix is never replaced by a fresh instance the way a pushed screen is, so it has no "closed" moment of its own to tie a Scope to, only a per-call ceiling. Kept inside this package so a bare context.Background() never has to appear in a screen package directly (AGENTS.md §4.8's own acceptance grep for this convention).

Types

type ID

type ID uint64

ID names one screen instance, or one reload of the same instance, that asked for something asynchronous. The zero value never equals anything New returns (New starts at 1), so a Model left with a zero-value ID by an oversight drops every stamped result rather than silently accepting one it never asked for.

func New

func New() ID

New allocates the next ID, process-wide unique. A screen calls it once at construction, and again whenever it reloads itself in place rather than being replaced by a fresh Model — see the package doc.

type Result

type Result[T any] struct {
	From ID
	V    T
}

Result wraps a plain value with the ID that asked for it — the carrier Do and After return.

func (Result[T]) StampedBy

func (r Result[T]) StampedBy() ID

StampedBy implements Stamped.

type Scope

type Scope struct {
	ID
	// contains filtered or unexported fields
}

Scope is an owned context.Context paired with the ID that names it. Open ties both to a screen instance's own lifetime: Close cancels every DoCtx call still outstanding for it the moment the root actually removes that screen from the stack (internal/app's own pop/truncate), rather than leaving each one to run until its own per-call timeout fires — the two mechanisms are complementary, not redundant: a screen that is never popped (the matrix) still needs DoCtx's timeout, and a screen popped in the middle of a call still needs Close's immediate cancellation.

func Open

func Open() Scope

Open starts a new Scope: a fresh ID (New) and a cancellable context derived from context.Background() — the one place besides internal/app/session's own long-lived controller ctx that hoist's TUI is allowed to start a context from nothing, since a freshly constructed screen instance has no parent context to inherit (AGENTS.md §4.8, "each screen owns its ctx").

func (Scope) Close

func (s Scope) Close()

Close cancels this Scope's ctx. Safe to call more than once (context.CancelFunc already is) and safe on a zero Scope, where it is a no-op — a screen adapter that never got as far as opening one has nothing to cancel.

func (Scope) Ctx

func (s Scope) Ctx() context.Context

Ctx returns this Scope's own context — context.Background() for a zero Scope, so a caller never has to nil-check before using it. Prefer DoCtx when building a single command; Ctx is for a caller with its own multi-command shape to fit the context into (internal/app/tags' several independent fetches, for instance).

type Stamped

type Stamped interface {
	StampedBy() ID
}

Stamped is implemented by an async result message that carries the ID of whoever asked for it. Foreign is the only thing that reads it.

Jump to

Keyboard shortcuts

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