render_timer.go RawHistoryFolder
1package daemon
2
3import (
4 "sync"
5 "time"
6
7 "github.com/bnema/vev/internal/ports"
8)
9
10// timerToken is the complete, immutable identity of one timer worker. A lane
11// owns either this token or nothing; cancellation and completion are never
12// retained separately where a stale callback could reacquire them.
13type timerToken struct {
14 generation uint64
15 timer ports.Timer
16 cancel chan struct{}
17 done chan struct{}
18}
19
20// timerLane is coordinator-owned and must be accessed while renderCoordinator.mu
21// is held. Detach removes the complete token before any external Timer call.
22type timerLane struct {
23 generation uint64
24 token *timerToken
25}
26
27func (l *timerLane) replaceLocked() (uint64, *timerToken) {
28 l.generation++
29 return l.generation, l.detachLocked()
30}
31
32func (l *timerLane) detachLocked() *timerToken {
33 token := l.token
34 l.token = nil
35 if token != nil {
36 close(token.cancel)
37 }
38 return token
39}
40
41func (l *timerLane) publishLocked(generation uint64, timer ports.Timer) *timerToken {
42 if l.generation != generation || l.token != nil {
43 return nil
44 }
45 token := &timerToken{
46 generation: generation,
47 timer: timer,
48 cancel: make(chan struct{}),
49 done: make(chan struct{}),
50 }
51 l.token = token
52 return token
53}
54
55func (l *timerLane) clearLocked(token *timerToken) bool {
56 if token == nil || l.generation != token.generation || l.token != token {
57 return false
58 }
59 l.token = nil
60 return true
61}
62
63// timerSupervisor owns worker accounting. Workers are registered only while
64// coordinator ownership is held and terminal teardown rejects registrations
65// before Wait begins, so Add cannot race the terminal Wait.
66type timerSupervisor struct{ workers sync.WaitGroup }
67
68func (s *timerSupervisor) startLocked(token *timerToken, timerC <-chan time.Time, fire func()) {
69 s.startTaskLocked(func() {
70 defer close(token.done)
71 select {
72 case <-timerC:
73 fire()
74 case <-token.cancel:
75 }
76 })
77}
78
79// startTaskLocked accounts for coordinator work that is not backed by a timer
80// token. The caller must hold the coordinator lock so terminal teardown cannot
81// begin waiting between registration and launch.
82func (s *timerSupervisor) startTaskLocked(task func()) {
83 s.workers.Add(1)
84 go func() {
85 defer s.workers.Done()
86 task()
87 }()
88}
89
90func (s *timerSupervisor) wait() { s.workers.Wait() }
91
92// stopDetachedTimer is intentionally non-blocking. Ordinary lifecycle and
93// attachment transitions may cancel/stop stale workers but callback stacks
94// never wait for a worker to finish.
95func stopDetachedTimer(token *timerToken) {
96 if token != nil && token.timer != nil {
97 token.timer.Stop()
98 }
99}