Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1e8e3a2ed | ||
|
|
4a7acbb630 | ||
|
|
a7ace9099f |
@@ -3,6 +3,7 @@ package via
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ryanhamamura/via/h"
|
||||
)
|
||||
@@ -21,8 +22,16 @@ type triggerOpts struct {
|
||||
hasSignal bool
|
||||
signalID string
|
||||
value string
|
||||
window bool
|
||||
}
|
||||
|
||||
type withWindowOpt struct{}
|
||||
|
||||
func (o withWindowOpt) apply(opts *triggerOpts) { opts.window = true }
|
||||
|
||||
// WithWindow scopes the event listener to the window instead of the element.
|
||||
func WithWindow() ActionTriggerOption { return withWindowOpt{} }
|
||||
|
||||
type withSignalOpt struct {
|
||||
signalID string
|
||||
value string
|
||||
@@ -92,5 +101,34 @@ func (a *actionTrigger) OnKeyDown(key string, options ...ActionTriggerOption) h.
|
||||
if key != "" {
|
||||
condition = fmt.Sprintf("evt.key==='%s' &&", key)
|
||||
}
|
||||
return h.Data("on:keydown", fmt.Sprintf("%s%s", condition, buildOnExpr(actionURL(a.id), &opts)))
|
||||
attrName := "on:keydown"
|
||||
if opts.window {
|
||||
attrName = "on:keydown__window"
|
||||
}
|
||||
return h.Data(attrName, fmt.Sprintf("%s%s", condition, buildOnExpr(actionURL(a.id), &opts)))
|
||||
}
|
||||
|
||||
// KeyBinding pairs a key name with action trigger options for use with OnKeyDownMap.
|
||||
type KeyBinding struct {
|
||||
Key string
|
||||
Options []ActionTriggerOption
|
||||
}
|
||||
|
||||
// KeyBind creates a KeyBinding for use with OnKeyDownMap.
|
||||
func KeyBind(key string, options ...ActionTriggerOption) KeyBinding {
|
||||
return KeyBinding{Key: key, Options: options}
|
||||
}
|
||||
|
||||
// OnKeyDownMap combines multiple key bindings into a single data-on:keydown__window
|
||||
// attribute using a JS ternary chain. This avoids HTML attribute deduplication issues
|
||||
// that occur when multiple OnKeyDown calls target the same element.
|
||||
func (a *actionTrigger) OnKeyDownMap(bindings ...KeyBinding) h.H {
|
||||
var parts []string
|
||||
for _, b := range bindings {
|
||||
opts := applyOptions(b.Options...)
|
||||
expr := buildOnExpr(actionURL(a.id), &opts)
|
||||
parts = append(parts, fmt.Sprintf("evt.key==='%s' ? (%s)", b.Key, expr))
|
||||
}
|
||||
combined := strings.Join(parts, " : ") + " : void 0"
|
||||
return h.Data("on:keydown__window", combined)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package via
|
||||
|
||||
import "github.com/alexedwards/scs/v2"
|
||||
import (
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
func ptr(l zerolog.Level) *zerolog.Level { return &l }
|
||||
|
||||
const (
|
||||
undefined LogLevel = iota
|
||||
LogLevelError
|
||||
LogLevelWarn
|
||||
LogLevelInfo
|
||||
LogLevelDebug
|
||||
var (
|
||||
LogLevelDebug = ptr(zerolog.DebugLevel)
|
||||
LogLevelInfo = ptr(zerolog.InfoLevel)
|
||||
LogLevelWarn = ptr(zerolog.WarnLevel)
|
||||
LogLevelError = ptr(zerolog.ErrorLevel)
|
||||
)
|
||||
|
||||
// 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.
|
||||
@@ -23,9 +25,12 @@ type Options struct {
|
||||
// The http server address. e.g. ':3000'
|
||||
ServerAddress string
|
||||
|
||||
// Level of the logs to write to stdout.
|
||||
// Options: Error, Warn, Info, Debug.
|
||||
LogLvl LogLevel
|
||||
// LogLevel sets the minimum log level. nil keeps the default (Info).
|
||||
LogLevel *zerolog.Level
|
||||
|
||||
// Logger overrides the default logger entirely. When set, LogLevel and
|
||||
// DevMode have no effect on logging.
|
||||
Logger *zerolog.Logger
|
||||
|
||||
// The title of the HTML document.
|
||||
DocumentTitle string
|
||||
|
||||
20
context.go
20
context.go
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -33,6 +32,7 @@ type Context struct {
|
||||
reqCtx context.Context
|
||||
subscriptions []Subscription
|
||||
subsMu sync.Mutex
|
||||
disposeOnce sync.Once
|
||||
}
|
||||
|
||||
// View defines the UI rendered by this context.
|
||||
@@ -351,11 +351,23 @@ func (c *Context) ReplaceURLf(format string, a ...any) {
|
||||
c.ReplaceURL(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// stopAllRoutines stops all go routines tied to this Context preventing goroutine leaks.
|
||||
// dispose idempotently tears down this context: unsubscribes all pubsub
|
||||
// subscriptions and closes ctxDisposedChan to stop routines and exit the SSE loop.
|
||||
func (c *Context) dispose() {
|
||||
c.disposeOnce.Do(func() {
|
||||
c.unsubscribeAll()
|
||||
c.stopAllRoutines()
|
||||
})
|
||||
}
|
||||
|
||||
// stopAllRoutines closes ctxDisposedChan, broadcasting to all listening
|
||||
// goroutines (OnIntervalRoutine, SSE loop) that this context is done.
|
||||
func (c *Context) stopAllRoutines() {
|
||||
select {
|
||||
case c.ctxDisposedChan <- struct{}{}:
|
||||
case <-c.ctxDisposedChan:
|
||||
// already closed
|
||||
default:
|
||||
close(c.ctxDisposedChan)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +468,7 @@ func (c *Context) unsubscribeAll() {
|
||||
|
||||
func newContext(id string, route string, v *V) *Context {
|
||||
if v == nil {
|
||||
log.Fatal("create context failed: app pointer is nil")
|
||||
panic("create context failed: app pointer is nil")
|
||||
}
|
||||
|
||||
return &Context{
|
||||
|
||||
3
go.mod
3
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/delaneyj/toolbelt v0.9.1
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/nats-io/nats.go v1.48.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/starfederation/datastar-go v1.0.3
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
@@ -24,6 +25,8 @@ require (
|
||||
github.com/google/go-tpm v0.9.7 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.0 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.12.2 // indirect
|
||||
|
||||
15
go.sum
15
go.sum
@@ -13,6 +13,7 @@ github.com/antithesishq/antithesis-sdk-go v0.5.0 h1:cudCFF83pDDANcXFzkQPUHHedfnn
|
||||
github.com/antithesishq/antithesis-sdk-go v0.5.0/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -20,6 +21,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/delaneyj/toolbelt v0.9.1 h1:QJComn2qoaQ4azl5uRkGpdHSO9e+JtoxDTXCiQHvH8o=
|
||||
github.com/delaneyj/toolbelt v0.9.1/go.mod h1:eNXpPuThjTD4tpRNCBl4JEz9jdg9LpyzNuyG+stnIbs=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
|
||||
@@ -33,6 +35,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
@@ -50,11 +58,15 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/starfederation/datastar-go v1.0.3 h1:DnzgsJ6tDHDM6y5Nxsk0AGW/m8SyKch2vQg3P1xGTcU=
|
||||
github.com/starfederation/datastar-go v1.0.3/go.mod h1:stm83LQkhZkwa5GzzdPEN6dLuu8FVwxIv0w1DYkbD3w=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -74,6 +86,9 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
|
||||
@@ -22,7 +22,7 @@ func main() {
|
||||
v.Config(via.Options{
|
||||
DevMode: true,
|
||||
DocumentTitle: "ViaChat",
|
||||
LogLvl: via.LogLevelInfo,
|
||||
LogLevel: via.LogLevelInfo,
|
||||
})
|
||||
|
||||
v.AppendToHead(
|
||||
|
||||
@@ -14,7 +14,7 @@ func main() {
|
||||
v.Config(via.Options{
|
||||
DocumentTitle: "Live Reload Demo",
|
||||
DevMode: true,
|
||||
LogLvl: via.LogLevelDebug,
|
||||
LogLevel: via.LogLevelDebug,
|
||||
Plugins: []via.Plugin{
|
||||
// picocss.Default
|
||||
},
|
||||
|
||||
@@ -60,7 +60,7 @@ func main() {
|
||||
v.Config(via.Options{
|
||||
DevMode: true,
|
||||
DocumentTitle: "NATS Chat",
|
||||
LogLvl: via.LogLevelInfo,
|
||||
LogLevel: via.LogLevelInfo,
|
||||
ServerAddress: ":7331",
|
||||
PubSub: ps,
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ func main() {
|
||||
v := via.New()
|
||||
|
||||
v.Config(via.Options{
|
||||
LogLvl: via.LogLevelDebug,
|
||||
LogLevel: via.LogLevelDebug,
|
||||
DevMode: true,
|
||||
Plugins: []via.Plugin{
|
||||
// picocss.Default,
|
||||
|
||||
@@ -54,7 +54,7 @@ func main() {
|
||||
v.Config(via.Options{
|
||||
DevMode: true,
|
||||
DocumentTitle: "Search",
|
||||
LogLvl: via.LogLevelWarn,
|
||||
LogLevel: via.LogLevelWarn,
|
||||
})
|
||||
|
||||
v.AppendToHead(
|
||||
|
||||
157
via.go
157
via.go
@@ -7,21 +7,25 @@
|
||||
package via
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
ossignal "os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/ryanhamamura/via/h"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
)
|
||||
@@ -34,6 +38,8 @@ var datastarJS []byte
|
||||
type V struct {
|
||||
cfg Options
|
||||
mux *http.ServeMux
|
||||
server *http.Server
|
||||
logger zerolog.Logger
|
||||
contextRegistry map[string]*Context
|
||||
contextRegistryMutex sync.RWMutex
|
||||
documentHeadIncludes []h.H
|
||||
@@ -46,52 +52,52 @@ type V struct {
|
||||
datastarOnce sync.Once
|
||||
}
|
||||
|
||||
func (v *V) logEvent(evt *zerolog.Event, c *Context) *zerolog.Event {
|
||||
if c != nil && c.id != "" {
|
||||
evt = evt.Str("via-ctx", c.id)
|
||||
}
|
||||
return evt
|
||||
}
|
||||
|
||||
func (v *V) logFatal(format string, a ...any) {
|
||||
log.Printf("[fatal] msg=%q", fmt.Sprintf(format, a...))
|
||||
v.logEvent(v.logger.WithLevel(zerolog.FatalLevel), nil).Msgf(format, a...)
|
||||
}
|
||||
|
||||
func (v *V) logErr(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
log.Printf("[error] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
v.logEvent(v.logger.Error(), c).Msgf(format, a...)
|
||||
}
|
||||
|
||||
func (v *V) logWarn(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
if v.cfg.LogLvl >= LogLevelWarn {
|
||||
log.Printf("[warn] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
}
|
||||
v.logEvent(v.logger.Warn(), c).Msgf(format, a...)
|
||||
}
|
||||
|
||||
func (v *V) logInfo(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
if v.cfg.LogLvl >= LogLevelInfo {
|
||||
log.Printf("[info] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
}
|
||||
v.logEvent(v.logger.Info(), c).Msgf(format, a...)
|
||||
}
|
||||
|
||||
func (v *V) logDebug(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
if v.cfg.LogLvl == LogLevelDebug {
|
||||
log.Printf("[debug] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
}
|
||||
v.logEvent(v.logger.Debug(), c).Msgf(format, a...)
|
||||
}
|
||||
|
||||
func newConsoleLogger(level zerolog.Level) zerolog.Logger {
|
||||
return zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}).
|
||||
With().Timestamp().Logger().Level(level)
|
||||
}
|
||||
|
||||
// Config overrides the default configuration with the given options.
|
||||
func (v *V) Config(cfg Options) {
|
||||
if cfg.LogLvl != undefined {
|
||||
v.cfg.LogLvl = cfg.LogLvl
|
||||
if cfg.Logger != nil {
|
||||
v.logger = *cfg.Logger
|
||||
} else if cfg.LogLevel != nil || cfg.DevMode != v.cfg.DevMode {
|
||||
level := zerolog.InfoLevel
|
||||
if cfg.LogLevel != nil {
|
||||
level = *cfg.LogLevel
|
||||
}
|
||||
if cfg.DevMode {
|
||||
v.logger = newConsoleLogger(level)
|
||||
} else {
|
||||
v.logger = zerolog.New(os.Stderr).With().Timestamp().Logger().Level(level)
|
||||
}
|
||||
}
|
||||
if cfg.DocumentTitle != "" {
|
||||
v.cfg.DocumentTitle = cfg.DocumentTitle
|
||||
@@ -253,14 +259,82 @@ func (v *V) getCtx(id string) (*Context, error) {
|
||||
return nil, fmt.Errorf("ctx '%s' not found", id)
|
||||
}
|
||||
|
||||
// Start starts the Via HTTP server on the given address.
|
||||
// Start starts the Via HTTP server and blocks until a SIGINT or SIGTERM
|
||||
// signal is received, then performs a graceful shutdown.
|
||||
func (v *V) Start() {
|
||||
v.logInfo(nil, "via started at [%s]", v.cfg.ServerAddress)
|
||||
handler := http.Handler(v.mux)
|
||||
if v.sessionManager != nil {
|
||||
handler = v.sessionManager.LoadAndSave(v.mux)
|
||||
}
|
||||
log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, handler))
|
||||
v.server = &http.Server{
|
||||
Addr: v.cfg.ServerAddress,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- v.server.ListenAndServe()
|
||||
}()
|
||||
|
||||
v.logInfo(nil, "via started at [%s]", v.cfg.ServerAddress)
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
ossignal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
v.logInfo(nil, "received signal %v, shutting down", sig)
|
||||
case err := <-errCh:
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
v.logger.Fatal().Err(err).Msg("http server failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
v.shutdown()
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the server and all contexts.
|
||||
// Safe for programmatic or test use.
|
||||
func (v *V) Shutdown() {
|
||||
v.shutdown()
|
||||
}
|
||||
|
||||
func (v *V) shutdown() {
|
||||
v.logInfo(nil, "draining all contexts")
|
||||
v.drainAllContexts()
|
||||
|
||||
if v.server != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := v.server.Shutdown(ctx); err != nil {
|
||||
v.logErr(nil, "http server shutdown error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if v.pubsub != nil {
|
||||
if err := v.pubsub.Close(); err != nil {
|
||||
v.logErr(nil, "pubsub close error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
v.logInfo(nil, "shutdown complete")
|
||||
}
|
||||
|
||||
func (v *V) drainAllContexts() {
|
||||
v.contextRegistryMutex.Lock()
|
||||
contexts := make([]*Context, 0, len(v.contextRegistry))
|
||||
for _, c := range v.contextRegistry {
|
||||
contexts = append(contexts, c)
|
||||
}
|
||||
v.contextRegistry = make(map[string]*Context)
|
||||
v.contextRegistryMutex.Unlock()
|
||||
|
||||
for _, c := range contexts {
|
||||
v.logDebug(c, "disposing context")
|
||||
c.dispose()
|
||||
}
|
||||
v.logInfo(nil, "drained %d context(s)", len(contexts))
|
||||
}
|
||||
|
||||
// HTTPServeMux returns the underlying HTTP request multiplexer to enable user extentions, middleware and
|
||||
@@ -284,7 +358,7 @@ func (v *V) ensureDatastarHandler() {
|
||||
func (v *V) devModePersist(c *Context) {
|
||||
p := filepath.Join(".via", "devmode", "ctx.json")
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
log.Fatalf("failed to create directory for devmode files: %v", err)
|
||||
v.logFatal("failed to create directory for devmode files: %v", err)
|
||||
}
|
||||
|
||||
// load persisted list from file, or empty list if file not found
|
||||
@@ -398,6 +472,7 @@ func New() *V {
|
||||
|
||||
v := &V{
|
||||
mux: mux,
|
||||
logger: newConsoleLogger(zerolog.InfoLevel),
|
||||
contextRegistry: make(map[string]*Context),
|
||||
devModePageInitFnMap: make(map[string]func(*Context)),
|
||||
sessionManager: scs.New(),
|
||||
@@ -406,7 +481,6 @@ func New() *V {
|
||||
cfg: Options{
|
||||
DevMode: false,
|
||||
ServerAddress: ":3000",
|
||||
LogLvl: LogLevelInfo,
|
||||
DocumentTitle: "⚡ Via",
|
||||
},
|
||||
}
|
||||
@@ -444,10 +518,10 @@ func New() *V {
|
||||
case <-sse.Context().Done():
|
||||
v.logDebug(c, "SSE connection ended")
|
||||
return
|
||||
case patch, ok := <-c.patchChan:
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
case <-c.ctxDisposedChan:
|
||||
v.logDebug(c, "context disposed, closing SSE")
|
||||
return
|
||||
case patch := <-c.patchChan:
|
||||
switch patch.typ {
|
||||
case patchTypeElements:
|
||||
if err := sse.PatchElements(patch.content); err != nil {
|
||||
@@ -518,7 +592,7 @@ func New() *V {
|
||||
v.mux.HandleFunc("POST /_session/close", func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Error reading body: %v", err)
|
||||
v.logErr(nil, "error reading body: %v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -529,8 +603,7 @@ func New() *V {
|
||||
v.logErr(c, "failed to handle session close: %v", err)
|
||||
return
|
||||
}
|
||||
c.unsubscribeAll()
|
||||
c.stopAllRoutines()
|
||||
c.dispose()
|
||||
v.logDebug(c, "session close event triggered")
|
||||
if v.cfg.DevMode {
|
||||
v.devModeRemovePersisted(c)
|
||||
|
||||
Reference in New Issue
Block a user