routes.go RawHistoryFolder
1package ports
2
3import (
4 "errors"
5 "fmt"
6 "unicode"
7 "unicode/utf8"
8
9 "github.com/bnema/vev/internal/domain"
10)
11
12// RouteOrigin identifies how a client reached a daemon. The origin is client
13// composition metadata, not an authority granted to the daemon.
14type RouteOrigin uint8
15
16const (
17 RouteOriginLocal RouteOrigin = iota + 1
18 RouteOriginRemote
19 RouteOriginDiscovery
20)
21
22func (o RouteOrigin) valid() bool {
23 switch o {
24 case RouteOriginLocal, RouteOriginRemote, RouteOriginDiscovery:
25 return true
26 default:
27 return false
28 }
29}
30
31func (o RouteOrigin) Validate() error {
32 if !o.valid() {
33 return errors.New("invalid route origin")
34 }
35 return nil
36}
37
38// RouteKind identifies the kind of target represented by one display entry.
39type RouteKind uint8
40
41const (
42 RouteKindLocal RouteKind = iota + 1
43 RouteKindRemote
44)
45
46func (k RouteKind) valid() bool {
47 switch k {
48 case RouteKindLocal, RouteKindRemote:
49 return true
50 default:
51 return false
52 }
53}
54
55func (k RouteKind) Validate() error {
56 if !k.valid() {
57 return errors.New("invalid route kind")
58 }
59 return nil
60}
61
62// RouteReachability is a closed display hint. It is never used as proof that
63// a route can be attached; the exact target and daemon still decide that.
64type RouteReachability uint8
65
66const (
67 RouteReachabilityUnknown RouteReachability = iota
68 RouteReachabilityReachable
69 RouteReachabilityUnavailable
70)
71
72func (r RouteReachability) valid() bool {
73 switch r {
74 case RouteReachabilityUnknown, RouteReachabilityReachable, RouteReachabilityUnavailable:
75 return true
76 default:
77 return false
78 }
79}
80
81func (r RouteReachability) Validate() error {
82 if !r.valid() {
83 return errors.New("invalid route reachability")
84 }
85 return nil
86}
87
88// ValidateRouteLabel rejects malformed or terminal-control text before it can
89// enter a daemon-rendered route snapshot. Empty labels are allowed only when
90// the caller explicitly marks them optional.
91func ValidateRouteLabel(value string, allowEmpty bool) error {
92 if !allowEmpty && value == "" {
93 return errors.New("route label is empty")
94 }
95 if len(value) > RouteLabelMaxBytes {
96 return errors.New("route label is too long")
97 }
98 if !utf8.ValidString(value) {
99 return errors.New("route label is not valid UTF-8")
100 }
101 for _, r := range value {
102 if unicode.IsControl(r) {
103 return errors.New("route label contains a control character")
104 }
105 if unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
106 return errors.New("route label contains a bidirectional or line-separator control character")
107 }
108 }
109 return nil
110}
111
112// ExactSessionTarget is the daemon-neutral identity required to attach to a
113// specific session lifecycle. The transport endpoint is deliberately absent:
114// the selected route's dialer owns that boundary.
115type ExactSessionTarget struct {
116 LifecycleID domain.SessionLifecycleID
117 SessionName string
118}
119
120func (t ExactSessionTarget) Validate() error {
121 if t.LifecycleID == (domain.SessionLifecycleID{}) {
122 return errors.New("missing session lifecycle")
123 }
124 if err := domain.ValidateSessionName(t.SessionName); err != nil {
125 return fmt.Errorf("invalid session name: %w", err)
126 }
127 return nil
128}
129
130// CommittedRouteIdentity is the identity a daemon commits after accepting an
131// attach. It is separate from display data so a stale presentation cannot
132// become an attach authority.
133type CommittedRouteIdentity struct {
134 Target ExactSessionTarget
135 Ephemeral bool
136}
137
138func (i CommittedRouteIdentity) Validate() error {
139 if err := i.Target.Validate(); err != nil {
140 return err
141 }
142 return nil
143}
144
145// RoutePosition is mutable per-client route state published by the daemon.
146// Target binds the tab cursor to one exact session lifecycle so delayed frames
147// cannot update another route.
148type RoutePosition struct {
149 Target ExactSessionTarget
150 ActiveTabID domain.TabStableID
151}
152
153func (p RoutePosition) Validate() error {
154 if err := p.Target.Validate(); err != nil {
155 return err
156 }
157 if err := domain.ValidateTabStableID(p.ActiveTabID); err != nil {
158 return fmt.Errorf("invalid active tab ID: %w", err)
159 }
160 return nil
161}
162
163// RouteRef is an opaque client-ledger reference. It is intentionally not a
164// lifecycle identifier and is meaningful only with the matching generation.
165type RouteRef struct {
166 Key uint64
167 Generation uint64
168}
169
170func (r RouteRef) empty() bool { return r.Key == 0 && r.Generation == 0 }
171
172func (r RouteRef) Validate() error {
173 if r.empty() {
174 return nil
175 }
176 if r.Key == 0 || r.Generation == 0 {
177 return errors.New("route reference must contain key and generation")
178 }
179 return nil
180}
181
182// RecentRouteEntry is the complete daemon-neutral display view of one route.
183// It contains no dialer, credential, endpoint, live session pointer, or attach
184// target. The client ledger keeps those private beside this value.
185type RecentRouteEntry struct {
186 Key uint64
187 Generation uint64
188 Name string
189 HostLabel string
190 Kind RouteKind
191 Ephemeral bool
192 Attention bool
193 Reachability RouteReachability
194}
195
196// RecentRouteSnapshot is an immutable, bounded publication of the client
197// route ledger. Entries are ordered from most recent to least recent.
198type RecentRouteSnapshot struct {
199 Generation uint64
200 Active RouteRef
201 Previous RouteRef
202 Home RouteRef
203 Entries []RecentRouteEntry
204}
205
206// RouteNavigationAction asks the client to resolve one entry from a specific
207// complete snapshot. It carries no session name, endpoint, or credential.
208type RouteNavigationAction struct {
209 SnapshotGeneration uint64
210 Key uint64
211 Generation uint64
212}
213
214// RouteNavigationFailure is a bounded taxonomy for a rejected or stale route
215// action. User-facing text remains local to the receiving client.
216type RouteNavigationFailure struct {
217 Key uint64
218 Generation uint64
219 Code RouteFailureCode
220}
221
222type RouteFailureCode uint8
223
224const (
225 RouteFailureStaleSelection RouteFailureCode = iota + 1
226 RouteFailureNoSuchRoute
227 RouteFailureUnavailable
228 RouteFailureTargetChanged
229 RouteFailureOriginUnavailable
230)
231
232func (c RouteFailureCode) valid() bool {
233 switch c {
234 case RouteFailureStaleSelection, RouteFailureNoSuchRoute, RouteFailureUnavailable, RouteFailureTargetChanged, RouteFailureOriginUnavailable:
235 return true
236 default:
237 return false
238 }
239}
240
241const (
242 // RouteSnapshotMaxEntries bounds one immutable publication and the amount
243 // of work a receiver performs before returning to its transport loop. The
244 // private client history may use a smaller product cap.
245 RouteSnapshotMaxEntries = 32
246 RouteLabelMaxBytes = 128
247)