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
24 changes: 19 additions & 5 deletions chat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ type Message struct {
Timestamp string `json:"timestamp"`
}

// Mount is called once per session group. Sets up initial empty state.
// Mount runs once per session group. Subscribes the self-topic so peer tabs
// receive the UserJoined / NewMessage / UserLeft dispatches Publish'd below.
func (c *ChatController) Mount(state ChatState, ctx *livetemplate.Context) (ChatState, error) {
if err := ctx.Subscribe(ctx.SelfTopic()); err != nil {
return state, err
}
c.mu.RLock()
defer c.mu.RUnlock()
state.Messages = c.copyMessages()
Expand Down Expand Up @@ -73,8 +77,14 @@ func (c *ChatController) Join(state ChatState, ctx *livetemplate.Context) (ChatS
state.OnlineCount = c.countOnline()
c.mu.Unlock()

// Tell other tabs someone joined
ctx.BroadcastAction("UserJoined", nil)
// Tell other tabs someone joined. We propagate Publish's error rather than
// log-and-swallow because the only errors it can return are programmer
// errors (empty SelfTopic from a misconfigured Authenticator, or the
// per-action publish cap exceeded). Surfacing them loudly is a feature.
// Same pattern applies to every Publish call site in this file.
if err := ctx.Publish(ctx.SelfTopic(), "UserJoined", nil); err != nil {
return state, err
}
return state, nil
}

Expand Down Expand Up @@ -109,7 +119,9 @@ func (c *ChatController) Send(state ChatState, ctx *livetemplate.Context) (ChatS
c.mu.Unlock()

// Tell other tabs about the new message
ctx.BroadcastAction("NewMessage", nil)
if err := ctx.Publish(ctx.SelfTopic(), "NewMessage", nil); err != nil {
return state, err
}
return state, nil
}

Expand All @@ -135,7 +147,9 @@ func (c *ChatController) Leave(state ChatState, ctx *livetemplate.Context) (Chat
state.OnlineCount = c.countOnline()
c.mu.Unlock()

ctx.BroadcastAction("UserLeft", nil)
if err := ctx.Publish(ctx.SelfTopic(), "UserLeft", nil); err != nil {
return state, err
}
return state, nil
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/chromedp/chromedp v0.14.2
github.com/go-playground/validator/v10 v10.30.1
github.com/gorilla/websocket v1.5.3
github.com/livetemplate/livetemplate v0.9.0
github.com/livetemplate/livetemplate v0.9.2
github.com/livetemplate/lvt v0.1.6
github.com/livetemplate/lvt/components v0.1.2
modernc.org/sqlite v1.43.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +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.9.0 h1:6vHNNpZLIY4gO08/Gth3vr1F3A3ANwSKTQvfuy1I8js=
github.com/livetemplate/livetemplate v0.9.0/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs=
github.com/livetemplate/livetemplate v0.9.2 h1:zF/mbhXxp5uLX3UBdgkdBk+Jmhfx3g5DAlUCisTXoZM=
github.com/livetemplate/livetemplate v0.9.2/go.mod h1:GMvZKyPUq8LSGfgD3pftKOHa6v+I+RDYyff2mNjeAYs=
github.com/livetemplate/lvt v0.1.6 h1:1rDU5hDo+EtZ0mT+868wYD9czF2EHEgdacS4kpIUPQ4=
github.com/livetemplate/lvt v0.1.6/go.mod h1:OrTdx3zvh0WeuugVueQoRG3ILRNJe/dThErxKsos6Rw=
github.com/livetemplate/lvt/components v0.1.2 h1:MM2M5IZnsUAu0py9ZbtcQCo0bvUrL4Z3Ly/yDkYNyag=
Expand Down
14 changes: 7 additions & 7 deletions landing-demo/landing_demo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,27 +213,27 @@ func TestLandingDemoE2E(t *testing.T) {
}
})

t.Run("BroadcastAction_Propagates_To_Peer_Tab", func(t *testing.T) {
t.Run("Publish_Propagates_To_Peer_Tab", func(t *testing.T) {
// SKIP rationale: this scenario IS what the README and landing
// page describe — "open this page in another tab and watch them
// sync." The controller calls BroadcastAction after counter
// mutations to dispatch peer-tab updates. But
// reliably exercising that across two chromedp browser contexts
// turns out to need more work than fits this PR:
// sync." The controller Publishes to SelfTopic() after counter
// mutations to dispatch peer-tab updates. But reliably exercising
// that across two chromedp browser contexts turns out to need more
// work than fits this PR:
// - chromedp.NewContext(parent) shares the browser allocator
// but each new target gets its own request context, so the
// session cookie may not propagate the way two real-world
// tabs in the same browser would.
// - Even when both tabs DO land in the same session group,
// the peer-side broadcast round-trip wasn't observed within 10s
// the peer-side publish round-trip wasn't observed within 10s
// in this harness, suggesting either a session-group
// mismatch or an artifact of how the docker-chrome target
// attaches.
// Manual verification: load https://lt-landing-demo.fly.dev/ in
// two tabs of the same browser and click +1 in either; both
// counters move. The cross-tab claim on the landing page holds.
// Tracking proper e2e coverage as a follow-up.
t.Skip("cross-tab BroadcastAction e2e: tracked at livetemplate/examples#94 — pending session-group propagation work in chromedp peer context; manual verification documented in test comment")
t.Skip("cross-tab Publish e2e: tracked at livetemplate/examples#94 — pending session-group propagation work in chromedp peer context; manual verification documented in test comment")
})

t.Run("HTTP_POST_Fallback_Without_JS", func(t *testing.T) {
Expand Down
25 changes: 20 additions & 5 deletions landing-demo/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Minimal LiveTemplate counter, sized for a landing-page demo. The whole
// app fits in this file; the template is a single counter.tmpl. Per-session
// state means each visitor has their own counter; explicit BroadcastAction
// calls keep their WebSocket-connected tabs in step.
// state means each visitor has their own counter; explicit Publish-to-
// SelfTopic calls keep their WebSocket-connected tabs in step.
package main

import (
Expand All @@ -19,9 +19,20 @@ type CounterState struct {
Count int `json:"count" lvt:"persist"`
}

// Mount subscribes the self-topic so peer tabs receive the Increment /
// Decrement / Reset dispatches Publish'd from the actions below.
func (c *CounterController) Mount(s CounterState, ctx *livetemplate.Context) (CounterState, error) {
if err := ctx.Subscribe(ctx.SelfTopic()); err != nil {
return s, err
}
return s, nil
}

func (c *CounterController) Increment(s CounterState, ctx *livetemplate.Context) (CounterState, error) {
s.Count++
ctx.BroadcastAction("Increment", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Increment", nil); err != nil {
return s, err
}
return s, nil
}

Expand All @@ -31,13 +42,17 @@ func (c *CounterController) Decrement(s CounterState, ctx *livetemplate.Context)
if s.Count > 0 {
s.Count--
}
ctx.BroadcastAction("Decrement", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Decrement", nil); err != nil {
return s, err
}
return s, nil
}

func (c *CounterController) Reset(s CounterState, ctx *livetemplate.Context) (CounterState, error) {
s.Count = 0
ctx.BroadcastAction("Reset", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Reset", nil); err != nil {
return s, err
}
return s, nil
}

Expand Down
9 changes: 8 additions & 1 deletion shared-notepad/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ type NotepadState struct {
}

func (c *NotepadController) Mount(state NotepadState, ctx *livetemplate.Context) (NotepadState, error) {
// Subscribe the self-topic so peer tabs of the same user receive the
// Refresh dispatch from Save's Publish below — multi-device sync.
if err := ctx.Subscribe(ctx.SelfTopic()); err != nil {
return state, err
}
state.Username = ctx.UserID()
c.mu.RLock()
if saved, ok := c.notes[ctx.UserID()]; ok {
Expand All @@ -45,7 +50,9 @@ func (c *NotepadController) Save(state NotepadState, ctx *livetemplate.Context)
c.notes[ctx.UserID()] = state
c.mu.Unlock()

ctx.BroadcastAction("Refresh", nil)
if err := ctx.Publish(ctx.SelfTopic(), "Refresh", nil); err != nil {
return state, err
}
return state, nil
}

Expand Down
21 changes: 17 additions & 4 deletions todos/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ type TodoController struct {
}

func (c *TodoController) Mount(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
// Subscribe self-topic so peer tabs of the same user receive the
// RefreshTodos dispatch from Publish calls in actions below.
if err := ctx.Subscribe(ctx.SelfTopic()); err != nil {
return state, err
}
state.Username = ctx.UserID()
state = initComponents(state)
return c.loadTodos(context.Background(), state, ctx.UserID())
Expand Down Expand Up @@ -60,7 +65,9 @@ func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoSt
if err != nil {
return state, err
}
ctx.BroadcastAction("RefreshTodos", nil)
if err := ctx.Publish(ctx.SelfTopic(), "RefreshTodos", nil); err != nil {
return state, err
}
return state, nil
}

Expand Down Expand Up @@ -99,7 +106,9 @@ func (c *TodoController) Toggle(state TodoState, ctx *livetemplate.Context) (Tod
if err != nil {
return state, err
}
ctx.BroadcastAction("RefreshTodos", nil)
if err := ctx.Publish(ctx.SelfTopic(), "RefreshTodos", nil); err != nil {
return state, err
}
return state, nil
}

Expand Down Expand Up @@ -134,7 +143,9 @@ func (c *TodoController) ConfirmDeleteConfirm(state TodoState, ctx *livetemplate
if err != nil {
return state, err
}
ctx.BroadcastAction("RefreshTodos", nil)
if err := ctx.Publish(ctx.SelfTopic(), "RefreshTodos", nil); err != nil {
return state, err
}
return state, nil
}

Expand Down Expand Up @@ -225,7 +236,9 @@ func (c *TodoController) ClearCompleted(state TodoState, ctx *livetemplate.Conte
if err != nil {
return state, err
}
ctx.BroadcastAction("RefreshTodos", nil)
if err := ctx.Publish(ctx.SelfTopic(), "RefreshTodos", nil); err != nil {
return state, err
}
return state, nil
}

Expand Down
Loading