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	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}

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