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