detached 4429457

refactor(daemon): consume typed session messages

commit
44294570fdee51089f2af811cb71db025889dabf
author
brice · · 2026-08-28 06:33 UTC
committer
brice · · 2026-08-28 13:08 UTC
parent
f791584
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
50 files · +2949 -473
A internal/adapters/sessionwire/server.go +373 -0
@@ -0,0 +1,373 @@
1+// Package sessionwire adapts typed session messages to strict binary frames.
2+package sessionwire
3+
4+import (
5+ "encoding/binary"
6+ "errors"
7+
8+ "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/wire"
11+)
12+
13+var (
14+ ErrUnknownMessageType = errors.New("sessionwire: unknown message type")
15+ ErrWrongDirection = errors.New("sessionwire: wrong message direction")
16+ ErrInvalidMessage = errors.New("sessionwire: invalid message")
17+ ErrUnsupportedSend = errors.New("sessionwire: send mode is unsupported")
18+)
19+
20+type serverConnection struct{ raw ports.Transport }
21+
22+var _ ports.ServerConnection = (*serverConnection)(nil)
23+
24+// NewServerConnection wraps one raw connection incarnation exactly once.
25+func NewServerConnection(raw ports.Transport) ports.ServerConnection {
26+ if raw == nil {
27+ return nil
28+ }
29+ return &serverConnection{raw: raw}
30+}
31+
32+func (c *serverConnection) ReceiveClient() (protocol.ClientMessage, error) {
33+ frame, err := c.raw.Recv()
34+ if err != nil {
35+ return nil, err
36+ }
37+ message, err := decodeClient(frame)
38+ if err == nil {
39+ return message, nil
40+ }
41+ failure := &protocol.DecodeFailure{Category: protocol.DecodeMalformed, Type: uint8(frame.Type), Err: err}
42+ if errors.Is(err, ErrUnknownMessageType) {
43+ failure.Category = protocol.DecodeUnknownType
44+ } else if errors.Is(err, ErrWrongDirection) {
45+ failure.Category = protocol.DecodeWrongDirection
46+ }
47+ switch frame.Type {
48+ case wire.MsgHello:
49+ failure.Kind = protocol.DecodeMessageHello
50+ failure.Version, _ = wire.PeekHelloVersion(frame.Payload)
51+ case wire.MsgCommand:
52+ failure.Kind = protocol.DecodeMessageCommand
53+ failure.Version, _ = wire.PeekCommandVersion(frame.Payload)
54+ if len(frame.Payload) >= 10 {
55+ failure.RequestID = binary.BigEndian.Uint64(frame.Payload[2:10])
56+ }
57+ case wire.MsgKill:
58+ failure.Kind = protocol.DecodeMessageKill
59+ case wire.MsgRemotePreviewRequest:
60+ failure.Kind = protocol.DecodeMessageRemotePreview
61+ }
62+ return nil, failure
63+}
64+
65+func (c *serverConnection) SendServer(message protocol.ServerMessage) error {
66+ frame, err := encodeServer(message)
67+ if err != nil {
68+ return err
69+ }
70+ return c.raw.Send(frame)
71+}
72+
73+func (c *serverConnection) SendServerAsync(message protocol.ServerMessage) error {
74+ transport, ok := c.raw.(ports.AsyncTransport)
75+ if !ok {
76+ return ErrUnsupportedSend
77+ }
78+ frame, err := encodeServer(message)
79+ if err != nil {
80+ return err
81+ }
82+ return transport.SendAsync(frame)
83+}
84+
85+func (c *serverConnection) SendServerSynchronous(message protocol.ServerMessage) error {
86+ transport, ok := c.raw.(ports.OwnedSynchronousTransport)
87+ if !ok {
88+ return ErrUnsupportedSend
89+ }
90+ frame, err := encodeServer(message)
91+ if err != nil {
92+ return err
93+ }
94+ return transport.SendSynchronous(frame)
95+}
96+
97+func (c *serverConnection) SendOutput(output protocol.Output) error {
98+ frame, err := encodeOutput(output)
99+ if err != nil {
100+ return err
101+ }
102+ return c.raw.Send(frame)
103+}
104+
105+func (c *serverConnection) SendOutputAsync(output protocol.Output) error {
106+ transport, ok := c.raw.(ports.AsyncTransport)
107+ if !ok {
108+ return ErrUnsupportedSend
109+ }
110+ frame, err := encodeOutput(output)
111+ if err != nil {
112+ return err
113+ }
114+ return transport.SendAsync(frame)
115+}
116+
117+func (c *serverConnection) SendOutputSynchronous(output protocol.Output) error {
118+ transport, ok := c.raw.(ports.OwnedSynchronousTransport)
119+ if !ok {
120+ return ErrUnsupportedSend
121+ }
122+ frame, err := encodeOutput(output)
123+ if err != nil {
124+ return err
125+ }
126+ return transport.SendSynchronous(frame)
127+}
128+
129+func encodeOutput(output protocol.Output) (wire.Frame, error) {
130+ payload, err := wire.MarshalOutput(output)
131+ return wire.Frame{Type: wire.MsgOutput, Payload: payload}, err
132+}
133+
134+func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities {
135+ _, datagram := c.raw.(ports.DatagramTransport)
136+ _, async := c.raw.(ports.AsyncTransport)
137+ _, synchronous := c.raw.(ports.OwnedSynchronousTransport)
138+ _, linkState := c.raw.(ports.LinkStateReporter)
139+ window := uint8(8)
140+ if datagram {
141+ window = 1
142+ }
143+ return protocol.ConnectionCapabilities{
144+ OutputDataLimit: protocol.MaxOutputDataLen,
145+ PreferredOutputWindow: window,
146+ AsyncSend: async,
147+ OwnedSynchronousSend: synchronous,
148+ LinkState: linkState,
149+ }
150+}
151+
152+func (c *serverConnection) LinkState() ports.LinkState {
153+ if reporter, ok := c.raw.(ports.LinkStateReporter); ok {
154+ return reporter.LinkState()
155+ }
156+ return ports.LinkStateConnected
157+}
158+
159+func (c *serverConnection) LinkEvents() <-chan ports.LinkEvent {
160+ if reporter, ok := c.raw.(ports.LinkStateReporter); ok {
161+ return reporter.LinkEvents()
162+ }
163+ return nil
164+}
165+
166+func (c *serverConnection) Close() error { return c.raw.Close() }
167+
168+type serverListener struct{ raw ports.Listener }
169+
170+var _ ports.ServerListener = (*serverListener)(nil)
171+
172+// NewServerListener wraps every accepted raw connection in a stable typed adapter.
173+func NewServerListener(raw ports.Listener) ports.ServerListener {
174+ if raw == nil {
175+ return nil
176+ }
177+ return &serverListener{raw: raw}
178+}
179+
180+func (l *serverListener) Accept() (ports.ServerConnection, error) {
181+ raw, err := l.raw.Accept()
182+ if err != nil {
183+ return nil, err
184+ }
185+ return NewServerConnection(raw), nil
186+}
187+
188+func (l *serverListener) Close() error { return l.raw.Close() }
189+func (l *serverListener) Addr() string { return l.raw.Addr() }
190+
191+func decodeClient(frame wire.Frame) (protocol.ClientMessage, error) {
192+ switch frame.Type {
193+ case wire.MsgHello:
194+ return wire.UnmarshalHello(frame.Payload)
195+ case wire.MsgInput:
196+ return wire.UnmarshalInput(frame.Payload)
197+ case wire.MsgResize:
198+ return wire.UnmarshalResize(frame.Payload)
199+ case wire.MsgDetach:
200+ return protocol.Detach{}, nil
201+ case wire.MsgPing:
202+ return protocol.Ping{}, nil
203+ case wire.MsgList:
204+ return protocol.List{}, nil
205+ case wire.MsgKill:
206+ return wire.UnmarshalKill(frame.Payload)
207+ case wire.MsgTheme:
208+ return wire.UnmarshalTheme(frame.Payload)
209+ case wire.MsgAck:
210+ return wire.UnmarshalAck(frame.Payload)
211+ case wire.MsgImagePush:
212+ return wire.UnmarshalImagePush(frame.Payload)
213+ case wire.MsgClientNotice:
214+ return wire.UnmarshalClientNotice(frame.Payload)
215+ case wire.MsgCommand:
216+ return wire.UnmarshalCommandRequest(frame.Payload)
217+ case wire.MsgOutputResetRequest:
218+ return wire.UnmarshalOutputResetRequest(frame.Payload)
219+ case wire.MsgRemotePreviewRequest:
220+ return wire.UnmarshalRemotePreviewRequest(frame.Payload)
221+ case wire.MsgRouteAttentionSubscription:
222+ return wire.UnmarshalRouteAttentionSubscription(frame.Payload)
223+ case wire.MsgSamePeerSwitchRequest:
224+ return wire.UnmarshalSamePeerSwitchRequest(frame.Payload)
225+ case wire.MsgParkedRouteRequest:
226+ return wire.UnmarshalParkedRouteRequest(frame.Payload)
227+ case wire.MsgRecentRouteSnapshot:
228+ return wire.UnmarshalRecentRouteSnapshot(frame.Payload)
229+ case wire.MsgRouteNavigationFailure:
230+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
231+ case wire.MsgWelcome, wire.MsgError, wire.MsgOutput, wire.MsgDetached, wire.MsgPong,
232+ wire.MsgSessions, wire.MsgCommandResult, wire.MsgNavigationAction,
233+ wire.MsgAttachTarget, wire.MsgRemotePreviewResponse,
234+ wire.MsgCommittedRouteIdentity, wire.MsgNavigateRecentRoute,
235+ wire.MsgRoutePosition, wire.MsgSamePeerSwitchFailure, wire.MsgParkedRouteResponse:
236+ return nil, ErrWrongDirection
237+ default:
238+ return nil, ErrUnknownMessageType
239+ }
240+}
241+
242+func encodeServer(message protocol.ServerMessage) (wire.Frame, error) {
243+ switch m := message.(type) {
244+ case protocol.Welcome:
245+ payload := wire.MarshalWelcome(m)
246+ if payload == nil {
247+ return wire.Frame{}, ErrInvalidMessage
248+ }
249+ return wire.Frame{Type: wire.MsgWelcome, Payload: payload}, nil
250+ case protocol.ErrorMsg:
251+ return wire.Frame{Type: wire.MsgError, Payload: wire.MarshalErrorMsg(m)}, nil
252+ case protocol.Output:
253+ payload, err := wire.MarshalOutput(m)
254+ return wire.Frame{Type: wire.MsgOutput, Payload: payload}, err
255+ case protocol.Detached:
256+ if err := protocol.ValidateDetached(m); err != nil {
257+ return wire.Frame{}, err
258+ }
259+ return wire.Frame{Type: wire.MsgDetached, Payload: wire.MarshalDetached(m)}, nil
260+ case protocol.Pong:
261+ return wire.Frame{Type: wire.MsgPong, Payload: wire.MarshalPong(m)}, nil
262+ case protocol.Sessions:
263+ return wire.Frame{Type: wire.MsgSessions, Payload: wire.MarshalSessions(m)}, nil
264+ case protocol.CommandResult:
265+ return wire.Frame{Type: wire.MsgCommandResult, Payload: wire.MarshalCommandResult(m)}, nil
266+ case protocol.NavigationDirective:
267+ payload := wire.MarshalNavigationDirective(m)
268+ if payload == nil {
269+ return wire.Frame{}, ErrInvalidMessage
270+ }
271+ return wire.Frame{Type: wire.MsgNavigationAction, Payload: payload}, nil
272+ case protocol.AttachTarget:
273+ payload := wire.MarshalAttachTarget(m)
274+ if payload == nil {
275+ return wire.Frame{}, ErrInvalidMessage
276+ }
277+ return wire.Frame{Type: wire.MsgAttachTarget, Payload: payload}, nil
278+ case protocol.RemotePreview:
279+ payload := wire.MarshalRemotePreview(m)
280+ if payload == nil {
281+ return wire.Frame{}, ErrInvalidMessage
282+ }
283+ return wire.Frame{Type: wire.MsgRemotePreviewResponse, Payload: payload}, nil
284+ case protocol.CommittedRouteIdentity:
285+ payload, err := wire.MarshalCommittedRouteIdentity(m)
286+ return wire.Frame{Type: wire.MsgCommittedRouteIdentity, Payload: payload}, err
287+ case protocol.RouteNavigationAction:
288+ payload, err := wire.MarshalRouteNavigationAction(m)
289+ return wire.Frame{Type: wire.MsgNavigateRecentRoute, Payload: payload}, err
290+ case protocol.RouteNavigationFailure:
291+ payload, err := wire.MarshalRouteNavigationFailure(m)
292+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}, err
293+ case protocol.RoutePosition:
294+ payload, err := wire.MarshalRoutePosition(m)
295+ return wire.Frame{Type: wire.MsgRoutePosition, Payload: payload}, err
296+ case protocol.SamePeerSwitchFailure:
297+ payload, err := wire.MarshalSamePeerSwitchFailure(m)
298+ return wire.Frame{Type: wire.MsgSamePeerSwitchFailure, Payload: payload}, err
299+ case protocol.ParkedRouteResponse:
300+ payload := wire.MarshalParkedRouteResponse(m)
301+ if payload == nil {
302+ return wire.Frame{}, ErrInvalidMessage
303+ }
304+ return wire.Frame{Type: wire.MsgParkedRouteResponse, Payload: payload}, nil
305+ case *protocol.Welcome:
306+ if m != nil {
307+ return encodeServer(*m)
308+ }
309+ case *protocol.ErrorMsg:
310+ if m != nil {
311+ return encodeServer(*m)
312+ }
313+ case *protocol.Output:
314+ if m != nil {
315+ return encodeServer(*m)
316+ }
317+ case *protocol.Detached:
318+ if m != nil {
319+ return encodeServer(*m)
320+ }
321+ case *protocol.Pong:
322+ if m != nil {
323+ return encodeServer(*m)
324+ }
325+ case *protocol.Sessions:
326+ if m != nil {
327+ return encodeServer(*m)
328+ }
329+ case *protocol.CommandResult:
330+ if m != nil {
331+ return encodeServer(*m)
332+ }
333+ case *protocol.NavigationDirective:
334+ if m != nil {
335+ return encodeServer(*m)
336+ }
337+ case *protocol.AttachTarget:
338+ if m != nil {
339+ return encodeServer(*m)
340+ }
341+ case *protocol.RemotePreview:
342+ if m != nil {
343+ return encodeServer(*m)
344+ }
345+ case *protocol.CommittedRouteIdentity:
346+ if m != nil {
347+ return encodeServer(*m)
348+ }
349+ case *protocol.RouteNavigationAction:
350+ if m != nil {
351+ return encodeServer(*m)
352+ }
353+ case *protocol.RouteNavigationFailure:
354+ if m != nil {
355+ return encodeServer(*m)
356+ }
357+ case *protocol.RoutePosition:
358+ if m != nil {
359+ return encodeServer(*m)
360+ }
361+ case *protocol.SamePeerSwitchFailure:
362+ if m != nil {
363+ return encodeServer(*m)
364+ }
365+ case *protocol.ParkedRouteResponse:
366+ if m != nil {
367+ return encodeServer(*m)
368+ }
369+ default:
370+ return wire.Frame{}, ErrWrongDirection
371+ }
372+ return wire.Frame{}, ErrInvalidMessage
373+}
A internal/adapters/sessionwire/server_test.go +254 -0
@@ -0,0 +1,254 @@
1+package sessionwire
2+
3+import (
4+ "errors"
5+ "io"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
11+ "github.com/bnema/vev/internal/protocol/wire"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+type scriptedTransport struct {
16+ recv wire.Frame
17+ recvErr error
18+ sent wire.Frame
19+ sendErr error
20+ closeErr error
21+ closeCall int
22+}
23+
24+func (t *scriptedTransport) Send(frame wire.Frame) error { t.sent = frame; return t.sendErr }
25+func (t *scriptedTransport) Recv() (wire.Frame, error) { return t.recv, t.recvErr }
26+func (t *scriptedTransport) Close() error { t.closeCall++; return t.closeErr }
27+
28+type capableTransport struct {
29+ scriptedTransport
30+ async wire.Frame
31+ synchronous wire.Frame
32+ events chan ports.LinkEvent
33+}
34+
35+func (*capableTransport) DatagramTransport() {}
36+func (t *capableTransport) SendAsync(frame wire.Frame) error {
37+ t.async = frame
38+ return t.sendErr
39+}
40+func (t *capableTransport) SendSynchronous(frame wire.Frame) error {
41+ t.synchronous = frame
42+ return t.sendErr
43+}
44+func (*capableTransport) LinkState() ports.LinkState { return ports.LinkStateDegraded }
45+func (t *capableTransport) LinkEvents() <-chan ports.LinkEvent { return t.events }
46+
47+func testTarget() domain.RemoteSessionTarget {
48+ return domain.RemoteSessionTarget{
49+ Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{1},
50+ SessionName: "work", LiveTabID: "tab-1",
51+ }
52+}
53+
54+func TestServerConnectionDecodesEveryClientMessage(t *testing.T) {
55+ target := testTarget()
56+ exact := protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}
57+ lease := protocol.ParkedRouteLeaseID{1}
58+ commandPayload, err := wire.MarshalCommandRequest(protocol.CommandRequest{Version: protocol.Version, RequestID: 7, Slug: "list-sessions"})
59+ require.NoError(t, err)
60+ resizePayload, err := wire.MarshalResize(protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}})
61+ require.NoError(t, err)
62+ ackPayload, err := wire.MarshalAck(protocol.Ack{Epoch: 1, State: 2})
63+ require.NoError(t, err)
64+ attentionPayload, err := wire.MarshalRouteAttentionSubscription(protocol.RouteAttentionSubscription{})
65+ require.NoError(t, err)
66+ switchPayload, err := wire.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{RequestID: 3, Target: exact})
67+ require.NoError(t, err)
68+ snapshotPayload, err := wire.MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{})
69+ require.NoError(t, err)
70+ failurePayload, err := wire.MarshalRouteNavigationFailure(protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable})
71+ require.NoError(t, err)
72+
73+ tests := []struct {
74+ name string
75+ frame wire.Frame
76+ want protocol.ClientMessage
77+ }{
78+ {name: "hello", frame: wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}})}, want: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}},
79+ {name: "input", frame: wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{InputSeq: 1, Data: []byte("x")})}, want: protocol.Input{InputSeq: 1, Data: []byte("x")}},
80+ {name: "resize", frame: wire.Frame{Type: wire.MsgResize, Payload: resizePayload}, want: protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}},
81+ {name: "detach", frame: wire.Frame{Type: wire.MsgDetach}, want: protocol.Detach{}},
82+ {name: "ping", frame: wire.Frame{Type: wire.MsgPing}, want: protocol.Ping{}},
83+ {name: "list", frame: wire.Frame{Type: wire.MsgList}, want: protocol.List{}},
84+ {name: "kill", frame: wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(protocol.Kill{Name: "work"})}, want: protocol.Kill{Name: "work"}},
85+ {name: "theme", frame: wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(protocol.Theme{TrueColor: true})}, want: protocol.Theme{TrueColor: true}},
86+ {name: "ack", frame: wire.Frame{Type: wire.MsgAck, Payload: ackPayload}, want: protocol.Ack{Epoch: 1, State: 2}},
87+ {name: "image", frame: wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}})}, want: protocol.ImagePush{InputSeq: 2, Mime: "image/png", Data: []byte{1}}},
88+ {name: "notice", frame: wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}, want: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}},
89+ {name: "command", frame: wire.Frame{Type: wire.MsgCommand, Payload: commandPayload}, want: protocol.CommandRequest{Version: protocol.Version, RequestID: 7, Slug: "list-sessions"}},
90+ {name: "reset", frame: wire.Frame{Type: wire.MsgOutputResetRequest}, want: protocol.OutputResetRequest{}},
91+ {name: "preview", frame: wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: wire.MarshalRemotePreviewRequest(protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1})}, want: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}},
92+ {name: "attention", frame: wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload}, want: protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{}}},
93+ {name: "same peer", frame: wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: switchPayload}, want: protocol.SamePeerSwitchRequest{RequestID: 3, Target: exact}},
94+ {name: "parked", frame: wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(protocol.ParkedRouteRequest{RequestID: 4, LeaseID: lease, Action: protocol.ParkedRoutePrepare})}, want: protocol.ParkedRouteRequest{RequestID: 4, LeaseID: lease, Action: protocol.ParkedRoutePrepare}},
95+ {name: "snapshot", frame: wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload}, want: protocol.RecentRouteSnapshot{}},
96+ {name: "route failure", frame: wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: failurePayload}, want: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
97+ }
98+ for _, tt := range tests {
99+ t.Run(tt.name, func(t *testing.T) {
100+ raw := &scriptedTransport{recv: tt.frame}
101+ got, err := NewServerConnection(raw).ReceiveClient()
102+ require.NoError(t, err)
103+ require.Equal(t, tt.want, got)
104+ })
105+ }
106+}
107+
108+func TestServerConnectionEncodesEveryServerMessage(t *testing.T) {
109+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
110+ lease := protocol.ParkedRouteLeaseID{1}
111+ tests := []struct {
112+ name string
113+ message protocol.ServerMessage
114+ typeID wire.MsgType
115+ }{
116+ {name: "welcome", message: protocol.Welcome{SessionID: "session"}, typeID: wire.MsgWelcome},
117+ {name: "error", message: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"}, typeID: wire.MsgError},
118+ {name: "output", message: protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}, typeID: wire.MsgOutput},
119+ {name: "detached", message: protocol.Detached{Reason: protocol.ReasonDetach}, typeID: wire.MsgDetached},
120+ {name: "pong", message: protocol.Pong{}, typeID: wire.MsgPong},
121+ {name: "sessions", message: protocol.Sessions{}, typeID: wire.MsgSessions},
122+ {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}, typeID: wire.MsgCommandResult},
123+ {name: "navigation", message: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}, typeID: wire.MsgNavigationAction},
124+ {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}, typeID: wire.MsgAttachTarget},
125+ {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}, typeID: wire.MsgRemotePreviewResponse},
126+ {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}, typeID: wire.MsgCommittedRouteIdentity},
127+ {name: "route action", message: protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 1, Generation: 1}, typeID: wire.MsgNavigateRecentRoute},
128+ {name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}, typeID: wire.MsgRouteNavigationFailure},
129+ {name: "route position", message: protocol.RoutePosition{Target: exact, ActiveTabID: "tab-1"}, typeID: wire.MsgRoutePosition},
130+ {name: "switch failure", message: protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchUnavailable}, typeID: wire.MsgSamePeerSwitchFailure},
131+ {name: "parked response", message: protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}, typeID: wire.MsgParkedRouteResponse},
132+ }
133+ for _, tt := range tests {
134+ t.Run(tt.name, func(t *testing.T) {
135+ raw := &scriptedTransport{}
136+ require.NoError(t, NewServerConnection(raw).SendServer(tt.message))
137+ require.Equal(t, tt.typeID, raw.sent.Type)
138+ })
139+ }
140+}
141+
142+func TestServerConnectionPreservesPayloadAgnosticClientControls(t *testing.T) {
143+ for _, tt := range []struct {
144+ name string
145+ frame wire.Frame
146+ want protocol.ClientMessage
147+ }{
148+ {name: "list", frame: wire.Frame{Type: wire.MsgList, Payload: []byte{1}}, want: protocol.List{}},
149+ {name: "detach", frame: wire.Frame{Type: wire.MsgDetach, Payload: []byte{1}}, want: protocol.Detach{}},
150+ {name: "ping", frame: wire.Frame{Type: wire.MsgPing, Payload: []byte{1}}, want: protocol.Ping{}},
151+ } {
152+ t.Run(tt.name, func(t *testing.T) {
153+ message, err := NewServerConnection(&scriptedTransport{recv: tt.frame}).ReceiveClient()
154+ require.NoError(t, err)
155+ require.Equal(t, tt.want, message)
156+ })
157+ }
158+}
159+
160+func TestServerConnectionClassifiesDecodeFailures(t *testing.T) {
161+ tests := []struct {
162+ name string
163+ frame wire.Frame
164+ category protocol.DecodeCategory
165+ version uint16
166+ request uint64
167+ }{
168+ {name: "malformed hello", frame: wire.Frame{Type: wire.MsgHello, Payload: []byte{0, 38}}, category: protocol.DecodeMalformed, version: 38},
169+ {name: "malformed command", frame: wire.Frame{Type: wire.MsgCommand, Payload: []byte{0, 37, 0, 0, 0, 0, 0, 0, 0, 9}}, category: protocol.DecodeMalformed, version: 37, request: 9},
170+ {name: "wrong direction", frame: wire.Frame{Type: wire.MsgOutput}, category: protocol.DecodeWrongDirection},
171+ {name: "unknown", frame: wire.Frame{Type: 255}, category: protocol.DecodeUnknownType},
172+ }
173+ for _, tt := range tests {
174+ t.Run(tt.name, func(t *testing.T) {
175+ _, err := NewServerConnection(&scriptedTransport{recv: tt.frame}).ReceiveClient()
176+ var failure *protocol.DecodeFailure
177+ require.ErrorAs(t, err, &failure)
178+ require.Equal(t, tt.category, failure.Category)
179+ require.Equal(t, tt.version, failure.Version)
180+ require.Equal(t, tt.request, failure.RequestID)
181+ })
182+ }
183+}
184+
185+func TestServerConnectionPreservesSendModesCapabilitiesAndErrors(t *testing.T) {
186+ sendErr := errors.New("send failed")
187+ events := make(chan ports.LinkEvent, 1)
188+ raw := &capableTransport{scriptedTransport: scriptedTransport{sendErr: sendErr}, events: events}
189+ connection := NewServerConnection(raw)
190+
191+ caps := connection.Capabilities()
192+ require.Equal(t, uint8(1), caps.PreferredOutputWindow)
193+ require.Equal(t, protocol.MaxOutputDataLen, caps.OutputDataLimit)
194+ require.True(t, caps.AsyncSend)
195+ require.True(t, caps.OwnedSynchronousSend)
196+ require.True(t, caps.LinkState)
197+ require.Equal(t, ports.LinkStateDegraded, connection.LinkState())
198+ require.Equal(t, (<-chan ports.LinkEvent)(events), connection.LinkEvents())
199+
200+ message := protocol.Pong{}
201+ require.ErrorIs(t, connection.SendServer(message), sendErr)
202+ require.Equal(t, wire.MsgPong, raw.sent.Type)
203+ require.ErrorIs(t, connection.SendServerAsync(message), sendErr)
204+ require.Equal(t, wire.MsgPong, raw.async.Type)
205+ require.ErrorIs(t, connection.SendServerSynchronous(message), sendErr)
206+ require.Equal(t, wire.MsgPong, raw.synchronous.Type)
207+
208+ output := protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}
209+ require.ErrorIs(t, connection.SendOutput(output), sendErr)
210+ require.Equal(t, wire.MsgOutput, raw.sent.Type)
211+ require.ErrorIs(t, connection.SendOutputAsync(output), sendErr)
212+ require.Equal(t, wire.MsgOutput, raw.async.Type)
213+ require.ErrorIs(t, connection.SendOutputSynchronous(output), sendErr)
214+ require.Equal(t, wire.MsgOutput, raw.synchronous.Type)
215+
216+ plain := NewServerConnection(&scriptedTransport{})
217+ require.ErrorIs(t, plain.SendServerAsync(message), ErrUnsupportedSend)
218+ require.ErrorIs(t, plain.SendServerSynchronous(message), ErrUnsupportedSend)
219+ require.ErrorIs(t, plain.SendOutputAsync(output), ErrUnsupportedSend)
220+ require.ErrorIs(t, plain.SendOutputSynchronous(output), ErrUnsupportedSend)
221+ require.Equal(t, uint8(8), plain.Capabilities().PreferredOutputWindow)
222+ require.Equal(t, ports.LinkStateConnected, plain.LinkState())
223+ require.Nil(t, plain.LinkEvents())
224+}
225+
226+func TestServerConnectionForwardsReceiveAndCloseErrors(t *testing.T) {
227+ recvErr := errors.Join(io.EOF, errors.New("closed"))
228+ closeErr := errors.New("close failed")
229+ raw := &scriptedTransport{recvErr: recvErr, closeErr: closeErr}
230+ connection := NewServerConnection(raw)
231+ _, err := connection.ReceiveClient()
232+ require.ErrorIs(t, err, io.EOF)
233+ require.ErrorIs(t, connection.Close(), closeErr)
234+ require.Equal(t, 1, raw.closeCall)
235+}
236+
237+func TestServerListenerWrapsEachAcceptedIncarnation(t *testing.T) {
238+ raw := &scriptedTransport{recv: wire.Frame{Type: wire.MsgPing}}
239+ listener := &scriptedListener{transport: raw}
240+ wrapped := NewServerListener(listener)
241+ first, err := wrapped.Accept()
242+ require.NoError(t, err)
243+ message, err := first.ReceiveClient()
244+ require.NoError(t, err)
245+ require.Equal(t, protocol.Ping{}, message)
246+ require.Equal(t, "test", wrapped.Addr())
247+ require.NoError(t, wrapped.Close())
248+}
249+
250+type scriptedListener struct{ transport ports.Transport }
251+
252+func (l *scriptedListener) Accept() (ports.Transport, error) { return l.transport, nil }
253+func (*scriptedListener) Close() error { return nil }
254+func (*scriptedListener) Addr() string { return "test" }
M internal/app/daemon_integration_test.go +7 -6
@@ -24,6 +24,7 @@ import (
2424 "github.com/bnema/vev/internal/adapters/ipc"
2525 "github.com/bnema/vev/internal/adapters/lifecycle"
2626 "github.com/bnema/vev/internal/adapters/pty"
27+ "github.com/bnema/vev/internal/adapters/sessionwire"
2728 "github.com/bnema/vev/internal/adapters/snapshot"
2829 "github.com/bnema/vev/internal/domain"
2930 "github.com/bnema/vev/internal/persist"
@@ -58,7 +59,7 @@ func startDaemonInDir(t *testing.T, dir string, opts ...daemon.Option) (string,
5859 d := daemon.New(pty.NewFactory(), clock.New(), discardLog(), opts...)
5960 ctx, cancel := context.WithCancel(context.Background())
6061 ch := make(chan error, 1)
61- go func() { ch <- d.Serve(ctx, ln) }()
62+ go func() { ch <- d.Serve(ctx, sessionwire.NewServerListener(ln)) }()
6263 t.Cleanup(cancel)
6364 return dir, ch
6465 }
@@ -611,7 +612,7 @@ func TestLifecycleOwnershipOutlivesMaintenanceWriter(t *testing.T) {
611612 if err != nil {
612613 return err
613614 }
614- return d.Serve(ctx, listener)
615+ return d.Serve(ctx, sessionwire.NewServerListener(listener))
615616 })
616617 }()
617618
@@ -746,7 +747,7 @@ func TestLifecycleOwnershipOutlivesRestorationWriter(t *testing.T) {
746747 daemon.WithCatalogue(opened.Catalogue, opened.Records),
747748 daemon.WithSnapshotRepository(blocking),
748749 )
749- return d.Serve(ctx, listener)
750+ return d.Serve(ctx, sessionwire.NewServerListener(listener))
750751 })
751752 }()
752753
@@ -800,7 +801,7 @@ func publishRestorableCheckpoint(t *testing.T, stateDir string, repository *snap
800801 daemon.WithRecoveryCoordinator(coordinator),
801802 )
802803 served := make(chan error, 1)
803- go func() { served <- d.Serve(ctx, listener) }()
804+ go func() { served <- d.Serve(ctx, sessionwire.NewServerListener(listener)) }()
804805
805806 tr, _ := attach(t, runtimeDir, protocol.IntentNew, name, domain.Size{Cols: 80, Rows: 24})
806807 require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{Data: []byte("checkpoint me\n")})}))
@@ -883,7 +884,7 @@ func runBlockedSnapshotWriterShutdown(t *testing.T) blockedSnapshotWriterShutdow
883884 daemon.WithShell("/bin/cat", nil),
884885 daemon.WithSnapshotRepository(repository),
885886 )
886- return d.Serve(ctx, observed)
887+ return d.Serve(ctx, sessionwire.NewServerListener(observed))
887888 })
888889 }()
889890
@@ -1074,7 +1075,7 @@ func TestLifecycleSocketCloseCatalogueRace(t *testing.T) {
10741075 oldCtx, stopOld := context.WithCancel(context.Background())
10751076 oldDone := make(chan error, 1)
10761077 go func() {
1077- serveErr := oldDaemon.Serve(oldCtx, controlledListener)
1078+ serveErr := oldDaemon.Serve(oldCtx, sessionwire.NewServerListener(controlledListener))
10781079 teardownEvents <- "owner-release"
10791080 close(ownerReleaseEntered)
10801081 <-allowOwnerRelease
M internal/app/run.go +2 -1
@@ -37,6 +37,7 @@ import (
3737 "github.com/bnema/vev/internal/adapters/observability"
3838 "github.com/bnema/vev/internal/adapters/pty"
3939 remoteadapter "github.com/bnema/vev/internal/adapters/remote"
40+ "github.com/bnema/vev/internal/adapters/sessionwire"
4041 "github.com/bnema/vev/internal/adapters/shellcmd"
4142 snapshotadapter "github.com/bnema/vev/internal/adapters/snapshot"
4243 "github.com/bnema/vev/internal/adapters/sshstdio"
@@ -742,7 +743,7 @@ func runDaemonOwnedWithLogger(ctx context.Context, log *slog.Logger) (retErr err
742743 log.Warn("config watcher stopped", "path", configPath, "err", err)
743744 }
744745 }()
745- if err := d.Serve(ctx, ln); err != nil {
746+ if err := d.Serve(ctx, sessionwire.NewServerListener(ln)); err != nil {
746747 log.Error("daemon exited", "err", err)
747748 return err
748749 }
A internal/ports/mocks/session_mock.go +747 -0
@@ -0,0 +1,747 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewMockServerConnection creates a new instance of MockServerConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewMockServerConnection(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *MockServerConnection {
19+ mock := &MockServerConnection{}
20+ mock.Mock.Test(t)
21+
22+ t.Cleanup(func() { mock.AssertExpectations(t) })
23+
24+ return mock
25+}
26+
27+// MockServerConnection is an autogenerated mock type for the ServerConnection type
28+type MockServerConnection struct {
29+ mock.Mock
30+}
31+
32+type MockServerConnection_Expecter struct {
33+ mock *mock.Mock
34+}
35+
36+func (_m *MockServerConnection) EXPECT() *MockServerConnection_Expecter {
37+ return &MockServerConnection_Expecter{mock: &_m.Mock}
38+}
39+
40+// Capabilities provides a mock function for the type MockServerConnection
41+func (_mock *MockServerConnection) Capabilities() protocol.ConnectionCapabilities {
42+ ret := _mock.Called()
43+
44+ if len(ret) == 0 {
45+ panic("no return value specified for Capabilities")
46+ }
47+
48+ var r0 protocol.ConnectionCapabilities
49+ if returnFunc, ok := ret.Get(0).(func() protocol.ConnectionCapabilities); ok {
50+ r0 = returnFunc()
51+ } else {
52+ r0 = ret.Get(0).(protocol.ConnectionCapabilities)
53+ }
54+ return r0
55+}
56+
57+// MockServerConnection_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities'
58+type MockServerConnection_Capabilities_Call struct {
59+ *mock.Call
60+}
61+
62+// Capabilities is a helper method to define mock.On call
63+func (_e *MockServerConnection_Expecter) Capabilities() *MockServerConnection_Capabilities_Call {
64+ return &MockServerConnection_Capabilities_Call{Call: _e.mock.On("Capabilities")}
65+}
66+
67+func (_c *MockServerConnection_Capabilities_Call) Run(run func()) *MockServerConnection_Capabilities_Call {
68+ _c.Call.Run(func(args mock.Arguments) {
69+ run()
70+ })
71+ return _c
72+}
73+
74+func (_c *MockServerConnection_Capabilities_Call) Return(connectionCapabilities protocol.ConnectionCapabilities) *MockServerConnection_Capabilities_Call {
75+ _c.Call.Return(connectionCapabilities)
76+ return _c
77+}
78+
79+func (_c *MockServerConnection_Capabilities_Call) RunAndReturn(run func() protocol.ConnectionCapabilities) *MockServerConnection_Capabilities_Call {
80+ _c.Call.Return(run)
81+ return _c
82+}
83+
84+// Close provides a mock function for the type MockServerConnection
85+func (_mock *MockServerConnection) Close() error {
86+ ret := _mock.Called()
87+
88+ if len(ret) == 0 {
89+ panic("no return value specified for Close")
90+ }
91+
92+ var r0 error
93+ if returnFunc, ok := ret.Get(0).(func() error); ok {
94+ r0 = returnFunc()
95+ } else {
96+ r0 = ret.Error(0)
97+ }
98+ return r0
99+}
100+
101+// MockServerConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
102+type MockServerConnection_Close_Call struct {
103+ *mock.Call
104+}
105+
106+// Close is a helper method to define mock.On call
107+func (_e *MockServerConnection_Expecter) Close() *MockServerConnection_Close_Call {
108+ return &MockServerConnection_Close_Call{Call: _e.mock.On("Close")}
109+}
110+
111+func (_c *MockServerConnection_Close_Call) Run(run func()) *MockServerConnection_Close_Call {
112+ _c.Call.Run(func(args mock.Arguments) {
113+ run()
114+ })
115+ return _c
116+}
117+
118+func (_c *MockServerConnection_Close_Call) Return(err error) *MockServerConnection_Close_Call {
119+ _c.Call.Return(err)
120+ return _c
121+}
122+
123+func (_c *MockServerConnection_Close_Call) RunAndReturn(run func() error) *MockServerConnection_Close_Call {
124+ _c.Call.Return(run)
125+ return _c
126+}
127+
128+// LinkEvents provides a mock function for the type MockServerConnection
129+func (_mock *MockServerConnection) LinkEvents() <-chan ports.LinkEvent {
130+ ret := _mock.Called()
131+
132+ if len(ret) == 0 {
133+ panic("no return value specified for LinkEvents")
134+ }
135+
136+ var r0 <-chan ports.LinkEvent
137+ if returnFunc, ok := ret.Get(0).(func() <-chan ports.LinkEvent); ok {
138+ r0 = returnFunc()
139+ } else {
140+ if ret.Get(0) != nil {
141+ r0 = ret.Get(0).(<-chan ports.LinkEvent)
142+ }
143+ }
144+ return r0
145+}
146+
147+// MockServerConnection_LinkEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkEvents'
148+type MockServerConnection_LinkEvents_Call struct {
149+ *mock.Call
150+}
151+
152+// LinkEvents is a helper method to define mock.On call
153+func (_e *MockServerConnection_Expecter) LinkEvents() *MockServerConnection_LinkEvents_Call {
154+ return &MockServerConnection_LinkEvents_Call{Call: _e.mock.On("LinkEvents")}
155+}
156+
157+func (_c *MockServerConnection_LinkEvents_Call) Run(run func()) *MockServerConnection_LinkEvents_Call {
158+ _c.Call.Run(func(args mock.Arguments) {
159+ run()
160+ })
161+ return _c
162+}
163+
164+func (_c *MockServerConnection_LinkEvents_Call) Return(linkEventCh <-chan ports.LinkEvent) *MockServerConnection_LinkEvents_Call {
165+ _c.Call.Return(linkEventCh)
166+ return _c
167+}
168+
169+func (_c *MockServerConnection_LinkEvents_Call) RunAndReturn(run func() <-chan ports.LinkEvent) *MockServerConnection_LinkEvents_Call {
170+ _c.Call.Return(run)
171+ return _c
172+}
173+
174+// LinkState provides a mock function for the type MockServerConnection
175+func (_mock *MockServerConnection) LinkState() ports.LinkState {
176+ ret := _mock.Called()
177+
178+ if len(ret) == 0 {
179+ panic("no return value specified for LinkState")
180+ }
181+
182+ var r0 ports.LinkState
183+ if returnFunc, ok := ret.Get(0).(func() ports.LinkState); ok {
184+ r0 = returnFunc()
185+ } else {
186+ r0 = ret.Get(0).(ports.LinkState)
187+ }
188+ return r0
189+}
190+
191+// MockServerConnection_LinkState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkState'
192+type MockServerConnection_LinkState_Call struct {
193+ *mock.Call
194+}
195+
196+// LinkState is a helper method to define mock.On call
197+func (_e *MockServerConnection_Expecter) LinkState() *MockServerConnection_LinkState_Call {
198+ return &MockServerConnection_LinkState_Call{Call: _e.mock.On("LinkState")}
199+}
200+
201+func (_c *MockServerConnection_LinkState_Call) Run(run func()) *MockServerConnection_LinkState_Call {
202+ _c.Call.Run(func(args mock.Arguments) {
203+ run()
204+ })
205+ return _c
206+}
207+
208+func (_c *MockServerConnection_LinkState_Call) Return(linkState ports.LinkState) *MockServerConnection_LinkState_Call {
209+ _c.Call.Return(linkState)
210+ return _c
211+}
212+
213+func (_c *MockServerConnection_LinkState_Call) RunAndReturn(run func() ports.LinkState) *MockServerConnection_LinkState_Call {
214+ _c.Call.Return(run)
215+ return _c
216+}
217+
218+// ReceiveClient provides a mock function for the type MockServerConnection
219+func (_mock *MockServerConnection) ReceiveClient() (protocol.ClientMessage, error) {
220+ ret := _mock.Called()
221+
222+ if len(ret) == 0 {
223+ panic("no return value specified for ReceiveClient")
224+ }
225+
226+ var r0 protocol.ClientMessage
227+ var r1 error
228+ if returnFunc, ok := ret.Get(0).(func() (protocol.ClientMessage, error)); ok {
229+ return returnFunc()
230+ }
231+ if returnFunc, ok := ret.Get(0).(func() protocol.ClientMessage); ok {
232+ r0 = returnFunc()
233+ } else {
234+ if ret.Get(0) != nil {
235+ r0 = ret.Get(0).(protocol.ClientMessage)
236+ }
237+ }
238+ if returnFunc, ok := ret.Get(1).(func() error); ok {
239+ r1 = returnFunc()
240+ } else {
241+ r1 = ret.Error(1)
242+ }
243+ return r0, r1
244+}
245+
246+// MockServerConnection_ReceiveClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveClient'
247+type MockServerConnection_ReceiveClient_Call struct {
248+ *mock.Call
249+}
250+
251+// ReceiveClient is a helper method to define mock.On call
252+func (_e *MockServerConnection_Expecter) ReceiveClient() *MockServerConnection_ReceiveClient_Call {
253+ return &MockServerConnection_ReceiveClient_Call{Call: _e.mock.On("ReceiveClient")}
254+}
255+
256+func (_c *MockServerConnection_ReceiveClient_Call) Run(run func()) *MockServerConnection_ReceiveClient_Call {
257+ _c.Call.Run(func(args mock.Arguments) {
258+ run()
259+ })
260+ return _c
261+}
262+
263+func (_c *MockServerConnection_ReceiveClient_Call) Return(clientMessage protocol.ClientMessage, err error) *MockServerConnection_ReceiveClient_Call {
264+ _c.Call.Return(clientMessage, err)
265+ return _c
266+}
267+
268+func (_c *MockServerConnection_ReceiveClient_Call) RunAndReturn(run func() (protocol.ClientMessage, error)) *MockServerConnection_ReceiveClient_Call {
269+ _c.Call.Return(run)
270+ return _c
271+}
272+
273+// SendOutput provides a mock function for the type MockServerConnection
274+func (_mock *MockServerConnection) SendOutput(output protocol.Output) error {
275+ ret := _mock.Called(output)
276+
277+ if len(ret) == 0 {
278+ panic("no return value specified for SendOutput")
279+ }
280+
281+ var r0 error
282+ if returnFunc, ok := ret.Get(0).(func(protocol.Output) error); ok {
283+ r0 = returnFunc(output)
284+ } else {
285+ r0 = ret.Error(0)
286+ }
287+ return r0
288+}
289+
290+// MockServerConnection_SendOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendOutput'
291+type MockServerConnection_SendOutput_Call struct {
292+ *mock.Call
293+}
294+
295+// SendOutput is a helper method to define mock.On call
296+// - output protocol.Output
297+func (_e *MockServerConnection_Expecter) SendOutput(output any) *MockServerConnection_SendOutput_Call {
298+ return &MockServerConnection_SendOutput_Call{Call: _e.mock.On("SendOutput", output)}
299+}
300+
301+func (_c *MockServerConnection_SendOutput_Call) Run(run func(output protocol.Output)) *MockServerConnection_SendOutput_Call {
302+ _c.Call.Run(func(args mock.Arguments) {
303+ var arg0 protocol.Output
304+ if args[0] != nil {
305+ arg0 = args[0].(protocol.Output)
306+ }
307+ run(
308+ arg0,
309+ )
310+ })
311+ return _c
312+}
313+
314+func (_c *MockServerConnection_SendOutput_Call) Return(err error) *MockServerConnection_SendOutput_Call {
315+ _c.Call.Return(err)
316+ return _c
317+}
318+
319+func (_c *MockServerConnection_SendOutput_Call) RunAndReturn(run func(output protocol.Output) error) *MockServerConnection_SendOutput_Call {
320+ _c.Call.Return(run)
321+ return _c
322+}
323+
324+// SendOutputAsync provides a mock function for the type MockServerConnection
325+func (_mock *MockServerConnection) SendOutputAsync(output protocol.Output) error {
326+ ret := _mock.Called(output)
327+
328+ if len(ret) == 0 {
329+ panic("no return value specified for SendOutputAsync")
330+ }
331+
332+ var r0 error
333+ if returnFunc, ok := ret.Get(0).(func(protocol.Output) error); ok {
334+ r0 = returnFunc(output)
335+ } else {
336+ r0 = ret.Error(0)
337+ }
338+ return r0
339+}
340+
341+// MockServerConnection_SendOutputAsync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendOutputAsync'
342+type MockServerConnection_SendOutputAsync_Call struct {
343+ *mock.Call
344+}
345+
346+// SendOutputAsync is a helper method to define mock.On call
347+// - output protocol.Output
348+func (_e *MockServerConnection_Expecter) SendOutputAsync(output any) *MockServerConnection_SendOutputAsync_Call {
349+ return &MockServerConnection_SendOutputAsync_Call{Call: _e.mock.On("SendOutputAsync", output)}
350+}
351+
352+func (_c *MockServerConnection_SendOutputAsync_Call) Run(run func(output protocol.Output)) *MockServerConnection_SendOutputAsync_Call {
353+ _c.Call.Run(func(args mock.Arguments) {
354+ var arg0 protocol.Output
355+ if args[0] != nil {
356+ arg0 = args[0].(protocol.Output)
357+ }
358+ run(
359+ arg0,
360+ )
361+ })
362+ return _c
363+}
364+
365+func (_c *MockServerConnection_SendOutputAsync_Call) Return(err error) *MockServerConnection_SendOutputAsync_Call {
366+ _c.Call.Return(err)
367+ return _c
368+}
369+
370+func (_c *MockServerConnection_SendOutputAsync_Call) RunAndReturn(run func(output protocol.Output) error) *MockServerConnection_SendOutputAsync_Call {
371+ _c.Call.Return(run)
372+ return _c
373+}
374+
375+// SendOutputSynchronous provides a mock function for the type MockServerConnection
376+func (_mock *MockServerConnection) SendOutputSynchronous(output protocol.Output) error {
377+ ret := _mock.Called(output)
378+
379+ if len(ret) == 0 {
380+ panic("no return value specified for SendOutputSynchronous")
381+ }
382+
383+ var r0 error
384+ if returnFunc, ok := ret.Get(0).(func(protocol.Output) error); ok {
385+ r0 = returnFunc(output)
386+ } else {
387+ r0 = ret.Error(0)
388+ }
389+ return r0
390+}
391+
392+// MockServerConnection_SendOutputSynchronous_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendOutputSynchronous'
393+type MockServerConnection_SendOutputSynchronous_Call struct {
394+ *mock.Call
395+}
396+
397+// SendOutputSynchronous is a helper method to define mock.On call
398+// - output protocol.Output
399+func (_e *MockServerConnection_Expecter) SendOutputSynchronous(output any) *MockServerConnection_SendOutputSynchronous_Call {
400+ return &MockServerConnection_SendOutputSynchronous_Call{Call: _e.mock.On("SendOutputSynchronous", output)}
401+}
402+
403+func (_c *MockServerConnection_SendOutputSynchronous_Call) Run(run func(output protocol.Output)) *MockServerConnection_SendOutputSynchronous_Call {
404+ _c.Call.Run(func(args mock.Arguments) {
405+ var arg0 protocol.Output
406+ if args[0] != nil {
407+ arg0 = args[0].(protocol.Output)
408+ }
409+ run(
410+ arg0,
411+ )
412+ })
413+ return _c
414+}
415+
416+func (_c *MockServerConnection_SendOutputSynchronous_Call) Return(err error) *MockServerConnection_SendOutputSynchronous_Call {
417+ _c.Call.Return(err)
418+ return _c
419+}
420+
421+func (_c *MockServerConnection_SendOutputSynchronous_Call) RunAndReturn(run func(output protocol.Output) error) *MockServerConnection_SendOutputSynchronous_Call {
422+ _c.Call.Return(run)
423+ return _c
424+}
425+
426+// SendServer provides a mock function for the type MockServerConnection
427+func (_mock *MockServerConnection) SendServer(serverMessage protocol.ServerMessage) error {
428+ ret := _mock.Called(serverMessage)
429+
430+ if len(ret) == 0 {
431+ panic("no return value specified for SendServer")
432+ }
433+
434+ var r0 error
435+ if returnFunc, ok := ret.Get(0).(func(protocol.ServerMessage) error); ok {
436+ r0 = returnFunc(serverMessage)
437+ } else {
438+ r0 = ret.Error(0)
439+ }
440+ return r0
441+}
442+
443+// MockServerConnection_SendServer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendServer'
444+type MockServerConnection_SendServer_Call struct {
445+ *mock.Call
446+}
447+
448+// SendServer is a helper method to define mock.On call
449+// - serverMessage protocol.ServerMessage
450+func (_e *MockServerConnection_Expecter) SendServer(serverMessage any) *MockServerConnection_SendServer_Call {
451+ return &MockServerConnection_SendServer_Call{Call: _e.mock.On("SendServer", serverMessage)}
452+}
453+
454+func (_c *MockServerConnection_SendServer_Call) Run(run func(serverMessage protocol.ServerMessage)) *MockServerConnection_SendServer_Call {
455+ _c.Call.Run(func(args mock.Arguments) {
456+ var arg0 protocol.ServerMessage
457+ if args[0] != nil {
458+ arg0 = args[0].(protocol.ServerMessage)
459+ }
460+ run(
461+ arg0,
462+ )
463+ })
464+ return _c
465+}
466+
467+func (_c *MockServerConnection_SendServer_Call) Return(err error) *MockServerConnection_SendServer_Call {
468+ _c.Call.Return(err)
469+ return _c
470+}
471+
472+func (_c *MockServerConnection_SendServer_Call) RunAndReturn(run func(serverMessage protocol.ServerMessage) error) *MockServerConnection_SendServer_Call {
473+ _c.Call.Return(run)
474+ return _c
475+}
476+
477+// SendServerAsync provides a mock function for the type MockServerConnection
478+func (_mock *MockServerConnection) SendServerAsync(serverMessage protocol.ServerMessage) error {
479+ ret := _mock.Called(serverMessage)
480+
481+ if len(ret) == 0 {
482+ panic("no return value specified for SendServerAsync")
483+ }
484+
485+ var r0 error
486+ if returnFunc, ok := ret.Get(0).(func(protocol.ServerMessage) error); ok {
487+ r0 = returnFunc(serverMessage)
488+ } else {
489+ r0 = ret.Error(0)
490+ }
491+ return r0
492+}
493+
494+// MockServerConnection_SendServerAsync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendServerAsync'
495+type MockServerConnection_SendServerAsync_Call struct {
496+ *mock.Call
497+}
498+
499+// SendServerAsync is a helper method to define mock.On call
500+// - serverMessage protocol.ServerMessage
501+func (_e *MockServerConnection_Expecter) SendServerAsync(serverMessage any) *MockServerConnection_SendServerAsync_Call {
502+ return &MockServerConnection_SendServerAsync_Call{Call: _e.mock.On("SendServerAsync", serverMessage)}
503+}
504+
505+func (_c *MockServerConnection_SendServerAsync_Call) Run(run func(serverMessage protocol.ServerMessage)) *MockServerConnection_SendServerAsync_Call {
506+ _c.Call.Run(func(args mock.Arguments) {
507+ var arg0 protocol.ServerMessage
508+ if args[0] != nil {
509+ arg0 = args[0].(protocol.ServerMessage)
510+ }
511+ run(
512+ arg0,
513+ )
514+ })
515+ return _c
516+}
517+
518+func (_c *MockServerConnection_SendServerAsync_Call) Return(err error) *MockServerConnection_SendServerAsync_Call {
519+ _c.Call.Return(err)
520+ return _c
521+}
522+
523+func (_c *MockServerConnection_SendServerAsync_Call) RunAndReturn(run func(serverMessage protocol.ServerMessage) error) *MockServerConnection_SendServerAsync_Call {
524+ _c.Call.Return(run)
525+ return _c
526+}
527+
528+// SendServerSynchronous provides a mock function for the type MockServerConnection
529+func (_mock *MockServerConnection) SendServerSynchronous(serverMessage protocol.ServerMessage) error {
530+ ret := _mock.Called(serverMessage)
531+
532+ if len(ret) == 0 {
533+ panic("no return value specified for SendServerSynchronous")
534+ }
535+
536+ var r0 error
537+ if returnFunc, ok := ret.Get(0).(func(protocol.ServerMessage) error); ok {
538+ r0 = returnFunc(serverMessage)
539+ } else {
540+ r0 = ret.Error(0)
541+ }
542+ return r0
543+}
544+
545+// MockServerConnection_SendServerSynchronous_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendServerSynchronous'
546+type MockServerConnection_SendServerSynchronous_Call struct {
547+ *mock.Call
548+}
549+
550+// SendServerSynchronous is a helper method to define mock.On call
551+// - serverMessage protocol.ServerMessage
552+func (_e *MockServerConnection_Expecter) SendServerSynchronous(serverMessage any) *MockServerConnection_SendServerSynchronous_Call {
553+ return &MockServerConnection_SendServerSynchronous_Call{Call: _e.mock.On("SendServerSynchronous", serverMessage)}
554+}
555+
556+func (_c *MockServerConnection_SendServerSynchronous_Call) Run(run func(serverMessage protocol.ServerMessage)) *MockServerConnection_SendServerSynchronous_Call {
557+ _c.Call.Run(func(args mock.Arguments) {
558+ var arg0 protocol.ServerMessage
559+ if args[0] != nil {
560+ arg0 = args[0].(protocol.ServerMessage)
561+ }
562+ run(
563+ arg0,
564+ )
565+ })
566+ return _c
567+}
568+
569+func (_c *MockServerConnection_SendServerSynchronous_Call) Return(err error) *MockServerConnection_SendServerSynchronous_Call {
570+ _c.Call.Return(err)
571+ return _c
572+}
573+
574+func (_c *MockServerConnection_SendServerSynchronous_Call) RunAndReturn(run func(serverMessage protocol.ServerMessage) error) *MockServerConnection_SendServerSynchronous_Call {
575+ _c.Call.Return(run)
576+ return _c
577+}
578+
579+// NewMockServerListener creates a new instance of MockServerListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
580+// The first argument is typically a *testing.T value.
581+func NewMockServerListener(t interface {
582+ mock.TestingT
583+ Cleanup(func())
584+}) *MockServerListener {
585+ mock := &MockServerListener{}
586+ mock.Mock.Test(t)
587+
588+ t.Cleanup(func() { mock.AssertExpectations(t) })
589+
590+ return mock
591+}
592+
593+// MockServerListener is an autogenerated mock type for the ServerListener type
594+type MockServerListener struct {
595+ mock.Mock
596+}
597+
598+type MockServerListener_Expecter struct {
599+ mock *mock.Mock
600+}
601+
602+func (_m *MockServerListener) EXPECT() *MockServerListener_Expecter {
603+ return &MockServerListener_Expecter{mock: &_m.Mock}
604+}
605+
606+// Accept provides a mock function for the type MockServerListener
607+func (_mock *MockServerListener) Accept() (ports.ServerConnection, error) {
608+ ret := _mock.Called()
609+
610+ if len(ret) == 0 {
611+ panic("no return value specified for Accept")
612+ }
613+
614+ var r0 ports.ServerConnection
615+ var r1 error
616+ if returnFunc, ok := ret.Get(0).(func() (ports.ServerConnection, error)); ok {
617+ return returnFunc()
618+ }
619+ if returnFunc, ok := ret.Get(0).(func() ports.ServerConnection); ok {
620+ r0 = returnFunc()
621+ } else {
622+ if ret.Get(0) != nil {
623+ r0 = ret.Get(0).(ports.ServerConnection)
624+ }
625+ }
626+ if returnFunc, ok := ret.Get(1).(func() error); ok {
627+ r1 = returnFunc()
628+ } else {
629+ r1 = ret.Error(1)
630+ }
631+ return r0, r1
632+}
633+
634+// MockServerListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'
635+type MockServerListener_Accept_Call struct {
636+ *mock.Call
637+}
638+
639+// Accept is a helper method to define mock.On call
640+func (_e *MockServerListener_Expecter) Accept() *MockServerListener_Accept_Call {
641+ return &MockServerListener_Accept_Call{Call: _e.mock.On("Accept")}
642+}
643+
644+func (_c *MockServerListener_Accept_Call) Run(run func()) *MockServerListener_Accept_Call {
645+ _c.Call.Run(func(args mock.Arguments) {
646+ run()
647+ })
648+ return _c
649+}
650+
651+func (_c *MockServerListener_Accept_Call) Return(serverConnection ports.ServerConnection, err error) *MockServerListener_Accept_Call {
652+ _c.Call.Return(serverConnection, err)
653+ return _c
654+}
655+
656+func (_c *MockServerListener_Accept_Call) RunAndReturn(run func() (ports.ServerConnection, error)) *MockServerListener_Accept_Call {
657+ _c.Call.Return(run)
658+ return _c
659+}
660+
661+// Addr provides a mock function for the type MockServerListener
662+func (_mock *MockServerListener) Addr() string {
663+ ret := _mock.Called()
664+
665+ if len(ret) == 0 {
666+ panic("no return value specified for Addr")
667+ }
668+
669+ var r0 string
670+ if returnFunc, ok := ret.Get(0).(func() string); ok {
671+ r0 = returnFunc()
672+ } else {
673+ r0 = ret.Get(0).(string)
674+ }
675+ return r0
676+}
677+
678+// MockServerListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'
679+type MockServerListener_Addr_Call struct {
680+ *mock.Call
681+}
682+
683+// Addr is a helper method to define mock.On call
684+func (_e *MockServerListener_Expecter) Addr() *MockServerListener_Addr_Call {
685+ return &MockServerListener_Addr_Call{Call: _e.mock.On("Addr")}
686+}
687+
688+func (_c *MockServerListener_Addr_Call) Run(run func()) *MockServerListener_Addr_Call {
689+ _c.Call.Run(func(args mock.Arguments) {
690+ run()
691+ })
692+ return _c
693+}
694+
695+func (_c *MockServerListener_Addr_Call) Return(s string) *MockServerListener_Addr_Call {
696+ _c.Call.Return(s)
697+ return _c
698+}
699+
700+func (_c *MockServerListener_Addr_Call) RunAndReturn(run func() string) *MockServerListener_Addr_Call {
701+ _c.Call.Return(run)
702+ return _c
703+}
704+
705+// Close provides a mock function for the type MockServerListener
706+func (_mock *MockServerListener) Close() error {
707+ ret := _mock.Called()
708+
709+ if len(ret) == 0 {
710+ panic("no return value specified for Close")
711+ }
712+
713+ var r0 error
714+ if returnFunc, ok := ret.Get(0).(func() error); ok {
715+ r0 = returnFunc()
716+ } else {
717+ r0 = ret.Error(0)
718+ }
719+ return r0
720+}
721+
722+// MockServerListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
723+type MockServerListener_Close_Call struct {
724+ *mock.Call
725+}
726+
727+// Close is a helper method to define mock.On call
728+func (_e *MockServerListener_Expecter) Close() *MockServerListener_Close_Call {
729+ return &MockServerListener_Close_Call{Call: _e.mock.On("Close")}
730+}
731+
732+func (_c *MockServerListener_Close_Call) Run(run func()) *MockServerListener_Close_Call {
733+ _c.Call.Run(func(args mock.Arguments) {
734+ run()
735+ })
736+ return _c
737+}
738+
739+func (_c *MockServerListener_Close_Call) Return(err error) *MockServerListener_Close_Call {
740+ _c.Call.Return(err)
741+ return _c
742+}
743+
744+func (_c *MockServerListener_Close_Call) RunAndReturn(run func() error) *MockServerListener_Close_Call {
745+ _c.Call.Return(run)
746+ return _c
747+}
A internal/ports/session.go +26 -0
@@ -0,0 +1,26 @@
1+package ports
2+
3+import "github.com/bnema/vev/internal/protocol"
4+
5+// ServerConnection is one typed client/daemon session channel. Close must be
6+// concurrent-safe and unblock ReceiveClient and every send mode.
7+type ServerConnection interface {
8+ ReceiveClient() (protocol.ClientMessage, error)
9+ SendServer(protocol.ServerMessage) error
10+ SendServerAsync(protocol.ServerMessage) error
11+ SendServerSynchronous(protocol.ServerMessage) error
12+ SendOutput(protocol.Output) error
13+ SendOutputAsync(protocol.Output) error
14+ SendOutputSynchronous(protocol.Output) error
15+ Capabilities() protocol.ConnectionCapabilities
16+ LinkState() LinkState
17+ LinkEvents() <-chan LinkEvent
18+ Close() error
19+}
20+
21+// ServerListener accepts typed server-side session connections.
22+type ServerListener interface {
23+ Accept() (ServerConnection, error)
24+ Close() error
25+ Addr() string
26+}
A internal/protocol/decode.go +48 -0
@@ -0,0 +1,48 @@
1+package protocol
2+
3+import "fmt"
4+
5+// DecodeCategory classifies encoded input rejected before it reaches a use case.
6+type DecodeCategory uint8
7+
8+const (
9+ DecodeMalformed DecodeCategory = iota + 1
10+ DecodeUnknownType
11+ DecodeWrongDirection
12+)
13+
14+// DecodeMessageKind identifies message families whose malformed prefixes need
15+// compatibility responses before a typed message can be produced.
16+type DecodeMessageKind uint8
17+
18+const (
19+ DecodeMessageUnknown DecodeMessageKind = iota
20+ DecodeMessageHello
21+ DecodeMessageCommand
22+ DecodeMessageKill
23+ DecodeMessageRemotePreview
24+)
25+
26+// DecodeFailure describes rejected encoded input without retaining payload bytes.
27+type DecodeFailure struct {
28+ Category DecodeCategory
29+ Kind DecodeMessageKind
30+ Type uint8
31+ Version uint16
32+ RequestID uint64
33+ Err error
34+}
35+
36+func (e *DecodeFailure) Error() string {
37+ if e == nil {
38+ return "protocol: decode failure"
39+ }
40+ return fmt.Sprintf("protocol: decode message type %d: %v", e.Type, e.Err)
41+}
42+
43+func (e *DecodeFailure) Unwrap() error {
44+ if e == nil {
45+ return nil
46+ }
47+ return e.Err
48+}
A internal/protocol/messages.go +44 -0
@@ -0,0 +1,44 @@
1+package protocol
2+
3+// ClientMessage is a closed set of messages accepted from a session client.
4+type ClientMessage interface{ clientMessage() }
5+
6+// ServerMessage is a closed set of messages emitted by a session daemon.
7+type ServerMessage interface{ serverMessage() }
8+
9+func (Hello) clientMessage() {}
10+func (Input) clientMessage() {}
11+func (Resize) clientMessage() {}
12+func (Detach) clientMessage() {}
13+func (Ping) clientMessage() {}
14+func (List) clientMessage() {}
15+func (Kill) clientMessage() {}
16+func (Theme) clientMessage() {}
17+func (Ack) clientMessage() {}
18+func (ImagePush) clientMessage() {}
19+func (ClientNotice) clientMessage() {}
20+func (CommandRequest) clientMessage() {}
21+func (OutputResetRequest) clientMessage() {}
22+func (RemotePreviewRequest) clientMessage() {}
23+func (RouteAttentionSubscription) clientMessage() {}
24+func (SamePeerSwitchRequest) clientMessage() {}
25+func (ParkedRouteRequest) clientMessage() {}
26+func (RecentRouteSnapshot) clientMessage() {}
27+func (RouteNavigationFailure) clientMessage() {}
28+
29+func (Welcome) serverMessage() {}
30+func (ErrorMsg) serverMessage() {}
31+func (Output) serverMessage() {}
32+func (Detached) serverMessage() {}
33+func (Pong) serverMessage() {}
34+func (Sessions) serverMessage() {}
35+func (CommandResult) serverMessage() {}
36+func (NavigationDirective) serverMessage() {}
37+func (AttachTarget) serverMessage() {}
38+func (RemotePreview) serverMessage() {}
39+func (CommittedRouteIdentity) serverMessage() {}
40+func (RouteNavigationAction) serverMessage() {}
41+func (RouteNavigationFailure) serverMessage() {}
42+func (RoutePosition) serverMessage() {}
43+func (SamePeerSwitchFailure) serverMessage() {}
44+func (ParkedRouteResponse) serverMessage() {}
M internal/usecase/daemon/attachment_connection.go +2 -2
@@ -16,7 +16,7 @@ type attachmentCapability struct {
1616 rebase bool
1717 }
1818
19-func captureAttachmentCapability(entry *session, ac *attachedClient, tr ports.Transport) attachmentCapability {
19+func captureAttachmentCapability(entry *session, ac *attachedClient, tr ports.ServerConnection) attachmentCapability {
2020 if entry == nil || entry.core() == nil || ac == nil || tr == nil {
2121 return attachmentCapability{}
2222 }
@@ -54,7 +54,7 @@ func captureAttachmentCapability(entry *session, ac *attachedClient, tr ports.Tr
5454 return token
5555 }
5656
57-func (s *session) captureAttachmentCapability(ac *attachedClient, tr ports.Transport) attachmentCapability {
57+func (s *session) captureAttachmentCapability(ac *attachedClient, tr ports.ServerConnection) attachmentCapability {
5858 return captureAttachmentCapability(s, ac, tr)
5959 }
6060
M internal/usecase/daemon/attachment_effect_gate.go +5 -5
@@ -6,7 +6,7 @@ import (
66 "sync/atomic"
77
88 "github.com/bnema/vev/internal/ports"
9- "github.com/bnema/vev/internal/protocol/wire"
9+ "github.com/bnema/vev/internal/protocol"
1010 )
1111
1212 type attachmentEffectPhase uint8
@@ -189,7 +189,7 @@ func (t *attachmentEffect) endTransportSend() {
189189 g.mu.Unlock()
190190 }
191191
192-func (t *attachmentEffect) sendControl(frame wire.Frame) error {
192+func (t *attachmentEffect) sendControl(message protocol.ServerMessage) error {
193193 if !t.current() || t.ac == nil || t.transport.transport == nil {
194194 return errAttachmentTransition
195195 }
@@ -198,7 +198,7 @@ func (t *attachmentEffect) sendControl(frame wire.Frame) error {
198198 if !t.current() || !t.ac.transportSnapshotCurrent(t.transport) || !t.beginTransportSend(t.transport) {
199199 return errAttachmentTransition
200200 }
201- err := t.transport.transport.Send(frame)
201+ err := t.transport.transport.SendServer(message)
202202 if err != nil {
203203 t.reportTransportFailure(t.transport)
204204 }
@@ -229,12 +229,12 @@ func (ac *attachedClient) beginAttachmentEffect(token attachmentCapability) (*at
229229 // admits the capability derived from the session registry. Handshakes use it
230230 // after Welcome has completed so a replacement blocked behind that send can
231231 // publish before readiness or first paint.
232-func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.Transport) (attachmentCapability, *attachmentEffect, bool) {
232+func (ac *attachedClient) beginCurrentAttachmentEffect(sess *session, tr ports.ServerConnection) (attachmentCapability, *attachmentEffect, bool) {
233233 //nolint:contextcheck // This compatibility wrapper deliberately delegates with a fresh context; cancellable callers use the context variant below.
234234 return ac.beginCurrentAttachmentEffectContext(context.Background(), sess, tr)
235235 }
236236
237-func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.Transport) (attachmentCapability, *attachmentEffect, bool) {
237+func (ac *attachedClient) beginCurrentAttachmentEffectContext(ctx context.Context, sess *session, tr ports.ServerConnection) (attachmentCapability, *attachmentEffect, bool) {
238238 if ctx == nil || ac == nil || sess == nil || tr == nil {
239239 return attachmentCapability{}, nil, false
240240 }
M internal/usecase/daemon/attachment_effect_gate_test.go +1 -1
@@ -127,7 +127,7 @@ func startBlockedAttachmentSend(t *testing.T, token attachmentCapability) <-chan
127127 go func() {
128128 defer close(done)
129129 defer ticket.End()
130- _ = token.ac.sendExpectedTransportForAttachment(token.transport, framePong(), ticket)
130+ _ = token.ac.sendExpectedTransportForAttachment(token.transport, serverPong(), ticket)
131131 }()
132132 return done
133133 }
M internal/usecase/daemon/attachment_output.go +20 -21
@@ -10,7 +10,6 @@ import (
1010 "github.com/bnema/vev/internal/domain"
1111 "github.com/bnema/vev/internal/ports"
1212 "github.com/bnema/vev/internal/protocol"
13- "github.com/bnema/vev/internal/protocol/wire"
1413 )
1514
1615 // attachmentOutput owns the terminal-output dependency chain and emitted
@@ -154,18 +153,18 @@ func (o *attachmentOutput) prepareFrame(d *Daemon, state *capturedRenderState, f
154153 return &preparedAttachmentOutput{output: o, ansi: ansi, graphics: preparedGraphics, cursor: cursor, data: data}, nil
155154 }
156155
157-func (p *preparedAttachmentOutput) send(echoAck uint64, send func(wire.Frame) error) error {
156+func (p *preparedAttachmentOutput) send(echoAck uint64, send func(protocol.Output) error) error {
158157 if p == nil || p.ansi == nil {
159158 return nil
160159 }
161- sendFrame := send
160+ sendOutput := send
162161 if p.graphics != nil {
163- sendFrame = func(frame wire.Frame) error {
162+ sendOutput = func(output protocol.Output) error {
164163 p.graphics.markSendAttempted()
165- return send(frame)
164+ return send(output)
166165 }
167166 }
168- if err := p.ansi.send(p.data, echoAck, sendFrame); err != nil {
167+ if err := p.ansi.send(p.data, echoAck, sendOutput); err != nil {
169168 p.abortGraphics()
170169 return err
171170 }
@@ -286,7 +285,7 @@ func (s *attachmentOutput) prepare(frame renderer.Frame, damage []renderer.Damag
286285 }, nil
287286 }
288287
289-func (p *preparedOutput) send(data []byte, echoAck uint64, send func(wire.Frame) error) error {
288+func (p *preparedOutput) send(data []byte, echoAck uint64, send func(protocol.Output) error) error {
290289 if p == nil || p.stream == nil || p.attempted {
291290 return nil
292291 }
@@ -380,30 +379,30 @@ func (p *preparedOutput) currentLocked() bool {
380379 (p.stream.attachment == nil || p.stream.attachment.lifecycle.generationValue() == p.connectionGeneration)
381380 }
382381
383-func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (wire.Frame, error) {
384- payload, err := wire.MarshalOutput(protocol.Output{
382+func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (protocol.Output, error) {
383+ output := protocol.Output{
385384 Epoch: epoch, Base: base, New: next, Echo: echoAck, ViewRevision: viewRevision,
386385 Size: size, Full: full, Data: data,
387- })
388- if err != nil {
389- return wire.Frame{}, err
390386 }
391- return wire.Frame{Type: wire.MsgOutput, Payload: payload}, nil
387+ if err := protocol.ValidateOutput(output); err != nil {
388+ return protocol.Output{}, err
389+ }
390+ return output, nil
392391 }
393392
394-// sideEffect builds an output frame without advancing the state stream. When
395-// an attachment is present, callers must hold its sendMu; attachment-bound
396-// sends use sideEffectLocked after validating their transport incarnation.
397-func (s *attachmentOutput) sideEffect(data []byte, echoAck uint64) (wire.Frame, error) {
393+// sideEffect builds output without advancing the state stream. When an
394+// attachment is present, callers must hold its sendMu; attachment-bound sends
395+// use sideEffectLocked after validating their connection incarnation.
396+func (s *attachmentOutput) sideEffect(data []byte, echoAck uint64) (protocol.Output, error) {
398397 s.lockView()
399398 defer s.unlockView()
400399 return s.sideEffectLocked(data, echoAck)
401400 }
402401
403-// sideEffectLocked requires the attachment view lock for both frame creation
404-// and transport emission. Keeping that lock across the send prevents a view
405-// publication from overtaking a frame that was fenced just before it.
406-func (s *attachmentOutput) sideEffectLocked(data []byte, echoAck uint64) (wire.Frame, error) {
402+// sideEffectLocked requires the attachment view lock for both output creation
403+// and connection emission. Keeping that lock across the send prevents a view
404+// publication from overtaking output fenced just before it.
405+func (s *attachmentOutput) sideEffectLocked(data []byte, echoAck uint64) (protocol.Output, error) {
407406 _, epoch, viewRevision := s.fenceLocked()
408407 size := domain.Size{Cols: 1, Rows: 1}
409408 if s != nil && s.attachment != nil {
M internal/usecase/daemon/attachment_output_test.go +29 -27
@@ -92,11 +92,11 @@ func TestAttachmentOutputCapacityProbeDoesNotRaceWithSend(t *testing.T) {
9292 release := make(chan struct{})
9393 sendDone := make(chan error, 1)
9494 go func() {
95- sendDone <- prepared.send(prepared.data, 0, func(wire.Frame) error {
95+ sendDone <- prepared.send(prepared.data, 0, outputFrameSender(func(wire.Frame) error {
9696 close(entered)
9797 <-release
9898 return nil
99- })
99+ }))
100100 }()
101101 <-entered
102102
@@ -134,10 +134,10 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
134134 firstPrepared, err := first.prepare(frame, nil, true)
135135 require.NoError(t, err)
136136 var firstFrame wire.Frame
137- require.NoError(t, firstPrepared.send(firstPrepared.data, 0, func(frame wire.Frame) error {
137+ require.NoError(t, firstPrepared.send(firstPrepared.data, 0, outputFrameSender(func(frame wire.Frame) error {
138138 firstFrame = frame
139139 return nil
140- }))
140+ })))
141141 firstOutput, err := wire.UnmarshalOutput(firstFrame.Payload)
142142 require.NoError(t, err)
143143 require.Equal(t, uint64(1), firstOutput.Epoch)
@@ -147,7 +147,7 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
147147
148148 secondPrepared, err := second.prepare(frame, nil, true)
149149 require.NoError(t, err)
150- require.NoError(t, secondPrepared.send(secondPrepared.data, 0, func(wire.Frame) error { return nil }))
150+ require.NoError(t, secondPrepared.send(secondPrepared.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
151151 require.Equal(t, uint64(1), second.epoch)
152152 require.Equal(t, uint64(1), second.next)
153153 require.Equal(t, uint64(1), first.next)
@@ -161,10 +161,10 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
161161 require.Zero(t, first.next)
162162 require.Zero(t, first.acked)
163163 var staleSent bool
164- require.NoError(t, pending.send(pending.data, 0, func(wire.Frame) error {
164+ require.NoError(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Frame) error {
165165 staleSent = true
166166 return nil
167- }))
167+ })))
168168 require.False(t, staleSent)
169169 require.Zero(t, first.next)
170170 first.ack(1, 1)
@@ -173,10 +173,10 @@ func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *t
173173 reset, err := first.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
174174 require.NoError(t, err)
175175 var resetFrame wire.Frame
176- require.NoError(t, reset.send(reset.data, 0, func(frame wire.Frame) error {
176+ require.NoError(t, reset.send(reset.data, 0, outputFrameSender(func(frame wire.Frame) error {
177177 resetFrame = frame
178178 return nil
179- }))
179+ })))
180180 resetOutput, err := wire.UnmarshalOutput(resetFrame.Payload)
181181 require.NoError(t, err)
182182 require.Equal(t, uint64(2), resetOutput.Epoch)
@@ -205,23 +205,23 @@ func TestPreparedOutputDropsReplacedConnectionAndView(t *testing.T) {
205205 fillOutputStateRows(frame, []string{"abc"})
206206 initial, err := stream.prepare(frame, nil, true)
207207 require.NoError(t, err)
208- require.NoError(t, initial.send(initial.data, 0, func(wire.Frame) error { return nil }))
208+ require.NoError(t, initial.send(initial.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
209209 changed := frame.Clone()
210210 changed.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
211211 pending, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
212212 require.NoError(t, err)
213213 tt.change(ac)
214214 var sent bool
215- require.NoError(t, pending.send(pending.data, 0, func(wire.Frame) error {
215+ require.NoError(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Frame) error {
216216 sent = true
217217 return nil
218- }))
218+ })))
219219 require.False(t, sent)
220220 require.Equal(t, uint64(1), stream.next)
221221
222222 retry, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
223223 require.NoError(t, err)
224- require.NoError(t, retry.send(retry.data, 0, func(wire.Frame) error { return nil }))
224+ require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
225225 require.Equal(t, uint64(2), stream.next)
226226 })
227227 }
@@ -242,9 +242,7 @@ func TestOutputStateSideEffectsDoNotAdvanceEpochStateOrACK(t *testing.T) {
242242 stream := newOutputStateStream()
243243 stream.ack(1, 0)
244244 beforeEpoch, beforeNext, beforeAck := stream.epoch, stream.next, stream.acked
245- frame, err := stream.sideEffect([]byte("pty"), 9)
246- require.NoError(t, err)
247- out, err := wire.UnmarshalOutput(frame.Payload)
245+ out, err := stream.sideEffect([]byte("pty"), 9)
248246 require.NoError(t, err)
249247 require.Equal(t, beforeEpoch, out.Epoch)
250248 require.Zero(t, out.Base)
@@ -261,7 +259,7 @@ func TestAttachmentOutputFailedSendRetriesSnapshotWithoutAdvancing(t *testing.T)
261259 fillOutputStateRows(initial, []string{"abc"})
262260 first, err := stream.prepare(initial, nil, true)
263261 require.NoError(t, err)
264- require.NoError(t, first.send(first.data, 0, func(wire.Frame) error { return nil }))
262+ require.NoError(t, first.send(first.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
265263 require.Equal(t, uint64(1), stream.next)
266264
267265 changed := initial.Clone()
@@ -273,7 +271,7 @@ func TestAttachmentOutputFailedSendRetriesSnapshotWithoutAdvancing(t *testing.T)
273271 require.Empty(t, probe.Bytes(), "preparation must not advance the renderer shadow")
274272
275273 sendErr := errors.New("send failed")
276- require.ErrorIs(t, pending.send(pending.data, 0, func(wire.Frame) error { return sendErr }), sendErr)
274+ require.ErrorIs(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Frame) error { return sendErr })), sendErr)
277275 require.Equal(t, uint64(1), stream.next, "failed send must not advance the state chain")
278276 pending.commitNoSend()
279277 probe, err = stream.renderer.Prepare(initial, nil, false)
@@ -283,19 +281,19 @@ func TestAttachmentOutputFailedSendRetriesSnapshotWithoutAdvancing(t *testing.T)
283281 retry, err := stream.prepare(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
284282 require.NoError(t, err)
285283 var sent wire.Frame
286- require.NoError(t, retry.send(retry.data, 0, func(frame wire.Frame) error {
284+ require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(frame wire.Frame) error {
287285 sent = frame
288286 return nil
289- }))
287+ })))
290288 out, err := wire.UnmarshalOutput(sent.Payload)
291289 require.NoError(t, err)
292290 require.Zero(t, out.Base, "retry after ambiguous send failure must be dependency-free")
293291 require.Equal(t, uint64(2), out.New, "state numbers remain monotonic across rebases")
294292 require.Equal(t, uint64(2), stream.next, "successful retry advances the chain exactly once")
295- require.NoError(t, retry.send(retry.data, 0, func(wire.Frame) error {
293+ require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(wire.Frame) error {
296294 t.Fatal("completed output sent twice")
297295 return nil
298- }))
296+ })))
299297 require.Equal(t, uint64(2), stream.next)
300298 probe, err = stream.renderer.Prepare(changed, nil, false)
301299 require.NoError(t, err)
@@ -308,7 +306,7 @@ func TestAttachmentOutputNoByteCommitAdvancesShadowWithoutState(t *testing.T) {
308306 fillOutputStateRows(frame, []string{"abc"})
309307 initial, err := stream.prepare(frame, nil, true)
310308 require.NoError(t, err)
311- require.NoError(t, initial.send(initial.data, 0, func(wire.Frame) error { return nil }))
309+ require.NoError(t, initial.send(initial.data, 0, outputFrameSender(func(wire.Frame) error { return nil })))
312310
313311 noOp, err := stream.prepare(frame, nil, false)
314312 require.NoError(t, err)
@@ -378,7 +376,11 @@ func outputStateFrame(stream *attachmentOutput, data []byte, reset bool, echoAck
378376 if reset {
379377 base = 0
380378 }
381- frame, err := frameOutputState(data, base, stream.next, echoAck)
379+ output, err := serverOutputState(data, base, stream.next, echoAck)
380+ if err != nil {
381+ panic(err)
382+ }
383+ frame, err := testServerFrame(output)
382384 if err != nil {
383385 panic(err)
384386 }
@@ -395,10 +397,10 @@ func drawOutputState(t *testing.T, stream *attachmentOutput, frame renderer.Fram
395397 return wire.Frame{}, false, err
396398 }
397399 var output wire.Frame
398- err = prepared.send(prepared.data, echoAck, func(frame wire.Frame) error {
400+ err = prepared.send(prepared.data, echoAck, outputFrameSender(func(frame wire.Frame) error {
399401 output = frame
400402 return nil
401- })
403+ }))
402404 if err != nil {
403405 return wire.Frame{}, false, err
404406 }
@@ -456,7 +458,7 @@ func TestAttachmentOutputFailedSendKeepsTextCursorAndGraphicsSpeculative(t *test
456458 require.NoError(t, err)
457459
458460 sendErr := errors.New("send failed")
459- require.ErrorIs(t, prepared.send(0, func(wire.Frame) error { return sendErr }), sendErr)
461+ require.ErrorIs(t, prepared.send(0, outputFrameSender(func(wire.Frame) error { return sendErr })), sendErr)
460462
461463 require.Equal(t, cursorOut{valid: true, row: 1, col: 1}, output.lastCursor)
462464 require.Empty(t, graphicsState.assets)
M internal/usecase/daemon/attachment_parking_test.go +2 -2
@@ -142,7 +142,7 @@ func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
142142 d.clientGone(sess, ac, oldTransport, false)
143143
144144 resumedTransport := &closeTrackingTransport{}
145- d.handleHello(resumedTransport, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", oldToken))})
145+ d.handleHello(resumedTransport, helloResumeCapable(protocol.IntentResume, "work", oldToken))
146146 newToken := ac.resumeToken
147147 require.NotEqual(t, oldToken, newToken)
148148 d.mu.Lock()
@@ -166,7 +166,7 @@ func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
166166 d.clientGone(sess, ac, oldTransport, false)
167167
168168 failed := &resumeWelcomeFailureTransport{}
169- d.handleHello(failed, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", token))})
169+ d.handleHello(failed, helloResumeCapable(protocol.IntentResume, "work", token))
170170 require.Nil(t, ac.transport())
171171 d.mu.Lock()
172172 parked := d.parked[token]
M internal/usecase/daemon/attachment_registry_test.go +2 -7
@@ -5,7 +5,6 @@ import (
55 "testing"
66
77 "github.com/bnema/vev/internal/domain"
8- "github.com/bnema/vev/internal/protocol/wire"
98 "github.com/stretchr/testify/require"
109 )
1110
@@ -97,9 +96,7 @@ func TestTabForAttachmentRepairRebasesOutputBeforePublishingRevision(t *testing.
9796 after := ac.viewSnapshot()
9897 require.Greater(t, after.revision, before.revision)
9998 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)
100- frame, err := ac.output.sideEffect([]byte("x"), 0)
101- require.NoError(t, err)
102- output, err := wire.UnmarshalOutput(frame.Payload)
99+ output, err := ac.output.sideEffect([]byte("x"), 0)
103100 require.NoError(t, err)
104101 require.Equal(t, after.revision, output.ViewRevision)
105102 require.Equal(t, ac.output.currentEpoch(), output.Epoch)
@@ -127,9 +124,7 @@ func TestPrepareRemovedTabViewRebasesOutputBeforeSideEffect(t *testing.T) {
127124 require.Equal(t, before.revision+1, after.revision)
128125 require.Greater(t, ac.output.currentEpoch(), beforeEpoch)
129126
130- frame, err := ac.output.sideEffect([]byte("replacement"), 0)
131- require.NoError(t, err)
132- output, err := wire.UnmarshalOutput(frame.Payload)
127+ output, err := ac.output.sideEffect([]byte("replacement"), 0)
133128 require.NoError(t, err)
134129 require.Equal(t, after.revision, output.ViewRevision)
135130 require.NotEqual(t, beforeEpoch, output.Epoch)
M internal/usecase/daemon/attachment_render_test.go +1 -1
@@ -283,7 +283,7 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
283283 secondLease := rc.attachmentLease(second)
284284 require.True(t, rc.markAttachmentReady(firstLease))
285285 require.True(t, rc.markAttachmentReady(secondLease))
286- resize := func(ac *attachedClient, tr ports.Transport, size domain.Size) {
286+ resize := func(ac *attachedClient, tr ports.ServerConnection, size domain.Size) {
287287 token := sess.captureAttachmentCapability(ac, tr)
288288 d.handleAttachmentClientFrame(token, wire.Frame{
289289 Type: wire.MsgResize,
M internal/usecase/daemon/attention_test.go +1 -2
@@ -13,7 +13,6 @@ import (
1313 renderer "github.com/bnema/vev-vt"
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
16- portsmocks "github.com/bnema/vev/internal/ports/mocks"
1716 "github.com/bnema/vev/internal/protocol/wire"
1817 "github.com/bnema/vev/internal/usecase/keys"
1918 "github.com/bnema/vev/internal/usecase/layout"
@@ -164,7 +163,7 @@ func TestNoteAttentionDoesNotBlockOnWedgedOtherClient(t *testing.T) {
164163 defer releases[1]()
165164 selectTestAttachmentTab(sess, 0)
166165
167- trW := portsmocks.NewMockTransport(t)
166+ trW := newMockServerConnection(t)
168167 block := make(chan struct{})
169168 t.Cleanup(func() { close(block) })
170169 trW.EXPECT().Send(mock.Anything).RunAndReturn(func(wire.Frame) error {
M internal/usecase/daemon/client.go +27 -28
@@ -14,14 +14,13 @@ import (
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
1616 "github.com/bnema/vev/internal/protocol"
17- "github.com/bnema/vev/internal/protocol/wire"
1817 "github.com/bnema/vev/internal/usecase/keys"
1918 "github.com/bnema/vev/internal/usecase/mouse"
2019 themeui "github.com/bnema/vev/internal/usecase/theme"
2120 )
2221
2322 type transportSnapshot struct {
24- transport ports.Transport
23+ transport ports.ServerConnection
2524 incarnation uint64
2625 }
2726
@@ -35,7 +34,7 @@ type appliedTheme struct {
3534 }
3635
3736 type attachedClient struct {
38- tr ports.Transport
37+ tr ports.ServerConnection
3938 transportIncarnation uint64
4039 output *attachmentOutput
4140 overlays *overlayRuntime
@@ -274,13 +273,13 @@ func (ac *attachedClient) setClientTheme(t themeui.Theme) {
274273 ac.themeMu.Unlock()
275274 }
276275
277-func (ac *attachedClient) transport() ports.Transport {
276+func (ac *attachedClient) transport() ports.ServerConnection {
278277 ac.linkMu.Lock()
279278 defer ac.linkMu.Unlock()
280279 return ac.tr
281280 }
282281
283-func (ac *attachedClient) replaceTransport(tr ports.Transport) {
282+func (ac *attachedClient) replaceTransport(tr ports.ServerConnection) {
284283 ac.linkMu.Lock()
285284 ac.tr = tr
286285 ac.transportIncarnation++
@@ -290,7 +289,7 @@ func (ac *attachedClient) replaceTransport(tr ports.Transport) {
290289 // revokeTransport removes tr only if it is still this attachment's current
291290 // link. It returns the revoked transport, so asynchronous retirement can
292291 // close exactly that link without touching a later connection incarnation.
293-func (ac *attachedClient) revokeTransport(tr ports.Transport) ports.Transport {
292+func (ac *attachedClient) revokeTransport(tr ports.ServerConnection) ports.ServerConnection {
294293 if tr == nil {
295294 return nil
296295 }
@@ -319,20 +318,20 @@ func (ac *attachedClient) transportSnapshotCurrent(expected transportSnapshot) b
319318 return ac.tr == expected.transport && ac.transportIncarnation == expected.incarnation
320319 }
321320
322-func (ac *attachedClient) closeCapturedTransport(tr ports.Transport) error {
321+func (ac *attachedClient) closeCapturedTransport(tr ports.ServerConnection) error {
323322 if tr == nil {
324323 return nil
325324 }
326325 return tr.Close()
327326 }
328327
329-func (ac *attachedClient) transportIs(tr ports.Transport) bool {
328+func (ac *attachedClient) transportIs(tr ports.ServerConnection) bool {
330329 ac.linkMu.Lock()
331330 defer ac.linkMu.Unlock()
332331 return ac.tr == tr
333332 }
334333
335-func (ac *attachedClient) currentTransportIs(tr ports.Transport) bool {
334+func (ac *attachedClient) currentTransportIs(tr ports.ServerConnection) bool {
336335 return tr != nil && ac.transportIs(tr)
337336 }
338337
@@ -410,7 +409,7 @@ var errTransportReplaced = errors.New("client transport was replaced")
410409
411410 // sendExpectedTransport writes only when expected is still the attachment's
412411 // current transport incarnation. It preserves sendMu -> linkMu lock ordering.
413-func (ac *attachedClient) sendExpectedTransport(expected transportSnapshot, f wire.Frame) error {
412+func (ac *attachedClient) sendExpectedTransport(expected transportSnapshot, message protocol.ServerMessage) error {
414413 ac.sendMu.Lock()
415414 defer ac.sendMu.Unlock()
416415 if !ac.transportSnapshotCurrent(expected) {
@@ -419,7 +418,7 @@ func (ac *attachedClient) sendExpectedTransport(expected transportSnapshot, f wi
419418 if expected.transport == nil {
420419 return errors.New("client transport is nil")
421420 }
422- return expected.transport.Send(f)
421+ return expected.transport.SendServer(message)
423422 }
424423
425424 // beginExpectedTransportSendLocked validates that expected is still the
@@ -436,7 +435,7 @@ func (ac *attachedClient) beginExpectedTransportSendLocked(expected transportSna
436435 return nil
437436 }
438437
439-func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportSnapshot, f wire.Frame, ticket *attachmentEffect) error {
438+func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportSnapshot, message protocol.ServerMessage, ticket *attachmentEffect) error {
440439 ac.sendMu.Lock()
441440 defer ac.sendMu.Unlock()
442441 // A attachment-bound send requires a live ticket, and reports every rejection,
@@ -447,7 +446,7 @@ func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportS
447446 if err := ac.beginExpectedTransportSendLocked(expected, ticket); err != nil {
448447 return errAttachmentTransition
449448 }
450- err := expected.transport.Send(f)
449+ err := expected.transport.SendServer(message)
451450 if err != nil {
452451 ticket.reportTransportFailure(expected)
453452 }
@@ -460,17 +459,17 @@ func (ac *attachedClient) sendExpectedTransportForAttachment(expected transportS
460459 // detachNotifyTimeout, the transport is force-closed, failing the in-flight
461460 // write. Detach/kill/shutdown paths use this so they are never gated on a
462461 // client that has stopped draining its socket.
463-func (d *Daemon) boundedSend(ac *attachedClient, f wire.Frame) {
464- _ = d.boundedSendErr(ac, f)
462+func (d *Daemon) boundedSend(ac *attachedClient, message protocol.ServerMessage) {
463+ _ = d.boundedSendErr(ac, message)
465464 }
466465
467-func (d *Daemon) boundedSendErr(ac *attachedClient, f wire.Frame) error {
466+func (d *Daemon) boundedSendErr(ac *attachedClient, message protocol.ServerMessage) error {
468467 expected := ac.transportSnapshot()
469468 if expected.transport == nil {
470469 return errors.New("client transport is nil")
471470 }
472471 tr, err := d.boundedSendWith(expected.transport, func() error {
473- return ac.sendExpectedTransport(expected, f)
472+ return ac.sendExpectedTransport(expected, message)
474473 })
475474 if err != nil && errors.Is(err, errSendTimedOut) {
476475 _ = ac.closeCapturedTransport(tr)
@@ -483,12 +482,12 @@ func (d *Daemon) boundedSendOutputErr(ac *attachedClient, b []byte) error {
483482 return err
484483 }
485484
486-func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (ports.Transport, error) {
485+func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (ports.ServerConnection, error) {
487486 expected := ac.transportSnapshot()
488487 if expected.transport == nil {
489488 return nil, errors.New("client transport is nil")
490489 }
491- if owned, ok := expected.transport.(ports.OwnedSynchronousTransport); ok {
490+ if expected.transport.Capabilities().OwnedSynchronousSend {
492491 ac.sendMu.Lock()
493492 defer ac.sendMu.Unlock()
494493 if !ac.transportSnapshotCurrent(expected) {
@@ -499,9 +498,9 @@ func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (po
499498 }
500499 ac.output.lockView()
501500 defer ac.output.unlockView()
502- frame, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())
501+ output, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())
503502 if err == nil {
504- err = owned.SendSynchronous(frame)
503+ err = expected.transport.SendOutputSynchronous(output)
505504 }
506505 return expected.transport, err
507506 }
@@ -516,21 +515,21 @@ func (d *Daemon) boundedSendOutputErrTransport(ac *attachedClient, b []byte) (po
516515 }
517516 ac.output.lockView()
518517 defer ac.output.unlockView()
519- frame, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())
518+ output, err := ac.output.sideEffectLocked(b, ac.echoAck.Load())
520519 if err != nil {
521520 return err
522521 }
523- return expected.transport.Send(frame)
522+ return expected.transport.SendOutput(output)
524523 })
525524 }
526525
527526 var errSendTimedOut = errors.New("send timed out")
528527
529-func (d *Daemon) boundedSendWith(tr ports.Transport, send func() error) (ports.Transport, error) {
528+func (d *Daemon) boundedSendWith(tr ports.ServerConnection, send func() error) (ports.ServerConnection, error) {
530529 return d.boundedSendWithTimeout(detachNotifyTimeout, tr, send)
531530 }
532531
533-func (d *Daemon) boundedSendWithTimeout(timeout time.Duration, tr ports.Transport, send func() error) (ports.Transport, error) {
532+func (d *Daemon) boundedSendWithTimeout(timeout time.Duration, tr ports.ServerConnection, send func() error) (ports.ServerConnection, error) {
534533 timer := d.clock.NewTimer(timeout)
535534 result := make(chan error, 1)
536535 go func() {
@@ -581,7 +580,7 @@ func (d *Daemon) notifyDetachedSnapshotAsync(snapshot detachedAttachmentSnapshot
581580 go func() {
582581 defer close(done)
583582 _, _ = d.boundedSendWith(snapshot.transport.transport, func() error {
584- return snapshot.ac.sendExpectedTransport(snapshot.transport, frameDetached(reason))
583+ return snapshot.ac.sendExpectedTransport(snapshot.transport, serverDetached(reason))
585584 })
586585 revoked := snapshot.ac.revokeTransport(snapshot.transport.transport)
587586 if revoked == nil {
@@ -619,7 +618,7 @@ type attachClientOptions struct {
619618 startupOverlay protocol.StartupOverlay
620619 }
621620
622-func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) {
621+func (d *Daemon) attachClient(sess *session, tr ports.ServerConnection, sz domain.Size, opts attachClientOptions) (*attachedClient, error) {
623622 d.mu.Lock()
624623 ac := d.prepareAttachedClientLocked(sess, tr, domain.Geometry{Size: sz}, opts)
625624 d.mu.Unlock()
@@ -664,7 +663,7 @@ func (d *Daemon) finishAttachedClient(sess *session, ac *attachedClient, opts at
664663 // prepareAttachedClientLocked allocates one detached attachment. Caller holds
665664 // d.mu only to allocate its resume token; attachment publication happens later after
666665 // the caller releases every architecture lock.
667-func (d *Daemon) prepareAttachedClientLocked(sess *session, tr ports.Transport, geometry domain.Geometry, opts attachClientOptions) *attachedClient {
666+func (d *Daemon) prepareAttachedClientLocked(sess *session, tr ports.ServerConnection, geometry domain.Geometry, opts attachClientOptions) *attachedClient {
668667 resumeToken := uint64(0)
669668 if opts.resumeCapable {
670669 resumeToken = d.nextResumeTokenLocked()
M internal/usecase/daemon/client_frame_routing.go +48 -91
@@ -7,7 +7,6 @@ import (
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
99 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
1110 "github.com/bnema/vev/internal/usecase/command"
1211 )
1312
@@ -16,7 +15,7 @@ const connectionSnapshotAttempts = 8
1615 // currentAttachmentConnection retries the lock-free session-to-role snapshot when a
1716 // handoff lands between those reads. The attachment gate still performs the final
1817 // mutation admission after this function returns.
19-func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (*session, attachmentCapability, bool) {
18+func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.ServerConnection) (*session, attachmentCapability, bool) {
2019 for range connectionSnapshotAttempts {
2120 if !ac.currentTransportIs(tr) {
2221 break
@@ -50,8 +49,12 @@ func (d *Daemon) runConnLoop(ac *attachedClient) {
5049 if !ac.currentTransportIs(tr) || ac.currentAttachmentSession() == nil {
5150 return
5251 }
53- f, err := tr.Recv()
52+ message, err := tr.ReceiveClient()
5453 if err != nil {
54+ var failure *protocol.DecodeFailure
55+ if errors.As(err, &failure) {
56+ continue
57+ }
5558 for range connectionSnapshotAttempts {
5659 sess, _, ok := d.currentAttachmentConnection(ac, tr)
5760 if !ok {
@@ -69,14 +72,14 @@ func (d *Daemon) runConnLoop(ac *attachedClient) {
6972 if !ok {
7073 return
7174 }
72- if d.handleAttachmentClientFrame(token, f) {
75+ if d.handleAttachmentClientMessage(token, message) {
7376 return
7477 }
7578 }
7679 }
7780
78-// handleAttachmentClientFrame owns the attached-client protocol.
79-func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f wire.Frame) bool {
81+// handleAttachmentClientMessage owns the attached-client protocol.
82+func (d *Daemon) handleAttachmentClientMessage(capability attachmentCapability, message protocol.ClientMessage) bool {
8083 if !capability.current() {
8184 return false
8285 }
@@ -91,63 +94,34 @@ func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f
9194 if d.afterAttachmentEffectAdmitted != nil {
9295 d.afterAttachmentEffectAdmitted(effect.capability())
9396 }
94- switch f.Type {
95- case wire.MsgInput:
96- if in, derr := wire.UnmarshalInput(f.Payload); derr == nil {
97- d.handleSequencedInputForAttachment(effect, in.InputSeq, in.Data)
98- }
99- case wire.MsgResize:
100- if rz, derr := wire.UnmarshalResize(f.Payload); derr == nil && effect.current() {
101- d.resizeAttachmentGeometryForLease(effect, rz.Geometry())
102- }
103- case wire.MsgTheme:
104- if th, derr := wire.UnmarshalTheme(f.Payload); derr == nil {
105- d.applyThemeForAttachment(effect, th)
106- }
107- case wire.MsgImagePush:
108- if ip, derr := wire.UnmarshalImagePush(f.Payload); derr == nil {
109- d.handleSequencedImagePushForAttachment(effect, ip.InputSeq, ip)
110- }
111- case wire.MsgClientNotice:
112- if notice, derr := wire.UnmarshalClientNotice(f.Payload); derr == nil {
113- d.handleClientNoticeForAttachment(effect, notice)
114- } else {
115- d.log.Warn("malformed client notice", "err", derr)
97+ switch message := message.(type) {
98+ case protocol.Input:
99+ d.handleSequencedInputForAttachment(effect, message.InputSeq, message.Data)
100+ case protocol.Resize:
101+ if effect.current() {
102+ d.resizeAttachmentGeometryForLease(effect, message.Geometry())
116103 }
117- case wire.MsgDetach:
104+ case protocol.Theme:
105+ d.applyThemeForAttachment(effect, message)
106+ case protocol.ImagePush:
107+ d.handleSequencedImagePushForAttachment(effect, message.InputSeq, message)
108+ case protocol.ClientNotice:
109+ d.handleClientNoticeForAttachment(effect, message)
110+ case protocol.Detach:
118111 if effect.current() {
119112 d.clientGoneForAttachment(effect, true)
120113 return true
121114 }
122- case wire.MsgAck:
123- if ack, derr := wire.UnmarshalAck(f.Payload); derr == nil {
124- d.ackOutput(effect, ack.Epoch, ack.State)
125- }
126- case wire.MsgOutputResetRequest:
127- if _, derr := wire.UnmarshalOutputResetRequest(f.Payload); derr == nil {
128- d.resetOutput(effect)
129- }
130- case wire.MsgSamePeerSwitchRequest:
131- request, derr := wire.UnmarshalSamePeerSwitchRequest(f.Payload)
132- if derr != nil {
133- d.log.Warn("malformed same-peer switch request", "err", derr)
134- break
135- }
136- d.switchSamePeerForAttachment(effect, request)
137- case wire.MsgParkedRouteRequest:
138- request, derr := wire.UnmarshalParkedRouteRequest(f.Payload)
139- if derr != nil {
140- d.log.Warn("malformed parked-route request", "err", derr)
141- break
142- }
143- d.handleParkedRouteRequest(effect, request)
144- case wire.MsgRecentRouteSnapshot:
145- snapshot, derr := wire.UnmarshalRecentRouteSnapshot(f.Payload)
146- if derr != nil {
147- d.log.Warn("malformed recent route snapshot", "err", derr)
148- break
149- }
150- replayIdentity := effect.ac.setRouteSnapshot(snapshot)
115+ case protocol.Ack:
116+ d.ackOutput(effect, message.Epoch, message.State)
117+ case protocol.OutputResetRequest:
118+ d.resetOutput(effect)
119+ case protocol.SamePeerSwitchRequest:
120+ d.switchSamePeerForAttachment(effect, message)
121+ case protocol.ParkedRouteRequest:
122+ d.handleParkedRouteRequest(effect, message)
123+ case protocol.RecentRouteSnapshot:
124+ replayIdentity := effect.ac.setRouteSnapshot(message)
151125 d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-snapshot")
152126 if !replayIdentity {
153127 break
@@ -155,61 +129,44 @@ func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f
155129 if err := d.sendCommittedRouteIdentityForAttachment(effect); err != nil {
156130 d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport)
157131 }
158- case wire.MsgRouteAttentionSubscription:
159- subscription, derr := wire.UnmarshalRouteAttentionSubscription(f.Payload)
160- if derr != nil {
161- d.log.Warn("malformed route attention subscription", "err", derr)
162- break
163- }
164- effect.ac.setRouteAttentionSubscription(subscription)
132+ case protocol.RouteAttentionSubscription:
133+ effect.ac.setRouteAttentionSubscription(message)
165134 d.invalidateRender(effect.sess, effect.ac, false, "client_frame_routing.go:route-attention")
166- case wire.MsgRouteNavigationFailure:
167- failure, derr := wire.UnmarshalRouteNavigationFailure(f.Payload)
168- if derr != nil {
169- d.log.Warn("malformed route navigation failure", "err", derr)
170- break
171- }
172- message := "route navigation failed"
173- switch failure.Code {
135+ case protocol.RouteNavigationFailure:
136+ notice := "route navigation failed"
137+ switch message.Code {
174138 case protocol.RouteFailureStaleSelection:
175- message = "that recent route is no longer available"
139+ notice = "that recent route is no longer available"
176140 case protocol.RouteFailureTargetChanged:
177- message = "that recent route changed before attach"
141+ notice = "that recent route changed before attach"
178142 case protocol.RouteFailureOriginUnavailable:
179- message = "the original route is no longer available"
143+ notice = "the original route is no longer available"
180144 case protocol.RouteFailureUnavailable:
181- message = "the selected route is unavailable"
145+ notice = "the selected route is unavailable"
182146 case protocol.RouteFailureNoSuchRoute:
183- message = "that recent route no longer exists"
184- }
185- d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, message, nil)
186- case wire.MsgCommand:
187- request, derr := wire.UnmarshalCommandRequest(f.Payload)
188- if derr != nil {
189- return false
147+ notice = "that recent route no longer exists"
190148 }
191- result := d.executeAttachedCommand(effect, request)
149+ d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, notice, nil)
150+ case protocol.CommandRequest:
151+ result := d.executeAttachedCommand(effect, message)
192152 resultEffect, ok := d.commandResultEffect(effect)
193153 if !ok {
194154 // A remote handoff or explicit detach deliberately retired the old
195155 // attachment; it must not be treated as a stale-result send error.
196156 return true
197157 }
198- if err := resultEffect.sendControl(frameCommandResult(result)); err != nil {
158+ if err := resultEffect.sendControl(serverCommandResult(result)); err != nil {
199159 resultEffect.End()
200160 d.detachOnAttachmentSendError(resultEffect.capability(), resultEffect.transport.transport)
201161 return true
202162 }
203163 resultEffect.End()
204- case wire.MsgPing:
205- if err := effect.sendControl(framePong()); err != nil {
164+ case protocol.Ping:
165+ if err := effect.sendControl(serverPong()); err != nil {
206166 effect.End()
207167 d.detachOnAttachmentSendError(effect.capability(), effect.transport.transport)
208168 return true
209169 }
210- default:
211- // Unknown/out-of-band client messages are ignored so a newer
212- // client can add message types without breaking an older daemon.
213170 }
214171 return false
215172 }
M internal/usecase/daemon/client_transport_errors.go +10 -10
@@ -7,15 +7,15 @@ import (
77
88 // clientGone detaches ac if it is still the session's current client. The
99 // session remains registered and headless after the client is gone.
10-func (d *Daemon) clientGone(sess *session, ac *attachedClient, failed ports.Transport, explicit bool) {
10+func (d *Daemon) clientGone(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit bool) {
1111 d.clientGoneWithNotice(sess, ac, failed, explicit, true)
1212 }
1313
14-func (d *Daemon) clientGoneWithoutNotice(sess *session, ac *attachedClient, failed ports.Transport, explicit bool) {
14+func (d *Daemon) clientGoneWithoutNotice(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit bool) {
1515 d.clientGoneWithNotice(sess, ac, failed, explicit, false)
1616 }
1717
18-func (d *Daemon) clientGoneWithNotice(sess *session, ac *attachedClient, failed ports.Transport, explicit, notice bool) {
18+func (d *Daemon) clientGoneWithNotice(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool) {
1919 if sess == nil || ac == nil {
2020 return
2121 }
@@ -65,7 +65,7 @@ func (d *Daemon) clientGoneForAttachment(effect *attachmentEffect, explicit bool
6565 return true
6666 }
6767
68-func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.Transport, explicit, notice bool) {
68+func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.ServerConnection, explicit, notice bool) {
6969 if sess == nil || ac == nil {
7070 return
7171 }
@@ -111,7 +111,7 @@ func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed port
111111 // (the client is actively awaiting it), but deadline-bounded so a
112112 // wedged client cannot pin this conn handler and hang Serve's
113113 // connWg.Wait.
114- d.boundedSend(ac, frameDetached(protocol.ReasonDetach))
114+ d.boundedSend(ac, serverDetached(protocol.ReasonDetach))
115115 }
116116 _ = ac.closeCapturedTransport(oldTr)
117117 d.log.Info("client detached", "session", name, "explicit", explicit)
@@ -119,7 +119,7 @@ func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed port
119119
120120 // detachOnSendError drops a client whose transport failed, leaving the session
121121 // registered and headless.
122-func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed ports.Transport) {
122+func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed ports.ServerConnection) {
123123 expected := transportSnapshot{}
124124 if failed != nil {
125125 expected = ac.transportSnapshot()
@@ -135,11 +135,11 @@ func (d *Daemon) detachOnSendError(sess *session, ac *attachedClient, failed por
135135 d.clearParkingInFlightIfAbandoned(sess, ac, parkingToken)
136136 }
137137
138-func (d *Daemon) detachOnAttachmentSendError(token attachmentCapability, failed ports.Transport) {
138+func (d *Daemon) detachOnAttachmentSendError(token attachmentCapability, failed ports.ServerConnection) {
139139 d.detachOnAttachmentSendErrorUntil(token, failed, nil)
140140 }
141141
142-func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, failed ports.Transport, done func() <-chan struct{}) {
142+func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, failed ports.ServerConnection, done func() <-chan struct{}) {
143143 // A delayed sender may report after the client has rebound. Only the exact
144144 // transport captured by this attachment is allowed to detach either lifecycle.
145145 if failed == nil || failed != token.transport.transport {
@@ -160,7 +160,7 @@ func (d *Daemon) detachOnAttachmentSendErrorUntil(token attachmentCapability, fa
160160 // reserveAttachmentSendErrorCleanup accounts for cleanup before End releases the
161161 // attachment gate. This closes the WaitGroup Add/Wait race with terminal teardown;
162162 // the returned launch function must be invoked immediately after ticket End.
163-func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, failed ports.Transport) func() {
163+func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, failed ports.ServerConnection) func() {
164164 d.attachmentCleanupWg.Add(1)
165165 return func() {
166166 go func() {
@@ -178,7 +178,7 @@ func (d *Daemon) reserveAttachmentSendErrorCleanup(token attachmentCapability, f
178178 }
179179 }
180180
181-func (d *Daemon) finishSendErrorDetach(sess *session, ac *attachedClient, failed ports.Transport) {
181+func (d *Daemon) finishSendErrorDetach(sess *session, ac *attachedClient, failed ports.ServerConnection) {
182182 ac.clearSamePeerOffer()
183183 ac.clearParkedRoute()
184184 name := sess.nameSnapshot()
M internal/usecase/daemon/clipboard.go +5 -5
@@ -255,13 +255,13 @@ func (d *Daemon) forwardClipboardAsync(owner paneEffectLease, b64 string) {
255255 sess.mu.Unlock()
256256 }
257257
258-func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *attachmentEffect) (ports.Transport, error) {
258+func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *attachmentEffect) (ports.ServerConnection, error) {
259259 token := item.token
260260 if token.ac == nil || ticket == nil || ticket.ended.Load() || token.transport.transport == nil {
261261 return token.transport.transport, errAttachmentTransition
262262 }
263263 expected := token.transport
264- ownedTransport, owned := expected.transport.(ports.OwnedSynchronousTransport)
264+ owned := expected.transport.Capabilities().OwnedSynchronousSend
265265 send := func() error {
266266 ac := token.ac
267267 ac.sendMu.Lock()
@@ -277,12 +277,12 @@ func (d *Daemon) boundedSendClipboardForward(item clipboardForward, ticket *atta
277277 }
278278 ac.output.lockView()
279279 defer ac.output.unlockView()
280- frame, err := ac.output.sideEffectLocked(item.seq, ac.echoAck.Load())
280+ output, err := ac.output.sideEffectLocked(item.seq, ac.echoAck.Load())
281281 if err == nil {
282282 if owned {
283- err = ownedTransport.SendSynchronous(frame)
283+ err = expected.transport.SendOutputSynchronous(output)
284284 } else {
285- err = expected.transport.Send(frame)
285+ err = expected.transport.SendOutput(output)
286286 }
287287 }
288288 if err != nil {
M internal/usecase/daemon/clipboard_test.go +1 -1
@@ -204,7 +204,7 @@ func chunkReadPTY(t *testing.T, chunks ...[]byte) *portsmocks.MockPTY {
204204 return p
205205 }
206206
207-func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.Transport) {
207+func publishActiveClipboardCapability(d *Daemon, sess *session, ac *attachedClient, tr ports.ServerConnection) {
208208 rc := d.attachCoordinator(sess, nil, ac, true)
209209 token := sess.captureAttachmentCapability(ac, tr)
210210 token.lease = rc.attachmentLease(ac)
M internal/usecase/daemon/control.go +9 -17
@@ -13,22 +13,16 @@ import (
1313 "github.com/bnema/vev/internal/domain"
1414 "github.com/bnema/vev/internal/ports"
1515 "github.com/bnema/vev/internal/protocol"
16- "github.com/bnema/vev/internal/protocol/wire"
1716 "github.com/bnema/vev/internal/usecase/command"
1817 "github.com/bnema/vev/internal/usecase/layout"
1918 )
2019
21-// handleCommand serves one one-shot control request. The leading version is
22-// checked before decoding the versioned body so newer layouts fail cleanly.
23-func (d *Daemon) handleCommand(tr ports.Transport, f wire.Frame) error {
20+// handleCommand serves one typed one-shot control request.
21+func (d *Daemon) handleCommand(tr ports.ServerConnection, request protocol.CommandRequest) error {
2422 defer func() { _ = tr.Close() }()
2523
26- if version, ok := wire.PeekCommandVersion(f.Payload); !ok || version != protocol.Version {
27- return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})
28- }
29- request, err := wire.UnmarshalCommandRequest(f.Payload)
30- if err != nil {
31- return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrInternal, Text: "malformed command request"})
24+ if request.Version != protocol.Version {
25+ return d.sendCommandResult(tr, protocol.CommandResult{RequestID: request.RequestID, Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})
3226 }
3327 if request.Attached {
3428 return d.sendCommandResult(tr, protocol.CommandResult{
@@ -69,8 +63,8 @@ func (d *Daemon) handleCommand(tr ports.Transport, f wire.Frame) error {
6963
7064 // sendCommandResult gives one-shot control responses the same observable
7165 // transport-failure behavior regardless of which validation path produced it.
72-func (d *Daemon) sendCommandResult(tr ports.Transport, result protocol.CommandResult) error {
73- if err := d.boundedControlSend(tr, frameCommandResult(result)); err != nil {
66+func (d *Daemon) sendCommandResult(tr ports.ServerConnection, result protocol.CommandResult) error {
67+ if err := d.boundedControlSend(tr, serverCommandResult(result)); err != nil {
7468 d.log.Warn("command response send failed", "err", err)
7569 return err
7670 }
@@ -80,17 +74,15 @@ func (d *Daemon) sendCommandResult(tr ports.Transport, result protocol.CommandRe
8074 // boundedControlSend keeps one-shot control handlers from waiting forever on a
8175 // client that stopped reading. A timeout closes the exact transport so a
8276 // blocked Send can unwind before the handler returns.
83-func (d *Daemon) boundedControlSend(tr ports.Transport, frame wire.Frame) error {
84- _, err := d.boundedSendWith(tr, func() error { return tr.Send(frame) })
77+func (d *Daemon) boundedControlSend(tr ports.ServerConnection, message protocol.ServerMessage) error {
78+ _, err := d.boundedSendWith(tr, func() error { return tr.SendServer(message) })
8579 if errors.Is(err, errSendTimedOut) {
8680 _ = tr.Close()
8781 }
8882 return err
8983 }
9084
91-func frameCommandResult(result protocol.CommandResult) wire.Frame {
92- return wire.Frame{Type: wire.MsgCommandResult, Payload: wire.MarshalCommandResult(result)}
93-}
85+func serverCommandResult(result protocol.CommandResult) protocol.CommandResult { return result }
9486
9587 func (d *Daemon) dispatchCommand(ctx context.Context, request protocol.CommandRequest) protocol.CommandResult {
9688 cmd, ok := command.BySlug(request.Slug)
M internal/usecase/daemon/control_test.go +9 -9
@@ -139,7 +139,7 @@ func TestHandleCommandTimesOutWithRequestGeneration(t *testing.T) {
139139 defer releaseConn()
140140 done := make(chan error, 1)
141141 go func() {
142- done <- d.handleCommand(tr, frame)
142+ done <- d.handleCommandFrame(tr, frame)
143143 }()
144144 <-factory.entered
145145 timer := <-clock.timers
@@ -159,7 +159,7 @@ func TestHandleCommandResponseSendTimeoutClosesTransport(t *testing.T) {
159159 tr := newBlockingControlSendTransport()
160160 frame := commandFrame(t, protocol.CommandRequest{RequestID: 41, Attached: true})
161161 done := make(chan error, 1)
162- go func() { done <- d.handleCommand(tr, frame) }()
162+ go func() { done <- d.handleCommandFrame(tr, frame) }()
163163
164164 <-tr.started
165165 timer := <-clock.timers
@@ -184,7 +184,7 @@ func TestHandleCommandAttachedRejectionReturnsSendFailure(t *testing.T) {
184184 sendErr := errors.New("command result send failed")
185185 tr := &commandSendErrorTransport{err: sendErr}
186186
187- err := d.handleCommand(tr, commandFrame(t, protocol.CommandRequest{
187+ err := d.handleCommandFrame(tr, commandFrame(t, protocol.CommandRequest{
188188 RequestID: 41,
189189 Attached: true,
190190 }))
@@ -201,7 +201,7 @@ func TestHandleCommandRejectsVersionBeforeDecodeOrDispatch(t *testing.T) {
201201 tr, sends, _ := newConn(t, frame)
202202
203203 d := newTestDaemon(t, nil, stubClock{})
204- require.NoError(t, d.handleCommand(tr, frame))
204+ require.NoError(t, d.handleCommandFrame(tr, frame))
205205
206206 result := awaitCommandResult(t, sends)
207207 require.False(t, result.OK)
@@ -887,7 +887,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
887887 firstTransport, firstSends, _ := newConn(t, firstFrame)
888888 firstDone := make(chan struct{})
889889 go func() {
890- if err := d.handleCommand(firstTransport, firstFrame); err != nil {
890+ if err := d.handleCommandFrame(firstTransport, firstFrame); err != nil {
891891 t.Errorf("handle first command: %v", err)
892892 }
893893 close(firstDone)
@@ -905,7 +905,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
905905 secondTransport, secondSends, _ := newConn(t, secondFrame)
906906 secondDone := make(chan struct{})
907907 go func() {
908- if err := d.handleCommand(secondTransport, secondFrame); err != nil {
908+ if err := d.handleCommandFrame(secondTransport, secondFrame); err != nil {
909909 t.Errorf("handle second command: %v", err)
910910 }
911911 close(secondDone)
@@ -1287,14 +1287,14 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
12871287 done := make(chan struct{}, 2)
12881288 go func() {
12891289 <-start
1290- if err := d.handleCommand(leftTransport, leftFrame); err != nil {
1290+ if err := d.handleCommandFrame(leftTransport, leftFrame); err != nil {
12911291 t.Errorf("handle left command: %v", err)
12921292 }
12931293 done <- struct{}{}
12941294 }()
12951295 go func() {
12961296 <-start
1297- if err := d.handleCommand(rightTransport, rightFrame); err != nil {
1297+ if err := d.handleCommandFrame(rightTransport, rightFrame); err != nil {
12981298 t.Errorf("handle right command: %v", err)
12991299 }
13001300 done <- struct{}{}
@@ -1339,7 +1339,7 @@ func sendCommand(t *testing.T, d *Daemon, request protocol.CommandRequest) proto
13391339 t.Helper()
13401340 frame := commandFrame(t, request)
13411341 tr, sends, _ := newConn(t, frame)
1342- require.NoError(t, d.handleCommand(tr, frame))
1342+ require.NoError(t, d.handleCommandFrame(tr, frame))
13431343 return awaitCommandResult(t, sends)
13441344 }
13451345
M internal/usecase/daemon/copymode_test.go +2 -2
@@ -68,7 +68,7 @@ func TestOwnedSynchronousSendReturnsCapturedTransportAcrossReplacement(t *testin
6868
6969 type swapErrorTransport struct {
7070 ac *attachedClient
71- replacement ports.Transport
71+ replacement ports.ServerConnection
7272 err error
7373 }
7474
@@ -82,7 +82,7 @@ func (t *swapErrorTransport) Close() error { return nil }
8282
8383 type ownedSwapErrorTransport struct {
8484 ac *attachedClient
85- replacement ports.Transport
85+ replacement ports.ServerConnection
8686 err error
8787 sent chan wire.Frame
8888 }
M internal/usecase/daemon/daemon.go +76 -64
@@ -18,7 +18,6 @@ import (
1818 "github.com/bnema/vev/internal/domain"
1919 "github.com/bnema/vev/internal/ports"
2020 "github.com/bnema/vev/internal/protocol"
21- "github.com/bnema/vev/internal/protocol/wire"
2221 "github.com/bnema/vev/internal/usecase/keys"
2322 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
2423 )
@@ -739,7 +738,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
739738 // Serve runs the accept loop over l, owning it for the loop's lifetime. It
740739 // returns when the last session is removed or ctx is cancelled; on the latter
741740 // path attached clients are detached with ReasonServerShutdown.
742-func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error {
741+func (d *Daemon) Serve(ctx context.Context, l ports.ServerListener) error {
743742 d.serveCtx, d.serveCancel = context.WithCancel(ctx)
744743 defer d.serveCancel()
745744 d.hardCtx, d.hardCancel = context.WithCancel(context.Background())
@@ -1012,54 +1011,58 @@ func (d *Daemon) sessionsSnapshotLocked() []*session {
10121011 return sessionsSnapshot(d.sessions)
10131012 }
10141013
1015-// handleConn reads the first frame off a fresh connection and routes it. A
1016-// context watcher closes the transport on serve-context cancel or handshake
1017-// timeout so blocked transport operations unwind.
1018-func (d *Daemon) handleConn(tr ports.Transport) {
1014+// handleConn reads the first typed message off a fresh connection and routes it.
1015+// A context watcher closes the connection on cancellation or handshake timeout.
1016+func (d *Daemon) handleConn(tr ports.ServerConnection) {
10191017 handshakeCtx, timedOut, finishHandshake := d.newHandshakeContext(d.hardCtx)
10201018 stopTransport := watchHandshakeTransport(handshakeCtx, tr)
10211019 defer finishHandshake()
10221020 defer stopTransport()
10231021
1024- var first wire.Frame
1022+ var first protocol.ClientMessage
10251023 err := boundedHandshakeOperation(handshakeCtx, tr, func() error {
1026- var recvErr error
1027- first, recvErr = tr.Recv()
1028- return recvErr
1024+ var receiveErr error
1025+ first, receiveErr = tr.ReceiveClient()
1026+ return receiveErr
10291027 })
10301028 if err != nil {
1031- err = handshakeContextError(d.hardCtx, timedOut, err)
1032- d.log.Warn("connection closed before hello", "err", err)
1029+ var failure *protocol.DecodeFailure
1030+ if errors.As(err, &failure) {
1031+ d.handleInitialDecodeFailure(handshakeCtx, tr, failure)
1032+ } else {
1033+ err = handshakeContextError(d.hardCtx, timedOut, err)
1034+ d.log.Warn("connection closed before hello", "err", err)
1035+ }
10331036 _ = tr.Close()
10341037 return
10351038 }
1036- switch first.Type {
1037- case wire.MsgList:
1039+ switch message := first.(type) {
1040+ case protocol.List:
10381041 stopTransport()
10391042 finishHandshake()
10401043 d.handleList(tr)
1041- case wire.MsgCommand:
1044+ case protocol.CommandRequest:
10421045 stopTransport()
10431046 finishHandshake()
1044- if err := d.handleCommand(tr, first); err != nil {
1047+ if err := d.handleCommand(tr, message); err != nil {
10451048 d.log.Warn("command handler failed", "err", err)
10461049 }
1047- case wire.MsgRemotePreviewRequest:
1050+ case protocol.RemotePreviewRequest:
10481051 stopTransport()
10491052 finishHandshake()
1050- if err := d.handleRemotePreview(tr, first); err != nil {
1053+ if err := d.handleRemotePreview(tr, message); err != nil {
10511054 d.log.Warn("remote preview handler failed", "err", err)
10521055 }
1053- case wire.MsgKill:
1056+ case protocol.Kill:
10541057 stopTransport()
10551058 finishHandshake()
1056- d.handleKill(tr, first)
1057- case wire.MsgHello:
1058- d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, first)
1059+ d.handleKill(tr, message)
1060+ case protocol.Hello:
1061+ d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, message)
10591062 default:
1060- d.log.Warn("hello rejected", "err", "expected hello", "type", first.Type)
1063+ d.log.Warn("hello rejected", "err", "expected hello")
10611064 _ = boundedHandshakeOperation(handshakeCtx, tr, func() error {
1062- return tr.Send(frameError(protocol.ErrInternal, "expected hello"))
1065+ return tr.SendServer(serverError(protocol.ErrInternal, "expected hello"))
10631066 })
10641067 stopTransport()
10651068 finishHandshake()
@@ -1067,9 +1070,35 @@ func (d *Daemon) handleConn(tr ports.Transport) {
10671070 }
10681071 }
10691072
1073+func (d *Daemon) handleInitialDecodeFailure(ctx context.Context, tr ports.ServerConnection, failure *protocol.DecodeFailure) {
1074+ send := func(message protocol.ServerMessage) {
1075+ _ = boundedHandshakeOperation(ctx, tr, func() error { return tr.SendServer(message) })
1076+ }
1077+ switch failure.Kind {
1078+ case protocol.DecodeMessageHello:
1079+ if failure.Version != 0 && failure.Version != protocol.Version {
1080+ send(serverError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1081+ return
1082+ }
1083+ send(serverError(protocol.ErrInternal, "malformed hello"))
1084+ case protocol.DecodeMessageCommand:
1085+ code, text := uint16(protocol.ErrInternal), "malformed command request"
1086+ if failure.Version != 0 && failure.Version != protocol.Version {
1087+ code, text = protocol.ErrVersionMismatch, "protocol version mismatch"
1088+ }
1089+ send(protocol.CommandResult{RequestID: failure.RequestID, Code: code, Text: text})
1090+ case protocol.DecodeMessageKill:
1091+ send(serverError(protocol.ErrInternal, "malformed kill request"))
1092+ case protocol.DecodeMessageRemotePreview:
1093+ send(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
1094+ default:
1095+ send(serverError(protocol.ErrInternal, "expected hello"))
1096+ }
1097+}
1098+
10701099 // handleList replies with the current session listing and closes the (one-shot
10711100 // control) connection.
1072-func (d *Daemon) handleList(tr ports.Transport) {
1101+func (d *Daemon) handleList(tr ports.ServerConnection) {
10731102 defer func() { _ = tr.Close() }()
10741103
10751104 d.mu.Lock()
@@ -1105,35 +1134,30 @@ func (d *Daemon) handleList(tr ports.Transport) {
11051134 d.mu.Unlock()
11061135
11071136 sort.Slice(infos, func(i, j int) bool { return infos[i].Name < infos[j].Name })
1108- _ = d.boundedControlSend(tr, frameSessions(infos))
1137+ _ = d.boundedControlSend(tr, serverSessions(infos))
11091138 }
11101139
11111140 // handleKill terminates the requested live session or stopped named session,
11121141 // or all sessions, and closes the control connection; the resulting EOF is the
11131142 // client's success signal.
1114-func (d *Daemon) handleKill(tr ports.Transport, f wire.Frame) {
1143+func (d *Daemon) handleKill(tr ports.ServerConnection, request protocol.Kill) {
11151144 defer func() { _ = tr.Close() }()
11161145
1117- k, err := wire.UnmarshalKill(f.Payload)
1118- if err != nil {
1119- _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "malformed kill request"))
1120- return
1121- }
1122- if k.All {
1146+ if request.All {
11231147 d.shutdownAll(protocol.ReasonServerShutdown)
11241148 return
11251149 }
11261150
11271151 d.mu.Lock()
1128- target := d.findByNameLocked(k.Name)
1152+ target := d.findByNameLocked(request.Name)
11291153 if target == nil {
1130- if _, ok := d.inactive[k.Name]; ok {
1154+ if _, ok := d.inactive[request.Name]; ok {
11311155 d.mu.Unlock()
11321156 // Stopped sessions use the same catalogue-first, incarnation-second
11331157 // deletion order as live and offline purges.
1134- if err := d.retryStoppedPurge(k.Name); err != nil {
1135- d.log.Warn("deleting stopped session failed", "err", err, "session", k.Name)
1136- _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting stopped session failed"))
1158+ if err := d.retryStoppedPurge(request.Name); err != nil {
1159+ d.log.Warn("deleting stopped session failed", "err", err, "session", request.Name)
1160+ _ = d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting stopped session failed"))
11371161 }
11381162 return
11391163 }
@@ -1141,54 +1165,42 @@ func (d *Daemon) handleKill(tr ports.Transport, f wire.Frame) {
11411165 d.mu.Unlock()
11421166
11431167 if target == nil {
1144- _ = d.boundedControlSend(tr, frameError(protocol.ErrNoSuchSession, "no such session: "+k.Name))
1168+ _ = d.boundedControlSend(tr, serverError(protocol.ErrNoSuchSession, "no such session: "+request.Name))
11451169 return
11461170 }
11471171 if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {
1148- _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting persisted session failed"))
1172+ _ = d.boundedControlSend(tr, serverError(protocol.ErrInternal, "deleting persisted session failed"))
11491173 }
11501174 }
11511175
11521176 // handleHello runs the attach handshake for direct package callers. Accepted
11531177 // connections use handleHelloWithContext so the deadline also covers the first
11541178 // frame read in handleConn.
1155-func (d *Daemon) handleHello(tr ports.Transport, f wire.Frame) {
1179+func (d *Daemon) handleHello(tr ports.ServerConnection, hello protocol.Hello) {
11561180 handshakeCtx, timedOut, finishHandshake := d.newHandshakeContext(context.Background())
11571181 stopTransport := watchHandshakeTransport(handshakeCtx, tr)
11581182 defer finishHandshake()
11591183 defer stopTransport()
1160- d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, f)
1184+ d.handleHelloWithContext(handshakeCtx, timedOut, stopTransport, finishHandshake, tr, hello)
11611185 }
11621186
11631187 // handleHelloWithContext runs the attach handshake: version check, intent
11641188 // routing, Welcome, guaranteed first paint, then the per-connection input loop.
1165-func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <-chan struct{}, stopHandshakeTransport, finishHandshake func(), tr ports.Transport, f wire.Frame) {
1189+func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <-chan struct{}, stopHandshakeTransport, finishHandshake func(), tr ports.ServerConnection, h protocol.Hello) {
11661190 if err := handshakeContextError(context.Background(), timedOut, handshakeCtx.Err()); err != nil {
11671191 _ = tr.Close()
11681192 return
11691193 }
1170- sendHandshake := func(frame wire.Frame) error {
1171- err := boundedHandshakeOperation(handshakeCtx, tr, func() error { return tr.Send(frame) })
1194+ sendHandshake := func(message protocol.ServerMessage) error {
1195+ err := boundedHandshakeOperation(handshakeCtx, tr, func() error { return tr.SendServer(message) })
11721196 if err != nil {
11731197 return handshakeContextError(context.Background(), timedOut, err)
11741198 }
11751199 return nil
11761200 }
1177- h, err := wire.UnmarshalHello(f.Payload)
1178- if err != nil {
1179- if version, ok := wire.PeekHelloVersion(f.Payload); ok && version != protocol.Version {
1180- d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", version, "expected", protocol.Version)
1181- _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1182- } else {
1183- d.log.Warn("hello rejected", "err", err)
1184- _ = sendHandshake(frameError(protocol.ErrInternal, "malformed hello"))
1185- }
1186- _ = tr.Close()
1187- return
1188- }
11891201 if h.Version != protocol.Version {
11901202 d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", h.Version, "expected", protocol.Version, "intent", h.Intent, "session", h.Name)
1191- _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1203+ _ = sendHandshake(serverError(protocol.ErrVersionMismatch, "protocol version mismatch"))
11921204 _ = tr.Close()
11931205 return
11941206 }
@@ -1197,9 +1209,9 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
11971209 if rerr != nil {
11981210 d.log.Warn("hello rejected", "err", rerr, "intent", h.Intent, "session", h.Name)
11991211 if pe, ok := errors.AsType[*protoErr](rerr); ok {
1200- _ = sendHandshake(frameError(pe.code, pe.text))
1212+ _ = sendHandshake(serverError(pe.code, pe.text))
12011213 } else {
1202- _ = sendHandshake(frameError(protocol.ErrInternal, rerr.Error()))
1214+ _ = sendHandshake(serverError(protocol.ErrInternal, rerr.Error()))
12031215 }
12041216 _ = tr.Close()
12051217 return
@@ -1230,7 +1242,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
12301242 return
12311243 }
12321244 welcomeDone, welcomeErr := boundedHandshakeOperationTracked(handshakeCtx, tr, func() error {
1233- frame, err := frameWelcome(sess, d.resumeTokenSnapshot(ac))
1245+ frame, err := serverWelcome(sess, d.resumeTokenSnapshot(ac))
12341246 if err != nil {
12351247 return err
12361248 }
@@ -1311,7 +1323,7 @@ func (e *protoErr) Error() string { return e.text }
13111323 // unlocking d.mu before returning, including every error path. It publishes
13121324 // terminal and role state before releasing d.mu, then defers coordinator
13131325 // cleanup so obsolete workers never delay the new handshake.
1314-func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello) (*attachedClient, error) {
1326+func (d *Daemon) finishAttach(sess *session, tr ports.ServerConnection, sz domain.Size, h protocol.Hello) (*attachedClient, error) {
13151327 initialTabIndex := -1
13161328 if h.RemoteTarget != nil {
13171329 var ok bool
@@ -1478,7 +1490,7 @@ func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol
14781490 return d.validateExactSessionTargetLocked(target)
14791491 }
14801492
1481-func (d *Daemon) route(h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
1493+func (d *Daemon) route(h protocol.Hello, tr ports.ServerConnection) (*session, *attachedClient, error) {
14821494 ctx := d.serveCtx
14831495 if ctx == nil {
14841496 ctx = context.Background()
@@ -1486,7 +1498,7 @@ func (d *Daemon) route(h protocol.Hello, tr ports.Transport) (*session, *attache
14861498 return d.routeWithContext(ctx, h, tr)
14871499 }
14881500
1489-func (d *Daemon) routeWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
1501+func (d *Daemon) routeWithContext(ctx context.Context, h protocol.Hello, tr ports.ServerConnection) (*session, *attachedClient, error) {
14901502 if ctx == nil {
14911503 ctx = context.Background()
14921504 }
M internal/usecase/daemon/daemon_test.go +7 -7
@@ -237,9 +237,9 @@ func newFactorySeq(t *testing.T, ptys ...ports.PTY) *portsmocks.MockPTYFactory {
237237 // newConn scripts a MockTransport: Recv yields first then more (in order),
238238 // then blocks until the connection is released or Closed (returning io.EOF).
239239 // Every Send is captured on the returned channel.
240-func newConn(t *testing.T, first wire.Frame, more ...wire.Frame) (*portsmocks.MockTransport, chan wire.Frame, func()) {
240+func newConn(t *testing.T, first wire.Frame, more ...wire.Frame) (*mockServerConnection, chan wire.Frame, func()) {
241241 t.Helper()
242- tr := portsmocks.NewMockTransport(t)
242+ tr := newMockServerConnection(t)
243243 sends := make(chan wire.Frame, 64)
244244 recvCh := make(chan wire.Frame, 1+len(more))
245245 recvCh <- first
@@ -331,7 +331,7 @@ func listSessions(t *testing.T, d *Daemon) protocol.Sessions {
331331 // attachment publication and Welcome acceptance. It uses the generated
332332 // transport mock while channels make both sides of the wire deterministic.
333333 type welcomeBlockingTransport struct {
334- tr *portsmocks.MockTransport
334+ tr *mockServerConnection
335335 sends chan wire.Frame
336336 welcomeEntered chan struct{}
337337 releaseWelcome chan struct{}
@@ -348,7 +348,7 @@ func newWelcomeBlockingTransport(t *testing.T) *welcomeBlockingTransport {
348348 releaseWelcome: make(chan struct{}),
349349 recvDone: make(chan struct{}),
350350 }
351- b.tr = portsmocks.NewMockTransport(t)
351+ b.tr = newMockServerConnection(t)
352352 b.tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {
353353 b.sends <- f
354354 if f.Type == wire.MsgWelcome {
@@ -375,9 +375,9 @@ func (b *welcomeBlockingTransport) finish() {
375375 b.closeOnce.Do(func() { close(b.recvDone) })
376376 }
377377
378-func newCapturingTransport(t testing.TB) (*portsmocks.MockTransport, chan wire.Frame) {
378+func newCapturingTransport(t testing.TB) (*mockServerConnection, chan wire.Frame) {
379379 t.Helper()
380- tr := portsmocks.NewMockTransport(t)
380+ tr := newMockServerConnection(t)
381381 sends := make(chan wire.Frame, 64)
382382 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {
383383 sends <- f
@@ -666,7 +666,7 @@ func TestHandleHelloDefersFreshOutputUntilWelcome(t *testing.T) {
666666 tr := newWelcomeBlockingTransport(t)
667667 done := make(chan struct{})
668668 go func() {
669- d.handleHello(tr.tr, mustHello(protocol.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))
669+ d.handleHelloFrame(tr.tr, mustHello(protocol.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))
670670 close(done)
671671 }()
672672
M internal/usecase/daemon/graphics_output.go +6 -7
@@ -15,7 +15,6 @@ import (
1515 renderer "github.com/bnema/vev-vt"
1616 "github.com/bnema/vev-vt/graphics"
1717 "github.com/bnema/vev/internal/domain"
18- "github.com/bnema/vev/internal/ports"
1918 "github.com/bnema/vev/internal/protocol"
2019 "github.com/bnema/vev/internal/usecase/layout"
2120 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -1008,23 +1007,23 @@ func (d *Daemon) cleanupAttachmentOutput(ac *attachedClient) error {
10081007 return errors.New("client transport is nil")
10091008 }
10101009 ac.output.lockView()
1011- frame, frameErr := ac.output.sideEffectLocked(prepared.data, ac.echoAck.Load())
1012- if frameErr != nil {
1010+ output, outputErr := ac.output.sideEffectLocked(prepared.data, ac.echoAck.Load())
1011+ if outputErr != nil {
10131012 ac.output.unlockView()
10141013 prepared.abort()
10151014 d.retireGraphicsOutput(ac, state)
1016- return frameErr
1015+ return outputErr
10171016 }
10181017 prepared.markSendAttempted()
10191018 var sendErr error
1020- if owned, ok := expected.transport.(ports.OwnedSynchronousTransport); ok {
1021- sendErr = owned.SendSynchronous(frame)
1019+ if expected.transport.Capabilities().OwnedSynchronousSend {
1020+ sendErr = expected.transport.SendOutputSynchronous(output)
10221021 } else {
10231022 _, sendErr = d.boundedSendWith(expected.transport, func() error {
10241023 if !ac.transportSnapshotCurrent(expected) {
10251024 return errTransportReplaced
10261025 }
1027- return expected.transport.Send(frame)
1026+ return expected.transport.SendOutput(output)
10281027 })
10291028 if errors.Is(sendErr, errSendTimedOut) {
10301029 _ = ac.closeCapturedTransport(expected.transport)
M internal/usecase/daemon/handshake.go +5 -5
@@ -52,7 +52,7 @@ func (d *Daemon) newHandshakeContext(parent context.Context) (context.Context, <
5252
5353 // watchHandshakeTransport closes a transport when the handshake context ends.
5454 // Transport.Close is required to interrupt blocked Send and Recv operations.
55-func watchHandshakeTransport(ctx context.Context, transport ports.Transport) func() {
55+func watchHandshakeTransport(ctx context.Context, transport ports.ServerConnection) func() {
5656 stop := make(chan struct{})
5757 done := make(chan struct{})
5858 go func() {
@@ -74,8 +74,8 @@ func watchHandshakeTransport(ctx context.Context, transport ports.Transport) fun
7474
7575 // boundedHandshakeOperation waits for one transport operation while the shared
7676 // handshake context remains live. Closing the transport is the interruption
77-// mechanism because ports.Transport methods do not accept contexts.
78-func boundedHandshakeOperation(ctx context.Context, transport ports.Transport, operation func() error) error {
77+// mechanism because ServerConnection methods do not accept contexts.
78+func boundedHandshakeOperation(ctx context.Context, transport ports.ServerConnection, operation func() error) error {
7979 _, err := boundedHandshakeOperationTracked(ctx, transport, operation)
8080 return err
8181 }
@@ -84,7 +84,7 @@ func boundedHandshakeOperation(ctx context.Context, transport ports.Transport, o
8484 // operation worker. Callers that own an effect gate must wait for this signal
8585 // before releasing the gate after cancellation; the transport operation itself
8686 // may still be unwinding after Close interrupts it.
87-func boundedHandshakeOperationTracked(ctx context.Context, transport ports.Transport, operation func() error) (<-chan struct{}, error) {
87+func boundedHandshakeOperationTracked(ctx context.Context, transport ports.ServerConnection, operation func() error) (<-chan struct{}, error) {
8888 done := make(chan struct{})
8989 if err := ctx.Err(); err != nil {
9090 close(done)
@@ -129,7 +129,7 @@ func handshakeContextError(parent context.Context, timedOut <-chan struct{}, fal
129129 // failHandshakeAttachment synchronously retires the exact connection admitted
130130 // by route. A fresh route-owned session is purged only after its attachment is
131131 // removed; existing sessions and their unrelated attachments are preserved.
132-func (d *Daemon) failHandshakeAttachment(sess *session, ac *attachedClient, tr ports.Transport, welcomed bool) {
132+func (d *Daemon) failHandshakeAttachment(sess *session, ac *attachedClient, tr ports.ServerConnection, welcomed bool) {
133133 if sess == nil || ac == nil {
134134 if tr != nil {
135135 _ = tr.Close()
M internal/usecase/daemon/handshake_test.go +6 -6
@@ -128,7 +128,7 @@ func TestHandshakeFirstPaintCancellationDoesNotRacePaintResult(t *testing.T) {
128128 tr := newHandshakeBlockingTransport(false)
129129 handshakeDone := make(chan struct{})
130130 go func() {
131- d.handleHelloWithContext(ctx, make(chan struct{}), func() {}, func() {}, tr,
131+ d.handleHelloFrameWithContext(ctx, make(chan struct{}), func() {}, func() {}, tr,
132132 mustHello(protocol.IntentNew, "paint-cancel", domain.Size{Cols: 80, Rows: 24}))
133133 close(handshakeDone)
134134 }()
@@ -183,7 +183,7 @@ func TestFailedResumeHandshakeRestoresParkedCredential(t *testing.T) {
183183 resumeTransport := newHandshakeBlockingTransport(true)
184184 done := make(chan struct{})
185185 go func() {
186- d.handleHello(resumeTransport, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, sess.name, token))})
186+ d.handleHelloFrame(resumeTransport, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(helloResumeCapable(protocol.IntentResume, sess.name, token))})
187187 close(done)
188188 }()
189189
@@ -211,7 +211,7 @@ func TestHandshakeTimeoutClosesBlockedWelcomeSend(t *testing.T) {
211211 tr := newHandshakeBlockingTransport(true)
212212 done := make(chan struct{})
213213 go func() {
214- d.handleHello(tr, mustHello(protocol.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))
214+ d.handleHelloFrame(tr, mustHello(protocol.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))
215215 close(done)
216216 }()
217217
@@ -247,7 +247,7 @@ func TestHandshakeTimeoutCancelsRouteRestoreWait(t *testing.T) {
247247 tr := newHandshakeBlockingTransport(false)
248248 done := make(chan struct{})
249249 go func() {
250- d.handleHello(tr, mustHello(protocol.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))
250+ d.handleHelloFrame(tr, mustHello(protocol.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))
251251 close(done)
252252 }()
253253
@@ -275,7 +275,7 @@ func TestHandshakeTimeoutRemovesRestoredEmptySession(t *testing.T) {
275275 tr := newHandshakeBlockingTransport(true)
276276 done := make(chan struct{})
277277 go func() {
278- d.handleHello(tr, mustHello(protocol.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))
278+ d.handleHelloFrame(tr, mustHello(protocol.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))
279279 close(done)
280280 }()
281281
@@ -305,7 +305,7 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
305305 tr := newHandshakeBlockingTransport(true)
306306 done := make(chan struct{})
307307 go func() {
308- d.handleHello(tr, mustHello(protocol.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))
308+ d.handleHelloFrame(tr, mustHello(protocol.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))
309309 close(done)
310310 }()
311311
M internal/usecase/daemon/notify_test.go +2 -3
@@ -15,7 +15,6 @@ import (
1515 vt "github.com/bnema/vev-vt"
1616 "github.com/bnema/vev/internal/domain"
1717 "github.com/bnema/vev/internal/ports"
18- portsmocks "github.com/bnema/vev/internal/ports/mocks"
1918 "github.com/bnema/vev/internal/protocol"
2019 "github.com/bnema/vev/internal/protocol/wire"
2120 )
@@ -55,8 +54,8 @@ func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing.
5554 func TestMalformedClientNoticeIsIgnored(t *testing.T) {
5655 d, sess, ac, _ := newNoticeFixture(t, &noticeClock{})
5756 d.attachCoordinator(sess, nil, ac, true)
58- tr, ok := ac.tr.(*portsmocks.MockTransport)
59- require.True(t, ok, "attached client transport must be a MockTransport")
57+ tr, ok := ac.tr.(*mockServerConnection)
58+ require.True(t, ok, "attached client connection must use the test wrapper")
6059 frames := []wire.Frame{
6160 {Type: wire.MsgClientNotice, Payload: []byte{0xff}},
6261 {Type: wire.MsgDetach, Payload: wire.MarshalDetach(protocol.Detach{})},
M internal/usecase/daemon/parked_route.go +2 -19
@@ -9,7 +9,6 @@ import (
99 "github.com/bnema/vev/internal/domain"
1010 "github.com/bnema/vev/internal/ports"
1111 "github.com/bnema/vev/internal/protocol"
12- "github.com/bnema/vev/internal/protocol/wire"
1312 )
1413
1514 const parkedRouteLeaseTTL = 15 * time.Minute
@@ -238,33 +237,17 @@ func (ac *attachedClient) consumePublishedParkedRoute(previous, published attach
238237 return 0
239238 }
240239
241-func parkedRouteResponseFrame(response protocol.ParkedRouteResponse) (wire.Frame, error) {
242- payload := wire.MarshalParkedRouteResponse(response)
243- if payload == nil {
244- return wire.Frame{}, errAttachmentTransition
245- }
246- return wire.Frame{Type: wire.MsgParkedRouteResponse, Payload: payload}, nil
247-}
248-
249240 func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response protocol.ParkedRouteResponse) error {
250- frame, err := parkedRouteResponseFrame(response)
251- if err != nil {
252- return err
253- }
254- return effect.sendControl(frame)
241+ return effect.sendControl(response)
255242 }
256243
257244 // sendParkedRouteResponseLocked preserves response -> full-output ordering.
258245 // Caller holds ac.sendMu and keeps parkedRouteOutput set through this send.
259246 func (d *Daemon) sendParkedRouteResponseLocked(ac *attachedClient, expected transportSnapshot, response protocol.ParkedRouteResponse) error {
260- frame, err := parkedRouteResponseFrame(response)
261- if err != nil {
262- return err
263- }
264247 if ac == nil || !ac.transportSnapshotCurrent(expected) || expected.transport == nil {
265248 return errAttachmentTransition
266249 }
267- return expected.transport.Send(frame)
250+ return expected.transport.SendServer(response)
268251 }
269252
270253 func (ac *attachedClient) releaseParkedOutputLocked() {
M internal/usecase/daemon/picker.go +9 -8
@@ -11,7 +11,6 @@ import (
1111 "github.com/bnema/vev/internal/domain"
1212 "github.com/bnema/vev/internal/ports"
1313 "github.com/bnema/vev/internal/protocol"
14- "github.com/bnema/vev/internal/protocol/wire"
1514 "github.com/bnema/vev/internal/usecase/layout"
1615 "github.com/bnema/vev/internal/usecase/picker"
1716 "github.com/bnema/vev/internal/usecase/ui"
@@ -924,14 +923,17 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
924923 // Explicit tab rows already take the direct transition path above, while
925924 // stopped sessions have no active target session and retain the fallback.
926925 samePeerEligible := guard.allowSamePeer && targetSess != nil && target.TabIndex <= 0
927- payload := wire.MarshalAttachTarget(protocol.AttachTarget{
926+ if exactTarget == nil {
927+ return errAttachmentTransition
928+ }
929+ handoff := protocol.AttachTarget{
928930 Session: sessionName,
929931 Intent: protocol.IntentAttach,
930932 ExactTarget: exactTarget,
931933 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
932934 SamePeer: samePeerEligible,
933- })
934- if payload == nil || exactTarget == nil {
935+ }
936+ if protocol.ValidateAttachTarget(handoff) != nil {
935937 return errAttachmentTransition
936938 }
937939 // A close-and-dial handoff leaves this daemon's Kitty namespace. A
@@ -944,7 +946,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
944946 } else {
945947 effect.ac.offerSamePeerTarget(*exactTarget)
946948 }
947- if err := effect.sendControl(wire.Frame{Type: wire.MsgAttachTarget, Payload: payload}); err != nil {
949+ if err := effect.sendControl(handoff); err != nil {
948950 if samePeerEligible {
949951 effect.ac.clearSamePeerOffer()
950952 }
@@ -990,11 +992,10 @@ func (d *Daemon) sendRemoteAttachTargetForAttachment(effect *attachmentEffect, t
990992 RemoteTarget: &remoteTarget,
991993 EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
992994 }
993- payload := wire.MarshalAttachTarget(handoff)
994- if payload == nil {
995+ if protocol.ValidateAttachTarget(handoff) != nil {
995996 return failUnavailable()
996997 }
997- if err := effect.sendControl(wire.Frame{Type: wire.MsgAttachTarget, Payload: payload}); err != nil {
998+ if err := effect.sendControl(handoff); err != nil {
998999 return domain.UserErr(domain.NoticeSessionUnavailable, "couldn't attach to remote session", err)
9991000 }
10001001 if guard.closePicker {
M internal/usecase/daemon/picker_test.go +2 -2
@@ -1011,7 +1011,7 @@ func TestPickerSessionSwitchPublishesBeforeInFlightPaintSendCompletes(t *testing
10111011 d := newTestDaemon(t, nil, stubClock{})
10121012 enteredSend := make(chan struct{})
10131013 releaseSend := make(chan struct{})
1014- tr := portsmocks.NewMockTransport(t)
1014+ tr := newMockServerConnection(t)
10151015 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(wire.Frame) error {
10161016 close(enteredSend)
10171017 <-releaseSend
@@ -1408,7 +1408,7 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
14081408 })).Return(floating, nil).Once()
14091409 d := newTestDaemon(t, f, stubClock{})
14101410 d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: protocol.SessionDown}
1411- tr := portsmocks.NewMockTransport(t)
1411+ tr := newMockServerConnection(t)
14121412 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
14131413 tr.EXPECT().Close().Return(nil).Maybe()
14141414 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
M internal/usecase/daemon/remote_attach.go +7 -15
@@ -7,14 +7,13 @@ import (
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
99 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
1110 )
1211
1312 // routeRemoteTargetWithContext is the exact-target branch for picker handoffs.
1413 // It is deliberately separate from legacy name routing: a lifecycle mismatch,
1514 // missing tab, broken record, or stopped-record race is a no-such-target error,
1615 // never an invitation to attach a same-name replacement.
17-func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
16+func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h protocol.Hello, tr ports.ServerConnection) (*session, *attachedClient, error) {
1817 if h.RemoteTarget == nil {
1918 return nil, nil, errors.New("daemon: missing remote target")
2019 }
@@ -120,12 +119,7 @@ func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, act
120119 effect.ac.clearParkedRoute()
121120 }
122121 }
123- payload := wire.MarshalNavigationDirective(directive)
124- if payload == nil {
125- rollback()
126- return errAttachmentTransition
127- }
128- if err := effect.sendControl(wire.Frame{Type: wire.MsgNavigationAction, Payload: payload}); err != nil {
122+ if err := effect.sendControl(directive); err != nil {
129123 rollback()
130124 return err
131125 }
@@ -133,11 +127,10 @@ func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, act
133127 }
134128
135129 func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action protocol.RouteNavigationAction) error {
136- payload, err := wire.MarshalRouteNavigationAction(action)
137- if err != nil {
130+ if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {
138131 return errAttachmentTransition
139132 }
140- return effect.sendControl(wire.Frame{Type: wire.MsgNavigateRecentRoute, Payload: payload})
133+ return effect.sendControl(action)
141134 }
142135
143136 func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffect) error {
@@ -150,14 +143,13 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffec
150143 Ephemeral: effect.sess.ephemeral,
151144 }
152145 effect.sess.mu.Unlock()
153- payload, err := wire.MarshalCommittedRouteIdentity(identity)
154- if err != nil {
146+ if err := identity.Validate(); err != nil {
155147 return errAttachmentTransition
156148 }
157- return effect.sendControl(wire.Frame{Type: wire.MsgCommittedRouteIdentity, Payload: payload})
149+ return effect.sendControl(identity)
158150 }
159151
160-func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello, routeCreated, purge bool) (*attachedClient, error) {
152+func (d *Daemon) finishRouteAttach(sess *session, tr ports.ServerConnection, sz domain.Size, h protocol.Hello, routeCreated, purge bool) (*attachedClient, error) {
161153 ac, err := d.finishAttach(sess, tr, sz, h)
162154 if err != nil && routeCreated {
163155 if cleanupErr := d.killSessionIfEmpty(sess, protocol.ReasonSessionKilled, purge); cleanupErr != nil {
M internal/usecase/daemon/remote_picker_test.go +3 -4
@@ -16,7 +16,6 @@ import (
1616
1717 "github.com/bnema/vev/internal/domain"
1818 "github.com/bnema/vev/internal/ports"
19- portsmocks "github.com/bnema/vev/internal/ports/mocks"
2019 "github.com/bnema/vev/internal/protocol"
2120 "github.com/bnema/vev/internal/protocol/wire"
2221 "github.com/bnema/vev/internal/usecase/picker"
@@ -890,7 +889,7 @@ func TestRemotePickerHandoffSendFailureKeepsPickerOpen(t *testing.T) {
890889 d.remoteCatalog.status["arch"] = remoteHostFresh
891890 d.remoteCatalog.mu.Unlock()
892891 cause := errors.New("remote attach send failed")
893- tr := portsmocks.NewMockTransport(t)
892+ tr := newMockServerConnection(t)
894893 tr.EXPECT().Send(mock.Anything).Return(cause)
895894 sess, ac, _ := addRemoteRefreshPickerOwner(t, d, "local", tr)
896895 ac.overlays.pickerMu.Lock()
@@ -921,9 +920,9 @@ func TestRemotePickerHandoffSendFailureKeepsPickerOpen(t *testing.T) {
921920 require.Same(t, sess, ac.currentAttachmentSession())
922921 }
923922
924-func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID, transports ...ports.Transport) (*session, *attachedClient, chan wire.Frame) {
923+func addRemoteRefreshPickerOwner(t *testing.T, d *Daemon, id domain.SessionID, transports ...ports.ServerConnection) (*session, *attachedClient, chan wire.Frame) {
925924 t.Helper()
926- var tr ports.Transport
925+ var tr ports.ServerConnection
927926 var sends chan wire.Frame
928927 if len(transports) != 0 {
929928 tr = transports[0]
M internal/usecase/daemon/remote_preview.go +5 -11
@@ -7,15 +7,10 @@ import (
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
99 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
1110 )
1211
13-func (d *Daemon) handleRemotePreview(tr ports.Transport, frame wire.Frame) error {
12+func (d *Daemon) handleRemotePreview(tr ports.ServerConnection, request protocol.RemotePreviewRequest) error {
1413 defer func() { _ = tr.Close() }()
15- request, err := wire.UnmarshalRemotePreviewRequest(frame.Payload)
16- if err != nil {
17- return d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
18- }
1914 preview, err := d.captureRemotePreview(request)
2015 if err != nil {
2116 status := protocol.RemotePreviewUnavailable
@@ -27,12 +22,11 @@ func (d *Daemon) handleRemotePreview(tr ports.Transport, frame wire.Frame) error
2722 return d.sendRemotePreview(tr, preview)
2823 }
2924
30-func (d *Daemon) sendRemotePreview(tr ports.Transport, preview protocol.RemotePreview) error {
31- payload := wire.MarshalRemotePreview(preview)
32- if payload == nil {
33- payload = wire.MarshalRemotePreview(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
25+func (d *Daemon) sendRemotePreview(tr ports.ServerConnection, preview protocol.RemotePreview) error {
26+ if protocol.ValidateRemotePreview(preview) != nil {
27+ preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed}
3428 }
35- return d.boundedControlSend(tr, wire.Frame{Type: wire.MsgRemotePreviewResponse, Payload: payload})
29+ return d.boundedControlSend(tr, preview)
3630 }
3731
3832 var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")
M internal/usecase/daemon/remote_route_test.go +1 -1
@@ -183,7 +183,7 @@ func TestFailedHandshakeCleanupPreservesConcurrentAttachment(t *testing.T) {
183183 d.clientGone(sess, winner(), winnerTransport, false)
184184 }
185185
186-func attachWhenRouteCleanupSnapshots(t *testing.T, d *Daemon, sess *session) (func() *attachedClient, ports.Transport) {
186+func attachWhenRouteCleanupSnapshots(t *testing.T, d *Daemon, sess *session) (func() *attachedClient, ports.ServerConnection) {
187187 t.Helper()
188188 winnerTransport, _ := newCapturingTransport(t)
189189 var winner *attachedClient
M internal/usecase/daemon/render_damage_receipt_test.go +3 -3
@@ -108,7 +108,7 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T
108108 for _, tt := range []struct {
109109 name string
110110 fail func(*composedRenderFrame, *attachedClient)
111- restore func(*session, *attachedClient, ports.Transport)
111+ restore func(*session, *attachedClient, ports.ServerConnection)
112112 }{
113113 {
114114 name: "prepare failure",
@@ -116,12 +116,12 @@ func TestRenderDamageReceiptRetainsRealVTDamageAcrossFailedEmission(t *testing.T
116116 // An invalid frame makes the real output.prepare reject after capture.
117117 c.frame = renderer.Frame{Width: 1}
118118 },
119- restore: func(_ *session, _ *attachedClient, _ ports.Transport) {},
119+ restore: func(_ *session, _ *attachedClient, _ ports.ServerConnection) {},
120120 },
121121 {
122122 name: "send failure",
123123 fail: func(_ *composedRenderFrame, ac *attachedClient) { ac.replaceTransport(cacheFailTransport{}) },
124- restore: func(sess *session, ac *attachedClient, healthy ports.Transport) {
124+ restore: func(sess *session, ac *attachedClient, healthy ports.ServerConnection) {
125125 sess.mu.Lock()
126126 sess.registerAttachmentLocked(ac)
127127 sess.mu.Unlock()
M internal/usecase/daemon/render_pipeline.go +9 -13
@@ -7,7 +7,6 @@ import (
77 "github.com/bnema/vev/internal/domain"
88 "github.com/bnema/vev/internal/ports"
99 "github.com/bnema/vev/internal/protocol"
10- "github.com/bnema/vev/internal/protocol/wire"
1110 "github.com/bnema/vev/internal/usecase/layout"
1211 "github.com/bnema/vev/internal/usecase/notices"
1312 "github.com/bnema/vev/internal/usecase/palette"
@@ -475,7 +474,7 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
475474 d.invalidateRender(entry, ac, true, "render_pipeline.go:prepare-failed")
476475 return true
477476 }
478- var sendTr ports.Transport
477+ var sendTr ports.ServerConnection
479478 var sendErr error
480479 suppressedGraphics := state.suppressedGraphics && !ac.terminalCapabilities.SupportsKittyGraphics()
481480 if len(data) > 0 {
@@ -485,9 +484,9 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
485484 if sendTr == nil {
486485 sendErr = errors.New("client transport is nil")
487486 } else {
488- send := sendTr.Send
489- if async, ok := sendTr.(ports.AsyncTransport); ok {
490- send = async.SendAsync
487+ send := sendTr.SendOutput
488+ if sendTr.Capabilities().AsyncSend {
489+ send = sendTr.SendOutputAsync
491490 }
492491 interruptible := false
493492 if marks.attachmentEffect != nil {
@@ -540,9 +539,8 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
540539 }
541540
542541 if position.ActiveTabID != "" && position != ac.output.lastRoutePosition {
543- payload, positionMarshalErr := wire.MarshalRoutePosition(position)
544- if positionMarshalErr != nil {
545- d.log.Error("marshal route position", "err", positionMarshalErr)
542+ if positionValidateErr := position.Validate(); positionValidateErr != nil {
543+ d.log.Error("validate route position", "err", positionValidateErr)
546544 } else {
547545 sendTransport := ac.transportSnapshot()
548546 sendTr = sendTransport.transport
@@ -556,12 +554,10 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
556554 if sendErr == nil {
557555 if sendTr == nil {
558556 sendErr = errors.New("client transport is nil")
557+ } else if sendTr.Capabilities().AsyncSend {
558+ sendErr = sendTr.SendServerAsync(position)
559559 } else {
560- send := sendTr.Send
561- if async, ok := sendTr.(ports.AsyncTransport); ok {
562- send = async.SendAsync
563- }
564- sendErr = send(wire.Frame{Type: wire.MsgRoutePosition, Payload: payload})
560+ sendErr = sendTr.SendServer(position)
565561 }
566562 }
567563 if marks.attachmentEffect != nil && interruptible {
M internal/usecase/daemon/render_pipeline_test.go +2 -2
@@ -333,10 +333,10 @@ func TestComposeEmitExactReplayTiledFloatingBarsOverlayAndCursor(t *testing.T) {
333333 prepared, err := stream.prepareFrame(nil, &state, composed.frame, composed.damage, composed.reset, composed.cursor)
334334 require.NoError(t, err)
335335 var outputFrame wire.Frame
336- require.NoError(t, prepared.send(0, func(frame wire.Frame) error {
336+ require.NoError(t, prepared.send(0, outputFrameSender(func(frame wire.Frame) error {
337337 outputFrame = frame
338338 return nil
339- }))
339+ })))
340340 output, err := wire.UnmarshalOutput(outputFrame.Payload)
341341 require.NoError(t, err)
342342 terminalBytes := output.Data
M internal/usecase/daemon/render_test.go +3 -3
@@ -676,7 +676,7 @@ func TestResizePreservesLiveContentAndEvictsScrollback(t *testing.T) {
676676 }
677677 win.focusedPane().screen.Row = 3
678678
679- tr := portsmocks.NewMockTransport(t)
679+ tr := newMockServerConnection(t)
680680 tr.EXPECT().Close().Return(nil).Maybe()
681681 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
682682
@@ -931,7 +931,7 @@ func TestResizeOrdersPTYBeforeScreen(t *testing.T) {
931931 win.focusedPane().pty = p
932932
933933 var gotOutput atomic.Bool
934- tr := portsmocks.NewMockTransport(t)
934+ tr := newMockServerConnection(t)
935935 tr.EXPECT().Close().Return(nil).Maybe()
936936 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Frame) error {
937937 if f.Type == wire.MsgOutput {
@@ -963,7 +963,7 @@ func TestSendErrorKeepsEphemeralHeadless(t *testing.T) {
963963 p := portsmocks.NewMockPTY(t)
964964 p.EXPECT().Close().Return(nil).Maybe()
965965
966- tr := portsmocks.NewMockTransport(t)
966+ tr := newMockServerConnection(t)
967967 tr.EXPECT().Send(mock.Anything).Return(io.ErrClosedPipe).Maybe()
968968 tr.EXPECT().Close().Return(nil).Maybe()
969969
M internal/usecase/daemon/resume.go +3 -3
@@ -480,7 +480,7 @@ func (d *Daemon) finishParkedAttachmentRetirements(retirements []parkedAttachmen
480480 // to the named session's active attachment because transport teardown has not
481481 // parked it yet. Only the exact live owner (same session, token, and client ID)
482482 // is accepted; arbitrary unknown tokens stay fail-closed.
483-func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
483+func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.ServerConnection, sz domain.Size) (*session, *attachedClient, bool, error) {
484484 d.mu.Lock()
485485 if d.closing {
486486 d.mu.Unlock()
@@ -583,7 +583,7 @@ func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.Transport, sz d
583583 // registry lock, preserving the global sendMu > Daemon.mu ordering. The parked
584584 // entry is revalidated after both locks are held because it may expire between
585585 // the initial lookup and lock acquisition.
586-func (d *Daemon) resumeParked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
586+func (d *Daemon) resumeParked(h protocol.Hello, tr ports.ServerConnection, sz domain.Size) (*session, *attachedClient, bool, error) {
587587 d.mu.Lock()
588588 parked := d.parked[h.ResumeToken]
589589 d.mu.Unlock()
@@ -632,7 +632,7 @@ func (d *Daemon) resumeParked(h protocol.Hello, tr ports.Transport, sz domain.Si
632632
633633 // resumeParkedLocked completes a validated resume. Caller holds both ac.sendMu
634634 // and d.mu in that order.
635-func (d *Daemon) resumeParkedLocked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
635+func (d *Daemon) resumeParkedLocked(h protocol.Hello, tr ports.ServerConnection, sz domain.Size) (*session, *attachedClient, bool, error) {
636636 parked := d.parked[h.ResumeToken]
637637 if parked == nil || h.ResumeToken == 0 {
638638 return nil, nil, false, nil
M internal/usecase/daemon/resume_test.go +3 -3
@@ -254,14 +254,14 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
254254 expected := ac.transportSnapshot()
255255 orphanDone := make(chan struct{})
256256 type boundedResult struct {
257- transport ports.Transport
257+ transport ports.ServerConnection
258258 err error
259259 }
260260 result := make(chan boundedResult, 1)
261261 go func() {
262262 transport, sendErr := d.boundedSendWithTimeout(time.Second, expected.transport, func() error {
263263 defer close(orphanDone)
264- return ac.sendExpectedTransport(expected, frameDetached(protocol.ReasonDetach))
264+ return ac.sendExpectedTransport(expected, serverDetached(protocol.ReasonDetach))
265265 })
266266 result <- boundedResult{transport: transport, err: sendErr}
267267 }()
@@ -327,7 +327,7 @@ func TestHandleHelloResumeDefersFreshOutputUntilWelcome(t *testing.T) {
327327 done := make(chan struct{})
328328 resumeHello := helloResumeCapable(protocol.IntentResume, sess.name, token)
329329 go func() {
330- d.handleHello(tr.tr, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(resumeHello)})
330+ d.handleHelloFrame(tr.tr, wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(resumeHello)})
331331 close(done)
332332 }()
333333
M internal/usecase/daemon/same_peer_switch.go +3 -4
@@ -3,7 +3,6 @@ package daemon
33 import (
44 "github.com/bnema/vev/internal/domain"
55 "github.com/bnema/vev/internal/protocol"
6- "github.com/bnema/vev/internal/protocol/wire"
76 )
87
98 func (ac *attachedClient) offerSamePeerTarget(target protocol.ExactSessionTarget) {
@@ -122,11 +121,11 @@ func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID u
122121 }
123122 defer sender.End()
124123 }
125- payload, err := wire.MarshalSamePeerSwitchFailure(protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code})
126- if err != nil {
124+ failure := protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code}
125+ if failure.Validate() != nil {
127126 return
128127 }
129- if err := sender.sendControl(wire.Frame{Type: wire.MsgSamePeerSwitchFailure, Payload: payload}); err != nil {
128+ if err := sender.sendControl(failure); err != nil {
130129 d.detachOnAttachmentSendError(sender.capability(), sender.transport.transport)
131130 }
132131 }
M internal/usecase/daemon/session_test.go +15 -15
@@ -398,7 +398,7 @@ func TestServeReturnsWhenLastSessionExits(t *testing.T) {
398398 p, releasePTY := newBlockingPTY(t)
399399 tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
400400
401- l := portsmocks.NewMockListener(t)
401+ l := newMockServerListener(t)
402402 connCh := make(chan ports.Transport, 1)
403403 connCh <- tr
404404 closed := make(chan struct{})
@@ -438,7 +438,7 @@ func TestServeGracefulShutdownOnContextCancel(t *testing.T) {
438438 defer releasePTY()
439439 tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "long", domain.Size{Cols: 80, Rows: 24}))
440440
441- l := portsmocks.NewMockListener(t)
441+ l := newMockServerListener(t)
442442 connCh := make(chan ports.Transport, 1)
443443 connCh <- tr
444444 closed := make(chan struct{})
@@ -573,7 +573,7 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
573573 func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) {
574574 p, _ := newBlockingPTY(t) // Close unblocks the parked reader
575575
576- tr := portsmocks.NewMockTransport(t)
576+ tr := newMockServerConnection(t)
577577 sends := make(chan wire.Frame, 64)
578578 recvCh := make(chan wire.Frame, 1)
579579 recvCh <- mustHello(protocol.IntentNew, "wedge", domain.Size{Cols: 80, Rows: 24})
@@ -600,7 +600,7 @@ func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) {
600600 }).Maybe()
601601 tr.EXPECT().Close().RunAndReturn(func() error { closeConn(); return nil }).Maybe()
602602
603- l := portsmocks.NewMockListener(t)
603+ l := newMockServerListener(t)
604604 connCh := make(chan ports.Transport, 1)
605605 connCh <- tr
606606 lnClosed := make(chan struct{})
@@ -1021,7 +1021,7 @@ func TestAttachRestoresPersistedTabNames(t *testing.T) {
10211021 require.NoError(t, err)
10221022 require.True(t, ok)
10231023 d.inactive["work"] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
1024- tr := portsmocks.NewMockTransport(t)
1024+ tr := newMockServerConnection(t)
10251025 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
10261026 tr.EXPECT().Close().Return(nil).Maybe()
10271027
@@ -1195,7 +1195,7 @@ func TestAttachResumesStoppedSessionFromStoredCwd(t *testing.T) {
11951195 WithStore(t, store)(d)
11961196 cwd := t.TempDir()
11971197 d.inactive["work"] = inactiveSession{name: "work", cwd: cwd, createdAt: 1, state: protocol.SessionDown}
1198- tr := portsmocks.NewMockTransport(t)
1198+ tr := newMockServerConnection(t)
11991199 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
12001200 tr.EXPECT().Close().Return(nil).Maybe()
12011201
@@ -1218,7 +1218,7 @@ func TestAttachStoppedMissingCwdFallsBackToHome(t *testing.T) {
12181218 d := newTestDaemon(t, newFactory(t, p), stubClock{})
12191219 WithStore(t, store)(d)
12201220 d.inactive["work"] = inactiveSession{name: "work", cwd: "/definitely/missing/vev", createdAt: 1, state: protocol.SessionDown}
1221- tr := portsmocks.NewMockTransport(t)
1221+ tr := newMockServerConnection(t)
12221222 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
12231223 tr.EXPECT().Close().Return(nil).Maybe()
12241224
@@ -1299,14 +1299,14 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) {
12991299 func TestIntentNewStoppedNameRejected(t *testing.T) {
13001300 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
13011301 d.inactive["taken"] = inactiveSession{name: "taken", cwd: "/tmp", createdAt: 1}
1302- tr := portsmocks.NewMockTransport(t)
1302+ tr := newMockServerConnection(t)
13031303 _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "taken", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
13041304 require.ErrorContains(t, err, "name already in use")
13051305 }
13061306
13071307 func TestIntentNewUnsafeNameRejected(t *testing.T) {
13081308 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
1309- tr := portsmocks.NewMockTransport(t)
1309+ tr := newMockServerConnection(t)
13101310 _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "my work", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
13111311 require.ErrorContains(t, err, domain.ErrInvalidSessionName.Error())
13121312 }
@@ -1446,7 +1446,7 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
14461446
14471447 d := newTestDaemon(t, f, stubClock{})
14481448 d.baseEnv = []string{"KEEP=1", "COLORTERM=old"}
1449- tr := portsmocks.NewMockTransport(t)
1449+ tr := newMockServerConnection(t)
14501450 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
14511451 tr.EXPECT().Close().Return(nil).Maybe()
14521452 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
@@ -1488,10 +1488,10 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
14881488 expectFloatingPrewarmOpen(f, normalSize, floating)
14891489
14901490 d := newTestDaemon(t, f, stubClock{})
1491- tr1 := portsmocks.NewMockTransport(t)
1491+ tr1 := newMockServerConnection(t)
14921492 tr1.EXPECT().Send(mock.Anything).Return(nil).Maybe()
14931493 tr1.EXPECT().Close().Return(nil).Maybe()
1494- tr2 := portsmocks.NewMockTransport(t)
1494+ tr2 := newMockServerConnection(t)
14951495 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()
14961496 tr2.EXPECT().Close().Return(nil).Maybe()
14971497 sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1)
@@ -1532,7 +1532,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) {
15321532 floating := newQuietPTY()
15331533 expectFloatingPrewarmOpen(f, normalSize, floating)
15341534 d := newTestDaemon(t, f, stubClock{})
1535- tr := portsmocks.NewMockTransport(t)
1535+ tr := newMockServerConnection(t)
15361536 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
15371537 tr.EXPECT().Close().Return(nil).Maybe()
15381538 sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
@@ -1601,10 +1601,10 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) {
16011601 floating := newQuietPTY()
16021602 expectFloatingPrewarmOpen(f, normalSize, floating)
16031603 d := newTestDaemon(t, f, stubClock{})
1604- tr1 := portsmocks.NewMockTransport(t)
1604+ tr1 := newMockServerConnection(t)
16051605 tr1.EXPECT().Send(mock.Anything).Return(nil).Maybe()
16061606 tr1.EXPECT().Close().Return(nil).Maybe()
1607- tr2 := portsmocks.NewMockTransport(t)
1607+ tr2 := newMockServerConnection(t)
16081608 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()
16091609 tr2.EXPECT().Close().Return(nil).Maybe()
16101610
M internal/usecase/daemon/snapshot_scheduler_test.go +2 -2
@@ -613,9 +613,9 @@ func (c *serveShutdownClock) nextFinalTimer(t *testing.T) *snapshotDeadlineTimer
613613 }
614614 }
615615
616-func serveSnapshotListener(t *testing.T, tr ports.Transport) *portsmocks.MockListener {
616+func serveSnapshotListener(t *testing.T, tr ports.Transport) *mockServerListener {
617617 t.Helper()
618- l := portsmocks.NewMockListener(t)
618+ l := newMockServerListener(t)
619619 connections := make(chan ports.Transport, 1)
620620 connections <- tr
621621 closed := make(chan struct{})
A internal/usecase/daemon/typed_connection_test.go +1077 -0
@@ -0,0 +1,1077 @@
1+package daemon
2+
3+import (
4+ "context"
5+ "errors"
6+
7+ "github.com/stretchr/testify/mock"
8+
9+ "github.com/bnema/vev/internal/ports"
10+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
11+ "github.com/bnema/vev/internal/protocol"
12+ "github.com/bnema/vev/internal/protocol/wire"
13+)
14+
15+type mockTestingT interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}
19+
20+type mockServerConnection struct{ *portsmocks.MockTransport }
21+
22+func newMockServerConnection(t mockTestingT) *mockServerConnection {
23+ return &mockServerConnection{MockTransport: portsmocks.NewMockTransport(t)}
24+}
25+
26+func (c *mockServerConnection) ReceiveClient() (protocol.ClientMessage, error) {
27+ return testReceiveClient(c.MockTransport)
28+}
29+func (c *mockServerConnection) SendServer(message protocol.ServerMessage) error {
30+ return testSendServer(c.MockTransport, message)
31+}
32+func (c *mockServerConnection) SendServerAsync(message protocol.ServerMessage) error {
33+ return testSendServerAsync(c.MockTransport, message)
34+}
35+func (c *mockServerConnection) SendServerSynchronous(message protocol.ServerMessage) error {
36+ return testSendServerSynchronous(c.MockTransport, message)
37+}
38+func (c *mockServerConnection) SendOutput(output protocol.Output) error {
39+ return testSendOutput(c.MockTransport, output)
40+}
41+func (c *mockServerConnection) SendOutputAsync(output protocol.Output) error {
42+ return testSendOutputAsync(c.MockTransport, output)
43+}
44+func (c *mockServerConnection) SendOutputSynchronous(output protocol.Output) error {
45+ return testSendOutputSynchronous(c.MockTransport, output)
46+}
47+func (c *mockServerConnection) Capabilities() protocol.ConnectionCapabilities {
48+ return testServerCapabilities(c.MockTransport)
49+}
50+func (c *mockServerConnection) LinkState() ports.LinkState { return testLinkState(c.MockTransport) }
51+func (c *mockServerConnection) LinkEvents() <-chan ports.LinkEvent {
52+ return testLinkEvents(c.MockTransport)
53+}
54+
55+type rawServerConnection struct{ raw ports.Transport }
56+
57+func (c *rawServerConnection) ReceiveClient() (protocol.ClientMessage, error) {
58+ return testReceiveClient(c.raw)
59+}
60+func (c *rawServerConnection) SendServer(message protocol.ServerMessage) error {
61+ return testSendServer(c.raw, message)
62+}
63+func (c *rawServerConnection) SendServerAsync(message protocol.ServerMessage) error {
64+ return testSendServerAsync(c.raw, message)
65+}
66+func (c *rawServerConnection) SendServerSynchronous(message protocol.ServerMessage) error {
67+ return testSendServerSynchronous(c.raw, message)
68+}
69+func (c *rawServerConnection) SendOutput(output protocol.Output) error {
70+ return testSendOutput(c.raw, output)
71+}
72+func (c *rawServerConnection) SendOutputAsync(output protocol.Output) error {
73+ return testSendOutputAsync(c.raw, output)
74+}
75+func (c *rawServerConnection) SendOutputSynchronous(output protocol.Output) error {
76+ return testSendOutputSynchronous(c.raw, output)
77+}
78+func (c *rawServerConnection) Capabilities() protocol.ConnectionCapabilities {
79+ return testServerCapabilities(c.raw)
80+}
81+func (c *rawServerConnection) LinkState() ports.LinkState { return testLinkState(c.raw) }
82+func (c *rawServerConnection) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(c.raw) }
83+func (c *rawServerConnection) Close() error { return c.raw.Close() }
84+
85+type mockServerListener struct{ *portsmocks.MockListener }
86+
87+func newMockServerListener(t mockTestingT) *mockServerListener {
88+ return &mockServerListener{MockListener: portsmocks.NewMockListener(t)}
89+}
90+
91+func (l *mockServerListener) Accept() (ports.ServerConnection, error) {
92+ raw, err := l.MockListener.Accept()
93+ if err != nil {
94+ return nil, err
95+ }
96+ return &rawServerConnection{raw: raw}, nil
97+}
98+
99+func testReceiveClient(raw ports.Transport) (protocol.ClientMessage, error) {
100+ frame, err := raw.Recv()
101+ if err != nil {
102+ return nil, err
103+ }
104+ message, err := testDecodeClientFrame(frame)
105+ if err == nil {
106+ return message, nil
107+ }
108+ failure := &protocol.DecodeFailure{Category: protocol.DecodeMalformed, Type: uint8(frame.Type), Err: err}
109+ switch frame.Type {
110+ case wire.MsgHello:
111+ failure.Kind = protocol.DecodeMessageHello
112+ failure.Version, _ = wire.PeekHelloVersion(frame.Payload)
113+ case wire.MsgCommand:
114+ failure.Kind = protocol.DecodeMessageCommand
115+ failure.Version, _ = wire.PeekCommandVersion(frame.Payload)
116+ case wire.MsgKill:
117+ failure.Kind = protocol.DecodeMessageKill
118+ case wire.MsgRemotePreviewRequest:
119+ failure.Kind = protocol.DecodeMessageRemotePreview
120+ }
121+ return nil, failure
122+}
123+
124+func testDecodeClientFrame(frame wire.Frame) (protocol.ClientMessage, error) {
125+ switch frame.Type {
126+ case wire.MsgHello:
127+ return wire.UnmarshalHello(frame.Payload)
128+ case wire.MsgInput:
129+ return wire.UnmarshalInput(frame.Payload)
130+ case wire.MsgResize:
131+ return wire.UnmarshalResize(frame.Payload)
132+ case wire.MsgDetach:
133+ return protocol.Detach{}, nil
134+ case wire.MsgPing:
135+ return protocol.Ping{}, nil
136+ case wire.MsgList:
137+ return protocol.List{}, nil
138+ case wire.MsgKill:
139+ return wire.UnmarshalKill(frame.Payload)
140+ case wire.MsgTheme:
141+ return wire.UnmarshalTheme(frame.Payload)
142+ case wire.MsgAck:
143+ return wire.UnmarshalAck(frame.Payload)
144+ case wire.MsgImagePush:
145+ return wire.UnmarshalImagePush(frame.Payload)
146+ case wire.MsgClientNotice:
147+ return wire.UnmarshalClientNotice(frame.Payload)
148+ case wire.MsgCommand:
149+ return wire.UnmarshalCommandRequest(frame.Payload)
150+ case wire.MsgOutputResetRequest:
151+ return wire.UnmarshalOutputResetRequest(frame.Payload)
152+ case wire.MsgRemotePreviewRequest:
153+ return wire.UnmarshalRemotePreviewRequest(frame.Payload)
154+ case wire.MsgRouteAttentionSubscription:
155+ return wire.UnmarshalRouteAttentionSubscription(frame.Payload)
156+ case wire.MsgSamePeerSwitchRequest:
157+ return wire.UnmarshalSamePeerSwitchRequest(frame.Payload)
158+ case wire.MsgParkedRouteRequest:
159+ return wire.UnmarshalParkedRouteRequest(frame.Payload)
160+ case wire.MsgRecentRouteSnapshot:
161+ return wire.UnmarshalRecentRouteSnapshot(frame.Payload)
162+ case wire.MsgRouteNavigationFailure:
163+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
164+ default:
165+ return nil, errors.New("test server connection: unsupported client frame")
166+ }
167+}
168+
169+func testSendServer(raw ports.Transport, message protocol.ServerMessage) error {
170+ frame, err := testServerFrame(message)
171+ if err != nil {
172+ return err
173+ }
174+ return raw.Send(frame)
175+}
176+
177+func testSendServerAsync(raw ports.Transport, message protocol.ServerMessage) error {
178+ transport, ok := raw.(ports.AsyncTransport)
179+ if !ok {
180+ return errors.New("test server connection: async unsupported")
181+ }
182+ frame, err := testServerFrame(message)
183+ if err != nil {
184+ return err
185+ }
186+ return transport.SendAsync(frame)
187+}
188+
189+func testSendServerSynchronous(raw ports.Transport, message protocol.ServerMessage) error {
190+ transport, ok := raw.(ports.OwnedSynchronousTransport)
191+ if !ok {
192+ return errors.New("test server connection: synchronous unsupported")
193+ }
194+ frame, err := testServerFrame(message)
195+ if err != nil {
196+ return err
197+ }
198+ return transport.SendSynchronous(frame)
199+}
200+
201+func testSendOutput(raw ports.Transport, output protocol.Output) error {
202+ frame, err := testServerFrame(output)
203+ if err != nil {
204+ return err
205+ }
206+ return raw.Send(frame)
207+}
208+
209+func testSendOutputAsync(raw ports.Transport, output protocol.Output) error {
210+ transport, ok := raw.(ports.AsyncTransport)
211+ if !ok {
212+ return errors.New("test server connection: async unsupported")
213+ }
214+ frame, err := testServerFrame(output)
215+ if err != nil {
216+ return err
217+ }
218+ return transport.SendAsync(frame)
219+}
220+
221+func testSendOutputSynchronous(raw ports.Transport, output protocol.Output) error {
222+ transport, ok := raw.(ports.OwnedSynchronousTransport)
223+ if !ok {
224+ return errors.New("test server connection: synchronous unsupported")
225+ }
226+ frame, err := testServerFrame(output)
227+ if err != nil {
228+ return err
229+ }
230+ return transport.SendSynchronous(frame)
231+}
232+
233+func testServerCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
234+ _, datagram := raw.(ports.DatagramTransport)
235+ _, async := raw.(ports.AsyncTransport)
236+ _, synchronous := raw.(ports.OwnedSynchronousTransport)
237+ _, link := raw.(ports.LinkStateReporter)
238+ window := uint8(8)
239+ if datagram {
240+ window = 1
241+ }
242+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, AsyncSend: async, OwnedSynchronousSend: synchronous, LinkState: link}
243+}
244+
245+func testLinkState(raw ports.Transport) ports.LinkState {
246+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
247+ return reporter.LinkState()
248+ }
249+ return ports.LinkStateConnected
250+}
251+
252+func testLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
253+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
254+ return reporter.LinkEvents()
255+ }
256+ return nil
257+}
258+
259+func outputFrameSender(send func(wire.Frame) error) func(protocol.Output) error {
260+ return func(output protocol.Output) error {
261+ frame, err := testServerFrame(output)
262+ if err != nil {
263+ return err
264+ }
265+ return send(frame)
266+ }
267+}
268+
269+func testServerFrame(message protocol.ServerMessage) (wire.Frame, error) {
270+ switch m := message.(type) {
271+ case protocol.Welcome:
272+ return wire.Frame{Type: wire.MsgWelcome, Payload: wire.MarshalWelcome(m)}, nil
273+ case protocol.ErrorMsg:
274+ return wire.Frame{Type: wire.MsgError, Payload: wire.MarshalErrorMsg(m)}, nil
275+ case protocol.Output:
276+ payload, err := wire.MarshalOutput(m)
277+ return wire.Frame{Type: wire.MsgOutput, Payload: payload}, err
278+ case protocol.Detached:
279+ return wire.Frame{Type: wire.MsgDetached, Payload: wire.MarshalDetached(m)}, nil
280+ case protocol.Pong:
281+ return wire.Frame{Type: wire.MsgPong, Payload: wire.MarshalPong(m)}, nil
282+ case protocol.Sessions:
283+ return wire.Frame{Type: wire.MsgSessions, Payload: wire.MarshalSessions(m)}, nil
284+ case protocol.CommandResult:
285+ return wire.Frame{Type: wire.MsgCommandResult, Payload: wire.MarshalCommandResult(m)}, nil
286+ case protocol.NavigationDirective:
287+ return wire.Frame{Type: wire.MsgNavigationAction, Payload: wire.MarshalNavigationDirective(m)}, nil
288+ case protocol.AttachTarget:
289+ return wire.Frame{Type: wire.MsgAttachTarget, Payload: wire.MarshalAttachTarget(m)}, nil
290+ case protocol.RemotePreview:
291+ return wire.Frame{Type: wire.MsgRemotePreviewResponse, Payload: wire.MarshalRemotePreview(m)}, nil
292+ case protocol.CommittedRouteIdentity:
293+ payload, err := wire.MarshalCommittedRouteIdentity(m)
294+ return wire.Frame{Type: wire.MsgCommittedRouteIdentity, Payload: payload}, err
295+ case protocol.RouteNavigationAction:
296+ payload, err := wire.MarshalRouteNavigationAction(m)
297+ return wire.Frame{Type: wire.MsgNavigateRecentRoute, Payload: payload}, err
298+ case protocol.RouteNavigationFailure:
299+ payload, err := wire.MarshalRouteNavigationFailure(m)
300+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}, err
301+ case protocol.RoutePosition:
302+ payload, err := wire.MarshalRoutePosition(m)
303+ return wire.Frame{Type: wire.MsgRoutePosition, Payload: payload}, err
304+ case protocol.SamePeerSwitchFailure:
305+ payload, err := wire.MarshalSamePeerSwitchFailure(m)
306+ return wire.Frame{Type: wire.MsgSamePeerSwitchFailure, Payload: payload}, err
307+ case protocol.ParkedRouteResponse:
308+ return wire.Frame{Type: wire.MsgParkedRouteResponse, Payload: wire.MarshalParkedRouteResponse(m)}, nil
309+ default:
310+ return wire.Frame{}, errors.New("test server connection: unsupported server message")
311+ }
312+}
313+
314+func (d *Daemon) handleCommandFrame(connection ports.ServerConnection, frame wire.Frame) error {
315+ request, err := wire.UnmarshalCommandRequest(frame.Payload)
316+ if err != nil {
317+ code := uint16(protocol.ErrInternal)
318+ if version, ok := wire.PeekCommandVersion(frame.Payload); ok && version != protocol.Version {
319+ code = protocol.ErrVersionMismatch
320+ }
321+ return d.sendCommandResult(connection, protocol.CommandResult{Code: code, Text: err.Error()})
322+ }
323+ return d.handleCommand(connection, request)
324+}
325+
326+func (d *Daemon) handleHelloFrame(connection ports.ServerConnection, frame wire.Frame) {
327+ hello, err := wire.UnmarshalHello(frame.Payload)
328+ if err != nil {
329+ return
330+ }
331+ d.handleHello(connection, hello)
332+}
333+
334+func (d *Daemon) handleHelloFrameWithContext(ctx context.Context, timedOut <-chan struct{}, stop, finish func(), connection ports.ServerConnection, frame wire.Frame) {
335+ hello, err := wire.UnmarshalHello(frame.Payload)
336+ if err != nil {
337+ return
338+ }
339+ d.handleHelloWithContext(ctx, timedOut, stop, finish, connection, hello)
340+}
341+
342+func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, frame wire.Frame) bool {
343+ message, err := testReceiveClient(&singleFrameTransport{frame: frame})
344+ if err != nil {
345+ return false
346+ }
347+ return d.handleAttachmentClientMessage(capability, message)
348+}
349+
350+type singleFrameTransport struct{ frame wire.Frame }
351+
352+func (*singleFrameTransport) Send(wire.Frame) error { return nil }
353+func (t *singleFrameTransport) Recv() (wire.Frame, error) { return t.frame, nil }
354+func (*singleFrameTransport) Close() error { return nil }
355+
356+// Methods below let the existing raw transport fixtures exercise the typed
357+// daemon boundary while preserving their byte-level assertions.
358+
359+func (t *blockedRenderReplacementTransport) ReceiveClient() (protocol.ClientMessage, error) {
360+ return testReceiveClient(t)
361+}
362+func (t *blockedRenderReplacementTransport) SendServer(m protocol.ServerMessage) error {
363+ return testSendServer(t, m)
364+}
365+func (t *blockedRenderReplacementTransport) SendServerAsync(m protocol.ServerMessage) error {
366+ return testSendServerAsync(t, m)
367+}
368+func (t *blockedRenderReplacementTransport) SendServerSynchronous(m protocol.ServerMessage) error {
369+ return testSendServerSynchronous(t, m)
370+}
371+func (t *blockedRenderReplacementTransport) Capabilities() protocol.ConnectionCapabilities {
372+ return testServerCapabilities(t)
373+}
374+func (t *blockedRenderReplacementTransport) LinkState() ports.LinkState { return testLinkState(t) }
375+func (t *blockedRenderReplacementTransport) LinkEvents() <-chan ports.LinkEvent {
376+ return testLinkEvents(t)
377+}
378+
379+func (t *teardownBlockingTransport) ReceiveClient() (protocol.ClientMessage, error) {
380+ return testReceiveClient(t)
381+}
382+func (t *teardownBlockingTransport) SendServer(m protocol.ServerMessage) error {
383+ return testSendServer(t, m)
384+}
385+func (t *teardownBlockingTransport) SendServerAsync(m protocol.ServerMessage) error {
386+ return testSendServerAsync(t, m)
387+}
388+func (t *teardownBlockingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
389+ return testSendServerSynchronous(t, m)
390+}
391+func (t *teardownBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
392+ return testServerCapabilities(t)
393+}
394+func (t *teardownBlockingTransport) LinkState() ports.LinkState { return testLinkState(t) }
395+func (t *teardownBlockingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
396+
397+func (t *closeTrackingTransport) ReceiveClient() (protocol.ClientMessage, error) {
398+ return testReceiveClient(t)
399+}
400+func (t *closeTrackingTransport) SendServer(m protocol.ServerMessage) error {
401+ return testSendServer(t, m)
402+}
403+func (t *closeTrackingTransport) SendServerAsync(m protocol.ServerMessage) error {
404+ return testSendServerAsync(t, m)
405+}
406+func (t *closeTrackingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
407+ return testSendServerSynchronous(t, m)
408+}
409+func (t *closeTrackingTransport) Capabilities() protocol.ConnectionCapabilities {
410+ return testServerCapabilities(t)
411+}
412+func (t *closeTrackingTransport) LinkState() ports.LinkState { return testLinkState(t) }
413+func (t *closeTrackingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
414+
415+func (t *closeCountingBlockedTransport) ReceiveClient() (protocol.ClientMessage, error) {
416+ return testReceiveClient(t)
417+}
418+func (t *closeCountingBlockedTransport) SendServer(m protocol.ServerMessage) error {
419+ return testSendServer(t, m)
420+}
421+func (t *closeCountingBlockedTransport) SendServerAsync(m protocol.ServerMessage) error {
422+ return testSendServerAsync(t, m)
423+}
424+func (t *closeCountingBlockedTransport) SendServerSynchronous(m protocol.ServerMessage) error {
425+ return testSendServerSynchronous(t, m)
426+}
427+func (t *closeCountingBlockedTransport) Capabilities() protocol.ConnectionCapabilities {
428+ return testServerCapabilities(t)
429+}
430+func (t *closeCountingBlockedTransport) LinkState() ports.LinkState { return testLinkState(t) }
431+func (t *closeCountingBlockedTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
432+
433+func (t *handshakeBlockingTransport) ReceiveClient() (protocol.ClientMessage, error) {
434+ return testReceiveClient(t)
435+}
436+func (t *handshakeBlockingTransport) SendServer(m protocol.ServerMessage) error {
437+ return testSendServer(t, m)
438+}
439+func (t *handshakeBlockingTransport) SendServerAsync(m protocol.ServerMessage) error {
440+ return testSendServerAsync(t, m)
441+}
442+func (t *handshakeBlockingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
443+ return testSendServerSynchronous(t, m)
444+}
445+func (t *handshakeBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
446+ return testServerCapabilities(t)
447+}
448+func (t *handshakeBlockingTransport) LinkState() ports.LinkState { return testLinkState(t) }
449+func (t *handshakeBlockingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
450+
451+func (t *lateGraphicsCleanupTransport) ReceiveClient() (protocol.ClientMessage, error) {
452+ return testReceiveClient(t)
453+}
454+func (t *lateGraphicsCleanupTransport) SendServer(m protocol.ServerMessage) error {
455+ return testSendServer(t, m)
456+}
457+func (t *lateGraphicsCleanupTransport) SendServerAsync(m protocol.ServerMessage) error {
458+ return testSendServerAsync(t, m)
459+}
460+func (t *lateGraphicsCleanupTransport) SendServerSynchronous(m protocol.ServerMessage) error {
461+ return testSendServerSynchronous(t, m)
462+}
463+func (t *lateGraphicsCleanupTransport) Capabilities() protocol.ConnectionCapabilities {
464+ return testServerCapabilities(t)
465+}
466+func (t *lateGraphicsCleanupTransport) LinkState() ports.LinkState { return testLinkState(t) }
467+func (t *lateGraphicsCleanupTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
468+
469+func (t *datagramTestTransport) ReceiveClient() (protocol.ClientMessage, error) {
470+ return testReceiveClient(t)
471+}
472+func (t *datagramTestTransport) SendServer(m protocol.ServerMessage) error {
473+ return testSendServer(t, m)
474+}
475+func (t *datagramTestTransport) SendServerAsync(m protocol.ServerMessage) error {
476+ return testSendServerAsync(t, m)
477+}
478+func (t *datagramTestTransport) SendServerSynchronous(m protocol.ServerMessage) error {
479+ return testSendServerSynchronous(t, m)
480+}
481+func (t *datagramTestTransport) Capabilities() protocol.ConnectionCapabilities {
482+ return testServerCapabilities(t)
483+}
484+func (t *datagramTestTransport) LinkState() ports.LinkState { return testLinkState(t) }
485+func (t *datagramTestTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
486+
487+func (t *asyncPaintTransport) ReceiveClient() (protocol.ClientMessage, error) {
488+ return testReceiveClient(t)
489+}
490+func (t *asyncPaintTransport) SendServer(m protocol.ServerMessage) error { return testSendServer(t, m) }
491+func (t *asyncPaintTransport) SendServerAsync(m protocol.ServerMessage) error {
492+ return testSendServerAsync(t, m)
493+}
494+func (t *asyncPaintTransport) SendServerSynchronous(m protocol.ServerMessage) error {
495+ return testSendServerSynchronous(t, m)
496+}
497+func (t *asyncPaintTransport) Capabilities() protocol.ConnectionCapabilities {
498+ return testServerCapabilities(t)
499+}
500+func (t *asyncPaintTransport) LinkState() ports.LinkState { return testLinkState(t) }
501+func (t *asyncPaintTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
502+
503+func (t *timedSideEffectTransport) ReceiveClient() (protocol.ClientMessage, error) {
504+ return testReceiveClient(t)
505+}
506+func (t *timedSideEffectTransport) SendServer(m protocol.ServerMessage) error {
507+ return testSendServer(t, m)
508+}
509+func (t *timedSideEffectTransport) SendServerAsync(m protocol.ServerMessage) error {
510+ return testSendServerAsync(t, m)
511+}
512+func (t *timedSideEffectTransport) SendServerSynchronous(m protocol.ServerMessage) error {
513+ return testSendServerSynchronous(t, m)
514+}
515+func (t *timedSideEffectTransport) Capabilities() protocol.ConnectionCapabilities {
516+ return testServerCapabilities(t)
517+}
518+func (t *timedSideEffectTransport) LinkState() ports.LinkState { return testLinkState(t) }
519+func (t *timedSideEffectTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
520+
521+func (t *scriptedReplayTransport) ReceiveClient() (protocol.ClientMessage, error) {
522+ return testReceiveClient(t)
523+}
524+func (t *scriptedReplayTransport) SendServer(m protocol.ServerMessage) error {
525+ return testSendServer(t, m)
526+}
527+func (t *scriptedReplayTransport) SendServerAsync(m protocol.ServerMessage) error {
528+ return testSendServerAsync(t, m)
529+}
530+func (t *scriptedReplayTransport) SendServerSynchronous(m protocol.ServerMessage) error {
531+ return testSendServerSynchronous(t, m)
532+}
533+func (t *scriptedReplayTransport) Capabilities() protocol.ConnectionCapabilities {
534+ return testServerCapabilities(t)
535+}
536+func (t *scriptedReplayTransport) LinkState() ports.LinkState { return testLinkState(t) }
537+func (t *scriptedReplayTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
538+
539+func (t *swapErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
540+ return testReceiveClient(t)
541+}
542+func (t *swapErrorTransport) SendServer(m protocol.ServerMessage) error { return testSendServer(t, m) }
543+func (t *swapErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
544+ return testSendServerAsync(t, m)
545+}
546+func (t *swapErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
547+ return testSendServerSynchronous(t, m)
548+}
549+func (t *swapErrorTransport) Capabilities() protocol.ConnectionCapabilities {
550+ return testServerCapabilities(t)
551+}
552+func (t *swapErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
553+func (t *swapErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
554+
555+func (t *ownedSwapErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
556+ return testReceiveClient(t)
557+}
558+func (t *ownedSwapErrorTransport) SendServer(m protocol.ServerMessage) error {
559+ return testSendServer(t, m)
560+}
561+func (t *ownedSwapErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
562+ return testSendServerAsync(t, m)
563+}
564+func (t *ownedSwapErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
565+ return testSendServerSynchronous(t, m)
566+}
567+func (t *ownedSwapErrorTransport) Capabilities() protocol.ConnectionCapabilities {
568+ return testServerCapabilities(t)
569+}
570+func (t *ownedSwapErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
571+func (t *ownedSwapErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
572+
573+func (t *blockingControlSendTransport) ReceiveClient() (protocol.ClientMessage, error) {
574+ return testReceiveClient(t)
575+}
576+func (t *blockingControlSendTransport) SendServer(m protocol.ServerMessage) error {
577+ return testSendServer(t, m)
578+}
579+func (t *blockingControlSendTransport) SendServerAsync(m protocol.ServerMessage) error {
580+ return testSendServerAsync(t, m)
581+}
582+func (t *blockingControlSendTransport) SendServerSynchronous(m protocol.ServerMessage) error {
583+ return testSendServerSynchronous(t, m)
584+}
585+func (t *blockingControlSendTransport) Capabilities() protocol.ConnectionCapabilities {
586+ return testServerCapabilities(t)
587+}
588+func (t *blockingControlSendTransport) LinkState() ports.LinkState { return testLinkState(t) }
589+func (t *blockingControlSendTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
590+
591+func (t *commandSendErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
592+ return testReceiveClient(t)
593+}
594+func (t *commandSendErrorTransport) SendServer(m protocol.ServerMessage) error {
595+ return testSendServer(t, m)
596+}
597+func (t *commandSendErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
598+ return testSendServerAsync(t, m)
599+}
600+func (t *commandSendErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
601+ return testSendServerSynchronous(t, m)
602+}
603+func (t *commandSendErrorTransport) Capabilities() protocol.ConnectionCapabilities {
604+ return testServerCapabilities(t)
605+}
606+func (t *commandSendErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
607+func (t *commandSendErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
608+
609+func (t *fanoutBlockingTransport) ReceiveClient() (protocol.ClientMessage, error) {
610+ return testReceiveClient(t)
611+}
612+func (t *fanoutBlockingTransport) SendServer(m protocol.ServerMessage) error {
613+ return testSendServer(t, m)
614+}
615+func (t *fanoutBlockingTransport) SendServerAsync(m protocol.ServerMessage) error {
616+ return testSendServerAsync(t, m)
617+}
618+func (t *fanoutBlockingTransport) SendServerSynchronous(m protocol.ServerMessage) error {
619+ return testSendServerSynchronous(t, m)
620+}
621+func (t *fanoutBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
622+ return testServerCapabilities(t)
623+}
624+func (t *fanoutBlockingTransport) LinkState() ports.LinkState { return testLinkState(t) }
625+func (t *fanoutBlockingTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
626+
627+func (t *staleClipboardErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
628+ return testReceiveClient(t)
629+}
630+func (t *staleClipboardErrorTransport) SendServer(m protocol.ServerMessage) error {
631+ return testSendServer(t, m)
632+}
633+func (t *staleClipboardErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
634+ return testSendServerAsync(t, m)
635+}
636+func (t *staleClipboardErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
637+ return testSendServerSynchronous(t, m)
638+}
639+func (t *staleClipboardErrorTransport) Capabilities() protocol.ConnectionCapabilities {
640+ return testServerCapabilities(t)
641+}
642+func (t *staleClipboardErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
643+func (t *staleClipboardErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
644+
645+func (t *movingClipboardErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
646+ return testReceiveClient(t)
647+}
648+func (t *movingClipboardErrorTransport) SendServer(m protocol.ServerMessage) error {
649+ return testSendServer(t, m)
650+}
651+func (t *movingClipboardErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
652+ return testSendServerAsync(t, m)
653+}
654+func (t *movingClipboardErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
655+ return testSendServerSynchronous(t, m)
656+}
657+func (t *movingClipboardErrorTransport) Capabilities() protocol.ConnectionCapabilities {
658+ return testServerCapabilities(t)
659+}
660+func (t *movingClipboardErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
661+func (t *movingClipboardErrorTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
662+
663+func (t *blockingClipboardTransport) ReceiveClient() (protocol.ClientMessage, error) {
664+ return testReceiveClient(t)
665+}
666+func (t *blockingClipboardTransport) SendServer(m protocol.ServerMessage) error {
667+ return testSendServer(t, m)
668+}
669+func (t *blockingClipboardTransport) SendServerAsync(m protocol.ServerMessage) error {
670+ return testSendServerAsync(t, m)
671+}
672+func (t *blockingClipboardTransport) SendServerSynchronous(m protocol.ServerMessage) error {
673+ return testSendServerSynchronous(t, m)
674+}
675+func (t *blockingClipboardTransport) Capabilities() protocol.ConnectionCapabilities {
676+ return testServerCapabilities(t)
677+}
678+func (t *blockingClipboardTransport) LinkState() ports.LinkState { return testLinkState(t) }
679+func (t *blockingClipboardTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
680+
681+func (t *blockedAttachmentTransport) ReceiveClient() (protocol.ClientMessage, error) {
682+ return testReceiveClient(t)
683+}
684+func (t *blockedAttachmentTransport) SendServer(m protocol.ServerMessage) error {
685+ return testSendServer(t, m)
686+}
687+func (t *blockedAttachmentTransport) SendServerAsync(m protocol.ServerMessage) error {
688+ return testSendServerAsync(t, m)
689+}
690+func (t *blockedAttachmentTransport) SendServerSynchronous(m protocol.ServerMessage) error {
691+ return testSendServerSynchronous(t, m)
692+}
693+func (t *blockedAttachmentTransport) Capabilities() protocol.ConnectionCapabilities {
694+ return testServerCapabilities(t)
695+}
696+func (t *blockedAttachmentTransport) LinkState() ports.LinkState { return testLinkState(t) }
697+func (t *blockedAttachmentTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
698+
699+func (t *resumeWelcomeFailureTransport) ReceiveClient() (protocol.ClientMessage, error) {
700+ return testReceiveClient(t)
701+}
702+func (t *resumeWelcomeFailureTransport) SendServer(m protocol.ServerMessage) error {
703+ return testSendServer(t, m)
704+}
705+func (t *resumeWelcomeFailureTransport) SendServerAsync(m protocol.ServerMessage) error {
706+ return testSendServerAsync(t, m)
707+}
708+func (t *resumeWelcomeFailureTransport) SendServerSynchronous(m protocol.ServerMessage) error {
709+ return testSendServerSynchronous(t, m)
710+}
711+func (t *resumeWelcomeFailureTransport) Capabilities() protocol.ConnectionCapabilities {
712+ return testServerCapabilities(t)
713+}
714+func (t *resumeWelcomeFailureTransport) LinkState() ports.LinkState { return testLinkState(t) }
715+func (t *resumeWelcomeFailureTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
716+
717+func (t *committedIdentityErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
718+ return testReceiveClient(t)
719+}
720+func (t *committedIdentityErrorTransport) SendServer(m protocol.ServerMessage) error {
721+ return testSendServer(t, m)
722+}
723+func (t *committedIdentityErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
724+ return testSendServerAsync(t, m)
725+}
726+func (t *committedIdentityErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
727+ return testSendServerSynchronous(t, m)
728+}
729+func (t *committedIdentityErrorTransport) Capabilities() protocol.ConnectionCapabilities {
730+ return testServerCapabilities(t)
731+}
732+func (t *committedIdentityErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
733+func (t *committedIdentityErrorTransport) LinkEvents() <-chan ports.LinkEvent {
734+ return testLinkEvents(t)
735+}
736+
737+func (t *countingOutputTransport) ReceiveClient() (protocol.ClientMessage, error) {
738+ return testReceiveClient(t)
739+}
740+func (t *countingOutputTransport) SendServer(m protocol.ServerMessage) error {
741+ return testSendServer(t, m)
742+}
743+func (t *countingOutputTransport) SendServerAsync(m protocol.ServerMessage) error {
744+ return testSendServerAsync(t, m)
745+}
746+func (t *countingOutputTransport) SendServerSynchronous(m protocol.ServerMessage) error {
747+ return testSendServerSynchronous(t, m)
748+}
749+func (t *countingOutputTransport) Capabilities() protocol.ConnectionCapabilities {
750+ return testServerCapabilities(t)
751+}
752+func (t *countingOutputTransport) LinkState() ports.LinkState { return testLinkState(t) }
753+func (t *countingOutputTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
754+
755+func (t *parkedRouteExpiryTransport) ReceiveClient() (protocol.ClientMessage, error) {
756+ return testReceiveClient(t)
757+}
758+func (t *parkedRouteExpiryTransport) SendServer(m protocol.ServerMessage) error {
759+ return testSendServer(t, m)
760+}
761+func (t *parkedRouteExpiryTransport) SendServerAsync(m protocol.ServerMessage) error {
762+ return testSendServerAsync(t, m)
763+}
764+func (t *parkedRouteExpiryTransport) SendServerSynchronous(m protocol.ServerMessage) error {
765+ return testSendServerSynchronous(t, m)
766+}
767+func (t *parkedRouteExpiryTransport) Capabilities() protocol.ConnectionCapabilities {
768+ return testServerCapabilities(t)
769+}
770+func (t *parkedRouteExpiryTransport) LinkState() ports.LinkState { return testLinkState(t) }
771+func (t *parkedRouteExpiryTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
772+
773+func (t *remotePickerSendErrorTransport) ReceiveClient() (protocol.ClientMessage, error) {
774+ return testReceiveClient(t)
775+}
776+func (t *remotePickerSendErrorTransport) SendServer(m protocol.ServerMessage) error {
777+ return testSendServer(t, m)
778+}
779+func (t *remotePickerSendErrorTransport) SendServerAsync(m protocol.ServerMessage) error {
780+ return testSendServerAsync(t, m)
781+}
782+func (t *remotePickerSendErrorTransport) SendServerSynchronous(m protocol.ServerMessage) error {
783+ return testSendServerSynchronous(t, m)
784+}
785+func (t *remotePickerSendErrorTransport) Capabilities() protocol.ConnectionCapabilities {
786+ return testServerCapabilities(t)
787+}
788+func (t *remotePickerSendErrorTransport) LinkState() ports.LinkState { return testLinkState(t) }
789+func (t *remotePickerSendErrorTransport) LinkEvents() <-chan ports.LinkEvent {
790+ return testLinkEvents(t)
791+}
792+
793+func (t failingOutputTransport) ReceiveClient() (protocol.ClientMessage, error) {
794+ return testReceiveClient(t)
795+}
796+func (t failingOutputTransport) SendServer(m protocol.ServerMessage) error {
797+ return testSendServer(t, m)
798+}
799+func (t failingOutputTransport) SendServerAsync(m protocol.ServerMessage) error {
800+ return testSendServerAsync(t, m)
801+}
802+func (t failingOutputTransport) SendServerSynchronous(m protocol.ServerMessage) error {
803+ return testSendServerSynchronous(t, m)
804+}
805+func (t failingOutputTransport) Capabilities() protocol.ConnectionCapabilities {
806+ return testServerCapabilities(t)
807+}
808+func (t failingOutputTransport) LinkState() ports.LinkState { return testLinkState(t) }
809+func (t failingOutputTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
810+
811+func (t cacheFailTransport) ReceiveClient() (protocol.ClientMessage, error) {
812+ return testReceiveClient(t)
813+}
814+func (t cacheFailTransport) SendServer(m protocol.ServerMessage) error { return testSendServer(t, m) }
815+func (t cacheFailTransport) SendServerAsync(m protocol.ServerMessage) error {
816+ return testSendServerAsync(t, m)
817+}
818+func (t cacheFailTransport) SendServerSynchronous(m protocol.ServerMessage) error {
819+ return testSendServerSynchronous(t, m)
820+}
821+func (t cacheFailTransport) Capabilities() protocol.ConnectionCapabilities {
822+ return testServerCapabilities(t)
823+}
824+func (t cacheFailTransport) LinkState() ports.LinkState { return testLinkState(t) }
825+func (t cacheFailTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
826+
827+func (t parkedRouteFailTransport) ReceiveClient() (protocol.ClientMessage, error) {
828+ return testReceiveClient(t)
829+}
830+func (t parkedRouteFailTransport) SendServer(m protocol.ServerMessage) error {
831+ return testSendServer(t, m)
832+}
833+func (t parkedRouteFailTransport) SendServerAsync(m protocol.ServerMessage) error {
834+ return testSendServerAsync(t, m)
835+}
836+func (t parkedRouteFailTransport) SendServerSynchronous(m protocol.ServerMessage) error {
837+ return testSendServerSynchronous(t, m)
838+}
839+func (t parkedRouteFailTransport) Capabilities() protocol.ConnectionCapabilities {
840+ return testServerCapabilities(t)
841+}
842+func (t parkedRouteFailTransport) LinkState() ports.LinkState { return testLinkState(t) }
843+func (t parkedRouteFailTransport) LinkEvents() <-chan ports.LinkEvent { return testLinkEvents(t) }
844+
845+func (t *blockedRenderReplacementTransport) SendOutput(o protocol.Output) error {
846+ return testSendOutput(t, o)
847+}
848+func (t *blockedRenderReplacementTransport) SendOutputAsync(o protocol.Output) error {
849+ return testSendOutputAsync(t, o)
850+}
851+func (t *blockedRenderReplacementTransport) SendOutputSynchronous(o protocol.Output) error {
852+ return testSendOutputSynchronous(t, o)
853+}
854+
855+func (t *teardownBlockingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
856+func (t *teardownBlockingTransport) SendOutputAsync(o protocol.Output) error {
857+ return testSendOutputAsync(t, o)
858+}
859+func (t *teardownBlockingTransport) SendOutputSynchronous(o protocol.Output) error {
860+ return testSendOutputSynchronous(t, o)
861+}
862+
863+func (t *closeTrackingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
864+func (t *closeTrackingTransport) SendOutputAsync(o protocol.Output) error {
865+ return testSendOutputAsync(t, o)
866+}
867+func (t *closeTrackingTransport) SendOutputSynchronous(o protocol.Output) error {
868+ return testSendOutputSynchronous(t, o)
869+}
870+
871+func (t *closeCountingBlockedTransport) SendOutput(o protocol.Output) error {
872+ return testSendOutput(t, o)
873+}
874+func (t *closeCountingBlockedTransport) SendOutputAsync(o protocol.Output) error {
875+ return testSendOutputAsync(t, o)
876+}
877+func (t *closeCountingBlockedTransport) SendOutputSynchronous(o protocol.Output) error {
878+ return testSendOutputSynchronous(t, o)
879+}
880+
881+func (t *handshakeBlockingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
882+func (t *handshakeBlockingTransport) SendOutputAsync(o protocol.Output) error {
883+ return testSendOutputAsync(t, o)
884+}
885+func (t *handshakeBlockingTransport) SendOutputSynchronous(o protocol.Output) error {
886+ return testSendOutputSynchronous(t, o)
887+}
888+
889+func (t *lateGraphicsCleanupTransport) SendOutput(o protocol.Output) error {
890+ return testSendOutput(t, o)
891+}
892+func (t *lateGraphicsCleanupTransport) SendOutputAsync(o protocol.Output) error {
893+ return testSendOutputAsync(t, o)
894+}
895+func (t *lateGraphicsCleanupTransport) SendOutputSynchronous(o protocol.Output) error {
896+ return testSendOutputSynchronous(t, o)
897+}
898+
899+func (t *datagramTestTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
900+func (t *datagramTestTransport) SendOutputAsync(o protocol.Output) error {
901+ return testSendOutputAsync(t, o)
902+}
903+func (t *datagramTestTransport) SendOutputSynchronous(o protocol.Output) error {
904+ return testSendOutputSynchronous(t, o)
905+}
906+
907+func (t *asyncPaintTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
908+func (t *asyncPaintTransport) SendOutputAsync(o protocol.Output) error {
909+ return testSendOutputAsync(t, o)
910+}
911+func (t *asyncPaintTransport) SendOutputSynchronous(o protocol.Output) error {
912+ return testSendOutputSynchronous(t, o)
913+}
914+
915+func (t *timedSideEffectTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
916+func (t *timedSideEffectTransport) SendOutputAsync(o protocol.Output) error {
917+ return testSendOutputAsync(t, o)
918+}
919+func (t *timedSideEffectTransport) SendOutputSynchronous(o protocol.Output) error {
920+ return testSendOutputSynchronous(t, o)
921+}
922+
923+func (t *scriptedReplayTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
924+func (t *scriptedReplayTransport) SendOutputAsync(o protocol.Output) error {
925+ return testSendOutputAsync(t, o)
926+}
927+func (t *scriptedReplayTransport) SendOutputSynchronous(o protocol.Output) error {
928+ return testSendOutputSynchronous(t, o)
929+}
930+
931+func (t *swapErrorTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
932+func (t *swapErrorTransport) SendOutputAsync(o protocol.Output) error {
933+ return testSendOutputAsync(t, o)
934+}
935+func (t *swapErrorTransport) SendOutputSynchronous(o protocol.Output) error {
936+ return testSendOutputSynchronous(t, o)
937+}
938+
939+func (t *ownedSwapErrorTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
940+func (t *ownedSwapErrorTransport) SendOutputAsync(o protocol.Output) error {
941+ return testSendOutputAsync(t, o)
942+}
943+func (t *ownedSwapErrorTransport) SendOutputSynchronous(o protocol.Output) error {
944+ return testSendOutputSynchronous(t, o)
945+}
946+
947+func (t *blockingControlSendTransport) SendOutput(o protocol.Output) error {
948+ return testSendOutput(t, o)
949+}
950+func (t *blockingControlSendTransport) SendOutputAsync(o protocol.Output) error {
951+ return testSendOutputAsync(t, o)
952+}
953+func (t *blockingControlSendTransport) SendOutputSynchronous(o protocol.Output) error {
954+ return testSendOutputSynchronous(t, o)
955+}
956+
957+func (t *commandSendErrorTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
958+func (t *commandSendErrorTransport) SendOutputAsync(o protocol.Output) error {
959+ return testSendOutputAsync(t, o)
960+}
961+func (t *commandSendErrorTransport) SendOutputSynchronous(o protocol.Output) error {
962+ return testSendOutputSynchronous(t, o)
963+}
964+
965+func (t *fanoutBlockingTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
966+func (t *fanoutBlockingTransport) SendOutputAsync(o protocol.Output) error {
967+ return testSendOutputAsync(t, o)
968+}
969+func (t *fanoutBlockingTransport) SendOutputSynchronous(o protocol.Output) error {
970+ return testSendOutputSynchronous(t, o)
971+}
972+
973+func (t *staleClipboardErrorTransport) SendOutput(o protocol.Output) error {
974+ return testSendOutput(t, o)
975+}
976+func (t *staleClipboardErrorTransport) SendOutputAsync(o protocol.Output) error {
977+ return testSendOutputAsync(t, o)
978+}
979+func (t *staleClipboardErrorTransport) SendOutputSynchronous(o protocol.Output) error {
980+ return testSendOutputSynchronous(t, o)
981+}
982+
983+func (t *movingClipboardErrorTransport) SendOutput(o protocol.Output) error {
984+ return testSendOutput(t, o)
985+}
986+func (t *movingClipboardErrorTransport) SendOutputAsync(o protocol.Output) error {
987+ return testSendOutputAsync(t, o)
988+}
989+func (t *movingClipboardErrorTransport) SendOutputSynchronous(o protocol.Output) error {
990+ return testSendOutputSynchronous(t, o)
991+}
992+
993+func (t *blockingClipboardTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
994+func (t *blockingClipboardTransport) SendOutputAsync(o protocol.Output) error {
995+ return testSendOutputAsync(t, o)
996+}
997+func (t *blockingClipboardTransport) SendOutputSynchronous(o protocol.Output) error {
998+ return testSendOutputSynchronous(t, o)
999+}
1000+
1001+func (t *blockedAttachmentTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1002+func (t *blockedAttachmentTransport) SendOutputAsync(o protocol.Output) error {
1003+ return testSendOutputAsync(t, o)
1004+}
1005+func (t *blockedAttachmentTransport) SendOutputSynchronous(o protocol.Output) error {
1006+ return testSendOutputSynchronous(t, o)
1007+}
1008+
1009+func (t *resumeWelcomeFailureTransport) SendOutput(o protocol.Output) error {
1010+ return testSendOutput(t, o)
1011+}
1012+func (t *resumeWelcomeFailureTransport) SendOutputAsync(o protocol.Output) error {
1013+ return testSendOutputAsync(t, o)
1014+}
1015+func (t *resumeWelcomeFailureTransport) SendOutputSynchronous(o protocol.Output) error {
1016+ return testSendOutputSynchronous(t, o)
1017+}
1018+
1019+func (t *committedIdentityErrorTransport) SendOutput(o protocol.Output) error {
1020+ return testSendOutput(t, o)
1021+}
1022+func (t *committedIdentityErrorTransport) SendOutputAsync(o protocol.Output) error {
1023+ return testSendOutputAsync(t, o)
1024+}
1025+func (t *committedIdentityErrorTransport) SendOutputSynchronous(o protocol.Output) error {
1026+ return testSendOutputSynchronous(t, o)
1027+}
1028+
1029+func (t *countingOutputTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1030+func (t *countingOutputTransport) SendOutputAsync(o protocol.Output) error {
1031+ return testSendOutputAsync(t, o)
1032+}
1033+func (t *countingOutputTransport) SendOutputSynchronous(o protocol.Output) error {
1034+ return testSendOutputSynchronous(t, o)
1035+}
1036+
1037+func (t *parkedRouteExpiryTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1038+func (t *parkedRouteExpiryTransport) SendOutputAsync(o protocol.Output) error {
1039+ return testSendOutputAsync(t, o)
1040+}
1041+func (t *parkedRouteExpiryTransport) SendOutputSynchronous(o protocol.Output) error {
1042+ return testSendOutputSynchronous(t, o)
1043+}
1044+
1045+func (t *remotePickerSendErrorTransport) SendOutput(o protocol.Output) error {
1046+ return testSendOutput(t, o)
1047+}
1048+func (t *remotePickerSendErrorTransport) SendOutputAsync(o protocol.Output) error {
1049+ return testSendOutputAsync(t, o)
1050+}
1051+func (t *remotePickerSendErrorTransport) SendOutputSynchronous(o protocol.Output) error {
1052+ return testSendOutputSynchronous(t, o)
1053+}
1054+
1055+func (t failingOutputTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1056+func (t failingOutputTransport) SendOutputAsync(o protocol.Output) error {
1057+ return testSendOutputAsync(t, o)
1058+}
1059+func (t failingOutputTransport) SendOutputSynchronous(o protocol.Output) error {
1060+ return testSendOutputSynchronous(t, o)
1061+}
1062+
1063+func (t cacheFailTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1064+func (t cacheFailTransport) SendOutputAsync(o protocol.Output) error {
1065+ return testSendOutputAsync(t, o)
1066+}
1067+func (t cacheFailTransport) SendOutputSynchronous(o protocol.Output) error {
1068+ return testSendOutputSynchronous(t, o)
1069+}
1070+
1071+func (t parkedRouteFailTransport) SendOutput(o protocol.Output) error { return testSendOutput(t, o) }
1072+func (t parkedRouteFailTransport) SendOutputAsync(o protocol.Output) error {
1073+ return testSendOutputAsync(t, o)
1074+}
1075+func (t parkedRouteFailTransport) SendOutputSynchronous(o protocol.Output) error {
1076+ return testSendOutputSynchronous(t, o)
1077+}
M internal/usecase/daemon/wire.go +21 -27
@@ -4,10 +4,9 @@ package daemon
44 import (
55 "github.com/bnema/vev/internal/domain"
66 "github.com/bnema/vev/internal/protocol"
7- "github.com/bnema/vev/internal/protocol/wire"
87 )
98
10-func frameWelcome(s *session, resumeToken uint64) (wire.Frame, error) {
9+func serverWelcome(s *session, resumeToken uint64) (protocol.Welcome, error) {
1110 s.mu.Lock()
1211 var identity *protocol.CommittedRouteIdentity
1312 if s.incarnation != (domain.SessionLifecycleID{}) {
@@ -16,7 +15,7 @@ func frameWelcome(s *session, resumeToken uint64) (wire.Frame, error) {
1615 Ephemeral: s.ephemeral,
1716 }
1817 }
19- w := protocol.Welcome{
18+ message := protocol.Welcome{
2019 SessionID: string(s.id),
2120 SessionName: s.name,
2221 Ephemeral: s.ephemeral,
@@ -24,37 +23,32 @@ func frameWelcome(s *session, resumeToken uint64) (wire.Frame, error) {
2423 CommittedIdentity: identity,
2524 }
2625 s.mu.Unlock()
27- w.ResumeToken = resumeToken
28- payload := wire.MarshalWelcome(w)
29- if payload == nil {
30- return wire.Frame{}, protocol.ErrInvalidRouteWire
26+ message.ResumeToken = resumeToken
27+ if identity != nil {
28+ if err := identity.Validate(); err != nil {
29+ return protocol.Welcome{}, err
30+ }
3131 }
32- return wire.Frame{Type: wire.MsgWelcome, Payload: payload}, nil
32+ return message, nil
3333 }
3434
35-func frameError(code uint16, text string) wire.Frame {
36- return wire.Frame{Type: wire.MsgError, Payload: wire.MarshalErrorMsg(protocol.ErrorMsg{Code: code, Text: text})}
35+func serverError(code uint16, text string) protocol.ErrorMsg {
36+ return protocol.ErrorMsg{Code: code, Text: text}
3737 }
3838
39-func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64) (wire.Frame, error) {
40- payload, err := wire.MarshalOutput(protocol.Output{
39+func serverOutputState(data []byte, baseState, state, echoAck uint64) (protocol.Output, error) {
40+ message := protocol.Output{
4141 Epoch: 1, Base: baseState, New: state, Echo: echoAck,
42- Size: domain.Size{Cols: 1, Rows: 1}, Full: state != 0 && baseState == 0, Data: b,
43- })
44- if err != nil {
45- return wire.Frame{}, err
42+ Size: domain.Size{Cols: 1, Rows: 1}, Full: state != 0 && baseState == 0, Data: data,
4643 }
47- return wire.Frame{Type: wire.MsgOutput, Payload: payload}, nil
48-}
49-
50-func frameDetached(reason uint8) wire.Frame {
51- return wire.Frame{Type: wire.MsgDetached, Payload: wire.MarshalDetached(protocol.Detached{Reason: reason})}
52-}
53-
54-func framePong() wire.Frame {
55- return wire.Frame{Type: wire.MsgPong, Payload: wire.MarshalPong(protocol.Pong{})}
44+ if err := protocol.ValidateOutput(message); err != nil {
45+ return protocol.Output{}, err
46+ }
47+ return message, nil
5648 }
5749
58-func frameSessions(infos []protocol.SessionInfo) wire.Frame {
59- return wire.Frame{Type: wire.MsgSessions, Payload: wire.MarshalSessions(protocol.Sessions{Sessions: infos})}
50+func serverDetached(reason uint8) protocol.Detached { return protocol.Detached{Reason: reason} }
51+func serverPong() protocol.Pong { return protocol.Pong{} }
52+func serverSessions(infos []protocol.SessionInfo) protocol.Sessions {
53+ return protocol.Sessions{Sessions: infos}
6054 }
M internal/usecase/daemon/wire_metadata_test.go +2 -8
@@ -4,8 +4,6 @@ import (
44 "fmt"
55 "sync"
66 "testing"
7-
8- "github.com/bnema/vev/internal/protocol/wire"
97 )
108
119 func TestFrameWelcomeSnapshotsSessionMetadataUnderLock(t *testing.T) {
@@ -23,13 +21,9 @@ func TestFrameWelcomeSnapshotsSessionMetadataUnderLock(t *testing.T) {
2321 })
2422
2523 for range 1000 {
26- frame, frameErr := frameWelcome(sess, ac.resumeToken)
27- if frameErr != nil {
28- t.Fatalf("frameWelcome() error = %v", frameErr)
29- }
30- welcome, err := wire.UnmarshalWelcome(frame.Payload)
24+ welcome, err := serverWelcome(sess, ac.resumeToken)
3125 if err != nil {
32- t.Fatalf("UnmarshalWelcome() error = %v", err)
26+ t.Fatalf("serverWelcome() error = %v", err)
3327 }
3428 if welcome.SessionID != "session" || welcome.SessionName == "" {
3529 t.Fatalf("welcome metadata = %+v", welcome)

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