refactor: simplify oninterval routine; fix(runtime): session end handler; update realtime chart example

This commit is contained in:
Joao Goncalves
2025-12-04 12:40:36 -01:00
parent 51218e7a2a
commit 26268f698a
4 changed files with 82 additions and 90 deletions

View File

@@ -7,6 +7,7 @@ import (
"log" "log"
"reflect" "reflect"
"sync" "sync"
"time"
"github.com/go-via/via/h" "github.com/go-via/via/h"
) )
@@ -115,18 +116,16 @@ func (c *Context) getActionFn(id string) (func(), error) {
return nil, fmt.Errorf("action '%s' not found", id) return nil, fmt.Errorf("action '%s' not found", id)
} }
// Routine uses the given initialization handler to define a safe concurrent goroutine // OnInterval starts a go routine that sets a time.Ticker with the given duration and executes
// that is tied to *Context. The returned *Routine instance provides methods // the given handler func() on every tick. Use *Routine.UpdateInterval to update the interval.
// to start, stop or update the routine. func (c *Context) OnInterval(duration time.Duration, handler func()) *OnIntervalRoutine {
func (c *Context) Routine(initRoutine func(*Routine)) *Routine {
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 := newRoutine(cn) r := newOnIntervalRoutine(cn, duration, handler)
initRoutine(r)
return r return r
} }
@@ -335,7 +334,7 @@ func newContext(id string, route string, v *V) *Context {
componentRegistry: make(map[string]*Context), componentRegistry: make(map[string]*Context),
actionRegistry: make(map[string]func()), actionRegistry: make(map[string]func()),
signals: new(sync.Map), signals: new(sync.Map),
patchChan: make(chan patch, 100), patchChan: make(chan patch, 1),
ctxDisposedChan: make(chan struct{}, 1), ctxDisposedChan: make(chan struct{}, 1),
} }
} }

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"math/rand" "math/rand"
"time" "time"
@@ -30,8 +29,8 @@ func main() {
chartComp := c.Component(chartCompFn) chartComp := c.Component(chartCompFn)
c.View(func() h.H { c.View(func() h.H {
return h.Div(h.Class("container"), return h.Div(h.Style("overflow-x:hidden"),
h.Section( h.Section(h.Class("container"),
h.Nav( h.Nav(
h.Ul(h.Li(h.H3(h.Text("⚡Via")))), h.Ul(h.Li(h.H3(h.Text("⚡Via")))),
h.Ul( h.Ul(
@@ -50,39 +49,30 @@ func main() {
} }
func chartCompFn(c *via.Context) { func chartCompFn(c *via.Context) {
data := make([]float64, 1000)
labels := make([]string, 1000)
isLive := true isLive := true
isLiveSig := c.Signal("on") isLiveSig := c.Signal("on")
refreshRate := c.Signal("1") refreshRate := c.Signal("24")
computedTickDuration := func() time.Duration { computedTickDuration := func() time.Duration {
return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond
} }
updateData := c.Routine(func(r *via.Routine) { updateData := c.OnInterval(computedTickDuration(), func() {
ts := time.Now().UnixMilli()
r.OnInterval(computedTickDuration(), func() { val := rand.ExpFloat64() * 10
labels = append(labels[1:], time.Now().Format("15:04:05.000"))
data = append(data[1:], rand.Float64()*10)
labelsTxt, _ := json.Marshal(labels)
dataTxt, _ := json.Marshal(data)
c.ExecScript(fmt.Sprintf(`
if (myChart)
myChart.setOption({
xAxis: [{data: %s}],
series:[{data: %s}]
},{
notMerge:false,
lazyUpdate:true
});
`, labelsTxt, dataTxt))
})
c.ExecScript(fmt.Sprintf(`
if (myChart) {
myChart.appendData({seriesIndex: 0, data: [[%d, %f]]});
myChart.setOption({
},{
notMerge:false,
lazyUpdate:true
});
};
`, ts, val))
}) })
updateData.Start() updateData.Start()
@@ -100,8 +90,8 @@ func chartCompFn(c *via.Context) {
}) })
c.View(func() h.H { c.View(func() h.H {
return h.Div( return h.Div(h.Div(h.ID("chart"), h.Style("width:100%;height:400px;"),
h.Div(h.ID("chart"), h.Style("width:100%;height:400px;"), h.Script(h.Raw(` h.Script(h.Raw(`
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); var prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
var myChart = echarts.init(document.getElementById('chart'), prefersDark.matches ? 'dark' : 'light'); var myChart = echarts.init(document.getElementById('chart'), prefersDark.matches ? 'dark' : 'light');
var option = { var option = {
@@ -111,25 +101,32 @@ func chartCompFn(c *via.Context) {
trigger: 'axis', trigger: 'axis',
position: function (pt) { position: function (pt) {
return [pt[0], '10%']; return [pt[0], '10%'];
} },
syncStrategy: 'closestSampledPoint',
backgroundColor: prefersDark.matches ? '#13171fc0' : '#eeeeeec0',
extraCssText: 'backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);'
}, },
title: { title: {
left: 'center', left: 'center',
text: '📈 Real-Time Chart Example' text: '📈 Real-Time Chart Example'
}, },
xAxis: { xAxis: {
type: 'category', type: 'time',
boundaryGap: false, boundaryGap: false,
data: [] axisLabel: {
hideOverlap: true
}
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
boundaryGap: [0, '100%'] boundaryGap: [0, '100%'],
min: 0,
max: 100
}, },
dataZoom: [ dataZoom: [
{ {
type: 'inside', type: 'inside',
start: 90, start: 1,
end: 100 end: 100
}, },
{ {
@@ -142,7 +139,7 @@ func chartCompFn(c *via.Context) {
name: 'Fake Data', name: 'Fake Data',
type: 'line', type: 'line',
symbol: 'none', symbol: 'none',
sampling: 'lttb', sampling: 'max',
itemStyle: { itemStyle: {
color: '#e8ae01' color: '#e8ae01'
}, },
@@ -159,6 +156,7 @@ func chartCompFn(c *via.Context) {
} }
]) ])
}, },
large: true,
data: [] data: []
} }
] ]

View File

@@ -6,9 +6,9 @@ import (
"time" "time"
) )
// Routine allows for defining concurrent goroutines safely. Goroutines started by *Routine // OnIntervalRoutine allows for defining concurrent goroutines safely. Goroutines started by *OnIntervalRoutine
// are tied to the *Context lifecycle. // are tied to the *Context lifecycle.
type Routine struct { type OnIntervalRoutine struct {
mu sync.RWMutex mu sync.RWMutex
ctxDisposed chan struct{} ctxDisposed chan struct{}
localInterrupt chan struct{} localInterrupt chan struct{}
@@ -18,14 +18,41 @@ type Routine struct {
updateTkrChan chan time.Duration updateTkrChan chan time.Duration
} }
// OnInterval starts a go routine that sets a time.Ticker with the given duration and executes // UpdateInterval sets a new interval duration for the internal *time.Ticker. If the provided
// the given func() on every tick. Use *Routine.UpdateInterval to update the interval. // duration is equal of less than 0, UpdateInterval does nothing.
// If the routine is running, it is stopped. func (r *OnIntervalRoutine) UpdateInterval(d time.Duration) {
func (r *Routine) OnInterval(d time.Duration, fn func()) { r.mu.Lock()
if r.isRunning.Load() == true { defer r.mu.Unlock()
r.Stop()
}
r.tckDuration = d 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.routineFn = func() {
r.mu.RLock() r.mu.RLock()
tkr := time.NewTicker(r.tckDuration) tkr := time.NewTicker(r.tckDuration)
@@ -40,44 +67,9 @@ func (r *Routine) OnInterval(d time.Duration, fn func()) {
case d := <-r.updateTkrChan: case d := <-r.updateTkrChan:
tkr.Reset(d) tkr.Reset(d)
case <-tkr.C: case <-tkr.C:
fn() handler()
} }
} }
} }
} return r
// 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 *Routine) 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 *Routine) 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 *Routine) Stop() {
if !r.isRunning.CompareAndSwap(true, false) || r.routineFn == nil {
return
}
r.localInterrupt <- struct{}{}
}
func newRoutine(ctxDisposedChan chan struct{}) *Routine {
return &Routine{
ctxDisposed: ctxDisposedChan,
localInterrupt: make(chan struct{}),
updateTkrChan: make(chan time.Duration),
}
} }

9
via.go
View File

@@ -392,7 +392,11 @@ func New() *V {
v.logDebug(c, "SSE connection established") v.logDebug(c, "SSE connection established")
go func() { go func() {
c.Sync() if v.cfg.DevMode {
c.Sync()
return
}
c.SyncSignals()
}() }()
for { for {
@@ -461,17 +465,16 @@ func New() *V {
defer r.Body.Close() defer r.Body.Close()
cID := string(body) cID := string(body)
c, err := v.getCtx(cID) c, err := v.getCtx(cID)
c.stopAllRoutines()
if err != nil { if err != nil {
v.logErr(c, "failed to handle session close: %v", err) v.logErr(c, "failed to handle session close: %v", err)
return return
} }
c.stopAllRoutines()
v.logDebug(c, "session close event triggered") v.logDebug(c, "session close event triggered")
if v.cfg.DevMode { if v.cfg.DevMode {
v.devModeRemovePersisted(c) v.devModeRemovePersisted(c)
} }
v.unregisterCtx(c) v.unregisterCtx(c)
}) })
return v return v
} }