v0.3.0 99dc266

attachment_transition.go RawHistoryFolder

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}

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