service.go RawHistoryFolder
1package snapshot
2
3import (
4 "context"
5 "sync"
6 "time"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/logging"
11)
12
13// Service handles debounced session state snapshots.
14type Service struct {
15 snapshotUC *usecase.SnapshotSessionUseCase
16 provider port.TabListProvider
17 interval time.Duration
18
19 mu sync.Mutex
20 timer *time.Timer
21 dirty bool
22 ctx context.Context
23 cancel context.CancelFunc
24}
25
26// NewService creates a new snapshot service.
27func NewService(
28 snapshotUC *usecase.SnapshotSessionUseCase,
29 provider port.TabListProvider,
30 intervalMs int,
31) *Service {
32 if intervalMs <= 0 {
33 intervalMs = 5000 // Default 5 seconds
34 }
35 return &Service{
36 snapshotUC: snapshotUC,
37 provider: provider,
38 interval: time.Duration(intervalMs) * time.Millisecond,
39 }
40}
41
42// Start begins watching for dirty state.
43func (s *Service) Start(ctx context.Context) {
44 s.mu.Lock()
45 defer s.mu.Unlock()
46
47 s.ctx, s.cancel = context.WithCancel(ctx)
48 logging.FromContext(ctx).Debug().Dur("interval", s.interval).Msg("snapshot service started")
49}
50
51// Stop stops the service and saves final state.
52func (s *Service) Stop(ctx context.Context) error {
53 s.mu.Lock()
54 if s.cancel != nil {
55 s.cancel()
56 }
57 if s.timer != nil {
58 s.timer.Stop()
59 s.timer = nil
60 }
61 s.mu.Unlock()
62
63 // Final save on shutdown
64 return s.SaveNow(ctx)
65}
66
67// MarkDirty signals that state has changed.
68// Debounces saves to avoid excessive DB writes.
69func (s *Service) MarkDirty() {
70 s.mu.Lock()
71 defer s.mu.Unlock()
72
73 s.dirty = true
74
75 // Reset or create timer
76 if s.timer != nil {
77 s.timer.Stop()
78 }
79
80 s.timer = time.AfterFunc(s.interval, func() {
81 s.mu.Lock()
82 ctx := s.ctx
83 s.mu.Unlock()
84
85 if ctx == nil {
86 return
87 }
88
89 if err := s.saveSnapshot(ctx); err != nil {
90 logging.FromContext(ctx).Error().Err(err).Msg("failed to save session snapshot")
91 }
92 })
93}
94
95// SaveNow forces immediate save (for shutdown).
96func (s *Service) SaveNow(ctx context.Context) error {
97 s.mu.Lock()
98 if s.timer != nil {
99 s.timer.Stop()
100 s.timer = nil
101 }
102 dirty := s.dirty
103 s.mu.Unlock()
104
105 if !dirty {
106 return nil
107 }
108
109 return s.saveSnapshot(ctx)
110}
111
112func (s *Service) saveSnapshot(ctx context.Context) error {
113 s.mu.Lock()
114 s.dirty = false
115 s.mu.Unlock()
116
117 tabList := s.provider.GetTabList()
118 sessionID := s.provider.GetSessionID()
119
120 if sessionID == "" {
121 return nil
122 }
123
124 return s.snapshotUC.Execute(ctx, usecase.SnapshotInput{
125 SessionID: sessionID,
126 TabList: tabList,
127 })
128}