diff --git a/README.md b/README.md index 478d68c..51b2e58 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ func main() { - **CSRF protection** — automatic token generation and validation on every 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 -- **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 - **Plugin system** — `func(v *V)` hooks for integrating CSS/JS libraries - **Structured logging** — zerolog with configurable levels; console output in dev, JSON in production diff --git a/context.go b/context.go index 36d4634..f7cd299 100644 --- a/context.go +++ b/context.go @@ -131,17 +131,17 @@ func (c *Context) getAction(id string) (actionEntry, error) { 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 -// the given handler func() on every tick. Use *Routine.UpdateInterval to update the interval. -func (c *Context) OnInterval(duration time.Duration, handler func()) *OnIntervalRoutine { +// OnInterval starts a goroutine that executes handler on every tick of the given duration. +// The goroutine is tied to the context lifecycle and will stop when the context is disposed. +// Returns a func() that stops the interval when called. +func (c *Context) OnInterval(duration time.Duration, handler func()) func() { var cn chan struct{} if c.isComponent() { // components use the chan on the parent page ctx cn = c.parentPageCtx.ctxDisposedChan } else { cn = c.ctxDisposedChan } - r := newOnIntervalRoutine(cn, duration, handler) - return r + return newOnInterval(cn, duration, handler) } // 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 -// goroutines (OnIntervalRoutine, SSE loop) that this context is done. +// goroutines (OnInterval, SSE loop) that this context is done. func (c *Context) stopAllRoutines() { select { case <-c.ctxDisposedChan: diff --git a/internal/examples/realtimechart/main.go b/internal/examples/realtimechart/main.go index af794b0..cb3ca47 100644 --- a/internal/examples/realtimechart/main.go +++ b/internal/examples/realtimechart/main.go @@ -37,29 +37,33 @@ func main() { return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond } - updateData := c.OnInterval(computedTickDuration(), func() { - ts := time.Now().UnixMilli() - val := rand.ExpFloat64() * 10 + var stopUpdate func() + startInterval := func() { + stopUpdate = c.OnInterval(computedTickDuration(), func() { + ts := time.Now().UnixMilli() + val := rand.ExpFloat64() * 10 - c.ExecScript(fmt.Sprintf(` - if (myChart) { - myChart.appendData({seriesIndex: 0, data: [[%d, %f]]}); - myChart.setOption({},{notMerge:false,lazyUpdate:true}); - }; - `, ts, val)) - }) - updateData.Start() + c.ExecScript(fmt.Sprintf(` + if (myChart) { + myChart.appendData({seriesIndex: 0, data: [[%d, %f]]}); + myChart.setOption({},{notMerge:false,lazyUpdate:true}); + }; + `, ts, val)) + }) + } + startInterval() updateRefreshRate := c.Action(func() { - updateData.UpdateInterval(computedTickDuration()) + stopUpdate() + startInterval() }) toggleIsLive := c.Action(func() { isLive = isLiveSig.Bool() if isLive { - updateData.Start() + startInterval() } else { - updateData.Stop() + stopUpdate() } }) c.View(func() h.H { diff --git a/routine.go b/routine.go index 03b4f9a..8ec0b9c 100644 --- a/routine.go +++ b/routine.go @@ -1,76 +1,32 @@ package via import ( - "sync" "sync/atomic" "time" ) -// OnIntervalRoutine allows for defining concurrent goroutines safely. Goroutines started by *OnIntervalRoutine -// are tied to the *Context lifecycle. -type OnIntervalRoutine struct { - mu sync.RWMutex - ctxDisposed chan struct{} - localInterrupt chan struct{} - isRunning atomic.Bool - routineFn func() - tckDuration time.Duration - updateTkrChan chan time.Duration -} +func newOnInterval(ctxDisposedChan chan struct{}, duration time.Duration, handler func()) func() { + localInterrupt := make(chan struct{}) + var stopped atomic.Bool -// UpdateInterval sets a new interval duration for the internal *time.Ticker. If the provided -// duration is equal of less than 0, UpdateInterval does nothing. -func (r *OnIntervalRoutine) UpdateInterval(d time.Duration) { - 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 + go func() { + tkr := time.NewTicker(duration) + defer tkr.Stop() for { select { - case <-r.ctxDisposed: // dispose of the routine when ctx is disposed + case <-ctxDisposedChan: return - case <-r.localInterrupt: // dispose of the routine on interrupt signal + case <-localInterrupt: return - case d := <-r.updateTkrChan: - tkr.Reset(d) case <-tkr.C: handler() } } + }() + + return func() { + if stopped.CompareAndSwap(false, true) { + close(localInterrupt) + } } - return r }