Merge pull request #2 from winkler1/race-condition
LiveReload, some small changes
This commit is contained in:
46
internal/examples/livereload/.air.toml
Normal file
46
internal/examples/livereload/.air.toml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
2
internal/examples/livereload/.gitignore
vendored
Normal file
2
internal/examples/livereload/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
tmp/
|
||||||
|
build-errors.log
|
||||||
28
internal/examples/livereload/README.md
Normal file
28
internal/examples/livereload/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Live Reload with Air
|
||||||
|
|
||||||
|
Hot-reloads your Go code and web page.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
If you don't have Air yet:
|
||||||
|
```bash
|
||||||
|
go install github.com/air-verse/air@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
```bash
|
||||||
|
air
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Air watches your Go files and rebuilds when you make changes.
|
||||||
|
|
||||||
|
LiveReloadPlugin handles browser refresh through a SSE connection at `/dev/reload`. When Air restarts the server, the connection drops, triggering an automatic page reload after 100ms. This only runs on localhost.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `.air.toml` - Air config
|
||||||
|
- `livereload.go` - Via plugin for browser auto-reload
|
||||||
57
internal/examples/livereload/livereload.go
Normal file
57
internal/examples/livereload/livereload.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func liveReloadScript() h.H {
|
||||||
|
return 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));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
}
|
||||||
46
internal/examples/livereload/main.go
Normal file
46
internal/examples/livereload/main.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-via/via"
|
||||||
|
"github.com/go-via/via/h"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Counter struct{ Count int }
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
v := via.New()
|
||||||
|
|
||||||
|
LiveReloadPlugin(v)
|
||||||
|
|
||||||
|
v.Config(via.Options{
|
||||||
|
DocumentTitle: "Live Reload",
|
||||||
|
DocumentHeadIncludes: []h.H{
|
||||||
|
liveReloadScript(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
v.Page("/", func(c *via.Context) {
|
||||||
|
data := Counter{Count: 0}
|
||||||
|
step := c.Signal(1)
|
||||||
|
|
||||||
|
increment := c.Action(func() {
|
||||||
|
data.Count += step.Int()
|
||||||
|
c.Sync()
|
||||||
|
})
|
||||||
|
|
||||||
|
c.View(func() h.H {
|
||||||
|
return h.Div(h.Class("container"),
|
||||||
|
h.H1(h.Text("Live Reload")),
|
||||||
|
h.P(h.Textf("Count: %d", data.Count)),
|
||||||
|
h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
|
||||||
|
h.Label(
|
||||||
|
h.Text("Update Step: "),
|
||||||
|
h.Input(h.Type("number"), step.Bind()),
|
||||||
|
),
|
||||||
|
h.Button(h.Text("Increment"), increment.OnClick()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
v.Start(":3000")
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ func (s *signal) Err() error {
|
|||||||
return s.err
|
return s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind binds this signal to an imput element. When the imput changes
|
// Bind binds this signal to an input element. When the input changes
|
||||||
// its value the signal updates in real-time in the browser.
|
// its value the signal updates in real-time in the browser.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
|
|||||||
5
via.go
5
via.go
@@ -75,6 +75,9 @@ func (v *V) Config(cfg Options) {
|
|||||||
if cfg.LogLvl != v.cfg.LogLvl {
|
if cfg.LogLvl != v.cfg.LogLvl {
|
||||||
v.cfg.LogLvl = cfg.LogLvl
|
v.cfg.LogLvl = cfg.LogLvl
|
||||||
}
|
}
|
||||||
|
if cfg.DocumentTitle != "" {
|
||||||
|
v.cfg.DocumentTitle = cfg.DocumentTitle
|
||||||
|
}
|
||||||
if cfg.DocumentHeadIncludes != nil {
|
if cfg.DocumentHeadIncludes != nil {
|
||||||
v.cfg.DocumentHeadIncludes = cfg.DocumentHeadIncludes
|
v.cfg.DocumentHeadIncludes = cfg.DocumentHeadIncludes
|
||||||
}
|
}
|
||||||
@@ -142,6 +145,8 @@ func (v *V) registerCtx(id string, c *Context) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
func (v *V) getCtx(id string) (*Context, error) {
|
func (v *V) getCtx(id string) (*Context, error) {
|
||||||
|
v.contextRegistryMutex.RLock()
|
||||||
|
defer v.contextRegistryMutex.RUnlock()
|
||||||
if c, ok := v.contextRegistry[id]; ok {
|
if c, ok := v.contextRegistry[id]; ok {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user