initial_navigation.go RawHistoryFolder

1package client
2
3import (
4	"errors"
5	"fmt"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/ports"
9	"github.com/bnema/vev/internal/protocol"
10)
11
12// Closed, one-shot initial navigation.
13//
14// InitialNavigation is the comparable, closed description of the single broker
15// navigation a client performs after its first committed catalogue publication.
16// It replaces the former single-variant enum outright: there is no alias, and
17// the zero value is a safe no-navigation "picker" intent so a configuration
18// that omits the intent opens nothing.
19//
20// The union carries no live authority of its own. Validate is total and
21// performs no I/O; Resolve revalidates the intent against one broker snapshot
22// and returns the exact ports.BrokerOpenStreamRequest gabarit, whose connection
23// and stream identity the supervisor fills from its own connection before it
24// opens the stream.
25//
26// Rules the closed union enforces:
27//
28//   - Picker carries nothing at all: a zero destination, a zero epoch, no name,
29//     and a zero target.
30//   - CreateEphemeral and CreateNamed carry a valid endpoint fence, a name
31//     only for the named variant, and no target.
32//   - AttachExact carries a valid endpoint fence, a nonzero epoch, no name, and
33//     a validated protocol.ExactSessionTarget.
34//   - AttachNamed carries a valid endpoint fence, a name, and no target; the
35//     daemon resolves the name and refuses an unknown one.
36//   - A local creation may state epoch zero, meaning "bind to the first adopted
37//     publication". Every remote intent, and every exact attach even locally,
38//     requires the epoch of the snapshot that resolved it, and a nonzero epoch
39//     must equal the current snapshot's epoch. Epochs never compare numerically
40//     across broker processes.
41type InitialNavigationKind uint8
42
43const (
44	// InitialNavigationPicker is the zero kind: present the picker and open
45	// nothing. A configuration that omits the intent is safe.
46	InitialNavigationPicker InitialNavigationKind = iota
47	// InitialNavigationCreateEphemeral creates one ephemeral session.
48	InitialNavigationCreateEphemeral
49	// InitialNavigationCreateNamed creates one named session.
50	InitialNavigationCreateNamed
51	// InitialNavigationAttachExact attaches to one exact session lifecycle.
52	InitialNavigationAttachExact
53	// InitialNavigationAttachNamed attaches to the session the daemon knows by
54	// name; existence is the daemon's decision, not the broker catalogue's.
55	InitialNavigationAttachNamed
56)
57
58func (k InitialNavigationKind) String() string {
59	switch k {
60	case InitialNavigationPicker:
61		return "picker"
62	case InitialNavigationCreateEphemeral:
63		return "create_ephemeral"
64	case InitialNavigationCreateNamed:
65		return "create_named"
66	case InitialNavigationAttachExact:
67		return "attach_exact"
68	case InitialNavigationAttachNamed:
69		return "attach_named"
70	default:
71		return fmt.Sprintf("invalid(%d)", uint8(k))
72	}
73}
74
75// InitialNavigation is one closed, one-shot navigation intent. Destination is
76// the existing ports.BrokerEndpointFence: a local destination is exactly
77// Local=true with no registration, and a remote destination carries the
78// complete registration observed in a broker snapshot, never a bare hostname
79// and never an invented daemon identity.
80type InitialNavigation struct {
81	Kind        InitialNavigationKind
82	Epoch       ports.BrokerEpoch
83	Destination ports.BrokerEndpointFence
84	Name        string
85	Target      protocol.ExactSessionTarget
86}
87
88// InitialNavigationResolver translates one composition-owned CLI target (an
89// attach name or a remote endpoint) into an exact navigation identity against
90// the first committed broker snapshot. It is a translation of a target into
91// identity, not an owner of navigation: it performs no I/O, it is consulted at
92// most once, and the supervisor remains the only component that consumes the
93// intent and opens the stream.
94type InitialNavigationResolver func(ports.BrokerSnapshot) (InitialNavigation, error)
95
96// Validate checks the closed union. It is total, pure, and never consults a
97// snapshot: a caller can always reject a contradictory intent offline.
98func (n InitialNavigation) Validate() error {
99	switch n.Kind {
100	case InitialNavigationPicker:
101		if n.Destination != (ports.BrokerEndpointFence{}) {
102			return errors.New("vev: picker navigation carries a destination")
103		}
104		if n.Epoch != 0 {
105			return errors.New("vev: picker navigation carries an epoch")
106		}
107		if n.Name != "" {
108			return errors.New("vev: picker navigation carries a session name")
109		}
110		if n.Target != (protocol.ExactSessionTarget{}) {
111			return errors.New("vev: picker navigation carries an exact target")
112		}
113		return nil
114	case InitialNavigationCreateEphemeral, InitialNavigationCreateNamed, InitialNavigationAttachExact, InitialNavigationAttachNamed:
115	default:
116		return fmt.Errorf("vev: unknown initial navigation kind %d", uint8(n.Kind))
117	}
118	if err := n.Destination.Validate(); err != nil {
119		return fmt.Errorf("vev: initial navigation destination: %w", err)
120	}
121	if !n.Destination.Local && n.Epoch == 0 {
122		// A remote destination cannot be decided before the connection: it
123		// requires the epoch of the snapshot that resolved its registration.
124		return errors.New("vev: remote initial navigation carries no epoch")
125	}
126	switch n.Kind {
127	case InitialNavigationCreateEphemeral:
128		if n.Name != "" {
129			return errors.New("vev: ephemeral creation navigation carries a session name")
130		}
131		if n.Target != (protocol.ExactSessionTarget{}) {
132			return errors.New("vev: ephemeral creation navigation carries an exact target")
133		}
134	case InitialNavigationCreateNamed, InitialNavigationAttachNamed:
135		if err := domain.ValidateSessionName(n.Name); err != nil {
136			return fmt.Errorf("vev: initial navigation session name: %w", err)
137		}
138		if n.Target != (protocol.ExactSessionTarget{}) {
139			return errors.New("vev: named navigation carries an exact target")
140		}
141	case InitialNavigationAttachExact:
142		if n.Epoch == 0 {
143			return errors.New("vev: exact attach navigation carries no epoch")
144		}
145		if n.Name != "" {
146			return errors.New("vev: exact attach navigation carries a session name")
147		}
148		if err := n.Target.Validate(); err != nil {
149			return fmt.Errorf("vev: initial navigation target: %w", err)
150		}
151	}
152	return nil
153}
154
155// Resolve revalidates the intent against one broker snapshot and returns the
156// exact stream-request gabarit it names. The gabarit deliberately leaves
157// Connection and Stream zero: the supervisor sets them from its adopted
158// connection before it opens the stream. Policy is copied from the selected
159// authority and the environment is never read from the broker process.
160//
161// Picker is a success without an operation for the supervisor, but resolving it
162// directly is refused: a caller never receives a nil-but-usable request for a
163// navigation that opens nothing.
164func (n InitialNavigation) Resolve(snapshot ports.BrokerSnapshot) (ports.BrokerOpenStreamRequest, error) {
165	if n.Kind == InitialNavigationPicker {
166		return ports.BrokerOpenStreamRequest{}, pickerCatalogueError{Code: pickerCatalogueNoSelection, Text: "picker navigation opens no stream"}
167	}
168	if err := n.Validate(); err != nil {
169		return ports.BrokerOpenStreamRequest{}, pickerCatalogueError{Code: initialNavigationRefusal(n), Text: "initial navigation is not valid"}
170	}
171	if err := snapshot.Validate(); err != nil {
172		return ports.BrokerOpenStreamRequest{}, pickerCatalogueError{Code: pickerCatalogueUnavailable, Text: "broker catalogue is unavailable"}
173	}
174	// The gabarit is validated end to end against a placeholder connection
175	// identity, then stripped of it: the supervisor is the only component that
176	// assigns the live connection and stream before it opens the stream.
177	request, err := resolveCatalogueRequest(snapshot, pickerSelectionFromNavigation(n), initialNavigationGabaritBase)
178	if err != nil {
179		return ports.BrokerOpenStreamRequest{}, err
180	}
181	request.Connection = ports.BrokerConnectionID{}
182	request.Stream = 0
183	return request, nil
184}
185
186// initialNavigationGabaritBase is the placeholder connection identity used to
187// validate a resolved initial-navigation request end to end before the
188// supervisor assigns its own live connection and stream identity. The gabarit
189// returned to the caller always carries a zero connection and stream.
190var initialNavigationGabaritBase = pickerResolveBase{Connection: ports.BrokerConnectionID{1}, Stream: ports.BrokerStreamID(1)}
191
192// pickerSelectionFromNavigation projects one validated intent into the exact
193// selection identity the shared resolver already understands, so the union and
194// the interactive picker cannot drift into two resolution semantics.
195func pickerSelectionFromNavigation(n InitialNavigation) pickerSelectionRef {
196	ref := pickerSelectionRef{
197		epoch:        n.Epoch,
198		local:        n.Destination.Local,
199		endpoint:     n.Destination.Registration.Endpoint,
200		registration: n.Destination.Registration,
201	}
202	switch n.Kind {
203	case InitialNavigationCreateNamed:
204		ref.kind = pickerSelectionCreateNamed
205		ref.createName = n.Name
206	case InitialNavigationCreateEphemeral:
207		ref.kind = pickerSelectionCreateEphemeral
208	case InitialNavigationAttachExact:
209		ref.kind = pickerSelectionExact
210		ref.lifecycle = n.Target.LifecycleID
211		ref.name = n.Target.SessionName
212	case InitialNavigationAttachNamed:
213		ref.kind = pickerSelectionAttachNamed
214		ref.name = n.Name
215	}
216	return ref
217}
218
219// initialNavigationRefusal classifies a failed intent validation into the
220// bounded picker refusal taxonomy without exposing free text: an invalid
221// creation name keeps its own code, and every other contradiction is an
222// unknown-kind refusal.
223func initialNavigationRefusal(n InitialNavigation) pickerCatalogueErrorCode {
224	if n.Kind == InitialNavigationCreateNamed || n.Kind == InitialNavigationAttachNamed {
225		if err := domain.ValidateSessionName(n.Name); err != nil {
226			return pickerCatalogueInvalidName
227		}
228	}
229	return pickerCatalogueUnknown
230}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close