detached ece7682

routes.go RawHistoryFolder

1package protocol
2
3import (
4	"errors"
5	"fmt"
6	"unicode"
7	"unicode/utf8"
8
9	"github.com/bnema/vev/internal/domain"
10)
11
12// ErrInvalidRouteWire preserves the public error classification used by
13// strict route codecs while route values live in the semantic protocol.
14var ErrInvalidRouteWire = errors.New("invalid route wire message")
15
16// RouteKind identifies the kind of target represented by one display entry.
17type RouteKind uint8
18
19const (
20	RouteKindLocal RouteKind = iota + 1
21	RouteKindRemote
22)
23
24func (k RouteKind) valid() bool {
25	switch k {
26	case RouteKindLocal, RouteKindRemote:
27		return true
28	default:
29		return false
30	}
31}
32
33func (k RouteKind) Validate() error {
34	if !k.valid() {
35		return errors.New("invalid route kind")
36	}
37	return nil
38}
39
40// RouteReachability is a closed display hint. It is never used as proof that
41// a route can be attached; the exact target and daemon still decide that.
42type RouteReachability uint8
43
44const (
45	RouteReachabilityUnknown RouteReachability = iota
46	RouteReachabilityReachable
47	RouteReachabilityUnavailable
48)
49
50func (r RouteReachability) valid() bool {
51	switch r {
52	case RouteReachabilityUnknown, RouteReachabilityReachable, RouteReachabilityUnavailable:
53		return true
54	default:
55		return false
56	}
57}
58
59func (r RouteReachability) Validate() error {
60	if !r.valid() {
61		return errors.New("invalid route reachability")
62	}
63	return nil
64}
65
66// ValidateRouteLabel rejects malformed or terminal-control text before it can
67// enter a daemon-rendered route snapshot. Empty labels are allowed only when
68// the caller explicitly marks them optional.
69func ValidateRouteLabel(value string, allowEmpty bool) error {
70	if !allowEmpty && value == "" {
71		return errors.New("route label is empty")
72	}
73	if len(value) > RouteLabelMaxBytes {
74		return errors.New("route label is too long")
75	}
76	if !utf8.ValidString(value) {
77		return errors.New("route label is not valid UTF-8")
78	}
79	for _, r := range value {
80		if unicode.IsControl(r) {
81			return errors.New("route label contains a control character")
82		}
83		if unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
84			return errors.New("route label contains a bidirectional or line-separator control character")
85		}
86	}
87	return nil
88}
89
90const NoTabIndex int32 = -1
91
92// SessionAttachTarget is the daemon-local, transport-neutral identity used by
93// session handshakes and handoffs. Endpoint, display origin, and registration
94// authority deliberately remain outside this contract.
95type SessionAttachTarget struct {
96	SessionID        domain.SessionID
97	LifecycleID      domain.SessionLifecycleID
98	SessionName      string
99	TabID            domain.TabStableID
100	TabIndex         int32
101	TabRawName       string
102	TabExpectedCount uint16
103	Stopped          bool
104}
105
106func (t SessionAttachTarget) selector() domain.TabSelector {
107	if t.TabID != "" {
108		return domain.NewStableTabSelector(t.TabID)
109	}
110	if t.TabIndex != NoTabIndex {
111		return domain.NewOrdinalTabSelector(uint16(t.TabIndex), t.TabRawName, t.TabExpectedCount)
112	}
113	return domain.TabSelector{}
114}
115
116func (t SessionAttachTarget) ResolveTab(tabs []domain.TabSelectorTab) (int, bool) {
117	selector := t.selector()
118	if selector == (domain.TabSelector{}) {
119		return 0, len(tabs) <= 1
120	}
121	return selector.Resolve(tabs)
122}
123
124func (t SessionAttachTarget) Validate() error {
125	if t.LifecycleID == (domain.SessionLifecycleID{}) {
126		return errors.New("missing session lifecycle")
127	}
128	if err := domain.ValidateSessionName(t.SessionName); err != nil {
129		return fmt.Errorf("invalid session name: %w", err)
130	}
131	if t.Stopped && t.SessionID != "" {
132		return errors.New("stopped target carries a session ID")
133	}
134	switch {
135	case t.TabID != "":
136		if domain.ValidateTabStableID(t.TabID) != nil || t.TabIndex != NoTabIndex || t.TabRawName != "" || t.TabExpectedCount != 0 {
137			return errors.New("invalid stable tab selector")
138		}
139	case t.TabIndex != NoTabIndex:
140		if t.TabIndex < 0 {
141			return errors.New("invalid ordinal tab selector")
142		}
143		if err := domain.NewOrdinalTabSelector(uint16(t.TabIndex), t.TabRawName, t.TabExpectedCount).Validate(); err != nil {
144			return fmt.Errorf("invalid ordinal tab selector: %w", err)
145		}
146	case t.TabRawName != "" || t.TabExpectedCount != 0:
147		return errors.New("incomplete tab selector")
148	}
149	if !t.Stopped && t.TabID == "" {
150		return errors.New("live target requires a stable tab selector")
151	}
152	return nil
153}
154
155// SessionAttachTargetFromRemote strips routing and presentation data from a
156// picker target before it enters the daemon session contract.
157func SessionAttachTargetFromRemote(remote domain.RemoteSessionTarget) SessionAttachTarget {
158	t := SessionAttachTarget{LifecycleID: remote.LifecycleID, SessionName: remote.SessionName, TabIndex: NoTabIndex, Stopped: remote.Stopped}
159	if !remote.Stopped {
160		t.TabID = remote.LiveTabID
161		return t
162	}
163	switch remote.StoppedTab.Kind {
164	case domain.TabSelectorByStableID:
165		t.TabID = remote.StoppedTab.StableID
166	case domain.TabSelectorByOrdinal:
167		t.TabIndex = int32(remote.StoppedTab.Ordinal)
168		t.TabRawName = remote.StoppedTab.RawName
169		t.TabExpectedCount = remote.StoppedTab.ExpectedCount
170	}
171	return t
172}
173
174// ExactSessionTarget is the daemon-neutral identity required to attach to a
175// specific session lifecycle. The transport endpoint is deliberately absent:
176// the selected route's dialer owns that boundary.
177type ExactSessionTarget struct {
178	LifecycleID domain.SessionLifecycleID
179	SessionName string
180}
181
182func (t ExactSessionTarget) Validate() error {
183	if t.LifecycleID == (domain.SessionLifecycleID{}) {
184		return errors.New("missing session lifecycle")
185	}
186	if err := domain.ValidateSessionName(t.SessionName); err != nil {
187		return fmt.Errorf("invalid session name: %w", err)
188	}
189	return nil
190}
191
192// CommittedRouteIdentity is the identity a daemon commits after accepting an
193// attach. It is separate from display data so a stale presentation cannot
194// become an attach authority.
195type CommittedRouteIdentity struct {
196	Target    ExactSessionTarget
197	Ephemeral bool
198}
199
200func (i CommittedRouteIdentity) Validate() error {
201	if err := i.Target.Validate(); err != nil {
202		return err
203	}
204	return nil
205}
206
207// RoutePosition is mutable per-client route state published by the daemon.
208// Target binds the tab cursor to one exact session lifecycle so delayed frames
209// cannot update another route.
210type RoutePosition struct {
211	Target      ExactSessionTarget
212	ActiveTabID domain.TabStableID
213}
214
215func (p RoutePosition) Validate() error {
216	if err := p.Target.Validate(); err != nil {
217		return err
218	}
219	if err := domain.ValidateTabStableID(p.ActiveTabID); err != nil {
220		return fmt.Errorf("invalid active tab ID: %w", err)
221	}
222	return nil
223}
224
225// RouteRef is an opaque client-ledger reference. It is intentionally not a
226// lifecycle identifier and is meaningful only with the matching generation.
227type RouteRef struct {
228	Key        uint64
229	Generation uint64
230}
231
232// RouteAttentionTarget binds one displayed route reference to an exact
233// lifecycle on the serving daemon or a monitored inventory source. It carries
234// no dialer, credential, or endpoint.
235type RouteAttentionTarget struct {
236	Ref    RouteRef
237	Target ExactSessionTarget
238	// SourceKey is empty for the serving daemon, or an opaque monitored
239	// inventory source. It never carries an endpoint or credential.
240	SourceKey string
241}
242
243// RouteRetired invalidates one subscribed exact lifecycle, not its name.
244// The reference fences delayed notifications against replacement routes.
245type RouteRetired struct {
246	Ref    RouteRef
247	Target ExactSessionTarget
248}
249
250// Validate rejects a retirement without the nonzero reference that fences it
251// and without a valid lifecycle target. Both encode and decode call it.
252func (r RouteRetired) Validate() error {
253	if r.Ref.IsZero() || r.Ref.Validate() != nil {
254		return fmt.Errorf("%w: retired route reference is zero", ErrInvalidRouteWire)
255	}
256	if err := r.Target.Validate(); err != nil {
257		return fmt.Errorf("%w: invalid retired route target: %v", ErrInvalidRouteWire, err)
258	}
259	return nil
260}
261
262// RouteAttentionSubscription is the bounded, client-owned mapping a daemon
263// uses to resolve live attention and retire authoritatively absent lifecycles.
264type RouteAttentionSubscription struct {
265	Targets []RouteAttentionTarget
266}
267
268// Validate enforces the bounded, duplicate-free attention mapping invariants
269// on both the encode and decode sides of the wire conversion.
270func (r RouteAttentionSubscription) Validate() error {
271	if len(r.Targets) > RouteSnapshotMaxEntries {
272		return fmt.Errorf("%w: too many attention targets", ErrInvalidRouteWire)
273	}
274	refs := make(map[RouteRef]struct{}, len(r.Targets))
275	for _, target := range r.Targets {
276		if err := ValidateRouteLabel(target.SourceKey, true); err != nil {
277			return fmt.Errorf("%w: invalid route source", ErrInvalidRouteWire)
278		}
279		if err := target.Ref.Validate(); err != nil || target.Ref.IsZero() {
280			return fmt.Errorf("%w: invalid attention route reference", ErrInvalidRouteWire)
281		}
282		if err := target.Target.Validate(); err != nil {
283			return fmt.Errorf("%w: invalid attention target: %v", ErrInvalidRouteWire, err)
284		}
285		if _, exists := refs[target.Ref]; exists {
286			return fmt.Errorf("%w: duplicate attention route reference", ErrInvalidRouteWire)
287		}
288		refs[target.Ref] = struct{}{}
289	}
290	return nil
291}
292
293func (r RouteRef) IsZero() bool { return r.Key == 0 && r.Generation == 0 }
294
295func (r RouteRef) Validate() error {
296	if r.IsZero() {
297		return nil
298	}
299	if r.Key == 0 || r.Generation == 0 {
300		return errors.New("route reference must contain key and generation")
301	}
302	return nil
303}
304
305// RecentRouteEntry is the complete daemon-neutral identity and display view of
306// one route. Target carries only the committed lifecycle UUID and name; it
307// contains no dialer, credential, endpoint, or live session pointer.
308type RecentRouteEntry struct {
309	Key          uint64
310	Generation   uint64
311	Target       ExactSessionTarget
312	Name         string
313	HostLabel    string
314	Kind         RouteKind
315	Ephemeral    bool
316	Attention    bool
317	Reachability RouteReachability
318	// AttentionSeq orders attention onsets across every daemon the client
319	// observes: a smaller value is older. It is zero without attention.
320	AttentionSeq uint64
321	// Visited reports that this client attached to the route. The status-bar
322	// history and jump-recent ranks show visited routes plus ringing ones.
323	Visited bool
324}
325
326// RouteHost is one daemon the client can create a session on, other than
327// the serving daemon. Its reference shares the entry namespace; it carries
328// no endpoint, dialer, or credential.
329type RouteHost struct {
330	Key        uint64
331	Generation uint64
332	Label      string
333	Kind       RouteKind
334}
335
336// RecentRouteSnapshot is an immutable, bounded publication of the client
337// route ledger. Entries are ordered from most recent to least recent.
338type RecentRouteSnapshot struct {
339	Generation uint64
340	Active     RouteRef
341	// ActiveEntry maps Active's committed lifecycle UUID to its presentation.
342	// It is not a navigation candidate and therefore remains separate from
343	// Entries, which are ordered from most recent to least recent.
344	ActiveEntry RecentRouteEntry
345	Previous    RouteRef
346	Home        RouteRef
347	Entries     []RecentRouteEntry
348	// Hosts are the creation destinations besides the serving daemon.
349	Hosts []RouteHost
350}
351
352// validateRecentRouteEntry enforces one entry's identity and display bounds.
353func validateRecentRouteEntry(entry RecentRouteEntry) error {
354	if entry.Key == 0 || entry.Generation == 0 {
355		return fmt.Errorf("%w: route entry identity is zero", ErrInvalidRouteWire)
356	}
357	if err := entry.Target.Validate(); err != nil {
358		return fmt.Errorf("%w: invalid route lifecycle target: %v", ErrInvalidRouteWire, err)
359	}
360	if entry.Name != entry.Target.SessionName {
361		return fmt.Errorf("%w: route name does not match lifecycle target", ErrInvalidRouteWire)
362	}
363	if err := ValidateRouteLabel(entry.Name, false); err != nil {
364		return fmt.Errorf("%w: route name: %v", ErrInvalidRouteWire, err)
365	}
366	if err := ValidateRouteLabel(entry.HostLabel, true); err != nil {
367		return fmt.Errorf("%w: route host label: %v", ErrInvalidRouteWire, err)
368	}
369	if entry.Kind.Validate() != nil {
370		return fmt.Errorf("%w: invalid route kind", ErrInvalidRouteWire)
371	}
372	if entry.Reachability.Validate() != nil {
373		return fmt.Errorf("%w: invalid route reachability", ErrInvalidRouteWire)
374	}
375	if entry.AttentionSeq != 0 && !entry.Attention {
376		return fmt.Errorf("%w: attention order without attention", ErrInvalidRouteWire)
377	}
378	return nil
379}
380
381func validateRouteHost(host RouteHost) error {
382	if host.Key == 0 || host.Generation == 0 {
383		return fmt.Errorf("%w: route host identity is zero", ErrInvalidRouteWire)
384	}
385	if err := ValidateRouteLabel(host.Label, false); err != nil {
386		return fmt.Errorf("%w: route host label: %v", ErrInvalidRouteWire, err)
387	}
388	if host.Kind.Validate() != nil {
389		return fmt.Errorf("%w: invalid route host kind", ErrInvalidRouteWire)
390	}
391	return nil
392}
393
394// Validate enforces the bounded, self-consistent invariants of one complete
395// route publication: entry budget, nonzero generation for non-empty
396// snapshots, matching active presentation, unique entry references, and
397// previous/home references that resolve inside the snapshot. Both encode and
398// decode call it so a malformed snapshot cannot be produced or accepted.
399func (s RecentRouteSnapshot) Validate() error {
400	if len(s.Entries) > RouteSnapshotMaxEntries {
401		return fmt.Errorf("%w: too many route entries", ErrInvalidRouteWire)
402	}
403	if len(s.Hosts) > RouteSnapshotMaxHosts {
404		return fmt.Errorf("%w: too many route hosts", ErrInvalidRouteWire)
405	}
406	if s.Generation == 0 && (len(s.Entries) != 0 || len(s.Hosts) != 0 || s.ActiveEntry != (RecentRouteEntry{})) {
407		return fmt.Errorf("%w: non-empty snapshot has zero generation", ErrInvalidRouteWire)
408	}
409	for _, ref := range []RouteRef{s.Active, s.Previous, s.Home} {
410		if err := ref.Validate(); err != nil {
411			return fmt.Errorf("%w: route reference: %v", ErrInvalidRouteWire, err)
412		}
413	}
414	if s.Active.IsZero() {
415		if s.ActiveEntry != (RecentRouteEntry{}) {
416			return fmt.Errorf("%w: active presentation has no active route", ErrInvalidRouteWire)
417		}
418	} else {
419		if err := validateRecentRouteEntry(s.ActiveEntry); err != nil {
420			return err
421		}
422		if s.Active != (RouteRef{Key: s.ActiveEntry.Key, Generation: s.ActiveEntry.Generation}) {
423			return fmt.Errorf("%w: active presentation does not match active route", ErrInvalidRouteWire)
424		}
425	}
426	if !s.Active.IsZero() && s.Active == s.Previous {
427		return fmt.Errorf("%w: active and previous routes are identical", ErrInvalidRouteWire)
428	}
429	refs := make(map[RouteRef]struct{}, len(s.Entries))
430	for _, entry := range s.Entries {
431		if s.Active == (RouteRef{Key: entry.Key, Generation: entry.Generation}) {
432			return fmt.Errorf("%w: active route must be metadata-only", ErrInvalidRouteWire)
433		}
434		if err := validateRecentRouteEntry(entry); err != nil {
435			return err
436		}
437		ref := RouteRef{Key: entry.Key, Generation: entry.Generation}
438		if _, exists := refs[ref]; exists {
439			return fmt.Errorf("%w: duplicate route entry", ErrInvalidRouteWire)
440		}
441		refs[ref] = struct{}{}
442	}
443	for _, host := range s.Hosts {
444		if err := validateRouteHost(host); err != nil {
445			return err
446		}
447		ref := RouteRef{Key: host.Key, Generation: host.Generation}
448		if _, exists := refs[ref]; exists || ref == s.Active {
449			return fmt.Errorf("%w: duplicate route host", ErrInvalidRouteWire)
450		}
451		refs[ref] = struct{}{}
452	}
453	for _, item := range []struct {
454		name string
455		ref  RouteRef
456	}{
457		{name: "previous", ref: s.Previous},
458		{name: "home", ref: s.Home},
459	} {
460		if item.ref.IsZero() || item.ref == s.Active {
461			// Active is metadata-only and intentionally excluded from the
462			// recent display entries. Home may point at active as well.
463			continue
464		}
465		if _, exists := refs[item.ref]; !exists {
466			return fmt.Errorf("%w: %s route reference is absent from snapshot", ErrInvalidRouteWire, item.name)
467		}
468	}
469	return nil
470}
471
472// RouteNavigationAction asks the client to resolve one entry from a specific
473// complete snapshot. It carries no session name, endpoint, or credential.
474type RouteNavigationAction struct {
475	CauseActionID      uint64
476	SnapshotGeneration uint64
477	Key                uint64
478	Generation         uint64
479}
480
481// Validate rejects a navigation action whose snapshot/entry identity is zero;
482// both encode and decode call it so a stale or malformed action never ships.
483func (a RouteNavigationAction) Validate() error {
484	if a.SnapshotGeneration == 0 || a.Key == 0 || a.Generation == 0 {
485		return fmt.Errorf("%w: navigation action identity is zero", ErrInvalidRouteWire)
486	}
487	return nil
488}
489
490// RouteCreateSessionAction asks the client to create a named session through
491// one exact route authority from the latest complete snapshot.
492type RouteCreateSessionAction struct {
493	CauseActionID      uint64
494	RequestID          uint64
495	SnapshotGeneration uint64
496	Key                uint64
497	Generation         uint64
498	SessionName        string
499}
500
501func (a RouteCreateSessionAction) Validate() error {
502	if a.RequestID == 0 || a.SnapshotGeneration == 0 || a.Key == 0 || a.Generation == 0 || domain.ValidateSessionName(a.SessionName) != nil {
503		return ErrInvalidRouteWire
504	}
505	return nil
506}
507
508// SamePeerSwitchRequest moves the attachment to another session of the same
509// daemon. The client sends it from the picker or to confirm a daemon offer; a
510// pending offer pins the only accepted target. It carries only the exact
511// lifecycle identity and the client-owned tab cursor; transport origin remains
512// proven by the existing authenticated connection.
513type SamePeerSwitchRequest struct {
514	RequestID      uint64
515	Target         ExactSessionTarget
516	PreferredTabID domain.TabStableID
517}
518
519func (r SamePeerSwitchRequest) Validate() error {
520	if r.RequestID == 0 {
521		return ErrInvalidRouteWire
522	}
523	if err := r.Target.Validate(); err != nil {
524		return ErrInvalidRouteWire
525	}
526	if r.PreferredTabID != "" {
527		if err := domain.ValidateTabStableID(r.PreferredTabID); err != nil {
528			return ErrInvalidRouteWire
529		}
530	}
531	return nil
532}
533
534// SamePeerSwitchFailureCode is a closed pre-commit rejection taxonomy.
535type SamePeerSwitchFailureCode uint8
536
537const (
538	SamePeerSwitchStaleTarget SamePeerSwitchFailureCode = iota + 1
539	SamePeerSwitchUnavailable
540)
541
542func (c SamePeerSwitchFailureCode) valid() bool {
543	return c == SamePeerSwitchStaleTarget || c == SamePeerSwitchUnavailable
544}
545
546func (c SamePeerSwitchFailureCode) Validate() error {
547	if !c.valid() {
548		return ErrInvalidRouteWire
549	}
550	return nil
551}
552
553// SamePeerSwitchFailure leaves the source attachment unchanged. RequestID
554// rejects a delayed failure after a later user selection.
555type SamePeerSwitchFailure struct {
556	RequestID uint64
557	Code      SamePeerSwitchFailureCode
558}
559
560func (f SamePeerSwitchFailure) Validate() error {
561	if f.RequestID == 0 || !f.Code.valid() {
562		return ErrInvalidRouteWire
563	}
564	return nil
565}
566
567// RouteNavigationFailure is a bounded taxonomy for a rejected or stale route
568// action. User-facing text remains local to the receiving client.
569type RouteNavigationFailure struct {
570	Key        uint64
571	Generation uint64
572	Code       RouteFailureCode
573}
574
575type RouteFailureCode uint8
576
577const (
578	RouteFailureStaleSelection RouteFailureCode = iota + 1
579	RouteFailureNoSuchRoute
580	RouteFailureUnavailable
581	RouteFailureTargetChanged
582	RouteFailureOriginUnavailable
583)
584
585func (c RouteFailureCode) valid() bool {
586	switch c {
587	case RouteFailureStaleSelection, RouteFailureNoSuchRoute, RouteFailureUnavailable, RouteFailureTargetChanged, RouteFailureOriginUnavailable:
588		return true
589	default:
590		return false
591	}
592}
593
594func (c RouteFailureCode) Validate() error {
595	if !c.valid() {
596		return ErrInvalidRouteWire
597	}
598	return nil
599}
600
601// Validate requires the nonzero entry identity that correlates the failure
602// with its navigation action, plus a bounded failure code. Both encode and
603// decode call it so a zero identity never ships.
604func (f RouteNavigationFailure) Validate() error {
605	if f.Key == 0 || f.Generation == 0 {
606		return fmt.Errorf("%w: failure identity is zero", ErrInvalidRouteWire)
607	}
608	return f.Code.Validate()
609}
610
611// SessionCreationFailure reports a correlated pre-commit create transition
612// failure after the client has restored the source route.
613type SessionCreationFailure struct {
614	RequestID uint64
615	Code      RouteFailureCode
616}
617
618func (f SessionCreationFailure) Validate() error {
619	if f.RequestID == 0 || !f.Code.valid() {
620		return ErrInvalidRouteWire
621	}
622	return nil
623}
624
625const (
626	// RouteSnapshotMaxEntries bounds one immutable publication and the amount
627	// of work a receiver performs before returning to its transport loop. The
628	// private client history may use a smaller product cap.
629	RouteSnapshotMaxEntries = 32
630	// RouteSnapshotMaxHosts bounds the creation destinations of one snapshot.
631	RouteSnapshotMaxHosts = 32
632	RouteLabelMaxBytes    = 256
633)

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