lazy_repos.go RawHistoryFolder

1// Package sqlite provides SQLite implementations of domain repositories.
2//
3// # Lazy Repository Infrastructure
4//
5// This file contains lazy-loading repository wrappers that defer database initialization
6// until first access. While currently unused (the application uses eager initialization
7// via OpenDatabase in RunParallelDBWebKit), this infrastructure is kept for potential
8// future optimization scenarios:
9//
10//   - CLI commands that may not need database access
11//   - Further startup latency reduction by deferring DB init past first paint
12//   - Testing with mock DatabaseProvider implementations
13//
14// The lazy wrappers implement the same repository interfaces as their eager counterparts,
15// making them drop-in replacements when lazy initialization becomes beneficial.
16package sqlite
17
18import (
19	"context"
20	"database/sql"
21	"fmt"
22	"sync"
23	"time"
24
25	"github.com/bnema/dumber/internal/application/port"
26	"github.com/bnema/dumber/internal/domain/entity"
27	"github.com/bnema/dumber/internal/domain/repository"
28)
29
30// LazyHistoryRepository wraps a history repository with lazy database initialization.
31type LazyHistoryRepository struct {
32	provider port.DatabaseProvider
33	repo     repository.HistoryRepository
34	once     sync.Once
35	initErr  error
36}
37
38// NewLazyHistoryRepository creates a lazy-loading history repository.
39func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.HistoryRepository {
40	return &LazyHistoryRepository{provider: provider}
41}
42
43// LazyPermissionRepository wraps a permission repository with lazy database initialization.
44type LazyPermissionRepository struct {
45	provider port.DatabaseProvider
46	repo     port.PermissionRepository
47	once     sync.Once
48	initErr  error
49}
50
51// NewLazyPermissionRepository creates a lazy-loading permission repository.
52func NewLazyPermissionRepository(provider port.DatabaseProvider) port.PermissionRepository {
53	return &LazyPermissionRepository{provider: provider}
54}
55
56func (r *LazyPermissionRepository) init(ctx context.Context) error {
57	r.once.Do(func() {
58		db, err := r.provider.DB(ctx)
59		if err != nil {
60			r.initErr = err
61			return
62		}
63		r.repo = NewPermissionRepository(db)
64	})
65	return r.initErr
66}
67
68func (r *LazyPermissionRepository) Get(
69	ctx context.Context,
70	origin string,
71	permType entity.PermissionType,
72) (*entity.PermissionRecord, error) {
73	if err := r.init(ctx); err != nil {
74		return nil, err
75	}
76	return r.repo.Get(ctx, origin, permType)
77}
78
79func (r *LazyPermissionRepository) Set(ctx context.Context, record *entity.PermissionRecord) error {
80	if err := r.init(ctx); err != nil {
81		return err
82	}
83	return r.repo.Set(ctx, record)
84}
85
86func (r *LazyPermissionRepository) Delete(ctx context.Context, origin string, permType entity.PermissionType) error {
87	if err := r.init(ctx); err != nil {
88		return err
89	}
90	return r.repo.Delete(ctx, origin, permType)
91}
92
93func (r *LazyPermissionRepository) GetAll(ctx context.Context, origin string) ([]*entity.PermissionRecord, error) {
94	if err := r.init(ctx); err != nil {
95		return nil, err
96	}
97	return r.repo.GetAll(ctx, origin)
98}
99
100func (r *LazyHistoryRepository) init(ctx context.Context) error {
101	r.once.Do(func() {
102		db, err := r.provider.DB(ctx)
103		if err != nil {
104			r.initErr = err
105			return
106		}
107		r.repo = NewHistoryRepository(db)
108	})
109	return r.initErr
110}
111
112func (r *LazyHistoryRepository) Save(ctx context.Context, entry *entity.HistoryEntry) error {
113	if err := r.init(ctx); err != nil {
114		return err
115	}
116	return r.repo.Save(ctx, entry)
117}
118
119func (r *LazyHistoryRepository) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) {
120	if err := r.init(ctx); err != nil {
121		return nil, err
122	}
123	return r.repo.FindByURL(ctx, url)
124}
125
126func (r *LazyHistoryRepository) UpdateMetadata(ctx context.Context, entry *entity.HistoryEntry) error {
127	if err := r.init(ctx); err != nil {
128		return err
129	}
130	if updater, ok := r.repo.(interface {
131		UpdateMetadata(context.Context, *entity.HistoryEntry) error
132	}); ok {
133		return updater.UpdateMetadata(ctx, entry)
134	}
135	return r.repo.Save(ctx, entry)
136}
137
138func (r *LazyHistoryRepository) Search(
139	ctx context.Context,
140	query string,
141	limit int,
142	scope repository.HistoryScope,
143) ([]entity.HistoryMatch, error) {
144	if err := r.init(ctx); err != nil {
145		return nil, err
146	}
147	return r.repo.Search(ctx, query, limit, scope)
148}
149
150func (r *LazyHistoryRepository) GetRecent(
151	ctx context.Context,
152	limit, offset int,
153	scope repository.HistoryScope,
154) ([]*entity.HistoryEntry, error) {
155	if err := r.init(ctx); err != nil {
156		return nil, err
157	}
158	return r.repo.GetRecent(ctx, limit, offset, scope)
159}
160
161func (r *LazyHistoryRepository) GetRecentByDomain(ctx context.Context, domain string, limit, offset int) ([]*entity.HistoryEntry, error) {
162	if err := r.init(ctx); err != nil {
163		return nil, err
164	}
165	return r.repo.GetRecentByDomain(ctx, domain, limit, offset)
166}
167
168func (r *LazyHistoryRepository) GetRecentWindow(
169	ctx context.Context,
170	before time.Time,
171	beforeID int64,
172	limit int,
173) ([]*entity.HistoryEntry, error) {
174	if err := r.init(ctx); err != nil {
175		return nil, err
176	}
177	return r.repo.GetRecentWindow(ctx, before, beforeID, limit)
178}
179
180func (r *LazyHistoryRepository) GetRecentWindowByDomain(
181	ctx context.Context,
182	domain string,
183	before time.Time,
184	beforeID int64,
185	limit int,
186) ([]*entity.HistoryEntry, error) {
187	if err := r.init(ctx); err != nil {
188		return nil, err
189	}
190	return r.repo.GetRecentWindowByDomain(ctx, domain, before, beforeID, limit)
191}
192
193func (r *LazyHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
194	if err := r.init(ctx); err != nil {
195		return nil, err
196	}
197	return r.repo.GetRecentSince(ctx, days)
198}
199
200func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
201	if err := r.init(ctx); err != nil {
202		return nil, err
203	}
204	return r.repo.GetMostVisited(ctx, days)
205}
206
207func (r *LazyHistoryRepository) GetMostVisitedWithin(
208	ctx context.Context,
209	limit int,
210	scope repository.HistoryScope,
211) ([]*entity.HistoryEntry, error) {
212	if err := r.init(ctx); err != nil {
213		return nil, err
214	}
215	return r.repo.GetMostVisitedWithin(ctx, limit, scope)
216}
217
218func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) {
219	if err := r.init(ctx); err != nil {
220		return nil, err
221	}
222	return r.repo.GetAllRecentHistory(ctx)
223}
224
225func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) {
226	if err := r.init(ctx); err != nil {
227		return nil, err
228	}
229	return r.repo.GetAllMostVisited(ctx)
230}
231
232func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error {
233	if err := r.init(ctx); err != nil {
234		return err
235	}
236	return r.repo.IncrementVisitCount(ctx, url)
237}
238
239func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url string, delta int) error {
240	if delta <= 0 {
241		return fmt.Errorf("visit delta must be > 0, got %d", delta)
242	}
243	if err := r.init(ctx); err != nil {
244		return err
245	}
246	if deltaRepo, ok := r.repo.(interface {
247		IncrementVisitCountBy(context.Context, string, int) error
248	}); ok {
249		return deltaRepo.IncrementVisitCountBy(ctx, url, delta)
250	}
251	// Fallback: single-increment loop for repos that don't support delta increment.
252	const fallbackCap = 256
253	if delta > fallbackCap {
254		return fmt.Errorf("visit delta %d exceeds fallback cap %d and repo does not support IncrementVisitCountBy", delta, fallbackCap)
255	}
256	for range delta {
257		if err := r.repo.IncrementVisitCount(ctx, url); err != nil {
258			return err
259		}
260	}
261	return nil
262}
263
264func (r *LazyHistoryRepository) Delete(ctx context.Context, id int64) error {
265	if err := r.init(ctx); err != nil {
266		return err
267	}
268	return r.repo.Delete(ctx, id)
269}
270
271func (r *LazyHistoryRepository) DeleteOlderThan(ctx context.Context, before time.Time) error {
272	if err := r.init(ctx); err != nil {
273		return err
274	}
275	return r.repo.DeleteOlderThan(ctx, before)
276}
277
278func (r *LazyHistoryRepository) DeleteSince(ctx context.Context, since time.Time) error {
279	if err := r.init(ctx); err != nil {
280		return err
281	}
282	return r.repo.DeleteSince(ctx, since)
283}
284
285func (r *LazyHistoryRepository) DeleteAll(ctx context.Context) error {
286	if err := r.init(ctx); err != nil {
287		return err
288	}
289	return r.repo.DeleteAll(ctx)
290}
291
292func (r *LazyHistoryRepository) DeleteByDomain(ctx context.Context, domain string) error {
293	if err := r.init(ctx); err != nil {
294		return err
295	}
296	return r.repo.DeleteByDomain(ctx, domain)
297}
298
299func (r *LazyHistoryRepository) GetStats(ctx context.Context) (*entity.HistoryStats, error) {
300	if err := r.init(ctx); err != nil {
301		return nil, err
302	}
303	return r.repo.GetStats(ctx)
304}
305
306func (r *LazyHistoryRepository) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
307	if err := r.init(ctx); err != nil {
308		return nil, err
309	}
310	return r.repo.GetDomainStats(ctx, limit)
311}
312
313func (r *LazyHistoryRepository) GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) {
314	if err := r.init(ctx); err != nil {
315		return nil, err
316	}
317	return r.repo.GetHourlyDistribution(ctx)
318}
319
320func (r *LazyHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) {
321	if err := r.init(ctx); err != nil {
322		return nil, err
323	}
324	return r.repo.GetDailyVisitCount(ctx, daysAgo)
325}
326
327// LazyFavoriteRepository wraps a favorite repository with lazy database initialization.
328type LazyFavoriteRepository struct {
329	provider port.DatabaseProvider
330	repo     repository.FavoriteRepository
331	once     sync.Once
332	initErr  error
333}
334
335// NewLazyFavoriteRepository creates a lazy-loading favorite repository.
336func NewLazyFavoriteRepository(provider port.DatabaseProvider) repository.FavoriteRepository {
337	return &LazyFavoriteRepository{provider: provider}
338}
339
340func (r *LazyFavoriteRepository) init(ctx context.Context) error {
341	r.once.Do(func() {
342		db, err := r.provider.DB(ctx)
343		if err != nil {
344			r.initErr = err
345			return
346		}
347		r.repo = NewFavoriteRepository(db)
348	})
349	return r.initErr
350}
351
352func (r *LazyFavoriteRepository) Save(ctx context.Context, fav *entity.Favorite) error {
353	if err := r.init(ctx); err != nil {
354		return err
355	}
356	return r.repo.Save(ctx, fav)
357}
358
359func (r *LazyFavoriteRepository) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
360	if err := r.init(ctx); err != nil {
361		return nil, err
362	}
363	return r.repo.FindByID(ctx, id)
364}
365
366func (r *LazyFavoriteRepository) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
367	if err := r.init(ctx); err != nil {
368		return nil, err
369	}
370	return r.repo.FindByURL(ctx, url)
371}
372
373func (r *LazyFavoriteRepository) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
374	if err := r.init(ctx); err != nil {
375		return nil, err
376	}
377	return r.repo.GetAll(ctx)
378}
379
380func (r *LazyFavoriteRepository) GetByTag(ctx context.Context, tagID entity.TagID) ([]*entity.Favorite, error) {
381	if err := r.init(ctx); err != nil {
382		return nil, err
383	}
384	return r.repo.GetByTag(ctx, tagID)
385}
386
387func (r *LazyFavoriteRepository) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
388	if err := r.init(ctx); err != nil {
389		return nil, err
390	}
391	return r.repo.GetByShortcut(ctx, key)
392}
393
394func (r *LazyFavoriteRepository) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
395	if err := r.init(ctx); err != nil {
396		return err
397	}
398	return r.repo.UpdatePosition(ctx, id, position)
399}
400
401func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
402	if err := r.init(ctx); err != nil {
403		return err
404	}
405	return r.repo.SetShortcut(ctx, id, key)
406}
407
408func (r *LazyFavoriteRepository) Delete(ctx context.Context, id entity.FavoriteID) error {
409	if err := r.init(ctx); err != nil {
410		return err
411	}
412	return r.repo.Delete(ctx, id)
413}
414
415// LazyTagRepository wraps a tag repository with lazy database initialization.
416type LazyTagRepository struct {
417	provider port.DatabaseProvider
418	repo     repository.TagRepository
419	once     sync.Once
420	initErr  error
421}
422
423// NewLazyTagRepository creates a lazy-loading tag repository.
424func NewLazyTagRepository(provider port.DatabaseProvider) repository.TagRepository {
425	return &LazyTagRepository{provider: provider}
426}
427
428func (r *LazyTagRepository) init(ctx context.Context) error {
429	r.once.Do(func() {
430		db, err := r.provider.DB(ctx)
431		if err != nil {
432			r.initErr = err
433			return
434		}
435		r.repo = NewTagRepository(db)
436	})
437	return r.initErr
438}
439
440func (r *LazyTagRepository) Save(ctx context.Context, tag *entity.Tag) error {
441	if err := r.init(ctx); err != nil {
442		return err
443	}
444	return r.repo.Save(ctx, tag)
445}
446
447func (r *LazyTagRepository) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) {
448	if err := r.init(ctx); err != nil {
449		return nil, err
450	}
451	return r.repo.FindByID(ctx, id)
452}
453
454func (r *LazyTagRepository) FindByName(ctx context.Context, name string) (*entity.Tag, error) {
455	if err := r.init(ctx); err != nil {
456		return nil, err
457	}
458	return r.repo.FindByName(ctx, name)
459}
460
461func (r *LazyTagRepository) GetAll(ctx context.Context) ([]*entity.Tag, error) {
462	if err := r.init(ctx); err != nil {
463		return nil, err
464	}
465	return r.repo.GetAll(ctx)
466}
467
468func (r *LazyTagRepository) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
469	if err := r.init(ctx); err != nil {
470		return err
471	}
472	return r.repo.AssignToFavorite(ctx, tagID, favID)
473}
474
475func (r *LazyTagRepository) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
476	if err := r.init(ctx); err != nil {
477		return err
478	}
479	return r.repo.RemoveFromFavorite(ctx, tagID, favID)
480}
481
482func (r *LazyTagRepository) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
483	if err := r.init(ctx); err != nil {
484		return nil, err
485	}
486	return r.repo.GetForFavorite(ctx, favID)
487}
488
489func (r *LazyTagRepository) Delete(ctx context.Context, id entity.TagID) error {
490	if err := r.init(ctx); err != nil {
491		return err
492	}
493	return r.repo.Delete(ctx, id)
494}
495
496// LazyZoomRepository wraps a zoom repository with lazy database initialization.
497type LazyZoomRepository struct {
498	provider port.DatabaseProvider
499	repo     repository.ZoomRepository
500	once     sync.Once
501	initErr  error
502}
503
504// NewLazyZoomRepository creates a lazy-loading zoom repository.
505func NewLazyZoomRepository(provider port.DatabaseProvider) repository.ZoomRepository {
506	return &LazyZoomRepository{provider: provider}
507}
508
509func (r *LazyZoomRepository) init(ctx context.Context) error {
510	r.once.Do(func() {
511		db, err := r.provider.DB(ctx)
512		if err != nil {
513			r.initErr = err
514			return
515		}
516		r.repo = NewZoomRepository(db)
517	})
518	return r.initErr
519}
520
521func (r *LazyZoomRepository) Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) {
522	if err := r.init(ctx); err != nil {
523		return nil, err
524	}
525	return r.repo.Get(ctx, domain)
526}
527
528func (r *LazyZoomRepository) Set(ctx context.Context, zoom *entity.ZoomLevel) error {
529	if err := r.init(ctx); err != nil {
530		return err
531	}
532	return r.repo.Set(ctx, zoom)
533}
534
535func (r *LazyZoomRepository) Delete(ctx context.Context, domain string) error {
536	if err := r.init(ctx); err != nil {
537		return err
538	}
539	return r.repo.Delete(ctx, domain)
540}
541
542func (r *LazyZoomRepository) GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) {
543	if err := r.init(ctx); err != nil {
544		return nil, err
545	}
546	return r.repo.GetAll(ctx)
547}
548
549// LazySessionRepository wraps a session repository with lazy database initialization.
550type LazySessionRepository struct {
551	provider port.DatabaseProvider
552	repo     repository.SessionRepository
553	once     sync.Once
554	initErr  error
555}
556
557// NewLazySessionRepository creates a lazy-loading session repository.
558func NewLazySessionRepository(provider port.DatabaseProvider) repository.SessionRepository {
559	return &LazySessionRepository{provider: provider}
560}
561
562func (r *LazySessionRepository) init(ctx context.Context) error {
563	r.once.Do(func() {
564		db, err := r.provider.DB(ctx)
565		if err != nil {
566			r.initErr = err
567			return
568		}
569		r.repo = NewSessionRepository(db)
570	})
571	return r.initErr
572}
573
574func (r *LazySessionRepository) Save(ctx context.Context, session *entity.Session) error {
575	if err := r.init(ctx); err != nil {
576		return err
577	}
578	return r.repo.Save(ctx, session)
579}
580
581func (r *LazySessionRepository) FindByID(ctx context.Context, id entity.SessionID) (*entity.Session, error) {
582	if err := r.init(ctx); err != nil {
583		return nil, err
584	}
585	return r.repo.FindByID(ctx, id)
586}
587
588func (r *LazySessionRepository) GetActive(ctx context.Context) (*entity.Session, error) {
589	if err := r.init(ctx); err != nil {
590		return nil, err
591	}
592	return r.repo.GetActive(ctx)
593}
594
595func (r *LazySessionRepository) GetRecent(ctx context.Context, limit int) ([]*entity.Session, error) {
596	if err := r.init(ctx); err != nil {
597		return nil, err
598	}
599	return r.repo.GetRecent(ctx, limit)
600}
601
602func (r *LazySessionRepository) MarkEnded(ctx context.Context, id entity.SessionID, endedAt time.Time) error {
603	if err := r.init(ctx); err != nil {
604		return err
605	}
606	return r.repo.MarkEnded(ctx, id, endedAt)
607}
608
609func (r *LazySessionRepository) Delete(ctx context.Context, id entity.SessionID) error {
610	if err := r.init(ctx); err != nil {
611		return err
612	}
613	return r.repo.Delete(ctx, id)
614}
615
616func (r *LazySessionRepository) DeleteOldestExited(ctx context.Context, keepCount int) (int64, error) {
617	if err := r.init(ctx); err != nil {
618		return 0, err
619	}
620	return r.repo.DeleteOldestExited(ctx, keepCount)
621}
622
623func (r *LazySessionRepository) DeleteExitedBefore(ctx context.Context, cutoff time.Time) (int64, error) {
624	if err := r.init(ctx); err != nil {
625		return 0, err
626	}
627	return r.repo.DeleteExitedBefore(ctx, cutoff)
628}
629
630// LazySessionStateRepository wraps a session state repository with lazy database initialization.
631type LazySessionStateRepository struct {
632	provider port.DatabaseProvider
633	repo     repository.SessionStateRepository
634	once     sync.Once
635	initErr  error
636}
637
638// NewLazySessionStateRepository creates a lazy-loading session state repository.
639func NewLazySessionStateRepository(provider port.DatabaseProvider) repository.SessionStateRepository {
640	return &LazySessionStateRepository{provider: provider}
641}
642
643func (r *LazySessionStateRepository) init(ctx context.Context) error {
644	r.once.Do(func() {
645		db, err := r.provider.DB(ctx)
646		if err != nil {
647			r.initErr = err
648			return
649		}
650		r.repo = NewSessionStateRepository(db)
651	})
652	return r.initErr
653}
654
655func (r *LazySessionStateRepository) SaveSnapshot(ctx context.Context, state *entity.SessionState) error {
656	if err := r.init(ctx); err != nil {
657		return err
658	}
659	return r.repo.SaveSnapshot(ctx, state)
660}
661
662func (r *LazySessionStateRepository) GetSnapshot(ctx context.Context, sessionID entity.SessionID) (*entity.SessionState, error) {
663	if err := r.init(ctx); err != nil {
664		return nil, err
665	}
666	return r.repo.GetSnapshot(ctx, sessionID)
667}
668
669func (r *LazySessionStateRepository) DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error {
670	if err := r.init(ctx); err != nil {
671		return err
672	}
673	return r.repo.DeleteSnapshot(ctx, sessionID)
674}
675
676func (r *LazySessionStateRepository) GetAllSnapshots(ctx context.Context) ([]*entity.SessionState, error) {
677	if err := r.init(ctx); err != nil {
678		return nil, err
679	}
680	return r.repo.GetAllSnapshots(ctx)
681}
682
683func (r *LazySessionStateRepository) GetTotalSnapshotsSize(ctx context.Context) (int64, error) {
684	if err := r.init(ctx); err != nil {
685		return 0, err
686	}
687	return r.repo.GetTotalSnapshotsSize(ctx)
688}
689
690// LazyContentWhitelistRepository wraps a content whitelist repository with lazy database initialization.
691type LazyContentWhitelistRepository struct {
692	provider port.DatabaseProvider
693	repo     repository.ContentWhitelistRepository
694	once     sync.Once
695	initErr  error
696}
697
698// NewLazyContentWhitelistRepository creates a lazy-loading content whitelist repository.
699func NewLazyContentWhitelistRepository(provider port.DatabaseProvider) repository.ContentWhitelistRepository {
700	return &LazyContentWhitelistRepository{provider: provider}
701}
702
703func (r *LazyContentWhitelistRepository) init(ctx context.Context) error {
704	r.once.Do(func() {
705		db, err := r.provider.DB(ctx)
706		if err != nil {
707			r.initErr = err
708			return
709		}
710		r.repo = NewContentWhitelistRepository(db)
711	})
712	return r.initErr
713}
714
715func (r *LazyContentWhitelistRepository) Add(ctx context.Context, domain string) error {
716	if err := r.init(ctx); err != nil {
717		return err
718	}
719	return r.repo.Add(ctx, domain)
720}
721
722func (r *LazyContentWhitelistRepository) Remove(ctx context.Context, domain string) error {
723	if err := r.init(ctx); err != nil {
724		return err
725	}
726	return r.repo.Remove(ctx, domain)
727}
728
729func (r *LazyContentWhitelistRepository) Contains(ctx context.Context, domain string) (bool, error) {
730	if err := r.init(ctx); err != nil {
731		return false, err
732	}
733	return r.repo.Contains(ctx, domain)
734}
735
736func (r *LazyContentWhitelistRepository) GetAll(ctx context.Context) ([]string, error) {
737	if err := r.init(ctx); err != nil {
738		return nil, err
739	}
740	return r.repo.GetAll(ctx)
741}
742
743// LazyRepositories holds all lazy-loaded repositories.
744type LazyRepositories struct {
745	History      repository.HistoryRepository
746	Favorite     repository.FavoriteRepository
747	Tag          repository.TagRepository
748	Zoom         repository.ZoomRepository
749	Session      repository.SessionRepository
750	SessionState repository.SessionStateRepository
751	Filter       repository.ContentWhitelistRepository
752	Permission   port.PermissionRepository
753}
754
755// NewLazyRepositories creates all lazy repositories from a database provider.
756func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
757	return &LazyRepositories{
758		History:      NewLazyHistoryRepository(provider),
759		Favorite:     NewLazyFavoriteRepository(provider),
760		Tag:          NewLazyTagRepository(provider),
761		Zoom:         NewLazyZoomRepository(provider),
762		Session:      NewLazySessionRepository(provider),
763		SessionState: NewLazySessionStateRepository(provider),
764		Filter:       NewLazyContentWhitelistRepository(provider),
765		Permission:   NewLazyPermissionRepository(provider),
766	}
767}
768
769// GetDB returns the underlying *sql.DB for operations that need it directly.
770// Returns nil if the database hasn't been initialized yet.
771func GetDB(provider port.DatabaseProvider, ctx context.Context) *sql.DB {
772	db, err := provider.DB(ctx)
773	if err != nil {
774		return nil
775	}
776	return db
777}

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