1package daemon
2
3import (
4 "errors"
5
6 "github.com/bnema/vev/internal/domain"
7)
8
9var errAttachmentTransition = errors.New("attachment transition is no longer valid")
10
11type attachmentLifecycleFence struct {
12 name string
13 createdAt int64
14 checkCreatedAt bool
15 incarnation domain.IncarnationID
16 checkIncarnation bool
17 tabID domain.TabStableID
18 tabIndex int
19 checkTab bool
20}
21
22type attachmentTransitionRequest struct {
23 source *session
24 target *session
25 next *attachedClient
26 expectedTransport transportSnapshot
27 sourceToken *attachmentConnectionToken
28 action string
29 activateTargetTab bool
30 targetTabIndex int
31 copySourceEnvironment bool
32 // preserveAttachment commits attachment-local navigation state without
33 // changing session membership. The initiating connection token remains the
34 // exact authority for this mutation.
35 preserveAttachment bool
36 expectedSourceTab *tab
37 // transferExpectedSourceTab permits an installed visible floating pane only
38 // when the caller atomically transfers this exact tab under its fences.
39 transferExpectedSourceTab bool
40 createTargetLocked func() (*session, error)
41 ready bool
42 expectedTargetLifecycle *attachmentLifecycleFence
43 preflighted bool
44 attachmentEffectsFrozen bool
45}
46
47func transitionSourceTokenMatchesRequest(token attachmentConnectionToken, source *session, req attachmentTransitionRequest) bool {
48 return source != nil && token.sess != nil && token.sess == source && token.ac == req.next &&
49 token.generation == req.next.connectionGeneration.Load() &&
50 token.transport.transport == req.expectedTransport.transport &&
51 token.transport.incarnation == req.expectedTransport.incarnation
52}
53
54// transitionSourceTokenCurrentLocked requires source.core().mu and, when a
55// lease exists, sourceCoordinator.mu. It is the canonical exact-connection
56// handoff check for client-originated navigation and lifecycle mutations.
57func transitionSourceTokenCurrentLocked(token attachmentConnectionToken, source *session, sourceCoordinator *renderCoordinator, req attachmentTransitionRequest) bool {
58 if token.sess != source || token.ac != req.next ||
59 token.generation != req.next.connectionGeneration.Load() ||
60 token.sess == nil || token.ac.currentAttachmentSession() != source ||
61 !attachmentRegisteredLocked(source, req.next) ||
62 !req.next.transportSnapshotCurrent(token.transport) {
63 return false
64 }
65 if token.lease == nil {
66 return true
67 }
68 return sourceCoordinator != nil && token.lease.attachment == req.next &&
69 sourceCoordinator.leaseCurrentLocked(token.lease, true)
70}
71
72type attachmentTransitionResult struct {
73 published attachmentConnectionToken
74 cleanups []renderLifecycleCleanup
75 sourceGeometrySession *session
76}
77
78type attachmentTransitionParticipants struct {
79 clients []*attachedClient
80 interrupts []attachmentTransportInterrupt
81}
82
83// snapshotAttachmentTransition discovers the exact connection participant
84// while d.mu is held, then releases architecture locks before effect gates are
85// frozen and drained. Existing target attachments are never displaced.
86func (d *Daemon) snapshotAttachmentTransition(req attachmentTransitionRequest) (attachmentTransitionRequest, attachmentTransitionParticipants, error) {
87 d.mu.Lock()
88 defer d.mu.Unlock()
89
90 source := req.source
91 if source == nil {
92 source = req.target
93 }
94 if source == nil || (req.target == nil && req.createTargetLocked == nil) {
95 return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition
96 }
97 sourceCore := source.core()
98 var targetCore *sessionCore
99 if req.target != nil {
100 targetCore = req.target.core()
101 }
102 if sourceCore == nil || req.target != nil && targetCore == nil || req.next == nil || d.closing ||
103 d.sessions[sourceCore.id] != source || req.target != nil && d.sessions[targetCore.id] != req.target {
104 return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition
105 }
106 if req.sourceToken != nil && !transitionSourceTokenMatchesRequest(*req.sourceToken, source, req) {
107 return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition
108 }
109 req.preflighted = true
110 return req, attachmentTransitionParticipants{clients: []*attachedClient{req.next}}, nil
111}
112
113// freezeAttachmentTransition drains the snapshotted connection without any
114// architecture lock held. The returned guard remains frozen through publication.
115func (d *Daemon) freezeAttachmentTransition(req attachmentTransitionRequest, participants attachmentTransitionParticipants) (frozenAttachmentEffectGates, error) {
116 if d.afterAttachmentEffectParticipantsSnapshotted != nil {
117 d.afterAttachmentEffectParticipantsSnapshotted(req.action, participants.clients)
118 }
119 frozen := freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{interrupts: participants.interrupts, afterFrozen: func(ac *attachedClient) {
120 if d.afterAttachmentEffectGateFrozen != nil {
121 d.afterAttachmentEffectGateFrozen(req.action, ac)
122 }
123 }}, participants.clients...)
124 if !frozen.acquired || !frozen.drained {
125 return frozen, errAttachmentTransition
126 }
127 if d.afterAttachmentEffectsFrozen != nil {
128 d.afterAttachmentEffectsFrozen()
129 }
130 return frozen, nil
131}
132
133// publishAttachmentTransition revalidates the frozen source, creates an
134// optional target, and performs exact membership publication under d.mu.
135func (d *Daemon) publishAttachmentTransition(req attachmentTransitionRequest) (attachmentTransitionResult, error) {
136 d.mu.Lock()
137 defer d.mu.Unlock()
138
139 req.attachmentEffectsFrozen = true
140 if req.target == nil {
141 if !d.transitionSourcePreflightLocked(req) {
142 return attachmentTransitionResult{}, errAttachmentTransition
143 }
144 var err error
145 req.target, err = req.createTargetLocked()
146 if err != nil {
147 return attachmentTransitionResult{}, err
148 }
149 }
150 result, err := d.transitionAttachmentLocked(req)
151 if err != nil {
152 return result, err
153 }
154 if req.source != nil && req.target != nil && req.source != req.target {
155 result.sourceGeometrySession = req.source
156 }
157 return result, nil
158}
159
160func (d *Daemon) transitionAttachment(req attachmentTransitionRequest) (attachmentTransitionResult, error) {
161 req, participants, err := d.snapshotAttachmentTransition(req)
162 if err != nil {
163 return attachmentTransitionResult{}, err
164 }
165 if req.sourceToken != nil {
166 d.endActionAttachmentEffect(req.sourceToken.effect, req.action)
167 }
168
169 frozen, err := d.freezeAttachmentTransition(req, participants)
170 if err != nil {
171 frozen.unfreeze()
172 return attachmentTransitionResult{}, err
173 }
174
175 result, err := d.publishAttachmentTransition(req)
176 frozen.unfreeze()
177 if err != nil {
178 return result, err
179 }
180 // A not-ready publication is still inside the Hello handshake. Welcome
181 // carries the committed identity for those attachments and must remain the
182 // first server frame. Ready transitions notify an already-running client.
183 if req.ready && result.published.ac != nil && result.published.ac.routeSnapshotCopy().Generation != 0 {
184 identityErr := errAttachmentTransition
185 if effect, admitted := result.published.ac.beginAttachmentEffect(result.published); admitted {
186 identityErr = d.sendCommittedRouteIdentityForAttachment(effect.connectionToken())
187 effect.End()
188 }
189 if identityErr != nil {
190 d.abortPublishedAttachmentTransition(result)
191 return attachmentTransitionResult{}, identityErr
192 }
193 }
194 if result.sourceGeometrySession != nil {
195 d.recalculateSessionGeometryAndInvalidateAsync(result.sourceGeometrySession, "attachment_transition.go")
196 }
197 return result, nil
198}
199
200// abortPublishedAttachmentTransition tears down the newly published link before
201// a post-publication control failure escapes to callers. The transition result is
202// deliberately not handed back because its target membership is no longer valid.
203func (d *Daemon) abortPublishedAttachmentTransition(result attachmentTransitionResult) {
204 if result.published.ac != nil && result.published.transport.transport != nil {
205 d.clientGoneWithoutNotice(result.published.sess, result.published.ac, result.published.transport.transport, true)
206 }
207 d.deferAttachmentTransitionCleanups(result)
208}
209
210func (d *Daemon) deferAttachmentTransitionCleanups(result attachmentTransitionResult) {
211 for _, cleanup := range result.cleanups {
212 d.attachmentCleanupWg.Go(cleanup.finish)
213 }
214}