detached 440f59b

feat(cef): add WebViewPool with simplified lifecycle

WebViewPool implements port.WebViewPool with acquire/release semantics.
Phase 1 uses destroy-on-release (no reuse). Includes prewarm support
for pre-creating WebViews to speed up tab creation.
commit
440f59b45ae53070451b93b4debba8856be17fef
author
brice · · 2026-03-19 15:41 UTC
committer
brice · · 2026-03-28 04:35 UTC
parent
1ef6a99
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +121 -0
A internal/infrastructure/cef/pool.go +121 -0
@@ -0,0 +1,121 @@
1+package cef
2+
3+import (
4+ "context"
5+ "errors"
6+ "sync"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+)
10+
11+// Compile-time interface check.
12+var _ port.WebViewPool = (*WebViewPool)(nil)
13+
14+// errPoolClosed is returned when an operation is attempted on a closed pool.
15+var errPoolClosed = errors.New("cef: webview pool is closed")
16+
17+// WebViewPool manages a pool of pre-created WebViews for fast tab creation.
18+// In Phase 1 the pool is simple: Acquire pops or creates, Release destroys.
19+type WebViewPool struct {
20+ factory *WebViewFactory
21+ mu sync.Mutex
22+ pool []*WebView
23+ closed bool
24+}
25+
26+// newWebViewPool returns a pool backed by the given factory.
27+func newWebViewPool(factory *WebViewFactory) *WebViewPool {
28+ return &WebViewPool{
29+ factory: factory,
30+ }
31+}
32+
33+// Acquire obtains a WebView from the pool. If a pre-warmed WebView is
34+// available it is returned immediately; otherwise a new one is created.
35+func (p *WebViewPool) Acquire(ctx context.Context) (port.WebView, error) {
36+ p.mu.Lock()
37+ if p.closed {
38+ p.mu.Unlock()
39+ return nil, errPoolClosed
40+ }
41+
42+ if len(p.pool) > 0 {
43+ wv := p.pool[len(p.pool)-1]
44+ p.pool = p.pool[:len(p.pool)-1]
45+ p.mu.Unlock()
46+ wv.generation.Add(1)
47+ return wv, nil
48+ }
49+ p.mu.Unlock()
50+
51+ return p.factory.Create(ctx)
52+}
53+
54+// Release destroys the WebView. In Phase 1 there is no reuse — the browser
55+// is closed and all resources are freed immediately.
56+func (p *WebViewPool) Release(wv port.WebView) {
57+ if wv == nil {
58+ return
59+ }
60+ wv.Destroy()
61+}
62+
63+// Prewarm creates count WebViews synchronously and adds them to the pool.
64+func (p *WebViewPool) Prewarm(count int) {
65+ if count <= 0 {
66+ return
67+ }
68+
69+ ctx := context.Background()
70+ views := make([]*WebView, 0, count)
71+ for range count {
72+ wv, err := p.factory.Create(ctx)
73+ if err != nil {
74+ continue
75+ }
76+ if cefWV, ok := wv.(*WebView); ok {
77+ views = append(views, cefWV)
78+ }
79+ }
80+
81+ p.mu.Lock()
82+ defer p.mu.Unlock()
83+ if p.closed {
84+ for _, wv := range views {
85+ wv.Destroy()
86+ }
87+ return
88+ }
89+ p.pool = append(p.pool, views...)
90+}
91+
92+// PrewarmAsync schedules WebView creation. In Phase 1 this calls Prewarm
93+// synchronously; a future phase will use the GTK idle loop to avoid blocking.
94+// TODO(phase2): schedule creation via glib.IdleAdd for non-blocking startup.
95+func (p *WebViewPool) PrewarmAsync(_ context.Context, count int) {
96+ p.Prewarm(count)
97+}
98+
99+// Size returns the number of available WebViews currently in the pool.
100+func (p *WebViewPool) Size() int {
101+ p.mu.Lock()
102+ defer p.mu.Unlock()
103+ return len(p.pool)
104+}
105+
106+// Close shuts down the pool and destroys all pooled WebViews.
107+func (p *WebViewPool) Close() {
108+ p.mu.Lock()
109+ if p.closed {
110+ p.mu.Unlock()
111+ return
112+ }
113+ p.closed = true
114+ views := p.pool
115+ p.pool = nil
116+ p.mu.Unlock()
117+
118+ for _, wv := range views {
119+ wv.Destroy()
120+ }
121+}
@@ -0,0 +1,121 @@
1+package cef
2+
3+import (
4+ "context"
5+ "errors"
6+ "sync"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+)
10+
11+// Compile-time interface check.
12+var _ port.WebViewPool = (*WebViewPool)(nil)
13+
14+// errPoolClosed is returned when an operation is attempted on a closed pool.
15+var errPoolClosed = errors.New("cef: webview pool is closed")
16+
17+// WebViewPool manages a pool of pre-created WebViews for fast tab creation.
18+// In Phase 1 the pool is simple: Acquire pops or creates, Release destroys.
19+type WebViewPool struct {
20+ factory *WebViewFactory
21+ mu sync.Mutex
22+ pool []*WebView
23+ closed bool
24+}
25+
26+// newWebViewPool returns a pool backed by the given factory.
27+func newWebViewPool(factory *WebViewFactory) *WebViewPool {
28+ return &WebViewPool{
29+ factory: factory,
30+ }
31+}
32+
33+// Acquire obtains a WebView from the pool. If a pre-warmed WebView is
34+// available it is returned immediately; otherwise a new one is created.
35+func (p *WebViewPool) Acquire(ctx context.Context) (port.WebView, error) {
36+ p.mu.Lock()
37+ if p.closed {
38+ p.mu.Unlock()
39+ return nil, errPoolClosed
40+ }
41+
42+ if len(p.pool) > 0 {
43+ wv := p.pool[len(p.pool)-1]
44+ p.pool = p.pool[:len(p.pool)-1]
45+ p.mu.Unlock()
46+ wv.generation.Add(1)
47+ return wv, nil
48+ }
49+ p.mu.Unlock()
50+
51+ return p.factory.Create(ctx)
52+}
53+
54+// Release destroys the WebView. In Phase 1 there is no reuse — the browser
55+// is closed and all resources are freed immediately.
56+func (p *WebViewPool) Release(wv port.WebView) {
57+ if wv == nil {
58+ return
59+ }
60+ wv.Destroy()
61+}
62+
63+// Prewarm creates count WebViews synchronously and adds them to the pool.
64+func (p *WebViewPool) Prewarm(count int) {
65+ if count <= 0 {
66+ return
67+ }
68+
69+ ctx := context.Background()
70+ views := make([]*WebView, 0, count)
71+ for range count {
72+ wv, err := p.factory.Create(ctx)
73+ if err != nil {
74+ continue
75+ }
76+ if cefWV, ok := wv.(*WebView); ok {
77+ views = append(views, cefWV)
78+ }
79+ }
80+
81+ p.mu.Lock()
82+ defer p.mu.Unlock()
83+ if p.closed {
84+ for _, wv := range views {
85+ wv.Destroy()
86+ }
87+ return
88+ }
89+ p.pool = append(p.pool, views...)
90+}
91+
92+// PrewarmAsync schedules WebView creation. In Phase 1 this calls Prewarm
93+// synchronously; a future phase will use the GTK idle loop to avoid blocking.
94+// TODO(phase2): schedule creation via glib.IdleAdd for non-blocking startup.
95+func (p *WebViewPool) PrewarmAsync(_ context.Context, count int) {
96+ p.Prewarm(count)
97+}
98+
99+// Size returns the number of available WebViews currently in the pool.
100+func (p *WebViewPool) Size() int {
101+ p.mu.Lock()
102+ defer p.mu.Unlock()
103+ return len(p.pool)
104+}
105+
106+// Close shuts down the pool and destroys all pooled WebViews.
107+func (p *WebViewPool) Close() {
108+ p.mu.Lock()
109+ if p.closed {
110+ p.mu.Unlock()
111+ return
112+ }
113+ p.closed = true
114+ views := p.pool
115+ p.pool = nil
116+ p.mu.Unlock()
117+
118+ for _, wv := range views {
119+ wv.Destroy()
120+ }
121+}

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 8

Tags 42

↑↓ moveenter openesc close