diff --git a/context.go b/context.go index 934afdd..19eff56 100644 --- a/context.go +++ b/context.go @@ -7,6 +7,7 @@ import ( "log" "reflect" "sync" + "time" "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) } -// Routine uses the given initialization handler to define a safe concurrent goroutine -// that is tied to *Context. The returned *Routine instance provides methods -// to start, stop or update the routine. -func (c *Context) Routine(initRoutine func(*Routine)) *Routine { +// 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 { 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 := newRoutine(cn) - initRoutine(r) + r := newOnIntervalRoutine(cn, duration, handler) return r } @@ -335,7 +334,7 @@ func newContext(id string, route string, v *V) *Context { componentRegistry: make(map[string]*Context), actionRegistry: make(map[string]func()), signals: new(sync.Map), - patchChan: make(chan patch, 100), + patchChan: make(chan patch, 1), ctxDisposedChan: make(chan struct{}, 1), } } diff --git a/internal/examples/realtimechart/main.go b/internal/examples/realtimechart/main.go index 4c6d775..7d28259 100644 --- a/internal/examples/realtimechart/main.go +++ b/internal/examples/realtimechart/main.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "math/rand" "time" @@ -30,8 +29,8 @@ func main() { chartComp := c.Component(chartCompFn) c.View(func() h.H { - return h.Div(h.Class("container"), - h.Section( + return h.Div(h.Style("overflow-x:hidden"), + h.Section(h.Class("container"), h.Nav( h.Ul(h.Li(h.H3(h.Text("⚡Via")))), h.Ul( @@ -50,39 +49,30 @@ func main() { } func chartCompFn(c *via.Context) { - data := make([]float64, 1000) - labels := make([]string, 1000) - isLive := true isLiveSig := c.Signal("on") - refreshRate := c.Signal("1") + refreshRate := c.Signal("24") computedTickDuration := func() time.Duration { return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond } - updateData := c.Routine(func(r *via.Routine) { - - r.OnInterval(computedTickDuration(), func() { - 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)) - }) + updateData := 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() @@ -100,8 +90,8 @@ func chartCompFn(c *via.Context) { }) c.View(func() h.H { - return h.Div( - h.Div(h.ID("chart"), h.Style("width:100%;height:400px;"), h.Script(h.Raw(` + return h.Div(h.Div(h.ID("chart"), h.Style("width:100%;height:400px;"), + h.Script(h.Raw(` var prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); var myChart = echarts.init(document.getElementById('chart'), prefersDark.matches ? 'dark' : 'light'); var option = { @@ -111,25 +101,32 @@ func chartCompFn(c *via.Context) { trigger: 'axis', position: function (pt) { return [pt[0], '10%']; - } + }, + syncStrategy: 'closestSampledPoint', + backgroundColor: prefersDark.matches ? '#13171fc0' : '#eeeeeec0', + extraCssText: 'backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);' }, title: { left: 'center', text: '📈 Real-Time Chart Example' }, xAxis: { - type: 'category', + type: 'time', boundaryGap: false, - data: [] + axisLabel: { + hideOverlap: true + } }, yAxis: { type: 'value', - boundaryGap: [0, '100%'] + boundaryGap: [0, '100%'], + min: 0, + max: 100 }, dataZoom: [ { type: 'inside', - start: 90, + start: 1, end: 100 }, { @@ -142,7 +139,7 @@ func chartCompFn(c *via.Context) { name: 'Fake Data', type: 'line', symbol: 'none', - sampling: 'lttb', + sampling: 'max', itemStyle: { color: '#e8ae01' }, @@ -159,6 +156,7 @@ func chartCompFn(c *via.Context) { } ]) }, + large: true, data: [] } ] diff --git a/routine.go b/routine.go index d0276db..b0826fd 100644 --- a/routine.go +++ b/routine.go @@ -6,9 +6,9 @@ import ( "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. -type Routine struct { +type OnIntervalRoutine struct { mu sync.RWMutex ctxDisposed chan struct{} localInterrupt chan struct{} @@ -18,14 +18,41 @@ type Routine struct { updateTkrChan chan time.Duration } -// OnInterval starts a go routine that sets a time.Ticker with the given duration and executes -// the given func() on every tick. Use *Routine.UpdateInterval to update the interval. -// If the routine is running, it is stopped. -func (r *Routine) OnInterval(d time.Duration, fn func()) { - if r.isRunning.Load() == true { - r.Stop() - } +// 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) @@ -40,44 +67,9 @@ func (r *Routine) OnInterval(d time.Duration, fn func()) { case d := <-r.updateTkrChan: tkr.Reset(d) case <-tkr.C: - fn() + handler() } } } -} - -// 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), - } + return r } diff --git a/via.go b/via.go index 9b86b1d..4698a9b 100644 --- a/via.go +++ b/via.go @@ -392,7 +392,11 @@ func New() *V { v.logDebug(c, "SSE connection established") go func() { - c.Sync() + if v.cfg.DevMode { + c.Sync() + return + } + c.SyncSignals() }() for { @@ -461,17 +465,16 @@ func New() *V { defer r.Body.Close() cID := string(body) c, err := v.getCtx(cID) - c.stopAllRoutines() if err != nil { v.logErr(c, "failed to handle session close: %v", err) return } + c.stopAllRoutines() v.logDebug(c, "session close event triggered") if v.cfg.DevMode { v.devModeRemovePersisted(c) } v.unregisterCtx(c) - }) return v }