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 sourceCapability *attachmentCapability
28 sourceEffect *attachmentEffect
29 action string
30 activateTargetTab bool
31 targetTabIndex int
32 copySourceEnvironment bool
33 // preserveAttachment commits Attachment-local navigation state without
34 // changing Session membership. The initiating capability remains the exact
35 // authority for this mutation.
36 preserveAttachment bool
37 expectedSourceTab *tab
38 // transferExpectedSourceTab permits an installed visible floating pane only
39 // when the caller atomically transfers this exact tab under its fences.
40 transferExpectedSourceTab bool
41 createTargetLocked func() (*session, error)
42 ready bool
43 expectedTargetLifecycle *attachmentLifecycleFence
44 preflighted bool
45 attachmentEffectsFrozen bool
46}
47
48type attachmentTransitionResult struct {
49 published attachmentCapability
50 cleanups []renderLifecycleCleanup
51 sourceGeometrySession *session
52}
53
54type attachmentTransitionParticipants struct {
55 clients []*attachedClient
56 interrupts []attachmentTransportInterrupt
57}
58
59// snapshotAttachmentTransition discovers the exact connection participant
60// while d.mu is held, then releases architecture locks before effect gates are
61// frozen and drained. Existing target attachments are never displaced.
62func (d *Daemon) snapshotAttachmentTransition(req attachmentTransitionRequest) (attachmentTransitionRequest, attachmentTransitionParticipants, error) {
63 d.mu.Lock()
64 defer d.mu.Unlock()
65
66 source := req.source
67 if source == nil {
68 source = req.target
69 }
70 if source == nil || (req.target == nil && req.createTargetLocked == nil) {
71 return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition
72 }
73 sourceCore := source.core()
74 var targetCore *sessionCore
75 if req.target != nil {
76 targetCore = req.target.core()
77 }
78 if sourceCore == nil || req.target != nil && targetCore == nil || req.next == nil || d.closing ||
79 d.sessions[sourceCore.id] != source || req.target != nil && d.sessions[targetCore.id] != req.target {
80 return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition
81 }
82 if req.sourceCapability != nil && !req.sourceCapability.matchesConnectionSnapshot(source, req.next, req.expectedTransport) {
83 return attachmentTransitionRequest{}, attachmentTransitionParticipants{}, errAttachmentTransition
84 }
85 req.preflighted = true
86 return req, attachmentTransitionParticipants{clients: []*attachedClient{req.next}}, nil
87}
88
89// freezeAttachmentTransition drains the snapshotted connection without any
90// architecture lock held. The returned guard remains frozen through publication.
91func (d *Daemon) freezeAttachmentTransition(req attachmentTransitionRequest, participants attachmentTransitionParticipants) (attachmentTransitionGuard, error) {
92 if d.afterAttachmentEffectParticipantsSnapshotted != nil {
93 d.afterAttachmentEffectParticipantsSnapshotted(req.action, participants.clients)
94 }
95 frozen := freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{interrupts: participants.interrupts, afterFrozen: func(ac *attachedClient) {
96 if d.afterAttachmentEffectGateFrozen != nil {
97 d.afterAttachmentEffectGateFrozen(req.action, ac)
98 }
99 }}, participants.clients...)
100 if !frozen.acquired || !frozen.drained {
101 return frozen, errAttachmentTransition
102 }
103 if d.afterAttachmentEffectsFrozen != nil {
104 d.afterAttachmentEffectsFrozen()
105 }
106 return frozen, nil
107}
108
109// publishAttachmentTransition revalidates the frozen source, creates an
110// optional target, and performs exact capability publication under d.mu.
111func (d *Daemon) publishAttachmentTransition(req attachmentTransitionRequest) (attachmentTransitionResult, error) {
112 d.mu.Lock()
113 defer d.mu.Unlock()
114
115 req.attachmentEffectsFrozen = true
116 if req.target == nil {
117 if !d.transitionSourcePreflightLocked(req) {
118 return attachmentTransitionResult{}, errAttachmentTransition
119 }
120 var err error
121 req.target, err = req.createTargetLocked()
122 if err != nil {
123 return attachmentTransitionResult{}, err
124 }
125 }
126 result, err := d.transitionAttachmentLocked(req)
127 if err != nil {
128 return result, err
129 }
130 if req.source != nil && req.target != nil && req.source != req.target {
131 result.sourceGeometrySession = req.source
132 }
133 return result, nil
134}
135
136func (d *Daemon) transitionAttachment(req attachmentTransitionRequest) (attachmentTransitionResult, error) {
137 req, participants, err := d.snapshotAttachmentTransition(req)
138 if err != nil {
139 return attachmentTransitionResult{}, err
140 }
141 if req.sourceEffect != nil {
142 d.endActionAttachmentEffect(req.sourceEffect, req.action)
143 }
144
145 frozen, err := d.freezeAttachmentTransition(req, participants)
146 if err != nil {
147 frozen.unfreeze()
148 return attachmentTransitionResult{}, err
149 }
150
151 result, err := d.publishAttachmentTransition(req)
152 frozen.unfreeze()
153 if err != nil {
154 return result, err
155 }
156 return d.finishAttachmentTransition(req, result)
157}
158
159// finishAttachmentTransition shares committed identity and geometry effects with
160// composite topology transactions. It runs after publication locks are released.
161func (d *Daemon) finishAttachmentTransition(req attachmentTransitionRequest, result attachmentTransitionResult) (attachmentTransitionResult, error) {
162 // A not-ready publication is still inside the Hello handshake. Welcome
163 // carries the committed identity for those attachments and must remain the
164 // first server frame. Ready transitions notify an already-running client.
165 if req.ready && result.published.ac != nil && result.published.ac.routeSnapshotCopy().Generation != 0 {
166 if d.beforeAttachmentTransitionIdentityAdmission != nil {
167 d.beforeAttachmentTransitionIdentityAdmission(result.published)
168 }
169 identityErr := errAttachmentTransition
170 if effect, admitted := result.published.ac.beginAttachmentEffect(result.published); admitted {
171 identityErr = d.sendCommittedRouteIdentityForAttachment(effect)
172 effect.End()
173 } else if token, effect, admitted := admitCurrentAttachmentCapabilityEffect(result.published); admitted {
174 // The exact published capability stopped being admissible while its
175 // attachment stayed live: a concurrent transition or link replacement
176 // superseded it after publication released the architecture locks. The
177 // committed identity is still owed to that live attachment, so re-admit
178 // its current capability and report it back so the caller's first paint
179 // and correlated completion use the same authority. A detached attachment
180 // or one whose current session no longer registers this link admits
181 // nothing and fails closed below; no stale transport is retried.
182 result.published = token
183 identityErr = d.sendCommittedRouteIdentityForAttachment(effect)
184 effect.End()
185 }
186 if identityErr != nil {
187 d.abortPublishedAttachmentTransition(result)
188 return attachmentTransitionResult{}, identityErr
189 }
190 }
191 if result.sourceGeometrySession != nil {
192 result.sourceGeometrySession.geometry.reconcileAndInvalidateAsync(d, result.sourceGeometrySession, "attachment_transition.go")
193 }
194 return result, nil
195}
196
197// abortPublishedAttachmentTransition tears down the newly published link before
198// a post-publication control failure escapes to callers. The transition result is
199// deliberately not handed back because its target membership is no longer valid.
200func (d *Daemon) abortPublishedAttachmentTransition(result attachmentTransitionResult) {
201 if result.published.ac != nil && result.published.transport.transport != nil {
202 d.clientGoneWithoutNotice(result.published.sess, result.published.ac, result.published.transport.transport, true)
203 }
204 d.deferAttachmentTransitionCleanups(result)
205}
206
207func (d *Daemon) deferAttachmentTransitionCleanups(result attachmentTransitionResult) {
208 for _, cleanup := range result.cleanups {
209 d.attachmentCleanupWg.Go(cleanup.finish)
210 }
211}