Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion avatar-upload/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ func main() {
lt := livetemplate.Must(livetemplate.New("avatar-upload",
livetemplate.WithParseFiles("avatar-upload.tmpl"),
livetemplate.WithDevMode(true),
livetemplate.WithStatePersistence(),
// Configure upload using WithUpload option
livetemplate.WithUpload("avatar", livetemplate.UploadConfig{
Accept: []string{"image/jpeg", "image/png", "image/gif"},
Expand Down
2 changes: 1 addition & 1 deletion counter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func main() {
LastUpdated: formatTime(),
}

opts := append(envConfig.ToOptions(), livetemplate.WithStatePersistence())
opts := envConfig.ToOptions()
tmpl := livetemplate.Must(livetemplate.New("counter", opts...))
liveHandler := tmpl.Handle(controller, livetemplate.AsState(initialState))

Expand Down
2 changes: 1 addition & 1 deletion flash-messages/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func main() {
initialState := &FlashState{}

// Create template with environment-based configuration
opts := append(envConfig.ToOptions(), livetemplate.WithStatePersistence())
opts := envConfig.ToOptions()
tmpl := livetemplate.Must(livetemplate.New("flash", opts...))

// Mount handler
Expand Down
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/go-playground/validator/v10 v10.30.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/livetemplate/livetemplate v0.8.9
github.com/livetemplate/livetemplate v0.8.10
github.com/livetemplate/lvt v0.0.0-20260327182801-53d6d40e692e
github.com/livetemplate/lvt/components v0.0.0-20260327182801-53d6d40e692e
modernc.org/sqlite v1.43.0
Expand Down Expand Up @@ -46,5 +46,3 @@ require (
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

replace github.com/livetemplate/livetemplate => ../livetemplate/.worktrees/per-connection-persist
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/livetemplate/livetemplate v0.8.10 h1:Lg62gb297Iq3/EShPnEUyqDFN8aeyTYchbW3wUFU370=
github.com/livetemplate/livetemplate v0.8.10/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs=
github.com/livetemplate/lvt v0.0.0-20260327182801-53d6d40e692e h1:nAV7BaOatFcbSaP6m9CgLrRsGPeWtbjPDZ8dOI6Zb+c=
github.com/livetemplate/lvt v0.0.0-20260327182801-53d6d40e692e/go.mod h1:17cFl500ntymD3gx8h+ZODnVnTictHgG8Wmz/By75sU=
github.com/livetemplate/lvt/components v0.0.0-20260327182801-53d6d40e692e h1:vuR0pQtEQHZOD2/HvTJfHPKEdoD77XUs1mq1kdjgVig=
Expand Down
2 changes: 1 addition & 1 deletion live-preview/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func main() {
controller := &PreviewController{}
initialState := &PreviewState{Preview: preview("")}

opts := append(envConfig.ToOptions(), livetemplate.WithStatePersistence())
opts := envConfig.ToOptions()
tmpl := livetemplate.Must(livetemplate.New("preview", opts...))
http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(initialState)))
http.HandleFunc("/livetemplate-client.js", e2etest.ServeClientLibrary)
Expand Down
2 changes: 1 addition & 1 deletion login/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func main() {
initialState := &AuthState{}

// Create template with environment-based configuration
opts := append(envConfig.ToOptions(), livetemplate.WithStatePersistence())
opts := envConfig.ToOptions()
tmpl := livetemplate.Must(livetemplate.New("auth", opts...))

// Set up handler with Controller+State pattern
Expand Down
2 changes: 1 addition & 1 deletion profile-progressive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func main() {
log.Fatal(err)
}

opts := append(envConfig.ToOptions(), livetemplate.WithStatePersistence())
opts := envConfig.ToOptions()
tmpl, err := livetemplate.New("profile", opts...)
if err != nil {
log.Fatal(err)
Expand Down
2 changes: 1 addition & 1 deletion progressive-enhancement/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func main() {

// Create template with configuration
// Progressive enhancement is enabled by default
opts := append(envConfig.ToOptions(), livetemplate.WithStatePersistence())
opts := envConfig.ToOptions()
tmpl := livetemplate.Must(livetemplate.New("progressive-enhancement", opts...))

// Mount handler
Expand Down
49 changes: 32 additions & 17 deletions shared-notepad/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,57 @@ import (
"log"
"net/http"
"os"
"sync"
"time"

"github.com/livetemplate/livetemplate"
e2etest "github.com/livetemplate/lvt/testing"
)

// NotepadController is a stateless singleton — all state lives in NotepadState
// and is shared across tabs via WithSharedState.
type NotepadController struct{}
type NotepadController struct {
mu sync.RWMutex
notes map[string]NotepadState // userID -> latest state
}
Comment on lines +14 to +17
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The controller stores per-user notes in an unbounded in-memory map (notes map[string]NotepadState). For a long-running server or many distinct usernames, this will grow without limit and can lead to memory pressure. Consider adding a simple eviction policy/TTL, limiting accepted usernames in the demo, or documenting that this is intentionally in-memory/demo-only.

Copilot uses AI. Check for mistakes.

// NotepadState is shared across all tabs of the same authenticated user.
// WithSharedState means any change auto-broadcasts to all other tabs.
type NotepadState struct {
Username string `json:"username"`
Content string `json:"content"`
SavedAt string `json:"saved_at"`
CharCount int `json:"char_count"`
}

// Mount initializes the notepad for a new session.
func (c *NotepadController) Mount(state NotepadState, ctx *livetemplate.Context) (NotepadState, error) {
state.Username = ctx.UserID()
c.mu.RLock()
if saved, ok := c.notes[ctx.UserID()]; ok {
state.Content = saved.Content
state.CharCount = saved.CharCount
state.SavedAt = saved.SavedAt
}
c.mu.RUnlock()
return state, nil
}

// Save persists the note (triggered by the Save button).
// WithSharedState auto-broadcasts to all other tabs of this user.
func (c *NotepadController) Save(state NotepadState, ctx *livetemplate.Context) (NotepadState, error) {
state.Content = ctx.GetString("content")
state.CharCount = len([]rune(state.Content))
state.SavedAt = time.Now().Format("15:04:05")

c.mu.Lock()
c.notes[ctx.UserID()] = state
c.mu.Unlock()

return state, nil
}

func (c *NotepadController) Sync(state NotepadState, ctx *livetemplate.Context) (NotepadState, error) {
c.mu.RLock()
if saved, ok := c.notes[ctx.UserID()]; ok {
state.Content = saved.Content
state.CharCount = saved.CharCount
state.SavedAt = saved.SavedAt
}
c.mu.RUnlock()
return state, nil
Comment on lines +50 to 58
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sync restores Content/CharCount/SavedAt but never sets state.Username = ctx.UserID(). If a sync happens on a state instance where Username is empty (or if future refactors call Sync before Mount), the template’s “Logged in as …” line can become blank. Setting Username in Sync makes the lifecycle handler self-contained.

Copilot uses AI. Check for mistakes.
}

Expand All @@ -49,21 +69,16 @@ func main() {
log.Fatalf("Invalid configuration: %v", err)
}

// BasicAuth: each user gets their own isolated session group.
// The library's ChallengeAuthenticator sends WWW-Authenticate header
// automatically, triggering the browser's login dialog.
auth := livetemplate.NewBasicAuthenticator(func(username, password string) (bool, error) {
// Demo: accept any username with password "demo"
return password == "demo", nil
})

opts := envConfig.ToOptions()
opts = append(opts,
livetemplate.WithAuthenticator(auth),
livetemplate.WithSharedState(), // All tabs of the same user share state
)
opts = append(opts, livetemplate.WithAuthenticator(auth))

controller := &NotepadController{}
controller := &NotepadController{
notes: make(map[string]NotepadState),
}
initialState := &NotepadState{}

tmpl := livetemplate.Must(livetemplate.New("notepad", opts...))
Expand Down
2 changes: 1 addition & 1 deletion shared-notepad/notepad.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<header><strong>How it works</strong></header>
<ul>
<li><strong>BasicAuth</strong> — each user gets an isolated session (alice's notes are separate from bob's)</li>
<li><strong>WithSharedState</strong> — all tabs of the same user share state (Save in one tab updates all)</li>
<li><strong>Sync()</strong> — all tabs of the same user auto-sync (Save in one tab updates all)</li>
<li>Notes persist across page refreshes within the same server session</li>
</ul>
<small>Demo credentials: any username, password <strong>demo</strong></small>
Expand Down
1 change: 0 additions & 1 deletion todos-components/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ func main() {
ltSets[i] = convertTemplateSet(set)
}
opts = append(opts, livetemplate.WithComponentTemplates(ltSets...))
opts = append(opts, livetemplate.WithStatePersistence())

// Create template with component templates registered
tmpl := livetemplate.Must(livetemplate.New("todos-components", opts...))
Expand Down
2 changes: 1 addition & 1 deletion todos-progressive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func main() {
log.Fatal(err)
}

opts := append(envConfig.ToOptions(), livetemplate.WithStatePersistence())
opts := envConfig.ToOptions()
tmpl, err := livetemplate.New("todos", opts...)
if err != nil {
log.Fatal(err)
Expand Down
Loading
Loading