1package usecase
2
3import (
4 "context"
5
6 "github.com/bnema/dumber/internal/domain/entity"
7 "github.com/bnema/dumber/internal/domain/repository"
8 "github.com/bnema/dumber/internal/logging"
9)
10
11const (
12 // maxSessionsToCheck is the number of recent sessions to check for restoration.
13 maxSessionsToCheck = 10
14)
15
16// GetLastRestorableSessionUseCase finds the most recent session that can be auto-restored.
17// A session is restorable if:
18// - It is not the current session
19// - It is not actively running (has ended or crashed without lock file)
20// - It has a valid snapshot with at least one tab
21type GetLastRestorableSessionUseCase struct {
22 sessionRepo repository.SessionRepository
23 stateRepo repository.SessionStateRepository
24 lockDir string
25}
26
27// NewGetLastRestorableSessionUseCase creates a new GetLastRestorableSessionUseCase.
28func NewGetLastRestorableSessionUseCase(
29 sessionRepo repository.SessionRepository,
30 stateRepo repository.SessionStateRepository,
31 lockDir string,
32) *GetLastRestorableSessionUseCase {
33 return &GetLastRestorableSessionUseCase{
34 sessionRepo: sessionRepo,
35 stateRepo: stateRepo,
36 lockDir: lockDir,
37 }
38}
39
40// GetLastRestorableSessionInput contains the parameters for finding a restorable session.
41type GetLastRestorableSessionInput struct {
42 // ExcludeSessionID is the current session ID to exclude from results.
43 ExcludeSessionID entity.SessionID
44}
45
46// GetLastRestorableSessionOutput contains the found restorable session, if any.
47type GetLastRestorableSessionOutput struct {
48 // SessionID is the ID of the restorable session, or empty if none found.
49 SessionID entity.SessionID
50 // State is the session state to restore, or nil if none found.
51 State *entity.SessionState
52}
53
54// Execute finds the most recent restorable session.
55// Returns an empty output (not an error) if no restorable session is found.
56func (uc *GetLastRestorableSessionUseCase) Execute(
57 ctx context.Context,
58 input GetLastRestorableSessionInput,
59) (*GetLastRestorableSessionOutput, error) {
60 log := logging.FromContext(ctx)
61
62 sessions, err := uc.sessionRepo.GetRecent(ctx, maxSessionsToCheck)
63 if err != nil {
64 return nil, err
65 }
66
67 for _, session := range sessions {
68 // Only consider browser sessions
69 if session.Type != entity.SessionTypeBrowser {
70 continue
71 }
72
73 // Skip current session
74 if session.ID == input.ExcludeSessionID {
75 continue
76 }
77
78 // Check if session is actively running
79 // A session is "active" if ended_at IS NULL AND it has a lock file
80 if session.IsActive() && isSessionLocked(uc.lockDir, session.ID) {
81 log.Debug().
82 Str("session_id", string(session.ID)).
83 Msg("auto-restore: skipping active session")
84 continue
85 }
86
87 // Try to get snapshot
88 state, err := uc.stateRepo.GetSnapshot(ctx, session.ID)
89 if err != nil {
90 log.Debug().
91 Err(err).
92 Str("session_id", string(session.ID)).
93 Msg("auto-restore: failed to get snapshot")
94 continue
95 }
96 if state == nil {
97 continue
98 }
99
100 // Skip sessions with no tabs
101 if len(state.Tabs) == 0 {
102 log.Debug().
103 Str("session_id", string(session.ID)).
104 Msg("auto-restore: skipping session with no tabs")
105 continue
106 }
107
108 // Found a valid restorable session
109 log.Info().
110 Str("session_id", string(session.ID)).
111 Int("tabs", len(state.Tabs)).
112 Int("panes", state.CountPanes()).
113 Msg("auto-restore: found restorable session")
114
115 return &GetLastRestorableSessionOutput{
116 SessionID: session.ID,
117 State: state,
118 }, nil
119 }
120
121 // No restorable session found
122 log.Debug().Msg("auto-restore: no restorable session found")
123 return &GetLastRestorableSessionOutput{}, nil
124}