1package sqlite
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8
9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/domain/repository"
11 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc"
12 "github.com/bnema/dumber/internal/logging"
13)
14
15type sessionStateRepo struct {
16 queries *sqlc.Queries
17}
18
19// NewSessionStateRepository creates a new session state repository.
20func NewSessionStateRepository(db *sql.DB) repository.SessionStateRepository {
21 return &sessionStateRepo{queries: sqlc.New(db)}
22}
23
24// SaveSnapshot saves or updates a session state snapshot.
25func (r *sessionStateRepo) SaveSnapshot(ctx context.Context, state *entity.SessionState) error {
26 log := logging.FromContext(ctx)
27 if state == nil {
28 return errors.New("session state cannot be nil")
29 }
30
31 stateJSON, err := json.Marshal(state)
32 if err != nil {
33 log.Error().Err(err).Msg("failed to marshal session state")
34 return err
35 }
36
37 log.Debug().
38 Str("session_id", string(state.SessionID)).
39 Int("tab_count", len(state.Tabs)).
40 Int("pane_count", state.CountPanes()).
41 Msg("saving session state snapshot")
42
43 return r.queries.UpsertSessionState(ctx, sqlc.UpsertSessionStateParams{
44 SessionID: string(state.SessionID),
45 StateJson: string(stateJSON),
46 Version: int64(state.Version),
47 TabCount: int64(len(state.Tabs)),
48 PaneCount: int64(state.CountPanes()),
49 UpdatedAt: state.SavedAt,
50 })
51}
52
53// GetSnapshot returns the latest snapshot for a session.
54func (r *sessionStateRepo) GetSnapshot(ctx context.Context, sessionID entity.SessionID) (*entity.SessionState, error) {
55 row, err := r.queries.GetSessionState(ctx, string(sessionID))
56 if err != nil {
57 if errors.Is(err, sql.ErrNoRows) {
58 return nil, nil
59 }
60 return nil, err
61 }
62
63 var state entity.SessionState
64 if err := json.Unmarshal([]byte(row.StateJson), &state); err != nil {
65 logging.FromContext(ctx).Error().Err(err).
66 Str("session_id", string(sessionID)).
67 Msg("failed to unmarshal session state")
68 return nil, err
69 }
70
71 return &state, nil
72}
73
74// DeleteSnapshot removes a session's snapshot.
75func (r *sessionStateRepo) DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error {
76 log := logging.FromContext(ctx)
77 log.Debug().Str("session_id", string(sessionID)).Msg("deleting session state snapshot")
78 return r.queries.DeleteSessionState(ctx, string(sessionID))
79}
80
81// GetAllSnapshots returns all snapshots with summary info.
82func (r *sessionStateRepo) GetAllSnapshots(ctx context.Context) ([]*entity.SessionState, error) {
83 rows, err := r.queries.GetAllSessionStates(ctx)
84 if err != nil {
85 return nil, err
86 }
87
88 states := make([]*entity.SessionState, 0, len(rows))
89 for _, row := range rows {
90 var state entity.SessionState
91 if err := json.Unmarshal([]byte(row.StateJson), &state); err != nil {
92 logging.FromContext(ctx).Warn().Err(err).
93 Str("session_id", row.SessionID).
94 Msg("skipping corrupted session state")
95 continue
96 }
97 states = append(states, &state)
98 }
99
100 return states, nil
101}
102
103// GetTotalSnapshotsSize returns the total size of all session snapshots in bytes.
104func (r *sessionStateRepo) GetTotalSnapshotsSize(ctx context.Context) (int64, error) {
105 result, err := r.queries.GetTotalSessionStatesSize(ctx)
106 if err != nil {
107 return 0, err
108 }
109 // SQLite returns int64 for SUM, but COALESCE wraps it as interface{}
110 switch v := result.(type) {
111 case int64:
112 return v, nil
113 case float64:
114 return int64(v), nil
115 default:
116 return 0, nil
117 }
118}