v0.3.0 99dc266

remote_preview_cache.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"slices"
8	"sync"
9	"time"
10
11	"github.com/bnema/vev/internal/domain"
12	"github.com/bnema/vev/internal/ports"
13)
14
15const (
16	remotePreviewCacheTTL = 5 * time.Second
17	remotePreviewCooldown = 1 * time.Second
18	remotePreviewCacheMax = 64
19	remotePreviewSlots    = 4
20)
21
22var errRemotePreviewCooldown = errors.New("remote preview request is cooling down")
23
24type remotePreviewCacheKey struct {
25	Endpoint      string
26	DisplayOrigin string
27	LifecycleID   domain.SessionLifecycleID
28	SessionName   string
29	LiveTabID     domain.TabStableID
30	Stopped       bool
31	StoppedTab    domain.TabSelector
32	Width         uint16
33	Height        uint16
34}
35
36type remotePreviewCacheEntry struct {
37	Preview ports.RemotePreview
38	Fetched time.Time
39	Used    time.Time
40}
41
42type remotePreviewFlight struct {
43	done    chan struct{}
44	preview ports.RemotePreview
45	err     error
46}
47
48type remotePreviewState struct {
49	mu        sync.Mutex
50	cache     map[remotePreviewCacheKey]remotePreviewCacheEntry
51	flights   map[remotePreviewCacheKey]*remotePreviewFlight
52	cooldowns map[remotePreviewCacheKey]time.Time
53	slots     chan struct{}
54}
55
56func cloneRemotePreview(preview ports.RemotePreview) ports.RemotePreview {
57	preview.Cells = slices.Clone(preview.Cells)
58	return preview
59}
60
61func (s *remotePreviewState) initializeLocked() {
62	if s.cache == nil {
63		s.cache = make(map[remotePreviewCacheKey]remotePreviewCacheEntry)
64	}
65	if s.flights == nil {
66		s.flights = make(map[remotePreviewCacheKey]*remotePreviewFlight)
67	}
68	if s.cooldowns == nil {
69		s.cooldowns = make(map[remotePreviewCacheKey]time.Time)
70	}
71	if s.slots == nil {
72		s.slots = make(chan struct{}, remotePreviewSlots)
73	}
74}
75
76func remotePreviewKeyFor(target domain.RemoteSessionTarget, width, height uint16) remotePreviewCacheKey {
77	return remotePreviewCacheKey{
78		Endpoint: target.Endpoint, DisplayOrigin: target.DisplayOrigin,
79		LifecycleID: target.LifecycleID, SessionName: target.SessionName,
80		LiveTabID: target.LiveTabID, Stopped: target.Stopped,
81		StoppedTab: target.StoppedTab, Width: width, Height: height,
82	}
83}
84
85func (d *Daemon) fetchRemotePreview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {
86	if d == nil || d.remotePreviewClient == nil {
87		return ports.RemotePreview{}, errors.New("remote preview client unavailable")
88	}
89	if ctx == nil {
90		ctx = context.Background()
91	}
92	if err := ctx.Err(); err != nil {
93		return ports.RemotePreview{}, err
94	}
95	request := ports.RemotePreviewRequest{
96		Version: ports.RemotePreviewSchemaVersion,
97		Target:  target,
98		Width:   width,
99		Height:  height,
100	}
101	if err := ports.ValidateRemotePreviewRequest(request); err != nil {
102		return ports.RemotePreview{}, err
103	}
104
105	key := remotePreviewKeyFor(target, width, height)
106	now := d.clock.Now()
107	d.remotePreview.mu.Lock()
108	d.remotePreview.initializeLocked()
109
110	if cached, ok := d.remotePreview.cache[key]; ok {
111		cached.Used = now
112		d.remotePreview.cache[key] = cached
113		if now.Sub(cached.Fetched) <= remotePreviewCacheTTL {
114			preview := cloneRemotePreview(cached.Preview)
115			d.remotePreview.mu.Unlock()
116			return preview, nil
117		}
118
119		// Expired data remains useful to the picker. Revalidation is detached
120		// from the navigation request so cursor movement never waits on remote
121		// I/O, and cooldowns prevent a failed host from being hammered.
122		if until, cooling := d.remotePreview.cooldowns[key]; cooling && now.Before(until) {
123			preview := cloneRemotePreview(cached.Preview)
124			d.remotePreview.mu.Unlock()
125			return preview, nil
126		}
127		if flight := d.remotePreview.flights[key]; flight != nil {
128			preview := cloneRemotePreview(cached.Preview)
129			d.remotePreview.mu.Unlock()
130			return preview, nil
131		}
132		flight := &remotePreviewFlight{done: make(chan struct{})}
133		d.remotePreview.flights[key] = flight
134		preview := cloneRemotePreview(cached.Preview)
135		d.remotePreview.mu.Unlock()
136		go d.runRemotePreviewFlight(d.remotePreviewContext(), key, target, width, height, flight)
137		return preview, nil
138	}
139
140	if until, cooling := d.remotePreview.cooldowns[key]; cooling && now.Before(until) {
141		d.remotePreview.mu.Unlock()
142		return ports.RemotePreview{}, errRemotePreviewCooldown
143	}
144	if flight := d.remotePreview.flights[key]; flight != nil {
145		d.remotePreview.mu.Unlock()
146		return waitRemotePreviewFlight(ctx, flight)
147	}
148	flight := &remotePreviewFlight{done: make(chan struct{})}
149	d.remotePreview.flights[key] = flight
150	d.remotePreview.mu.Unlock()
151
152	d.runRemotePreviewFlight(ctx, key, target, width, height, flight)
153	return cloneRemotePreview(flight.preview), flight.err
154}
155
156func (d *Daemon) remotePreviewContext() context.Context {
157	if d != nil && d.serveCtx != nil {
158		return d.serveCtx
159	}
160	return context.Background()
161}
162
163func waitRemotePreviewFlight(ctx context.Context, flight *remotePreviewFlight) (ports.RemotePreview, error) {
164	select {
165	case <-flight.done:
166		return cloneRemotePreview(flight.preview), flight.err
167	case <-ctx.Done():
168		return ports.RemotePreview{}, ctx.Err()
169	}
170}
171
172// awaitRemotePreviewRefresh lets a picker display expired content immediately
173// while still publishing the refreshed result once the same-key flight finishes.
174// A completed flight is read from the cache without starting a second remote
175// request.
176func (d *Daemon) awaitRemotePreviewRefresh(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {
177	key := remotePreviewKeyFor(target, width, height)
178	d.remotePreview.mu.Lock()
179	flight := d.remotePreview.flights[key]
180	d.remotePreview.mu.Unlock()
181	if flight != nil {
182		return waitRemotePreviewFlight(ctx, flight)
183	}
184	return d.fetchRemotePreview(ctx, target, width, height)
185}
186
187func (d *Daemon) runRemotePreviewFlight(ctx context.Context, key remotePreviewCacheKey, target domain.RemoteSessionTarget, width, height uint16, flight *remotePreviewFlight) {
188	select {
189	case d.remotePreview.slots <- struct{}{}:
190	case <-ctx.Done():
191		d.finishRemotePreviewFlight(key, flight, ports.RemotePreview{}, ctx.Err())
192		return
193	}
194	var preview ports.RemotePreview
195	var err error
196	defer func() {
197		if recovered := recover(); recovered != nil {
198			err = fmt.Errorf("remote preview flight panicked: %v", recovered)
199			preview = ports.RemotePreview{}
200			d.finishRemotePreviewFlight(key, flight, preview, err)
201			panic(recovered)
202		}
203		if err != nil {
204			preview = ports.RemotePreview{}
205		}
206		d.finishRemotePreviewFlight(key, flight, preview, err)
207	}()
208	defer func() { <-d.remotePreview.slots }()
209
210	preview, err = d.remotePreviewClient.Preview(ctx, target, width, height)
211	if err == nil {
212		if validationErr := ports.ValidateRemotePreview(preview); validationErr != nil {
213			err = validationErr
214		} else if preview.Status != ports.RemotePreviewOK {
215			err = fmt.Errorf("remote preview returned status %d", preview.Status)
216		} else if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {
217			err = errors.New("remote preview identity changed")
218		}
219	}
220}
221
222func (d *Daemon) finishRemotePreviewFlight(key remotePreviewCacheKey, flight *remotePreviewFlight, preview ports.RemotePreview, err error) {
223	now := d.clock.Now()
224	d.remotePreview.mu.Lock()
225	for cooldownKey, until := range d.remotePreview.cooldowns {
226		if !now.Before(until) {
227			delete(d.remotePreview.cooldowns, cooldownKey)
228		}
229	}
230	flight.preview = cloneRemotePreview(preview)
231	flight.err = err
232	delete(d.remotePreview.flights, key)
233	if err == nil {
234		d.remotePreview.cache[key] = remotePreviewCacheEntry{
235			Preview: cloneRemotePreview(preview), Fetched: now, Used: now,
236		}
237		delete(d.remotePreview.cooldowns, key)
238		for len(d.remotePreview.cache) > remotePreviewCacheMax {
239			var oldestKey remotePreviewCacheKey
240			var oldest time.Time
241			for candidate, entry := range d.remotePreview.cache {
242				used := entry.Used
243				if used.IsZero() {
244					used = entry.Fetched
245				}
246				if oldest.IsZero() || used.Before(oldest) {
247					oldestKey, oldest = candidate, used
248				}
249			}
250			delete(d.remotePreview.cache, oldestKey)
251		}
252	} else if !errors.Is(err, context.Canceled) {
253		d.remotePreview.cooldowns[key] = now.Add(remotePreviewCooldown)
254	}
255	close(flight.done)
256	d.remotePreview.mu.Unlock()
257}

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