refactor: simplify OnInterval API to auto-start and return stop func
Replace the exported OnIntervalRoutine struct (Start/Stop/UpdateInterval) with a single function that auto-starts the goroutine and returns an idempotent stop closure. Uses close(channel) instead of send-on-channel, fixing a potential deadlock when the goroutine exits via context disposal. Closes #5 item 4.
This commit is contained in:
@@ -69,7 +69,7 @@ func main() {
|
|||||||
- **CSRF protection** — automatic token generation and validation on every action
|
- **CSRF protection** — automatic token generation and validation on every action
|
||||||
- **Rate limiting** — token-bucket algorithm, configurable globally and per-action
|
- **Rate limiting** — token-bucket algorithm, configurable globally and per-action
|
||||||
- **Event handling** — `OnClick`, `OnChange`, `OnSubmit`, `OnInput`, `OnFocus`, `OnBlur`, `OnMouseEnter`, `OnMouseLeave`, `OnScroll`, `OnDblClick`, `OnKeyDown`, and `OnKeyDownMap` for multi-key bindings
|
- **Event handling** — `OnClick`, `OnChange`, `OnSubmit`, `OnInput`, `OnFocus`, `OnBlur`, `OnMouseEnter`, `OnMouseLeave`, `OnScroll`, `OnDblClick`, `OnKeyDown`, and `OnKeyDownMap` for multi-key bindings
|
||||||
- **Timed routines** — `OnInterval` with start/stop/update controls, tied to context lifecycle
|
- **Timed routines** — `OnInterval` auto-starts a ticker goroutine, returns a stop function, tied to context lifecycle
|
||||||
- **Redirects** — `Redirect`, `ReplaceURL`, and format-string variants
|
- **Redirects** — `Redirect`, `ReplaceURL`, and format-string variants
|
||||||
- **Plugin system** — `func(v *V)` hooks for integrating CSS/JS libraries
|
- **Plugin system** — `func(v *V)` hooks for integrating CSS/JS libraries
|
||||||
- **Structured logging** — zerolog with configurable levels; console output in dev, JSON in production
|
- **Structured logging** — zerolog with configurable levels; console output in dev, JSON in production
|
||||||
|
|||||||
12
context.go
12
context.go
@@ -131,17 +131,17 @@ func (c *Context) getAction(id string) (actionEntry, error) {
|
|||||||
return actionEntry{}, fmt.Errorf("action '%s' not found", id)
|
return actionEntry{}, fmt.Errorf("action '%s' not found", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnInterval starts a go routine that sets a time.Ticker with the given duration and executes
|
// OnInterval starts a goroutine that executes handler on every tick of the given duration.
|
||||||
// the given handler func() on every tick. Use *Routine.UpdateInterval to update the interval.
|
// The goroutine is tied to the context lifecycle and will stop when the context is disposed.
|
||||||
func (c *Context) OnInterval(duration time.Duration, handler func()) *OnIntervalRoutine {
|
// Returns a func() that stops the interval when called.
|
||||||
|
func (c *Context) OnInterval(duration time.Duration, handler func()) func() {
|
||||||
var cn chan struct{}
|
var cn chan struct{}
|
||||||
if c.isComponent() { // components use the chan on the parent page ctx
|
if c.isComponent() { // components use the chan on the parent page ctx
|
||||||
cn = c.parentPageCtx.ctxDisposedChan
|
cn = c.parentPageCtx.ctxDisposedChan
|
||||||
} else {
|
} else {
|
||||||
cn = c.ctxDisposedChan
|
cn = c.ctxDisposedChan
|
||||||
}
|
}
|
||||||
r := newOnIntervalRoutine(cn, duration, handler)
|
return newOnInterval(cn, duration, handler)
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal creates a reactive signal and initializes it with the given value.
|
// Signal creates a reactive signal and initializes it with the given value.
|
||||||
@@ -379,7 +379,7 @@ func (c *Context) dispose() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stopAllRoutines closes ctxDisposedChan, broadcasting to all listening
|
// stopAllRoutines closes ctxDisposedChan, broadcasting to all listening
|
||||||
// goroutines (OnIntervalRoutine, SSE loop) that this context is done.
|
// goroutines (OnInterval, SSE loop) that this context is done.
|
||||||
func (c *Context) stopAllRoutines() {
|
func (c *Context) stopAllRoutines() {
|
||||||
select {
|
select {
|
||||||
case <-c.ctxDisposedChan:
|
case <-c.ctxDisposedChan:
|
||||||
|
|||||||
@@ -37,29 +37,33 @@ func main() {
|
|||||||
return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond
|
return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
updateData := c.OnInterval(computedTickDuration(), func() {
|
var stopUpdate func()
|
||||||
ts := time.Now().UnixMilli()
|
startInterval := func() {
|
||||||
val := rand.ExpFloat64() * 10
|
stopUpdate = c.OnInterval(computedTickDuration(), func() {
|
||||||
|
ts := time.Now().UnixMilli()
|
||||||
|
val := rand.ExpFloat64() * 10
|
||||||
|
|
||||||
c.ExecScript(fmt.Sprintf(`
|
c.ExecScript(fmt.Sprintf(`
|
||||||
if (myChart) {
|
if (myChart) {
|
||||||
myChart.appendData({seriesIndex: 0, data: [[%d, %f]]});
|
myChart.appendData({seriesIndex: 0, data: [[%d, %f]]});
|
||||||
myChart.setOption({},{notMerge:false,lazyUpdate:true});
|
myChart.setOption({},{notMerge:false,lazyUpdate:true});
|
||||||
};
|
};
|
||||||
`, ts, val))
|
`, ts, val))
|
||||||
})
|
})
|
||||||
updateData.Start()
|
}
|
||||||
|
startInterval()
|
||||||
|
|
||||||
updateRefreshRate := c.Action(func() {
|
updateRefreshRate := c.Action(func() {
|
||||||
updateData.UpdateInterval(computedTickDuration())
|
stopUpdate()
|
||||||
|
startInterval()
|
||||||
})
|
})
|
||||||
|
|
||||||
toggleIsLive := c.Action(func() {
|
toggleIsLive := c.Action(func() {
|
||||||
isLive = isLiveSig.Bool()
|
isLive = isLiveSig.Bool()
|
||||||
if isLive {
|
if isLive {
|
||||||
updateData.Start()
|
startInterval()
|
||||||
} else {
|
} else {
|
||||||
updateData.Stop()
|
stopUpdate()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
c.View(func() h.H {
|
c.View(func() h.H {
|
||||||
|
|||||||
72
routine.go
72
routine.go
@@ -1,76 +1,32 @@
|
|||||||
package via
|
package via
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OnIntervalRoutine allows for defining concurrent goroutines safely. Goroutines started by *OnIntervalRoutine
|
func newOnInterval(ctxDisposedChan chan struct{}, duration time.Duration, handler func()) func() {
|
||||||
// are tied to the *Context lifecycle.
|
localInterrupt := make(chan struct{})
|
||||||
type OnIntervalRoutine struct {
|
var stopped atomic.Bool
|
||||||
mu sync.RWMutex
|
|
||||||
ctxDisposed chan struct{}
|
|
||||||
localInterrupt chan struct{}
|
|
||||||
isRunning atomic.Bool
|
|
||||||
routineFn func()
|
|
||||||
tckDuration time.Duration
|
|
||||||
updateTkrChan chan time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateInterval sets a new interval duration for the internal *time.Ticker. If the provided
|
go func() {
|
||||||
// duration is equal of less than 0, UpdateInterval does nothing.
|
tkr := time.NewTicker(duration)
|
||||||
func (r *OnIntervalRoutine) UpdateInterval(d time.Duration) {
|
defer tkr.Stop()
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
r.tckDuration = d
|
|
||||||
r.updateTkrChan <- d
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start executes the predifined goroutine. If no predifined goroutine exists, or it already
|
|
||||||
// started, Start does nothing.
|
|
||||||
func (r *OnIntervalRoutine) Start() {
|
|
||||||
if !r.isRunning.CompareAndSwap(false, true) || r.routineFn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go r.routineFn()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop interrupts the predifined goroutine. If no predifined goroutine exists, or it already
|
|
||||||
// ustopped, Stop does nothing.
|
|
||||||
func (r *OnIntervalRoutine) Stop() {
|
|
||||||
if !r.isRunning.CompareAndSwap(true, false) || r.routineFn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.localInterrupt <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOnIntervalRoutine(ctxDisposedChan chan struct{},
|
|
||||||
duration time.Duration, handler func()) *OnIntervalRoutine {
|
|
||||||
r := &OnIntervalRoutine{
|
|
||||||
ctxDisposed: ctxDisposedChan,
|
|
||||||
localInterrupt: make(chan struct{}),
|
|
||||||
updateTkrChan: make(chan time.Duration),
|
|
||||||
}
|
|
||||||
r.tckDuration = duration
|
|
||||||
r.routineFn = func() {
|
|
||||||
r.mu.RLock()
|
|
||||||
tkr := time.NewTicker(r.tckDuration)
|
|
||||||
r.mu.RUnlock()
|
|
||||||
defer tkr.Stop() // clean up the ticker when routine stops
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-r.ctxDisposed: // dispose of the routine when ctx is disposed
|
case <-ctxDisposedChan:
|
||||||
return
|
return
|
||||||
case <-r.localInterrupt: // dispose of the routine on interrupt signal
|
case <-localInterrupt:
|
||||||
return
|
return
|
||||||
case d := <-r.updateTkrChan:
|
|
||||||
tkr.Reset(d)
|
|
||||||
case <-tkr.C:
|
case <-tkr.C:
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
if stopped.CompareAndSwap(false, true) {
|
||||||
|
close(localInterrupt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user