feat: add devmode flag; introduce live reload support; update examples
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,3 +30,6 @@ go.work.sum
|
|||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
|
# Via related
|
||||||
|
.via
|
||||||
|
|||||||
@@ -9,10 +9,17 @@ const (
|
|||||||
LogLevelDebug
|
LogLevelDebug
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
DevMode bool
|
||||||
|
|
||||||
|
// The http server address. e.g. ':3000'
|
||||||
|
ServerAddress string
|
||||||
|
|
||||||
// Level of the logs to write to stdout.
|
// Level of the logs to write to stdout.
|
||||||
// Options: Error, Warn, Info, Debug.
|
// Options: Error, Warn, Info, Debug.
|
||||||
LogLvl LogLevel
|
LogLvl LogLevel
|
||||||
|
|||||||
16
context.go
16
context.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-via/via/h"
|
"github.com/go-via/via/h"
|
||||||
"github.com/starfederation/datastar-go/datastar"
|
"github.com/starfederation/datastar-go/datastar"
|
||||||
@@ -25,7 +24,6 @@ type Context struct {
|
|||||||
actionRegistry map[string]func()
|
actionRegistry map[string]func()
|
||||||
signals map[string]*signal
|
signals map[string]*signal
|
||||||
signalsMux sync.Mutex
|
signalsMux sync.Mutex
|
||||||
createdAt time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// View defines the UI rendered by this context.
|
// View defines the UI rendered by this context.
|
||||||
@@ -116,14 +114,6 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) Signals() map[string]*signal {
|
|
||||||
if c.signals == nil {
|
|
||||||
c.app.logErr(c, "failed to get signal: nil signals in ctx")
|
|
||||||
return make(map[string]*signal)
|
|
||||||
}
|
|
||||||
return c.signals
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal creates a reactive signal and initializes it with the given value.
|
// Signal creates a reactive signal and initializes it with the given value.
|
||||||
// Use Bind() to link the value of input elements to the signal and Text() to
|
// Use Bind() to link the value of input elements to the signal and Text() to
|
||||||
// display the signal value and watch the UI update live as the input changes.
|
// display the signal value and watch the UI update live as the input changes.
|
||||||
@@ -179,6 +169,11 @@ func (c *Context) injectSignals(sigs map[string]any) {
|
|||||||
}
|
}
|
||||||
for k, v := range sigs {
|
for k, v := range sigs {
|
||||||
if _, ok := c.signals[k]; !ok {
|
if _, ok := c.signals[k]; !ok {
|
||||||
|
c.signals[k] = &signal{
|
||||||
|
id: k,
|
||||||
|
t: reflect.TypeOf(v),
|
||||||
|
v: reflect.ValueOf(v),
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.signals[k].v = reflect.ValueOf(v)
|
c.signals[k].v = reflect.ValueOf(v)
|
||||||
@@ -313,6 +308,5 @@ func newContext(id string, a *V) *Context {
|
|||||||
componentRegistry: make(map[string]*Context),
|
componentRegistry: make(map[string]*Context),
|
||||||
actionRegistry: make(map[string]func()),
|
actionRegistry: make(map[string]func()),
|
||||||
signals: make(map[string]*signal),
|
signals: make(map[string]*signal),
|
||||||
createdAt: time.Now(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -9,6 +9,8 @@ require github.com/starfederation/datastar-go v1.0.3
|
|||||||
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/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -6,6 +6,8 @@ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -35,6 +37,8 @@ github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g
|
|||||||
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
v.Start(":3000")
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
v.Start(":3000")
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func counterCompFn(c *via.Context) {
|
func counterCompFn(c *via.Context) {
|
||||||
|
|||||||
@@ -30,5 +30,5 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
v.Start(":3000")
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/go-via/via"
|
|
||||||
"github.com/go-via/via/h"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LiveReloadPlugin(v *via.V) {
|
|
||||||
v.HandleFunc("GET /dev/reload", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
|
|
||||||
<-r.Context().Done()
|
|
||||||
})
|
|
||||||
v.AppendToFoot(h.Script(h.Raw(`
|
|
||||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
||||||
const evtSource = new EventSource('/dev/reload');
|
|
||||||
let overlay = null;
|
|
||||||
let showTimer = null;
|
|
||||||
|
|
||||||
evtSource.onerror = () => {
|
|
||||||
evtSource.close();
|
|
||||||
|
|
||||||
showTimer = setTimeout(() => {
|
|
||||||
if (!overlay) {
|
|
||||||
overlay = document.createElement('div');
|
|
||||||
overlay.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(200, 200, 200, 0.95); padding: 20px 40px; border-radius: 8px; color: #333; font-size: 24px; z-index: 999999; font-family: -apple-system, sans-serif;';
|
|
||||||
overlay.textContent = '🔌 Reconnecting...';
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
(async function poll() {
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/', { method: 'HEAD', signal: AbortSignal.timeout(1000) });
|
|
||||||
if (res.ok) {
|
|
||||||
clearTimeout(showTimer);
|
|
||||||
if (overlay) overlay.remove();
|
|
||||||
location.reload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
await new Promise(r => setTimeout(r, 50));
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
`)))
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,8 @@ func main() {
|
|||||||
|
|
||||||
v.Config(via.Options{
|
v.Config(via.Options{
|
||||||
DocumentTitle: "Live Reload",
|
DocumentTitle: "Live Reload",
|
||||||
Plugins: []via.Plugin{LiveReloadPlugin},
|
DevMode: true,
|
||||||
|
LogLvl: via.LogLevelDebug,
|
||||||
})
|
})
|
||||||
|
|
||||||
v.Page("/", func(c *via.Context) {
|
v.Page("/", func(c *via.Context) {
|
||||||
@@ -26,7 +27,7 @@ func main() {
|
|||||||
|
|
||||||
c.View(func() h.H {
|
c.View(func() h.H {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.H1(h.Text("Live Reload")),
|
h.H1(h.Text("Live Reload with Via DevMode !!!")),
|
||||||
h.P(h.Textf("Count: %d", data.Count)),
|
h.P(h.Textf("Count: %d", data.Count)),
|
||||||
h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
|
h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
|
||||||
h.Label(
|
h.Label(
|
||||||
@@ -38,5 +39,5 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
v.Start(":3000")
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ func main() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
v.Start(":3000")
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func main() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
v.Start(":3000")
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func PicoCSSPlugin(v *via.V) {
|
func PicoCSSPlugin(v *via.V) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
v.Start(":3000")
|
v.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func chartCompFn(c *via.Context) {
|
func chartCompFn(c *via.Context) {
|
||||||
|
|||||||
147
via.go
147
via.go
@@ -9,9 +9,13 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -31,6 +35,8 @@ type V struct {
|
|||||||
contextRegistryMutex sync.RWMutex
|
contextRegistryMutex sync.RWMutex
|
||||||
documentHeadIncludes []h.H
|
documentHeadIncludes []h.H
|
||||||
documentFootIncludes []h.H
|
documentFootIncludes []h.H
|
||||||
|
devModePageInitFnMap map[string]func(*Context)
|
||||||
|
devModePageInitFnMapMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *V) logErr(c *Context, format string, a ...any) {
|
func (v *V) logErr(c *Context, format string, a ...any) {
|
||||||
@@ -46,7 +52,7 @@ func (v *V) logWarn(c *Context, format string, a ...any) {
|
|||||||
if c != nil && c.id != "" {
|
if c != nil && c.id != "" {
|
||||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||||
}
|
}
|
||||||
if v.cfg.LogLvl <= LogLevelWarn {
|
if v.cfg.LogLvl >= LogLevelWarn {
|
||||||
log.Printf("[warn] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
log.Printf("[warn] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +62,7 @@ func (v *V) logInfo(c *Context, format string, a ...any) {
|
|||||||
if c != nil && c.id != "" {
|
if c != nil && c.id != "" {
|
||||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||||
}
|
}
|
||||||
if v.cfg.LogLvl <= LogLevelInfo {
|
if v.cfg.LogLvl >= LogLevelInfo {
|
||||||
log.Printf("[info] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
log.Printf("[info] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +92,12 @@ func (v *V) Config(cfg Options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cfg.DevMode != v.cfg.DevMode {
|
||||||
|
v.cfg.DevMode = cfg.DevMode
|
||||||
|
}
|
||||||
|
if cfg.ServerAddress != "" {
|
||||||
|
v.cfg.ServerAddress = cfg.ServerAddress
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendToHead appends the given h.H nodes to the head of the base HTML document.
|
// AppendToHead appends the given h.H nodes to the head of the base HTML document.
|
||||||
@@ -106,7 +118,6 @@ func (v *V) AppendToFoot(elements ...h.H) {
|
|||||||
v.documentFootIncludes = append(v.documentFootIncludes, el)
|
v.documentFootIncludes = append(v.documentFootIncludes, el)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page registers a route and its associated page handler.
|
// Page registers a route and its associated page handler.
|
||||||
@@ -119,7 +130,12 @@ func (v *V) AppendToFoot(elements ...h.H) {
|
|||||||
// return h.H1(h.Text("Hello, Via!"))
|
// return h.H1(h.Text("Hello, Via!"))
|
||||||
// })
|
// })
|
||||||
// })
|
// })
|
||||||
func (v *V) Page(route string, composeContext func(c *Context)) {
|
func (v *V) Page(route string, initContextFn func(c *Context)) {
|
||||||
|
if v.cfg.DevMode {
|
||||||
|
v.devModePageInitFnMapMutex.Lock()
|
||||||
|
defer v.devModePageInitFnMapMutex.Unlock()
|
||||||
|
v.devModePageInitFnMap[route] = initContextFn
|
||||||
|
}
|
||||||
v.mux.HandleFunc("GET "+route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
v.mux.HandleFunc("GET "+route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if strings.Contains(r.URL.Path, "favicon") {
|
if strings.Contains(r.URL.Path, "favicon") {
|
||||||
return
|
return
|
||||||
@@ -127,7 +143,7 @@ func (v *V) Page(route string, composeContext func(c *Context)) {
|
|||||||
id := fmt.Sprintf("%s_/%s", route, genRandID())
|
id := fmt.Sprintf("%s_/%s", route, genRandID())
|
||||||
c := newContext(id, v)
|
c := newContext(id, v)
|
||||||
v.logDebug(c, "GET %s", route)
|
v.logDebug(c, "GET %s", route)
|
||||||
composeContext(c)
|
initContextFn(c)
|
||||||
v.registerCtx(c.id, c)
|
v.registerCtx(c.id, 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))))
|
||||||
@@ -146,6 +162,10 @@ func (v *V) Page(route string, composeContext func(c *Context)) {
|
|||||||
func (v *V) registerCtx(id string, c *Context) {
|
func (v *V) registerCtx(id string, c *Context) {
|
||||||
v.contextRegistryMutex.Lock()
|
v.contextRegistryMutex.Lock()
|
||||||
defer v.contextRegistryMutex.Unlock()
|
defer v.contextRegistryMutex.Unlock()
|
||||||
|
if c == nil {
|
||||||
|
v.logErr(c, "failed to add nil context to registry")
|
||||||
|
return
|
||||||
|
}
|
||||||
v.contextRegistry[id] = c
|
v.contextRegistry[id] = c
|
||||||
v.logDebug(c, "new context added to registry")
|
v.logDebug(c, "new context added to registry")
|
||||||
}
|
}
|
||||||
@@ -174,83 +194,158 @@ 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(addr string) {
|
func (v *V) Start() {
|
||||||
v.logInfo(nil, "via started")
|
v.logInfo(nil, "via started on address: %s", v.cfg.ServerAddress)
|
||||||
log.Fatalf("via failed: %v", http.ListenAndServe(addr, v.mux))
|
log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, v.mux))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *V) persistCtx(c *Context) error {
|
||||||
|
idsplit := strings.Split(c.id, "_")
|
||||||
|
if len(idsplit) < 2 {
|
||||||
|
return fmt.Errorf("failed to identify ctx page route")
|
||||||
|
}
|
||||||
|
route := idsplit[0]
|
||||||
|
ctxmap := map[string]any{"id": c.id, "route": route}
|
||||||
|
|
||||||
|
p := path.Join(".via", "devmode", "ctx.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory for devmode files: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create file in devmode directory: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
if err := encoder.Encode(ctxmap); err != nil {
|
||||||
|
return fmt.Errorf("failed to encode ctx: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *V) restoreCtx() *Context {
|
||||||
|
p := path.Join(".via", "devmode", "ctx.json")
|
||||||
|
file, err := os.Open(p)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error opening file:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
var ctxmap map[string]any
|
||||||
|
if err := json.NewDecoder(file).Decode(&ctxmap); err != nil {
|
||||||
|
fmt.Println("Error restoring ctx:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctxId, ok := ctxmap["id"].(string)
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("Error restoring ctx")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pageRoute, ok := ctxmap["route"].(string)
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("Error restoring ctx")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pageInitFn, ok := v.devModePageInitFnMap[pageRoute]
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("devmode failed to restore ctx: ")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := newContext(ctxId, v)
|
||||||
|
pageInitFn(c)
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Via application with default configuration.
|
// New creates a new Via application with default configuration.
|
||||||
func New() *V {
|
func New() *V {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
app := &V{
|
v := &V{
|
||||||
mux: mux,
|
mux: mux,
|
||||||
contextRegistry: make(map[string]*Context),
|
contextRegistry: make(map[string]*Context),
|
||||||
|
devModePageInitFnMap: make(map[string]func(*Context)),
|
||||||
cfg: Options{
|
cfg: Options{
|
||||||
LogLvl: LogLevelDebug,
|
DevMode: false,
|
||||||
DocumentTitle: "Via Application",
|
ServerAddress: ":3000",
|
||||||
|
LogLvl: LogLevelInfo,
|
||||||
|
DocumentTitle: "⚡ Via",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mux.HandleFunc("GET /_datastar.js", func(w http.ResponseWriter, r *http.Request) {
|
v.mux.HandleFunc("GET /_datastar.js", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
_, _ = w.Write(datastarJS)
|
_, _ = w.Write(datastarJS)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.mux.HandleFunc("GET /_sse", func(w http.ResponseWriter, r *http.Request) {
|
v.mux.HandleFunc("GET /_sse", func(w http.ResponseWriter, r *http.Request) {
|
||||||
var sigs map[string]any
|
var sigs map[string]any
|
||||||
_ = datastar.ReadSignals(r, &sigs)
|
_ = datastar.ReadSignals(r, &sigs)
|
||||||
|
if v.cfg.DevMode && len(v.contextRegistry) == 0 {
|
||||||
|
restoredC := v.restoreCtx()
|
||||||
|
if restoredC != nil {
|
||||||
|
restoredC.injectSignals(sigs)
|
||||||
|
v.registerCtx(restoredC.id, restoredC)
|
||||||
|
}
|
||||||
|
}
|
||||||
cID, _ := sigs["via-ctx"].(string)
|
cID, _ := sigs["via-ctx"].(string)
|
||||||
c, err := app.getCtx(cID)
|
c, err := v.getCtx(cID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.logErr(nil, "failed to render page: %v", err)
|
v.logErr(nil, "failed to render page: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.sse = datastar.NewSSE(w, r)
|
c.sse = datastar.NewSSE(w, r)
|
||||||
app.logDebug(c, "SSE connection established")
|
v.logDebug(c, "SSE connection established")
|
||||||
|
if v.cfg.DevMode {
|
||||||
|
c.Sync()
|
||||||
|
v.persistCtx(c)
|
||||||
|
} else {
|
||||||
c.SyncSignals()
|
c.SyncSignals()
|
||||||
|
}
|
||||||
<-c.sse.Context().Done()
|
<-c.sse.Context().Done()
|
||||||
c.sse = nil
|
c.sse = nil
|
||||||
app.logDebug(c, "SSE connection closed")
|
v.logDebug(c, "SSE connection closed")
|
||||||
})
|
})
|
||||||
app.mux.HandleFunc("GET /_action/{id}", func(w http.ResponseWriter, r *http.Request) {
|
v.mux.HandleFunc("GET /_action/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
actionID := r.PathValue("id")
|
actionID := r.PathValue("id")
|
||||||
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)
|
||||||
active_ctx_count := 0
|
active_ctx_count := 0
|
||||||
inactive_ctx_count := 0
|
inactive_ctx_count := 0
|
||||||
for _, c := range app.contextRegistry {
|
for _, c := range v.contextRegistry {
|
||||||
if c.sse != nil {
|
if c.sse != nil {
|
||||||
active_ctx_count++
|
active_ctx_count++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
inactive_ctx_count++
|
inactive_ctx_count++
|
||||||
}
|
}
|
||||||
app.logDebug(nil, "active_ctx_count=%d inactive_ctx_count=%d", active_ctx_count, inactive_ctx_count)
|
v.logDebug(nil, "active_ctx_count=%d inactive_ctx_count=%d", active_ctx_count, inactive_ctx_count)
|
||||||
c, err := app.getCtx(cID)
|
c, err := v.getCtx(cID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.logErr(nil, "action '%s' failed: %v", actionID, err)
|
v.logErr(nil, "action '%s' failed: %v", actionID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actionFn, err := c.getActionFn(actionID)
|
actionFn, err := c.getActionFn(actionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.logDebug(c, "action '%s' failed: %v", actionID, err)
|
v.logDebug(c, "action '%s' failed: %v", actionID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// log err if actionFn panics
|
// log err if actionFn panics
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
app.logErr(c, "action '%s' failed: %v", actionID, r)
|
v.logErr(c, "action '%s' failed: %v", actionID, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
c.signalsMux.Lock()
|
c.signalsMux.Lock()
|
||||||
defer c.signalsMux.Unlock()
|
defer c.signalsMux.Unlock()
|
||||||
app.logDebug(c, "signals=%v", sigs)
|
v.logDebug(c, "signals=%v", sigs)
|
||||||
c.injectSignals(sigs)
|
c.injectSignals(sigs)
|
||||||
actionFn()
|
actionFn()
|
||||||
|
|
||||||
})
|
})
|
||||||
return app
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func genRandID() string {
|
func genRandID() string {
|
||||||
|
|||||||
Reference in New Issue
Block a user