refactor: simplify oninterval routine; fix(runtime): session end handler; update realtime chart example
This commit is contained in:
13
context.go
13
context.go
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
84
routine.go
84
routine.go
@@ -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
9
via.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user