refactor: simplify signals; small optimizations

This commit is contained in:
Joao Goncalves
2025-11-16 19:53:51 -01:00
parent 2bb5d80502
commit 472351d9a5
6 changed files with 32 additions and 122 deletions

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"reflect"
"sync" "sync"
"github.com/go-via/via/h" "github.com/go-via/via/h"
@@ -138,18 +137,15 @@ func (c *Context) Signal(v any) *signal {
sigID := genRandID() sigID := genRandID()
if v == nil { if v == nil {
c.app.logErr(c, "failed to bind signal: nil signal value") c.app.logErr(c, "failed to bind signal: nil signal value")
dummy := "Error"
return &signal{ return &signal{
id: sigID, id: sigID,
v: reflect.ValueOf(dummy), val: "error",
t: reflect.TypeOf(dummy),
err: fmt.Errorf("context '%s' failed to bind signal '%s': nil signal value", c.id, sigID), err: fmt.Errorf("context '%s' failed to bind signal '%s': nil signal value", c.id, sigID),
} }
} }
sig := &signal{ sig := &signal{
id: sigID, id: sigID,
v: reflect.ValueOf(v), val: v,
t: reflect.TypeOf(v),
changed: true, changed: true,
} }
@@ -176,14 +172,13 @@ func (c *Context) injectSignals(sigs map[string]any) {
if _, ok := c.signals.Load(sigID); !ok { if _, ok := c.signals.Load(sigID); !ok {
c.signals.Store(sigID, &signal{ c.signals.Store(sigID, &signal{
id: sigID, id: sigID,
t: reflect.TypeOf(val), val: val,
v: reflect.ValueOf(val),
}) })
continue continue
} }
item, _ := c.signals.Load(sigID) item, _ := c.signals.Load(sigID)
if sig, ok := item.(*signal); ok { if sig, ok := item.(*signal); ok {
sig.v = reflect.ValueOf(val) sig.val = val
sig.changed = false sig.changed = false
} }
} }
@@ -209,7 +204,7 @@ func (c *Context) prepareSignalsForPatch() map[string]any {
return true return true
} }
if sig.changed { if sig.changed {
updatedSigs[sigID.(string)] = fmt.Sprintf("%v", sig.v) updatedSigs[sigID.(string)] = fmt.Sprintf("%v", sig.val)
} }
} }
return true return true
@@ -288,7 +283,7 @@ func (c *Context) SyncSignals() {
c.app.logWarn(c, "signal out of sync'%s': %v", sig.id, sig.err) c.app.logWarn(c, "signal out of sync'%s': %v", sig.id, sig.err)
} }
if sig.changed && sig.err == nil { if sig.changed && sig.err == nil {
updatedSigs[id] = fmt.Sprintf("%v", sig.v) updatedSigs[id] = fmt.Sprintf("%v", sig.val)
sig.changed = false sig.changed = false
} }
return true // continue iteration return true // continue iteration

2
go.mod
View File

@@ -5,7 +5,6 @@ go 1.25.4
require maragu.dev/gomponents v1.2.0 require maragu.dev/gomponents v1.2.0
require ( require (
github.com/andybalholm/brotli v1.2.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-via/via-plugin-picocss v0.1.0 github.com/go-via/via-plugin-picocss v0.1.0
github.com/starfederation/datastar-go v1.0.3 github.com/starfederation/datastar-go v1.0.3
@@ -14,6 +13,7 @@ require (
require ( require (
github.com/CAFxX/httpcompression v0.0.9 // indirect github.com/CAFxX/httpcompression v0.0.9 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect

View File

@@ -153,9 +153,7 @@ func main() {
chat := currentRoom.GetData(func(c *Chat) Chat { chat := currentRoom.GetData(func(c *Chat) Chat {
n := len(c.Entries) n := len(c.Entries)
start := n - 50 start := n - 50
if start < 0 { start = max(start, 0)
start = 0
}
trimmed := make([]ChatEntry, n-start) trimmed := make([]ChatEntry, n-start)
copy(trimmed, c.Entries[start:]) copy(trimmed, c.Entries[start:])
out := *c out := *c

100
signal.go
View File

@@ -2,7 +2,6 @@ package via
import ( import (
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"strings" "strings"
@@ -16,8 +15,7 @@ import (
// reactively on an html element. // reactively on an html element.
type signal struct { type signal struct {
id string id string
v reflect.Value val any
t reflect.Type
changed bool changed bool
err error err error
} }
@@ -45,7 +43,7 @@ func (s *signal) Bind() h.H {
return h.Data("bind", s.id) return h.Data("bind", s.id)
} }
// Text binds the signal value to an html element as text. // Text binds the signal value to an html span element as text.
// //
// Example: // Example:
// //
@@ -57,130 +55,52 @@ func (s *signal) Text() h.H {
// SetValue updates the signals value and marks it for synchronization with the browser. // SetValue updates the signals value and marks it for synchronization with the browser.
// The change will be propagated to the browser using *Context.Sync() or *Context.SyncSignals(). // The change will be propagated to the browser using *Context.Sync() or *Context.SyncSignals().
func (s *signal) SetValue(v any) { func (s *signal) SetValue(v any) {
val := reflect.ValueOf(v) s.val = v
typ := reflect.TypeOf(v)
if typ != s.t {
s.err = fmt.Errorf("expected type '%s', got '%s'", s.t.String(), typ.String())
return
}
s.v = val
s.changed = true s.changed = true
s.err = nil s.err = nil
} }
// String return the signal value as a string. // String return the signal value as a string.
func (s *signal) String() string { func (s *signal) String() string {
return fmt.Sprintf("%v", s.v) return fmt.Sprintf("%v", s.val)
} }
// Bool tries to read the signal value as a bool. // Bool tries to read the signal value as a bool.
// Returns the value or false on failure. // Returns the value or false on failure.
func (s *signal) Bool() bool { func (s *signal) Bool() bool {
switch s.v.Kind() { val := strings.ToLower(s.String())
case reflect.Bool:
return s.v.Bool()
case reflect.String:
val := strings.ToLower(s.v.String())
return val == "true" || val == "1" || val == "yes" || val == "on" return val == "true" || val == "1" || val == "yes" || val == "on"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return s.v.Int() != 0
case reflect.Float32, reflect.Float64:
return s.v.Float() != 0
default:
return false
}
} }
// Int tries to read the signal value as an int. // Int tries to read the signal value as an int.
// Returns the value or 0 on failure. // Returns the value or 0 on failure.
func (s *signal) Int() int { func (s *signal) Int() int {
if n, err := strconv.Atoi(s.v.String()); err == nil { if n, err := strconv.Atoi(s.String()); err == nil {
return n return n
} }
if s.v.CanInt() {
return int(s.v.Int())
}
if s.v.CanFloat() {
return int(s.v.Float())
}
return 0 return 0
} }
// Int64 tries to read the signal value as an int64. // Int64 tries to read the signal value as an int64.
// Returns the value or 0 on failure. // Returns the value or 0 on failure.
func (s *signal) Int64() int64 { func (s *signal) Int64() int64 {
if n, err := strconv.ParseInt(s.v.String(), 10, 64); err == nil { if n, err := strconv.ParseInt(s.String(), 10, 64); err == nil {
return n return n
} }
if s.v.CanInt() {
return s.v.Int()
}
if s.v.CanFloat() {
return int64(s.v.Float())
}
return 0
}
// Uint64 tries to read the signal value as an uint64.
// Returns the value or 0 on failure.
func (s *signal) Uint64() uint64 {
if n, err := strconv.ParseUint(s.v.String(), 10, 64); err == nil {
return n
}
if s.v.CanUint() {
return s.v.Uint()
}
if s.v.CanFloat() {
return uint64(s.v.Float())
}
return 0 return 0
} }
// Float64 tries to read the signal value as a float64. // Float64 tries to read the signal value as a float64.
// Returns the value or 0.0 on failure. // Returns the value or 0.0 on failure.
func (s *signal) Float64() float64 { func (s *signal) Float() float64 {
if n, err := strconv.ParseFloat(s.v.String(), 64); err == nil { if n, err := strconv.ParseFloat(s.String(), 64); err == nil {
return n return n
} }
if s.v.CanFloat() {
return s.v.Float()
}
if s.v.CanInt() {
return float64(s.v.Int())
}
return 0.0 return 0.0
} }
// Complex128 tries to read the signal value as a complex128.
// Returns the value or 0 on failure.
func (s *signal) Complex128() complex128 {
if s.v.Kind() == reflect.Complex128 {
return s.v.Complex()
}
if s.v.Kind() == reflect.String {
if n, err := strconv.ParseComplex(s.v.String(), 128); err == nil {
return n
}
}
if s.v.CanFloat() {
return complex(s.v.Float(), 0)
}
if s.v.CanInt() {
return complex(float64(s.v.Int()), 0)
}
return complex(0, 0)
}
// Bytes tries to read the signal value as a []byte // Bytes tries to read the signal value as a []byte
// Returns the value or an empty []byte on failure. // Returns the value or an empty []byte on failure.
func (s *signal) Bytes() []byte { func (s *signal) Bytes() []byte {
switch s.v.Kind() { return []byte(s.String())
case reflect.Slice:
if s.v.Type().Elem().Kind() == reflect.Uint8 {
return s.v.Bytes()
}
case reflect.String:
return []byte(s.v.String())
}
return make([]byte, 0)
} }

25
via.go
View File

@@ -145,24 +145,21 @@ func (v *V) Page(route string, initContextFn func(c *Context)) {
if v.cfg.DevMode { if v.cfg.DevMode {
v.devModePersist(c) v.devModePersist(c)
} }
headElements := v.documentHeadIncludes v.AppendToHead(h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))))
headElements = append(headElements, h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id)))) v.AppendToHead(h.Meta(h.Data("init", "@get('/_sse')")))
unloadJS := fmt.Sprintf(` v.AppendToHead(h.Meta(h.Data("init", fmt.Sprintf(`window.addEventListener('beforeunload', (evt) => {
window.addEventListener('beforeunload', (evt) => { navigator.sendBeacon('/_session/close', '%s');});`, c.id))))
navigator.sendBeacon('/_session/close', '%s'); bodyElements := []h.H{c.view()}
});`, c.id) bodyElements = append(bodyElements, v.documentFootIncludes...)
headElements = append(headElements, h.Meta(h.Data("init", unloadJS)))
headElements = append(headElements, h.Meta(h.Data("init", "@get('/_sse')")))
bottomBodyElements := []h.H{c.view()}
bottomBodyElements = append(bottomBodyElements, v.documentFootIncludes...)
if v.cfg.DevMode { if v.cfg.DevMode {
bottomBodyElements = append(bottomBodyElements, h.Script(h.Type("module"), h.Src("https://cdn.jsdelivr.net/gh/dataSPA/dataSPA-inspector@latest/dataspa-inspector.bundled.js"))) bodyElements = append(bodyElements, h.Script(h.Type("module"),
bottomBodyElements = append(bottomBodyElements, h.Raw("<dataspa-inspector/>")) h.Src("https://cdn.jsdelivr.net/gh/dataSPA/dataSPA-inspector@latest/dataspa-inspector.bundled.js")))
bodyElements = append(bodyElements, h.Raw("<dataspa-inspector/>"))
} }
view := h.HTML5(h.HTML5Props{ view := h.HTML5(h.HTML5Props{
Title: v.cfg.DocumentTitle, Title: v.cfg.DocumentTitle,
Head: headElements, Head: v.documentHeadIncludes,
Body: bottomBodyElements, Body: bodyElements,
HTMLAttrs: []h.H{}, HTMLAttrs: []h.H{},
}) })
_ = view.Render(w) _ = view.Render(w)

View File

@@ -49,7 +49,7 @@ func TestSignal(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
v.mux.ServeHTTP(w, req) v.mux.ServeHTTP(w, req)
assert.Equal(t, "test", sig.v.Interface()) assert.Equal(t, "test", sig.String())
} }
func TestAction(t *testing.T) { func TestAction(t *testing.T) {