1// Package bootstrap provides initialization utilities for the browser.
2package bootstrap
3
4import (
5 "context"
6 "database/sql"
7 "fmt"
8 "os"
9 "path/filepath"
10 "sync"
11 "time"
12
13 "github.com/bnema/dumber/internal/application/usecase"
14 "github.com/bnema/dumber/internal/infrastructure/config"
15 "github.com/bnema/dumber/internal/infrastructure/deps"
16 "github.com/bnema/dumber/internal/infrastructure/media"
17 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
18 "github.com/bnema/dumber/internal/logging"
19 "github.com/bnema/dumber/internal/ui/theme"
20 sqlite3 "github.com/ncruces/go-sqlite3"
21)
22
23// DatabaseResult holds database connection and cleanup function.
24type DatabaseResult struct {
25 DB *sql.DB
26 Cleanup func()
27}
28
29// ParallelInitResult holds the results of parallel initialization phase.
30type ParallelInitResult struct {
31 DataDir string
32 CacheDir string
33 ThemeManager *theme.Manager
34 Duration time.Duration
35}
36
37// DeferredInitResult holds results from deferred initialization checks.
38type DeferredInitResult struct {
39 RuntimeErr error
40 MediaErr error
41 SQLiteErr error
42 Duration time.Duration
43}
44
45// ParallelInitInput holds the input for parallel initialization.
46type ParallelInitInput struct {
47 Ctx context.Context
48 Config *config.Config
49}
50
51// DeferredInitInput holds the input for deferred initialization.
52type DeferredInitInput struct {
53 Ctx context.Context
54 Config *config.Config
55}
56
57// RuntimeRequirementsError contains details about missing runtime dependencies.
58type RuntimeRequirementsError struct {
59 Checks []usecase.RuntimeDependencyStatus
60}
61
62func (*RuntimeRequirementsError) Error() string {
63 return "runtime requirements not met"
64}
65
66// LogDetails logs detailed information about missing dependencies.
67func (e *RuntimeRequirementsError) LogDetails(ctx context.Context) {
68 log := logging.FromContext(ctx)
69 for _, c := range e.Checks {
70 if c.Installed {
71 log.Error().
72 Str("dependency", c.PkgConfigName).
73 Str("have", c.Version).
74 Str("need", c.RequiredVersion).
75 Bool("ok", c.MeetsRequirement).
76 Msg("runtime dependency")
77 } else {
78 log.Error().
79 Str("dependency", c.PkgConfigName).
80 Str("need", c.RequiredVersion).
81 Msg("runtime dependency missing")
82 }
83 }
84}
85
86// RunParallelInit runs the essential parallel initialization phase.
87// This includes directory resolution and theme creation.
88// Returns the first fatal error encountered, or nil with the results.
89func RunParallelInit(input ParallelInitInput) (*ParallelInitResult, error) {
90 var (
91 dataDir, cacheDir string
92 themeManager *theme.Manager
93 dirsErr error
94 wg sync.WaitGroup
95 )
96
97 start := time.Now()
98 wg.Add(2)
99
100 // Resolve directories
101 go func() {
102 defer wg.Done()
103 dataDir, cacheDir, dirsErr = resolveWebKitDirs()
104 }()
105
106 // Theme manager (CPU-bound, no I/O)
107 go func() {
108 defer wg.Done()
109 themeManager = theme.NewManager(input.Ctx, input.Config)
110 }()
111
112 wg.Wait()
113 duration := time.Since(start)
114
115 // Return first fatal error
116 if dirsErr != nil {
117 return nil, fmt.Errorf("resolve directories: %w", dirsErr)
118 }
119
120 return &ParallelInitResult{
121 DataDir: dataDir,
122 CacheDir: cacheDir,
123 ThemeManager: themeManager,
124 Duration: duration,
125 }, nil
126}
127
128// RunDeferredInit runs deferred initialization checks off the critical path.
129// This includes SQLite WASM precompile, runtime requirements, and media checks.
130func RunDeferredInit(input DeferredInitInput) DeferredInitResult {
131 var (
132 runtimeErr error
133 mediaErr error
134 sqliteErr error
135 wg sync.WaitGroup
136 )
137
138 start := time.Now()
139 wg.Add(3)
140
141 go func() {
142 defer wg.Done()
143 sqliteErr = sqlite3.Initialize()
144 }()
145
146 go func() {
147 defer wg.Done()
148 runtimeErr = CheckRuntimeRequirements(input.Ctx, input.Config)
149 }()
150
151 go func() {
152 defer wg.Done()
153 mediaErr = CheckMediaRequirements(input.Ctx, input.Config)
154 }()
155
156 wg.Wait()
157
158 return DeferredInitResult{
159 RuntimeErr: runtimeErr,
160 MediaErr: mediaErr,
161 SQLiteErr: sqliteErr,
162 Duration: time.Since(start),
163 }
164}
165
166// CheckRuntimeRequirements verifies WebKitGTK and other runtime dependencies.
167// Returns error if requirements are not met; caller should log details and exit.
168func CheckRuntimeRequirements(ctx context.Context, cfg *config.Config) error {
169 probe := deps.NewPkgConfigProbe()
170 checkRuntimeUC := usecase.NewCheckRuntimeDependenciesUseCase(probe)
171 runtimeOut, err := checkRuntimeUC.Execute(ctx, usecase.CheckRuntimeDependenciesInput{
172 Prefix: cfg.Runtime.Prefix,
173 })
174 if err != nil {
175 return fmt.Errorf("runtime check execution failed: %w", err)
176 }
177 if runtimeOut.OK {
178 return nil
179 }
180 return &RuntimeRequirementsError{Checks: runtimeOut.Checks}
181}
182
183// CheckMediaRequirements verifies GStreamer and media codec availability.
184// Returns error if media subsystem initialization fails.
185func CheckMediaRequirements(ctx context.Context, cfg *config.Config) error {
186 mediaDiagAdapter := media.New()
187 checkMediaUC := usecase.NewCheckMediaUseCase(mediaDiagAdapter)
188 if _, err := checkMediaUC.Execute(ctx, usecase.CheckMediaInput{
189 ShowDiagnostics: cfg.Media.ShowDiagnosticsOnStartup,
190 }); err != nil {
191 return fmt.Errorf("media check failed: %w", err)
192 }
193 return nil
194}
195
196// resolveWebKitDirs resolves and creates the data and cache directories for WebKit.
197func resolveWebKitDirs() (dataDir, cacheDir string, err error) {
198 const cacheDirPerm = 0o755
199 dataDir, err = config.GetDataDir()
200 if err != nil {
201 return "", "", fmt.Errorf("resolve data directory: %w", err)
202 }
203 stateDir, err := config.GetStateDir()
204 if err != nil {
205 return "", "", fmt.Errorf("resolve state directory: %w", err)
206 }
207 cacheDir = filepath.Join(stateDir, "webkit-cache")
208 if mkErr := os.MkdirAll(cacheDir, cacheDirPerm); mkErr != nil {
209 return "", "", fmt.Errorf("create cache directory %s: %w", cacheDir, mkErr)
210 }
211 return dataDir, cacheDir, nil
212}
213
214// OpenDatabase opens and initializes the SQLite database using XDG paths.
215// Returns error if database cannot be opened or migrations fail.
216func OpenDatabase(ctx context.Context) (*DatabaseResult, error) {
217 dbPath, err := config.GetDatabaseFile()
218 if err != nil {
219 return nil, fmt.Errorf("resolve database path: %w", err)
220 }
221 db, err := sqlite.NewConnection(ctx, dbPath)
222 if err != nil {
223 return nil, fmt.Errorf("initialize database at %s: %w", dbPath, err)
224 }
225 return &DatabaseResult{
226 DB: db,
227 Cleanup: func() {
228 _ = sqlite.Close(db)
229 },
230 }, nil
231}
232
233// CreateLazyDatabase creates a lazy database provider that defers initialization.
234// The database is initialized on first access, allowing the UI to render faster.
235//
236// Currently unused: the application uses OpenDatabase with RunParallelDBWebKit for
237// eager initialization. This function is kept for potential future use when lazy
238// initialization past first paint becomes beneficial.
239func CreateLazyDatabase() (*sqlite.LazyDB, error) {
240 dbPath, err := config.GetDatabaseFile()
241 if err != nil {
242 return nil, fmt.Errorf("resolve database path: %w", err)
243 }
244 return sqlite.NewLazyDB(dbPath), nil
245}
246
247// ParallelDBWebKitResult holds results from parallel DB and WebKit initialization.
248type ParallelDBWebKitResult struct {
249 DB *sql.DB
250 DBCleanup func()
251 Stack WebKitStack
252}
253
254// ParallelDBWebKitInput holds inputs for parallel DB and WebKit initialization.
255type ParallelDBWebKitInput struct {
256 Ctx context.Context
257 Config *config.Config
258 DataDir string // For WebKit context
259 CacheDir string // For WebKit cache
260 ThemeManager *theme.Manager
261}
262
263// Note: Database path is resolved via config.GetDatabaseFile() internally.
264
265// dbInitResult holds the result of database initialization for channel communication.
266type dbInitResult struct {
267 db *sql.DB
268 cleanup func()
269 err error
270}
271
272// RunParallelDBWebKit initializes database in background while WebKit runs on main thread.
273// Database init happens in goroutine (pure Go/WASM), WebKit must stay on main thread (GTK).
274// This saves ~150ms by overlapping DB migrations with WebKit context creation.
275func RunParallelDBWebKit(input ParallelDBWebKitInput) (*ParallelDBWebKitResult, error) {
276 log := logging.FromContext(input.Ctx)
277
278 dbCh := make(chan dbInitResult, 1)
279
280 // Database initialization in background (WASM is thread-safe)
281 go func() {
282 dbResult, err := OpenDatabase(input.Ctx)
283 if err != nil {
284 dbCh <- dbInitResult{err: err}
285 return
286 }
287 dbCh <- dbInitResult{
288 db: dbResult.DB,
289 cleanup: dbResult.Cleanup,
290 }
291 }()
292
293 // WebKit stack on main thread (GTK requirement)
294 stack := BuildWebKitStack(input.Ctx, input.Config, input.DataDir, input.CacheDir, input.ThemeManager, *log)
295
296 // Wait for database
297 dbRes := <-dbCh
298
299 if dbRes.err != nil {
300 return nil, fmt.Errorf("database initialization: %w", dbRes.err)
301 }
302
303 return &ParallelDBWebKitResult{
304 DB: dbRes.db,
305 DBCleanup: dbRes.cleanup,
306 Stack: stack,
307 }, nil
308}