session_state_repo.go RawHistoryFolder

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

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