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

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