7 Commits

Author SHA1 Message Date
Ryan Hamamura
29228fc9ea feat: animate fleet of container ships along bay waypoints
Some checks failed
CI / Build and Test (push) Failing after 7s
CI / Build and Test (pull_request) Failing after 7s
Replace the static ship marker with three signal-backed ships that
lerp along waypoint loops through SF Bay (Golden Gate, Oakland, and
Sausalito ferry routes). All clients see the same ship positions via
shared Go state synced every second.
2026-02-20 11:03:59 -10:00
c2794fa0f9 style: simplify container ship SVG marker (#12)
All checks were successful
CI / Build and Test (push) Successful in 35s
2026-02-20 20:56:08 +00:00
7edd5ed1e6 style: replace simple ship icon with container vessel SVG (#11)
All checks were successful
CI / Build and Test (push) Successful in 36s
2026-02-20 20:53:04 +00:00
934805e707 feat: support custom HTML/SVG element markers in MapLibre (#10)
All checks were successful
CI / Build and Test (push) Successful in 34s
2026-02-20 20:40:19 +00:00
cbc5022e0d feat: sync all markers across clients in MapLibre example (#9)
All checks were successful
CI / Build and Test (push) Successful in 35s
2026-02-20 20:16:17 +00:00
74b32800f9 chore: gitignore nats-chatroom directory (#8)
Some checks failed
CI / Build and Test (push) Has been cancelled
2026-02-20 20:15:30 +00:00
cb13839157 fix: nil-close bug, stale docs, dead code, and tracked binaries (#7)
All checks were successful
CI / Build and Test (push) Successful in 34s
2026-02-20 20:00:44 +00:00
18 changed files with 277 additions and 96 deletions

18
.gitignore vendored
View File

@@ -37,21 +37,15 @@ go.work.sum
# Air artifacts # Air artifacts
*tmp/ *tmp/
# binaries # Example binaries and data files
internal/examples/chatroom/chatroom internal/examples/*/[a-z]*[!.go]
internal/examples/counter/counter internal/examples/shakespeare/shake.db
internal/examples/countercomp/countercomp
internal/examples/greeter/greeter
internal/examples/livereload/livereload
internal/examples/picocss/picocss
internal/examples/plugins/plugins
internal/examples/realtimechart/realtimechart
internal/examples/shakespeare/shakespeare
internal/examples/nats-chatroom/nats-chatroom
/nats-chatroom
# NATS data directory # NATS data directory
data/ data/
# Standalone experiments
nats-chatroom
# Claude Code worktrees # Claude Code worktrees
.claude/worktrees/ .claude/worktrees/

View File

@@ -74,14 +74,13 @@ func main() {
- **Plugin system** — `func(v *V)` hooks for integrating CSS/JS libraries - **Plugin system** — `func(v *V)` hooks for integrating CSS/JS libraries
- **Structured logging** — zerolog with configurable levels; console output in dev, JSON in production - **Structured logging** — zerolog with configurable levels; console output in dev, JSON in production
- **Graceful shutdown** — listens for SIGINT/SIGTERM, drains contexts, closes pub/sub - **Graceful shutdown** — listens for SIGINT/SIGTERM, drains contexts, closes pub/sub
- **Context lifecycle** — background reaper cleans up disconnected contexts; configurable TTL
- **HTML DSL** — the `h` package provides type-safe Go-native HTML composition - **HTML DSL** — the `h` package provides type-safe Go-native HTML composition
## Examples ## Examples
The `internal/examples/` directory contains 14 runnable examples: The `internal/examples/` directory contains 19 runnable examples:
`chatroom` · `counter` · `countercomp` · `greeter` · `keyboard` · `livereload` · `nats-chatroom` · `pathparams` · `picocss` · `plugins` · `pubsub-crud` · `realtimechart` · `session` · `shakespeare` `chatroom` · `counter` · `countercomp` · `effectspike` · `greeter` · `keyboard` · `livereload` · `maplibre` · `middleware` · `nats-chatroom` · `pathparams` · `picocss` · `plugins` · `pubsub-crud` · `realtimechart` · `session` · `shakespeare` · `signup` · `spa`
## Experimental ## Experimental

View File

@@ -6,9 +6,13 @@ set -o pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT" cd "$ROOT"
echo "== CI: Format code ==" echo "== CI: Check formatting =="
go fmt ./... if [ -n "$(gofmt -l .)" ]; then
echo "OK: formatting complete" echo "ERROR: files not formatted:"
gofmt -l .
exit 1
fi
echo "OK: all files formatted"
echo "== CI: Run go vet ==" echo "== CI: Run go vet =="
if ! go vet ./...; then if ! go vet ./...; then

View File

@@ -1,7 +1,6 @@
package via package via
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
@@ -51,5 +50,5 @@ func (s *computedSignal) recompute() {
} }
func (s *computedSignal) patchValue() string { func (s *computedSignal) patchValue() string {
return fmt.Sprintf("%v", s.lastVal) return s.lastVal
} }

View File

@@ -94,7 +94,7 @@ Available triggers:
|--------|-------|-------| |--------|-------|-------|
| `OnClick()` | `click` | | | `OnClick()` | `click` | |
| `OnDblClick()` | `dblclick` | | | `OnDblClick()` | `dblclick` | |
| `OnChange()` | `change` | 200ms debounce | | `OnChange()` | `change` | |
| `OnInput()` | `input` | No debounce | | `OnInput()` | `input` | No debounce |
| `OnSubmit()` | `submit` | | | `OnSubmit()` | `submit` | |
| `OnKeyDown(key)` | `keydown` | Filtered by key name (e.g. `"Enter"`, `"Escape"`) | | `OnKeyDown(key)` | `keydown` | Filtered by key name (e.g. `"Enter"`, `"Escape"`) |

2
h/h.go
View File

@@ -5,7 +5,7 @@
// //
// h.Div( // h.Div(
// h.H1(h.Text("Hello, Via")), // h.H1(h.Text("Hello, Via")),
// h.P(h.Text("Pure Go. No tmplates.")), // h.P(h.Text("Pure Go. No templates.")),
// ) // )
package h package h

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"github.com/ryanhamamura/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss"
"github.com/ryanhamamura/via/h" "github.com/ryanhamamura/via/h"
) )
@@ -15,9 +14,6 @@ func main() {
DocumentTitle: "Live Reload Demo", DocumentTitle: "Live Reload Demo",
DevMode: true, DevMode: true,
LogLevel: via.LogLevelDebug, LogLevel: via.LogLevelDebug,
Plugins: []via.Plugin{
// picocss.Default
},
}) })
v.Page("/", func(c *via.Context) { v.Page("/", func(c *via.Context) {

View File

@@ -1,7 +1,11 @@
package main package main
import ( import (
"fmt"
"math"
"math/rand" "math/rand"
"strconv"
"sync"
"time" "time"
"github.com/ryanhamamura/via" "github.com/ryanhamamura/via"
@@ -9,6 +13,68 @@ import (
"github.com/ryanhamamura/via/maplibre" "github.com/ryanhamamura/via/maplibre"
) )
type posMsg struct {
Lng float64 `json:"lng"`
Lat float64 `json:"lat"`
}
var (
vehicleOnce sync.Once
vehicle struct {
mu sync.RWMutex
lng, lat float64
}
)
// shipState tracks a ship lerping along a loop of waypoints.
type shipState struct {
lng, lat float64
waypoints [][2]float64 // [lng, lat] pairs
wpIdx int // index of next target waypoint
progress float64 // 0..1 toward next waypoint
speed float64 // progress increment per tick
}
func (s *shipState) tick() {
s.progress += s.speed
for s.progress >= 1 {
s.progress -= 1
s.wpIdx = (s.wpIdx + 1) % len(s.waypoints)
}
from := s.waypoints[(s.wpIdx-1+len(s.waypoints))%len(s.waypoints)]
to := s.waypoints[s.wpIdx]
s.lng = from[0] + (to[0]-from[0])*s.progress
s.lat = from[1] + (to[1]-from[1])*s.progress
}
// heading returns clockwise degrees from north (for SVG rotation).
func (s *shipState) heading() float64 {
from := s.waypoints[(s.wpIdx-1+len(s.waypoints))%len(s.waypoints)]
to := s.waypoints[s.wpIdx]
dx := to[0] - from[0]
dy := to[1] - from[1]
// atan2 gives angle from +X axis; convert to CW from north
return math.Mod(math.Atan2(dx, dy)*180/math.Pi+360, 360)
}
var (
fleetOnce sync.Once
fleet struct {
mu sync.RWMutex
ships [3]shipState
}
)
const shipSVG = `<svg width="48" height="28" viewBox="0 0 80 44" xmlns="http://www.w3.org/2000/svg">` +
`<path d="M2 30 L10 42 L70 42 L78 30 Z" fill="#1b3a5c"/>` +
`<rect x="12" y="24" width="56" height="6" rx="1" fill="#2c5f8a"/>` +
`<rect x="18" y="14" width="46" height="10" rx="1" fill="#2c5f8a" stroke="#1b3a5c" stroke-width="0.5"/>` +
`<rect x="8" y="12" width="9" height="12" rx="1" fill="#d5dfe8" stroke="#2c5f8a" stroke-width="0.5"/>` +
`<rect x="9.5" y="13" width="6" height="4" rx="0.5" fill="#85c1e9"/>` +
`<rect x="10" y="4" width="5" height="8" rx="0.5" fill="#1b3a5c"/>` +
`<rect x="9.5" y="2" width="6" height="3" rx="0.5" fill="#c0392b"/>` +
`</svg>`
func main() { func main() {
v := via.New() v := via.New()
v.Config(via.Options{ v.Config(via.Options{
@@ -18,6 +84,65 @@ func main() {
Plugins: []via.Plugin{maplibre.Plugin}, Plugins: []via.Plugin{maplibre.Plugin},
}) })
// Single goroutine moves the vehicle — all clients read the same position.
vehicle.lng = -122.43
vehicle.lat = 37.77
vehicleOnce.Do(func() {
go func() {
for {
time.Sleep(time.Second)
vehicle.mu.Lock()
vehicle.lng = -122.43 + (rand.Float64()-0.5)*0.02
vehicle.lat = 37.77 + (rand.Float64()-0.5)*0.02
vehicle.mu.Unlock()
}
}()
})
// Fleet of ships following waypoint loops through SF Bay.
fleetOnce.Do(func() {
fleet.ships = [3]shipState{
{ // Golden Gate → Alcatraz → Pier 39 → back out
waypoints: [][2]float64{
{-122.478, 37.819}, {-122.423, 37.827},
{-122.410, 37.809}, {-122.423, 37.827},
},
speed: 0.03,
},
{ // Oakland → Treasure Island → Angel Island → loop
waypoints: [][2]float64{
{-122.330, 37.795}, {-122.370, 37.823},
{-122.432, 37.860}, {-122.370, 37.823},
},
speed: 0.02,
},
{ // Sausalito → Pier 39 ferry route
waypoints: [][2]float64{
{-122.480, 37.859}, {-122.435, 37.840},
{-122.410, 37.809}, {-122.435, 37.840},
},
speed: 0.025,
},
}
// Set initial positions.
for i := range fleet.ships {
s := &fleet.ships[i]
s.lng = s.waypoints[0][0]
s.lat = s.waypoints[0][1]
s.wpIdx = 1
}
go func() {
for {
time.Sleep(time.Second)
fleet.mu.Lock()
for i := range fleet.ships {
fleet.ships[i].tick()
}
fleet.mu.Unlock()
}
}()
})
v.Page("/", func(c *via.Context) { v.Page("/", func(c *via.Context) {
m := maplibre.New(c, maplibre.Options{ m := maplibre.New(c, maplibre.Options{
Style: "https://demotiles.maplibre.org/style.json", Style: "https://demotiles.maplibre.org/style.json",
@@ -45,23 +170,81 @@ func main() {
}, },
}) })
// Signal-backed marker — server pushes position updates // Animated container ships following waypoint routes
shipNames := [3]string{"MSC Adriatica", "Evergreen Harmony", "Maersk Aurora"}
type shipSignals struct{ lng, lat *via.Signal }
var ships [3]shipSignals
fleet.mu.RLock()
for i, s := range fleet.ships {
ships[i].lng = c.Signal(s.lng)
ships[i].lat = c.Signal(s.lat)
m.AddMarker(fmt.Sprintf("ship-%d", i), maplibre.Marker{
LngSignal: ships[i].lng,
LatSignal: ships[i].lat,
Element: shipSVG,
Anchor: "center",
Rotation: s.heading(),
Popup: &maplibre.Popup{
Content: fmt.Sprintf("<strong>%s</strong>", shipNames[i]),
},
})
}
fleet.mu.RUnlock()
// Custom SVG vehicle marker — reads shared Go state
vehicleLng := c.Signal(-122.43) vehicleLng := c.Signal(-122.43)
vehicleLat := c.Signal(37.77) vehicleLat := c.Signal(37.77)
m.AddMarker("vehicle", maplibre.Marker{ m.AddMarker("vehicle", maplibre.Marker{
LngSignal: vehicleLng, LngSignal: vehicleLng,
LatSignal: vehicleLat, LatSignal: vehicleLat,
Color: "#9b59b6", Element: `<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">` +
`<circle cx="10" cy="10" r="9" fill="#9b59b6" stroke="#fff" stroke-width="2"/>` +
`</svg>`,
Anchor: "center",
}) })
c.OnInterval(time.Second, func() { c.OnInterval(time.Second, func() {
vehicleLng.SetValue(-122.43 + (rand.Float64()-0.5)*0.02) vehicle.mu.RLock()
vehicleLat.SetValue(37.77 + (rand.Float64()-0.5)*0.02) lng, lat := vehicle.lng, vehicle.lat
vehicle.mu.RUnlock()
vehicleLng.SetValue(lng)
vehicleLat.SetValue(lat)
fleet.mu.RLock()
for i, s := range fleet.ships {
ships[i].lng.SetValue(s.lng)
ships[i].lat.SetValue(s.lat)
}
fleet.mu.RUnlock()
c.SyncSignals() c.SyncSignals()
}) })
// Draggable marker — user drags, signals update // Yellow click marker — synced across clients via PubSub
clickLng := c.Signal(-122.4194)
clickLat := c.Signal(37.7749)
m.AddMarker("clicked", maplibre.Marker{
LngSignal: clickLng,
LatSignal: clickLat,
Color: "#f39c12",
})
via.Subscribe(c, "map.click", func(msg posMsg) {
clickLng.SetValue(msg.Lng)
clickLat.SetValue(msg.Lat)
c.SyncSignals()
})
click := m.OnClick()
handleClick := c.Action(func() {
e := click.Data()
via.Publish(c, "map.click", posMsg{Lng: e.LngLat.Lng, Lat: e.LngLat.Lat})
})
// Blue draggable pin — synced across clients via PubSub
pinLng := c.Signal(-122.41) pinLng := c.Signal(-122.41)
pinLat := c.Signal(37.78) pinLat := c.Signal(37.78)
@@ -72,14 +255,16 @@ func main() {
Draggable: true, Draggable: true,
}) })
// Click event — click to place a marker via.Subscribe(c, "map.pin", func(msg posMsg) {
click := m.OnClick() pinLng.SetValue(msg.Lng)
handleClick := c.Action(func() { pinLat.SetValue(msg.Lat)
e := click.Data() c.SyncSignals()
m.AddMarker("clicked", maplibre.Marker{ })
LngLat: e.LngLat,
Color: "#f39c12", handlePinDrag := c.Action(func() {
}) lng, _ := strconv.ParseFloat(pinLng.String(), 64)
lat, _ := strconv.ParseFloat(pinLat.String(), 64)
via.Publish(c, "map.pin", posMsg{Lng: lng, Lat: lat})
}) })
// GeoJSON polygon source + fill layer // GeoJSON polygon source + fill layer
@@ -111,7 +296,7 @@ func main() {
}, },
}) })
// FlyTo actions using CameraOptions // FlyTo actions
zoom14 := 14.0 zoom14 := 14.0
flyToSF := c.Action(func() { flyToSF := c.Action(func() {
m.FlyTo(maplibre.CameraOptions{ m.FlyTo(maplibre.CameraOptions{
@@ -134,6 +319,7 @@ func main() {
h.H1(h.Text("MapLibre GL Example")), h.H1(h.Text("MapLibre GL Example")),
m.Element( m.Element(
click.Input(handleClick.OnInput()), click.Input(handleClick.OnInput()),
h.Input(h.Type("hidden"), pinLng.Bind(), handlePinDrag.OnInput()),
), ),
h.Div(h.Attr("style", "margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap"), h.Div(h.Attr("style", "margin-top:1rem;display:flex;gap:0.5rem;flex-wrap:wrap"),
h.Button(h.Text("Fly to San Francisco"), flyToSF.OnClick()), h.Button(h.Text("Fly to San Francisco"), flyToSF.OnClick()),
@@ -142,6 +328,7 @@ func main() {
h.Div(h.Attr("style", "margin-top:0.5rem;font-size:0.9rem"), h.Div(h.Attr("style", "margin-top:0.5rem;font-size:0.9rem"),
h.P(h.Text("Zoom: "), m.Zoom.Text()), h.P(h.Text("Zoom: "), m.Zoom.Text()),
h.P(h.Text("Center: "), m.CenterLng.Text(), h.Text(", "), m.CenterLat.Text()), h.P(h.Text("Center: "), m.CenterLng.Text(), h.Text(", "), m.CenterLat.Text()),
h.P(h.Text("Click: "), clickLng.Text(), h.Text(", "), clickLat.Text()),
h.P(h.Text("Vehicle: "), vehicleLng.Text(), h.Text(", "), vehicleLat.Text()), h.P(h.Text("Vehicle: "), vehicleLng.Text(), h.Text(", "), vehicleLat.Text()),
h.P(h.Text("Draggable Pin: "), pinLng.Text(), h.Text(", "), pinLat.Text()), h.P(h.Text("Draggable Pin: "), pinLng.Text(), h.Text(", "), pinLat.Text()),
), ),

View File

@@ -4,17 +4,12 @@ import (
"strconv" "strconv"
"github.com/ryanhamamura/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss"
. "github.com/ryanhamamura/via/h" . "github.com/ryanhamamura/via/h"
) )
func main() { func main() {
v := via.New() v := via.New()
v.Config(via.Options{
// Plugins: []via.Plugin{picocss.Default},
})
v.Page("/counters/{counter_id}/{start_at_step}", func(c *via.Context) { v.Page("/counters/{counter_id}/{start_at_step}", func(c *via.Context) {
counterID := c.GetPathParam("counter_id") counterID := c.GetPathParam("counter_id")

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"github.com/ryanhamamura/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss"
"github.com/ryanhamamura/via/h" "github.com/ryanhamamura/via/h"
) )
@@ -13,11 +12,6 @@ func main() {
v.Config(via.Options{ v.Config(via.Options{
DocumentTitle: "Via Counter", DocumentTitle: "Via Counter",
// Plugin is placed here. Use picocss.WithOptions(pococss.Options) to add the plugin
// with a different color theme or to enable a classes for a wide range of colors.
// Plugins: []via.Plugin{
// picocss.Default,
// },
}) })
v.Page("/", func(c *via.Context) { v.Page("/", func(c *via.Context) {

View File

@@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/ryanhamamura/via" "github.com/ryanhamamura/via"
// "github.com/go-via/via-plugin-picocss/picocss"
"github.com/ryanhamamura/via/h" "github.com/ryanhamamura/via/h"
) )
@@ -16,9 +15,6 @@ func main() {
v.Config(via.Options{ v.Config(via.Options{
LogLevel: via.LogLevelDebug, LogLevel: via.LogLevelDebug,
DevMode: true, DevMode: true,
Plugins: []via.Plugin{
// picocss.Default,
},
}) })
v.AppendToHead( v.AppendToHead(

View File

@@ -22,6 +22,9 @@ type ShakeDB struct {
findByTextStmt *sql.Stmt findByTextStmt *sql.Stmt
} }
// Prepare opens shake.db, a ~22 MB SQLite database of Shakespeare's works.
// Download from https://github.com/nicholasgasior/gopher-fizzbuzz/raw/master/shake.db
// and place it in this directory before running.
func (shakeDB *ShakeDB) Prepare() { func (shakeDB *ShakeDB) Prepare() {
db, err := sql.Open("sqlite3", "shake.db") db, err := sql.Open("sqlite3", "shake.db")
if err != nil { if err != nil {

View File

@@ -179,10 +179,25 @@ func initScript(m *Map) string {
// markerBodyJS generates JS to add a marker, assuming `map` is in scope. // markerBodyJS generates JS to add a marker, assuming `map` is in scope.
func markerBodyJS(mapID, markerID string, mk Marker) string { func markerBodyJS(mapID, markerID string, mk Marker) string {
var b strings.Builder var b strings.Builder
if mk.Element != "" {
b.WriteString(fmt.Sprintf(
`var _mkEl=document.createElement('div');_mkEl.innerHTML=%s;`,
jsonStr(mk.Element)))
}
opts := "{" opts := "{"
if mk.Color != "" { if mk.Element != "" {
opts += `element:_mkEl.firstElementChild||_mkEl,`
} else if mk.Color != "" {
opts += fmt.Sprintf(`color:%s,`, jsonStr(mk.Color)) opts += fmt.Sprintf(`color:%s,`, jsonStr(mk.Color))
} }
if mk.Anchor != "" {
opts += fmt.Sprintf(`anchor:%s,`, jsonStr(mk.Anchor))
}
if mk.Rotation != 0 {
opts += fmt.Sprintf(`rotation:%s,`, formatFloat(mk.Rotation))
}
if mk.Draggable { if mk.Draggable {
opts += `draggable:true,` opts += `draggable:true,`
} }

View File

@@ -302,6 +302,19 @@ type Marker struct {
Draggable bool Draggable bool
Popup *Popup Popup *Popup
// Element is raw HTML/SVG used as a custom marker instead of the
// default pin. When set, Color is ignored.
// Do not pass untrusted user input without sanitizing it first.
Element string
// Anchor controls which part of the element sits at the coordinate.
// Values: "center" (default for custom elements), "bottom" (default
// for the pin), "top", "left", "right", "top-left", etc.
Anchor string
// Rotation is clockwise degrees. Useful for directional icons (ships, vehicles).
Rotation float64
// Signal-backed position. When set, signals drive marker position reactively. // Signal-backed position. When set, signals drive marker position reactively.
// Initial position is read from the signal values. LngLat is ignored when signals are set. // Initial position is read from the signal values. LngLat is ignored when signals are set.
// If Draggable is true, drag updates write back to these signals. // If Draggable is true, drag updates write back to these signals.

View File

@@ -1,7 +1,6 @@
package via package via
import ( import (
// "net/http/httptest"
"testing" "testing"
"github.com/ryanhamamura/via/h" "github.com/ryanhamamura/via/h"

65
via.go
View File

@@ -439,36 +439,39 @@ func (v *V) ensureDatastarHandler() {
}) })
} }
func loadDevModeMap(path string) map[string]string {
m := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return m
}
defer file.Close()
json.NewDecoder(file).Decode(&m)
return m
}
func saveDevModeMap(path string, m map[string]string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return json.NewEncoder(file).Encode(m)
}
func (v *V) devModePersist(c *Context) { 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 {
v.logFatal("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 ctxRegMap := loadDevModeMap(p)
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 { if _, ok := ctxRegMap[c.id]; !ok {
ctxRegMap[c.id] = c.route ctxRegMap[c.id] = c.route
} }
// write persisted list to file if err := saveDevModeMap(p, ctxRegMap); err != nil {
file, err = os.Create(p) v.logErr(c, "devmode failed to persist ctx: %v", err)
if err != nil {
v.logErr(c, "devmode failed to percist ctx: %v", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
if err := encoder.Encode(ctxRegMap); err != nil {
v.logErr(c, "devmode failed to persist ctx")
} }
v.logDebug(c, "devmode persisted ctx to file") v.logDebug(c, "devmode persisted ctx to file")
} }
@@ -476,27 +479,11 @@ func (v *V) devModePersist(c *Context) {
func (v *V) devModeRemovePersisted(c *Context) { func (v *V) devModeRemovePersisted(c *Context) {
p := filepath.Join(".via", "devmode", "ctx.json") p := filepath.Join(".via", "devmode", "ctx.json")
// load persisted list from file, or empty list if file not found ctxRegMap := loadDevModeMap(p)
file, err := os.Open(p)
ctxRegMap := make(map[string]string)
if err == nil {
json.NewDecoder(file).Decode(&ctxRegMap)
}
file.Close()
delete(ctxRegMap, c.id) delete(ctxRegMap, c.id)
// write persisted list to file if err := saveDevModeMap(p, ctxRegMap); err != nil {
file, err = os.Create(p) v.logErr(c, "devmode failed to remove persisted ctx: %v", err)
if err != nil {
v.logErr(c, "devmode failed to remove percisted ctx: %v", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
if err := encoder.Encode(ctxRegMap); err != nil {
v.logErr(c, "devmode failed to remove persisted ctx")
} }
v.logDebug(c, "devmode removed persisted ctx from file") v.logDebug(c, "devmode removed persisted ctx from file")
} }