v0.30.0 eb3068f

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(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) {
139	if err := r.init(ctx); err != nil {
140		return nil, err
141	}
142	return r.repo.Search(ctx, query, limit)
143}
144
145func (r *LazyHistoryRepository) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
146	if err := r.init(ctx); err != nil {
147		return nil, err
148	}
149	return r.repo.GetRecent(ctx, limit, offset)
150}
151
152func (r *LazyHistoryRepository) GetRecentByDomain(ctx context.Context, domain string, limit, offset int) ([]*entity.HistoryEntry, error) {
153	if err := r.init(ctx); err != nil {
154		return nil, err
155	}
156	return r.repo.GetRecentByDomain(ctx, domain, limit, offset)
157}
158
159func (r *LazyHistoryRepository) GetRecentWindow(
160	ctx context.Context,
161	before time.Time,
162	beforeID int64,
163	limit int,
164) ([]*entity.HistoryEntry, error) {
165	if err := r.init(ctx); err != nil {
166		return nil, err
167	}
168	return r.repo.GetRecentWindow(ctx, before, beforeID, limit)
169}
170
171func (r *LazyHistoryRepository) GetRecentWindowByDomain(
172	ctx context.Context,
173	domain string,
174	before time.Time,
175	beforeID int64,
176	limit int,
177) ([]*entity.HistoryEntry, error) {
178	if err := r.init(ctx); err != nil {
179		return nil, err
180	}
181	return r.repo.GetRecentWindowByDomain(ctx, domain, before, beforeID, limit)
182}
183
184func (r *LazyHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
185	if err := r.init(ctx); err != nil {
186		return nil, err
187	}
188	return r.repo.GetRecentSince(ctx, days)
189}
190
191func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
192	if err := r.init(ctx); err != nil {
193		return nil, err
194	}
195	return r.repo.GetMostVisited(ctx, days)
196}
197
198func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) {
199	if err := r.init(ctx); err != nil {
200		return nil, err
201	}
202	return r.repo.GetAllRecentHistory(ctx)
203}
204
205func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) {
206	if err := r.init(ctx); err != nil {
207		return nil, err
208	}
209	return r.repo.GetAllMostVisited(ctx)
210}
211
212func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error {
213	if err := r.init(ctx); err != nil {
214		return err
215	}
216	return r.repo.IncrementVisitCount(ctx, url)
217}
218
219func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url string, delta int) error {
220	if delta <= 0 {
221		return fmt.Errorf("visit delta must be > 0, got %d", delta)
222	}
223	if err := r.init(ctx); err != nil {
224		return err
225	}
226	if deltaRepo, ok := r.repo.(interface {
227		IncrementVisitCountBy(context.Context, string, int) error
228	}); ok {
229		return deltaRepo.IncrementVisitCountBy(ctx, url, delta)
230	}
231	// Fallback: single-increment loop for repos that don't support delta increment.
232	const fallbackCap = 256
233	if delta > fallbackCap {
234		return fmt.Errorf("visit delta %d exceeds fallback cap %d and repo does not support IncrementVisitCountBy", delta, fallbackCap)
235	}
236	for i := 0; i < delta; i++ {
237		if err := r.repo.IncrementVisitCount(ctx, url); err != nil {
238			return err
239		}
240	}
241	return nil
242}
243
244func (r *LazyHistoryRepository) Delete(ctx context.Context, id int64) error {
245	if err := r.init(ctx); err != nil {
246		return err
247	}
248	return r.repo.Delete(ctx, id)
249}
250
251func (r *LazyHistoryRepository) DeleteOlderThan(ctx context.Context, before time.Time) error {
252	if err := r.init(ctx); err != nil {
253		return err
254	}
255	return r.repo.DeleteOlderThan(ctx, before)
256}
257
258func (r *LazyHistoryRepository) DeleteSince(ctx context.Context, since time.Time) error {
259	if err := r.init(ctx); err != nil {
260		return err
261	}
262	return r.repo.DeleteSince(ctx, since)
263}
264
265func (r *LazyHistoryRepository) DeleteAll(ctx context.Context) error {
266	if err := r.init(ctx); err != nil {
267		return err
268	}
269	return r.repo.DeleteAll(ctx)
270}
271
272func (r *LazyHistoryRepository) DeleteByDomain(ctx context.Context, domain string) error {
273	if err := r.init(ctx); err != nil {
274		return err
275	}
276	return r.repo.DeleteByDomain(ctx, domain)
277}
278
279func (r *LazyHistoryRepository) GetStats(ctx context.Context) (*entity.HistoryStats, error) {
280	if err := r.init(ctx); err != nil {
281		return nil, err
282	}
283	return r.repo.GetStats(ctx)
284}
285
286func (r *LazyHistoryRepository) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
287	if err := r.init(ctx); err != nil {
288		return nil, err
289	}
290	return r.repo.GetDomainStats(ctx, limit)
291}
292
293func (r *LazyHistoryRepository) GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) {
294	if err := r.init(ctx); err != nil {
295		return nil, err
296	}
297	return r.repo.GetHourlyDistribution(ctx)
298}
299
300func (r *LazyHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) {
301	if err := r.init(ctx); err != nil {
302		return nil, err
303	}
304	return r.repo.GetDailyVisitCount(ctx, daysAgo)
305}
306
307// LazyFavoriteRepository wraps a favorite repository with lazy database initialization.
308type LazyFavoriteRepository struct {
309	provider port.DatabaseProvider
310	repo     repository.FavoriteRepository
311	once     sync.Once
312	initErr  error
313}
314
315// NewLazyFavoriteRepository creates a lazy-loading favorite repository.
316func NewLazyFavoriteRepository(provider port.DatabaseProvider) repository.FavoriteRepository {
317	return &LazyFavoriteRepository{provider: provider}
318}
319
320func (r *LazyFavoriteRepository) init(ctx context.Context) error {
321	r.once.Do(func() {
322		db, err := r.provider.DB(ctx)
323		if err != nil {
324			r.initErr = err
325			return
326		}
327		r.repo = NewFavoriteRepository(db)
328	})
329	return r.initErr
330}
331
332func (r *LazyFavoriteRepository) Save(ctx context.Context, fav *entity.Favorite) error {
333	if err := r.init(ctx); err != nil {
334		return err
335	}
336	return r.repo.Save(ctx, fav)
337}
338
339func (r *LazyFavoriteRepository) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
340	if err := r.init(ctx); err != nil {
341		return nil, err
342	}
343	return r.repo.FindByID(ctx, id)
344}
345
346func (r *LazyFavoriteRepository) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
347	if err := r.init(ctx); err != nil {
348		return nil, err
349	}
350	return r.repo.FindByURL(ctx, url)
351}
352
353func (r *LazyFavoriteRepository) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
354	if err := r.init(ctx); err != nil {
355		return nil, err
356	}
357	return r.repo.GetAll(ctx)
358}
359
360func (r *LazyFavoriteRepository) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {
361	if err := r.init(ctx); err != nil {
362		return nil, err
363	}
364	return r.repo.GetByFolder(ctx, folderID)
365}
366
367func (r *LazyFavoriteRepository) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
368	if err := r.init(ctx); err != nil {
369		return nil, err
370	}
371	return r.repo.GetByShortcut(ctx, key)
372}
373
374func (r *LazyFavoriteRepository) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
375	if err := r.init(ctx); err != nil {
376		return err
377	}
378	return r.repo.UpdatePosition(ctx, id, position)
379}
380
381func (r *LazyFavoriteRepository) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
382	if err := r.init(ctx); err != nil {
383		return err
384	}
385	return r.repo.SetFolder(ctx, id, folderID)
386}
387
388func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
389	if err := r.init(ctx); err != nil {
390		return err
391	}
392	return r.repo.SetShortcut(ctx, id, key)
393}
394
395func (r *LazyFavoriteRepository) Delete(ctx context.Context, id entity.FavoriteID) error {
396	if err := r.init(ctx); err != nil {
397		return err
398	}
399	return r.repo.Delete(ctx, id)
400}
401
402// LazyFolderRepository wraps a folder repository with lazy database initialization.
403type LazyFolderRepository struct {
404	provider port.DatabaseProvider
405	repo     repository.FolderRepository
406	once     sync.Once
407	initErr  error
408}
409
410// NewLazyFolderRepository creates a lazy-loading folder repository.
411func NewLazyFolderRepository(provider port.DatabaseProvider) repository.FolderRepository {
412	return &LazyFolderRepository{provider: provider}
413}
414
415func (r *LazyFolderRepository) init(ctx context.Context) error {
416	r.once.Do(func() {
417		db, err := r.provider.DB(ctx)
418		if err != nil {
419			r.initErr = err
420			return
421		}
422		r.repo = NewFolderRepository(db)
423	})
424	return r.initErr
425}
426
427func (r *LazyFolderRepository) Save(ctx context.Context, folder *entity.Folder) error {
428	if err := r.init(ctx); err != nil {
429		return err
430	}
431	return r.repo.Save(ctx, folder)
432}
433
434func (r *LazyFolderRepository) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
435	if err := r.init(ctx); err != nil {
436		return nil, err
437	}
438	return r.repo.FindByID(ctx, id)
439}
440
441func (r *LazyFolderRepository) GetAll(ctx context.Context) ([]*entity.Folder, error) {
442	if err := r.init(ctx); err != nil {
443		return nil, err
444	}
445	return r.repo.GetAll(ctx)
446}
447
448func (r *LazyFolderRepository) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {
449	if err := r.init(ctx); err != nil {
450		return nil, err
451	}
452	return r.repo.GetChildren(ctx, parentID)
453}
454
455func (r *LazyFolderRepository) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
456	if err := r.init(ctx); err != nil {
457		return err
458	}
459	return r.repo.UpdatePosition(ctx, id, position)
460}
461
462func (r *LazyFolderRepository) Delete(ctx context.Context, id entity.FolderID) error {
463	if err := r.init(ctx); err != nil {
464		return err
465	}
466	return r.repo.Delete(ctx, id)
467}
468
469// LazyTagRepository wraps a tag repository with lazy database initialization.
470type LazyTagRepository struct {
471	provider port.DatabaseProvider
472	repo     repository.TagRepository
473	once     sync.Once
474	initErr  error
475}
476
477// NewLazyTagRepository creates a lazy-loading tag repository.
478func NewLazyTagRepository(provider port.DatabaseProvider) repository.TagRepository {
479	return &LazyTagRepository{provider: provider}
480}
481
482func (r *LazyTagRepository) init(ctx context.Context) error {
483	r.once.Do(func() {
484		db, err := r.provider.DB(ctx)
485		if err != nil {
486			r.initErr = err
487			return
488		}
489		r.repo = NewTagRepository(db)
490	})
491	return r.initErr
492}
493
494func (r *LazyTagRepository) Save(ctx context.Context, tag *entity.Tag) error {
495	if err := r.init(ctx); err != nil {
496		return err
497	}
498	return r.repo.Save(ctx, tag)
499}
500
501func (r *LazyTagRepository) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) {
502	if err := r.init(ctx); err != nil {
503		return nil, err
504	}
505	return r.repo.FindByID(ctx, id)
506}
507
508func (r *LazyTagRepository) FindByName(ctx context.Context, name string) (*entity.Tag, error) {
509	if err := r.init(ctx); err != nil {
510		return nil, err
511	}
512	return r.repo.FindByName(ctx, name)
513}
514
515func (r *LazyTagRepository) GetAll(ctx context.Context) ([]*entity.Tag, error) {
516	if err := r.init(ctx); err != nil {
517		return nil, err
518	}
519	return r.repo.GetAll(ctx)
520}
521
522func (r *LazyTagRepository) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
523	if err := r.init(ctx); err != nil {
524		return err
525	}
526	return r.repo.AssignToFavorite(ctx, tagID, favID)
527}
528
529func (r *LazyTagRepository) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
530	if err := r.init(ctx); err != nil {
531		return err
532	}
533	return r.repo.RemoveFromFavorite(ctx, tagID, favID)
534}
535
536func (r *LazyTagRepository) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
537	if err := r.init(ctx); err != nil {
538		return nil, err
539	}
540	return r.repo.GetForFavorite(ctx, favID)
541}
542
543func (r *LazyTagRepository) Delete(ctx context.Context, id entity.TagID) error {
544	if err := r.init(ctx); err != nil {
545		return err
546	}
547	return r.repo.Delete(ctx, id)
548}
549
550// LazyZoomRepository wraps a zoom repository with lazy database initialization.
551type LazyZoomRepository struct {
552	provider port.DatabaseProvider
553	repo     repository.ZoomRepository
554	once     sync.Once
555	initErr  error
556}
557
558// NewLazyZoomRepository creates a lazy-loading zoom repository.
559func NewLazyZoomRepository(provider port.DatabaseProvider) repository.ZoomRepository {
560	return &LazyZoomRepository{provider: provider}
561}
562
563func (r *LazyZoomRepository) init(ctx context.Context) error {
564	r.once.Do(func() {
565		db, err := r.provider.DB(ctx)
566		if err != nil {
567			r.initErr = err
568			return
569		}
570		r.repo = NewZoomRepository(db)
571	})
572	return r.initErr
573}
574
575func (r *LazyZoomRepository) Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) {
576	if err := r.init(ctx); err != nil {
577		return nil, err
578	}
579	return r.repo.Get(ctx, domain)
580}
581
582func (r *LazyZoomRepository) Set(ctx context.Context, zoom *entity.ZoomLevel) error {
583	if err := r.init(ctx); err != nil {
584		return err
585	}
586	return r.repo.Set(ctx, zoom)
587}
588
589func (r *LazyZoomRepository) Delete(ctx context.Context, domain string) error {
590	if err := r.init(ctx); err != nil {
591		return err
592	}
593	return r.repo.Delete(ctx, domain)
594}
595
596func (r *LazyZoomRepository) GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) {
597	if err := r.init(ctx); err != nil {
598		return nil, err
599	}
600	return r.repo.GetAll(ctx)
601}
602
603// LazySessionRepository wraps a session repository with lazy database initialization.
604type LazySessionRepository struct {
605	provider port.DatabaseProvider
606	repo     repository.SessionRepository
607	once     sync.Once
608	initErr  error
609}
610
611// NewLazySessionRepository creates a lazy-loading session repository.
612func NewLazySessionRepository(provider port.DatabaseProvider) repository.SessionRepository {
613	return &LazySessionRepository{provider: provider}
614}
615
616func (r *LazySessionRepository) init(ctx context.Context) error {
617	r.once.Do(func() {
618		db, err := r.provider.DB(ctx)
619		if err != nil {
620			r.initErr = err
621			return
622		}
623		r.repo = NewSessionRepository(db)
624	})
625	return r.initErr
626}
627
628func (r *LazySessionRepository) Save(ctx context.Context, session *entity.Session) error {
629	if err := r.init(ctx); err != nil {
630		return err
631	}
632	return r.repo.Save(ctx, session)
633}
634
635func (r *LazySessionRepository) FindByID(ctx context.Context, id entity.SessionID) (*entity.Session, error) {
636	if err := r.init(ctx); err != nil {
637		return nil, err
638	}
639	return r.repo.FindByID(ctx, id)
640}
641
642func (r *LazySessionRepository) GetActive(ctx context.Context) (*entity.Session, error) {
643	if err := r.init(ctx); err != nil {
644		return nil, err
645	}
646	return r.repo.GetActive(ctx)
647}
648
649func (r *LazySessionRepository) GetRecent(ctx context.Context, limit int) ([]*entity.Session, error) {
650	if err := r.init(ctx); err != nil {
651		return nil, err
652	}
653	return r.repo.GetRecent(ctx, limit)
654}
655
656func (r *LazySessionRepository) MarkEnded(ctx context.Context, id entity.SessionID, endedAt time.Time) error {
657	if err := r.init(ctx); err != nil {
658		return err
659	}
660	return r.repo.MarkEnded(ctx, id, endedAt)
661}
662
663func (r *LazySessionRepository) Delete(ctx context.Context, id entity.SessionID) error {
664	if err := r.init(ctx); err != nil {
665		return err
666	}
667	return r.repo.Delete(ctx, id)
668}
669
670func (r *LazySessionRepository) DeleteOldestExited(ctx context.Context, keepCount int) (int64, error) {
671	if err := r.init(ctx); err != nil {
672		return 0, err
673	}
674	return r.repo.DeleteOldestExited(ctx, keepCount)
675}
676
677func (r *LazySessionRepository) DeleteExitedBefore(ctx context.Context, cutoff time.Time) (int64, error) {
678	if err := r.init(ctx); err != nil {
679		return 0, err
680	}
681	return r.repo.DeleteExitedBefore(ctx, cutoff)
682}
683
684// LazySessionStateRepository wraps a session state repository with lazy database initialization.
685type LazySessionStateRepository struct {
686	provider port.DatabaseProvider
687	repo     repository.SessionStateRepository
688	once     sync.Once
689	initErr  error
690}
691
692// NewLazySessionStateRepository creates a lazy-loading session state repository.
693func NewLazySessionStateRepository(provider port.DatabaseProvider) repository.SessionStateRepository {
694	return &LazySessionStateRepository{provider: provider}
695}
696
697func (r *LazySessionStateRepository) init(ctx context.Context) error {
698	r.once.Do(func() {
699		db, err := r.provider.DB(ctx)
700		if err != nil {
701			r.initErr = err
702			return
703		}
704		r.repo = NewSessionStateRepository(db)
705	})
706	return r.initErr
707}
708
709func (r *LazySessionStateRepository) SaveSnapshot(ctx context.Context, state *entity.SessionState) error {
710	if err := r.init(ctx); err != nil {
711		return err
712	}
713	return r.repo.SaveSnapshot(ctx, state)
714}
715
716func (r *LazySessionStateRepository) GetSnapshot(ctx context.Context, sessionID entity.SessionID) (*entity.SessionState, error) {
717	if err := r.init(ctx); err != nil {
718		return nil, err
719	}
720	return r.repo.GetSnapshot(ctx, sessionID)
721}
722
723func (r *LazySessionStateRepository) DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error {
724	if err := r.init(ctx); err != nil {
725		return err
726	}
727	return r.repo.DeleteSnapshot(ctx, sessionID)
728}
729
730func (r *LazySessionStateRepository) GetAllSnapshots(ctx context.Context) ([]*entity.SessionState, error) {
731	if err := r.init(ctx); err != nil {
732		return nil, err
733	}
734	return r.repo.GetAllSnapshots(ctx)
735}
736
737func (r *LazySessionStateRepository) GetTotalSnapshotsSize(ctx context.Context) (int64, error) {
738	if err := r.init(ctx); err != nil {
739		return 0, err
740	}
741	return r.repo.GetTotalSnapshotsSize(ctx)
742}
743
744// LazyContentWhitelistRepository wraps a content whitelist repository with lazy database initialization.
745type LazyContentWhitelistRepository struct {
746	provider port.DatabaseProvider
747	repo     repository.ContentWhitelistRepository
748	once     sync.Once
749	initErr  error
750}
751
752// NewLazyContentWhitelistRepository creates a lazy-loading content whitelist repository.
753func NewLazyContentWhitelistRepository(provider port.DatabaseProvider) repository.ContentWhitelistRepository {
754	return &LazyContentWhitelistRepository{provider: provider}
755}
756
757func (r *LazyContentWhitelistRepository) init(ctx context.Context) error {
758	r.once.Do(func() {
759		db, err := r.provider.DB(ctx)
760		if err != nil {
761			r.initErr = err
762			return
763		}
764		r.repo = NewContentWhitelistRepository(db)
765	})
766	return r.initErr
767}
768
769func (r *LazyContentWhitelistRepository) Add(ctx context.Context, domain string) error {
770	if err := r.init(ctx); err != nil {
771		return err
772	}
773	return r.repo.Add(ctx, domain)
774}
775
776func (r *LazyContentWhitelistRepository) Remove(ctx context.Context, domain string) error {
777	if err := r.init(ctx); err != nil {
778		return err
779	}
780	return r.repo.Remove(ctx, domain)
781}
782
783func (r *LazyContentWhitelistRepository) Contains(ctx context.Context, domain string) (bool, error) {
784	if err := r.init(ctx); err != nil {
785		return false, err
786	}
787	return r.repo.Contains(ctx, domain)
788}
789
790func (r *LazyContentWhitelistRepository) GetAll(ctx context.Context) ([]string, error) {
791	if err := r.init(ctx); err != nil {
792		return nil, err
793	}
794	return r.repo.GetAll(ctx)
795}
796
797// LazyRepositories holds all lazy-loaded repositories.
798type LazyRepositories struct {
799	History      repository.HistoryRepository
800	Favorite     repository.FavoriteRepository
801	Folder       repository.FolderRepository
802	Tag          repository.TagRepository
803	Zoom         repository.ZoomRepository
804	Session      repository.SessionRepository
805	SessionState repository.SessionStateRepository
806	Filter       repository.ContentWhitelistRepository
807	Permission   port.PermissionRepository
808}
809
810// NewLazyRepositories creates all lazy repositories from a database provider.
811func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
812	return &LazyRepositories{
813		History:      NewLazyHistoryRepository(provider),
814		Favorite:     NewLazyFavoriteRepository(provider),
815		Folder:       NewLazyFolderRepository(provider),
816		Tag:          NewLazyTagRepository(provider),
817		Zoom:         NewLazyZoomRepository(provider),
818		Session:      NewLazySessionRepository(provider),
819		SessionState: NewLazySessionStateRepository(provider),
820		Filter:       NewLazyContentWhitelistRepository(provider),
821		Permission:   NewLazyPermissionRepository(provider),
822	}
823}
824
825// GetDB returns the underlying *sql.DB for operations that need it directly.
826// Returns nil if the database hasn't been initialized yet.
827func GetDB(provider port.DatabaseProvider, ctx context.Context) *sql.DB {
828	db, err := provider.DB(ctx)
829	if err != nil {
830		return nil
831	}
832	return db
833}

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