feat: introduce ctx close mechanism using beforeunload event; small fixes and improvements; improve live reload example
This commit is contained in:
@@ -3,7 +3,8 @@ package via
|
|||||||
type LogLevel int
|
type LogLevel int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LogLevelError LogLevel = iota
|
undefined LogLevel = iota
|
||||||
|
LogLevelError
|
||||||
LogLevelWarn
|
LogLevelWarn
|
||||||
LogLevelInfo
|
LogLevelInfo
|
||||||
LogLevelDebug
|
LogLevelDebug
|
||||||
@@ -12,7 +13,7 @@ const (
|
|||||||
// Plugin is a func that can mutate the given *via.V app runtime. It is useful to integrate popular JS/CSS UI libraries or tools.
|
// Plugin is a func that can mutate the given *via.V app runtime. It is useful to integrate popular JS/CSS UI libraries or tools.
|
||||||
type Plugin func(v *V)
|
type Plugin func(v *V)
|
||||||
|
|
||||||
// Config defines configuration options for the via application
|
// Config defines configuration options for the via application.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// The development mode flag. If true, enables server and browser auto-reload on `.go` file changes.
|
// The development mode flag. If true, enables server and browser auto-reload on `.go` file changes.
|
||||||
DevMode bool
|
DevMode bool
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -1,11 +1,12 @@
|
|||||||
module github.com/go-via/via
|
module github.com/go-via/via
|
||||||
|
|
||||||
go 1.25.3
|
go 1.25.4
|
||||||
|
|
||||||
require maragu.dev/gomponents v1.2.0
|
require maragu.dev/gomponents v1.2.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
|
github.com/go-via/via-plugin-picocss v0.0.0-20251112183909-4485ba2e31d8
|
||||||
github.com/starfederation/datastar-go v1.0.3
|
github.com/starfederation/datastar-go v1.0.3
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/go-via/via-plugin-picocss v0.0.0-20251112183909-4485ba2e31d8 h1:FRjqaCz+MbG53a8EJOJglSK/dcUx7LwR8+UuJBs5bGU=
|
||||||
|
github.com/go-via/via-plugin-picocss v0.0.0-20251112183909-4485ba2e31d8/go.mod h1:5LEnLE7q8YfYY7jtH/TLPvfquB7Qt9WZ7TbKrskUW+0=
|
||||||
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
|
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
|
||||||
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
|
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
|
||||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ tmp_dir = "tmp"
|
|||||||
[build]
|
[build]
|
||||||
args_bin = []
|
args_bin = []
|
||||||
bin = "./tmp/main"
|
bin = "./tmp/main"
|
||||||
cmd = "go build -o ./tmp/main ."
|
cmd = "go build -race -o ./tmp/main ."
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-via/via"
|
"github.com/go-via/via"
|
||||||
|
"github.com/go-via/via-plugin-picocss/picocss"
|
||||||
"github.com/go-via/via/h"
|
"github.com/go-via/via/h"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,9 +12,10 @@ func main() {
|
|||||||
v := via.New()
|
v := via.New()
|
||||||
|
|
||||||
v.Config(via.Options{
|
v.Config(via.Options{
|
||||||
DocumentTitle: "Live Reload",
|
DocumentTitle: "Live Reload Demo",
|
||||||
DevMode: true,
|
DevMode: true,
|
||||||
LogLvl: via.LogLevelDebug,
|
LogLvl: via.LogLevelDebug,
|
||||||
|
Plugins: []via.Plugin{picocss.Default},
|
||||||
})
|
})
|
||||||
|
|
||||||
v.Page("/", func(c *via.Context) {
|
v.Page("/", func(c *via.Context) {
|
||||||
@@ -26,15 +28,17 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
c.View(func() h.H {
|
c.View(func() h.H {
|
||||||
return h.Div(
|
return h.Main(h.Class("container"), h.Br(),
|
||||||
h.H1(h.Text("⚡Via Live Reload!")),
|
h.H1(h.Text("⚡Via Live Reload Demo")),
|
||||||
h.P(h.Textf("Count: %d", data.Count)),
|
h.Hr(),
|
||||||
h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
|
h.Div(
|
||||||
h.Label(
|
h.H2(h.Strong(h.Text("Count - ")), h.Textf("%d", data.Count)),
|
||||||
h.Text("Update Step: "),
|
h.H5(h.Strong(h.Text("Step - ")), h.Span(step.Text())),
|
||||||
|
h.Div(h.Role("group"),
|
||||||
h.Input(h.Type("number"), step.Bind()),
|
h.Input(h.Type("number"), step.Bind()),
|
||||||
),
|
|
||||||
h.Button(h.Text("Increment"), increment.OnClick()),
|
h.Button(h.Text("Increment"), increment.OnClick()),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
107
via.go
107
via.go
@@ -14,7 +14,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -32,7 +31,7 @@ type V struct {
|
|||||||
cfg Options
|
cfg Options
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
contextRegistry map[string]*Context
|
contextRegistry map[string]*Context
|
||||||
contextRegistryMutex sync.RWMutex
|
contextRegistryMutex sync.Mutex
|
||||||
documentHeadIncludes []h.H
|
documentHeadIncludes []h.H
|
||||||
documentFootIncludes []h.H
|
documentFootIncludes []h.H
|
||||||
devModePageInitFnMap map[string]func(*Context)
|
devModePageInitFnMap map[string]func(*Context)
|
||||||
@@ -78,7 +77,7 @@ func (v *V) logDebug(c *Context, format string, a ...any) {
|
|||||||
|
|
||||||
// Config overrides the default configuration with the given options.
|
// Config overrides the default configuration with the given options.
|
||||||
func (v *V) Config(cfg Options) {
|
func (v *V) Config(cfg Options) {
|
||||||
if cfg.LogLvl != v.cfg.LogLvl {
|
if cfg.LogLvl != undefined {
|
||||||
v.cfg.LogLvl = cfg.LogLvl
|
v.cfg.LogLvl = cfg.LogLvl
|
||||||
}
|
}
|
||||||
if cfg.DocumentTitle != "" {
|
if cfg.DocumentTitle != "" {
|
||||||
@@ -141,12 +140,14 @@ func (v *V) Page(route string, initContextFn func(c *Context)) {
|
|||||||
id := fmt.Sprintf("%s_/%s", route, genRandID())
|
id := fmt.Sprintf("%s_/%s", route, genRandID())
|
||||||
c := newContext(id, route, v)
|
c := newContext(id, route, v)
|
||||||
initContextFn(c)
|
initContextFn(c)
|
||||||
v.registerCtx(c.id, c)
|
v.registerCtx(c)
|
||||||
if v.cfg.DevMode {
|
if v.cfg.DevMode {
|
||||||
v.Persist()
|
v.devModePersist(c)
|
||||||
}
|
}
|
||||||
headElements := v.documentHeadIncludes
|
headElements := v.documentHeadIncludes
|
||||||
headElements = append(headElements, 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))))
|
||||||
|
headElements = append(headElements, h.Meta(h.Data("init", `window.addEventListener('beforeunload', (evt) => {
|
||||||
|
evt.preventDefault(); evt.returnValue = ''; @post('/_session/close'); return ''; })`)))
|
||||||
headElements = append(headElements, h.Meta(h.Data("init", "@get('/_sse')")))
|
headElements = append(headElements, h.Meta(h.Data("init", "@get('/_sse')")))
|
||||||
bottomBodyElements := []h.H{c.view()}
|
bottomBodyElements := []h.H{c.view()}
|
||||||
bottomBodyElements = append(bottomBodyElements, v.documentFootIncludes...)
|
bottomBodyElements = append(bottomBodyElements, v.documentFootIncludes...)
|
||||||
@@ -154,33 +155,36 @@ func (v *V) Page(route string, initContextFn func(c *Context)) {
|
|||||||
Title: v.cfg.DocumentTitle,
|
Title: v.cfg.DocumentTitle,
|
||||||
Head: headElements,
|
Head: headElements,
|
||||||
Body: bottomBodyElements,
|
Body: bottomBodyElements,
|
||||||
|
HTMLAttrs: []h.H{},
|
||||||
})
|
})
|
||||||
_ = view.Render(w)
|
_ = view.Render(w)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *V) registerCtx(id string, c *Context) {
|
func (v *V) registerCtx(c *Context) {
|
||||||
v.contextRegistryMutex.Lock()
|
v.contextRegistryMutex.Lock()
|
||||||
defer v.contextRegistryMutex.Unlock()
|
defer v.contextRegistryMutex.Unlock()
|
||||||
if c == nil {
|
if c == nil {
|
||||||
v.logErr(c, "failed to add nil context to registry")
|
v.logErr(c, "failed to add nil context to registry")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v.contextRegistry[id] = c
|
v.contextRegistry[c.id] = c
|
||||||
v.logDebug(c, "new context added to registry")
|
v.logDebug(c, "new context added to registry")
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (a *App) unregisterCtx(id string) {
|
func (v *V) unregisterCtx(id string) {
|
||||||
// if _, ok := a.contextRegistry[id]; ok {
|
v.contextRegistryMutex.Lock()
|
||||||
// a.contextRegistryMutex.Lock()
|
defer v.contextRegistryMutex.Unlock()
|
||||||
// defer a.contextRegistryMutex.Unlock()
|
if id == "" {
|
||||||
// delete(a.contextRegistry, id)
|
return
|
||||||
// }
|
}
|
||||||
// }
|
v.logDebug(nil, "ctx '%s' removed from registry", id)
|
||||||
|
delete(v.contextRegistry, id)
|
||||||
|
}
|
||||||
|
|
||||||
func (v *V) getCtx(id string) (*Context, error) {
|
func (v *V) getCtx(id string) (*Context, error) {
|
||||||
v.contextRegistryMutex.RLock()
|
v.contextRegistryMutex.Lock()
|
||||||
defer v.contextRegistryMutex.RUnlock()
|
defer v.contextRegistryMutex.Unlock()
|
||||||
if c, ok := v.contextRegistry[id]; ok {
|
if c, ok := v.contextRegistry[id]; ok {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
@@ -195,59 +199,75 @@ func (v *V) HandleFunc(pattern string, f http.HandlerFunc) {
|
|||||||
|
|
||||||
// Start starts the Via HTTP server on the given address.
|
// Start starts the Via HTTP server on the given address.
|
||||||
func (v *V) Start() {
|
func (v *V) Start() {
|
||||||
|
if v.cfg.DevMode {
|
||||||
|
v.devModeRestore()
|
||||||
|
}
|
||||||
v.logInfo(nil, "via started at [%s]", v.cfg.ServerAddress)
|
v.logInfo(nil, "via started at [%s]", v.cfg.ServerAddress)
|
||||||
log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, v.mux))
|
log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, v.mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *V) Persist() {
|
func (v *V) devModePersist(c *Context) {
|
||||||
p := filepath.Join(".via", "devmode", "ctx.json")
|
p := filepath.Join(".via", "devmode", "ctx.json")
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||||
log.Fatalf("failed to create directory for devmode files: %v", err)
|
log.Fatalf("failed to create directory for devmode files: %v", err)
|
||||||
}
|
}
|
||||||
file, err := os.Create(p)
|
|
||||||
|
// load persisted list from file, or empty list if file not found
|
||||||
|
file, err := os.Open(p)
|
||||||
|
ctxRegMap := make(map[string]string)
|
||||||
|
if err == nil {
|
||||||
|
json.NewDecoder(file).Decode(&ctxRegMap)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
// add ctx to persisted list
|
||||||
|
if _, ok := ctxRegMap[c.id]; !ok {
|
||||||
|
ctxRegMap[c.id] = c.route
|
||||||
|
}
|
||||||
|
|
||||||
|
// write persisted list to file
|
||||||
|
file, err = os.Create(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("devmode: failed to persist ctx: %v", err)
|
v.logErr(c, "devmode failed to percist ctx: %v", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
m := make(map[string]string)
|
|
||||||
for ctxID, ctx := range v.contextRegistry {
|
|
||||||
m[ctxID] = ctx.route
|
|
||||||
}
|
|
||||||
encoder := json.NewEncoder(file)
|
encoder := json.NewEncoder(file)
|
||||||
if err := encoder.Encode(m); err != nil {
|
if err := encoder.Encode(ctxRegMap); err != nil {
|
||||||
log.Printf("devmode: failed to persist ctx: %s", err)
|
v.logErr(c, "devmode failed to persist ctx")
|
||||||
}
|
}
|
||||||
log.Printf("devmode persisted ctx registryv")
|
v.logDebug(c, "devmode persisted ctx to file")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *V) Restore() {
|
func (v *V) devModeRestore() {
|
||||||
p := path.Join(".via", "devmode", "ctx.json")
|
p := filepath.Join(".via", "devmode", "ctx.json")
|
||||||
file, err := os.Open(p)
|
file, err := os.Open(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.logErr(nil, "devmode failed to restore ctx: %v", err)
|
v.logWarn(nil, "devmode could not restore ctx from file: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
var ctxRegMap map[string]string
|
var ctxRegMap map[string]string
|
||||||
if err := json.NewDecoder(file).Decode(&ctxRegMap); err != nil {
|
if err := json.NewDecoder(file).Decode(&ctxRegMap); err != nil {
|
||||||
v.logErr(nil, "devmode failed to restore ctx: %v", err)
|
v.logWarn(nil, "devmode could not restore ctx from file: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for ctxID, pageRoute := range ctxRegMap {
|
for ctxID, pageRoute := range ctxRegMap {
|
||||||
pageInitFn, ok := v.devModePageInitFnMap[pageRoute]
|
pageInitFn, ok := v.devModePageInitFnMap[pageRoute]
|
||||||
if !ok {
|
if !ok {
|
||||||
fmt.Println("devmode failed to restore ctx: page init func of ctx not found")
|
v.logWarn(nil, "devmode could not restore ctx from file: page init fn for route '%s' not found", pageRoute)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c := newContext(ctxID, pageRoute, v)
|
c := newContext(ctxID, pageRoute, v)
|
||||||
pageInitFn(c)
|
pageInitFn(c)
|
||||||
v.registerCtx(ctxID, c)
|
v.registerCtx(c)
|
||||||
v.logDebug(nil, "devmode restored ctx reg=%v", v.contextRegistry)
|
|
||||||
}
|
}
|
||||||
|
v.logDebug(nil, "devmode restored ctx registry")
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Via application with default configuration.
|
// New creates a new *V application with default configuration.
|
||||||
func New() *V {
|
func New() *V {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
v := &V{
|
v := &V{
|
||||||
@@ -271,9 +291,6 @@ func New() *V {
|
|||||||
var sigs map[string]any
|
var sigs map[string]any
|
||||||
_ = datastar.ReadSignals(r, &sigs)
|
_ = datastar.ReadSignals(r, &sigs)
|
||||||
cID, _ := sigs["via-ctx"].(string)
|
cID, _ := sigs["via-ctx"].(string)
|
||||||
if v.cfg.DevMode && len(v.contextRegistry) == 0 {
|
|
||||||
v.Restore()
|
|
||||||
}
|
|
||||||
c, err := v.getCtx(cID)
|
c, err := v.getCtx(cID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.logErr(nil, "failed to render page: %v", err)
|
v.logErr(nil, "failed to render page: %v", err)
|
||||||
@@ -313,10 +330,22 @@ func New() *V {
|
|||||||
}()
|
}()
|
||||||
c.signalsMux.Lock()
|
c.signalsMux.Lock()
|
||||||
defer c.signalsMux.Unlock()
|
defer c.signalsMux.Unlock()
|
||||||
v.logDebug(c, "signals=%v", sigs)
|
|
||||||
c.injectSignals(sigs)
|
c.injectSignals(sigs)
|
||||||
actionFn()
|
actionFn()
|
||||||
})
|
})
|
||||||
|
v.mux.HandleFunc("POST /_session/close", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var sigs map[string]any
|
||||||
|
_ = datastar.ReadSignals(r, &sigs)
|
||||||
|
cID, _ := sigs["via-ctx"].(string)
|
||||||
|
c, err := v.getCtx(cID)
|
||||||
|
if err != nil {
|
||||||
|
v.logErr(c, "failed to handle session close: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.logDebug(c, "session close event triggered")
|
||||||
|
v.unregisterCtx(c.id)
|
||||||
|
|
||||||
|
})
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user