Multi-User Refresh

A counter clicked in one tab ticks up in every other tab. Mount opts each connection into peer fan-out with ctx.Subscribe(ctx.SelfTopic()); Increment bumps the controller's mutex-guarded counter and then calls ctx.Publish(ctx.SelfTopic(), "RefreshCounter", nil), so every subscribed peer runs RefreshCounter and reloads the shared value. The publish is explicit — the counter only converges because the action fans the refresh out.

Multi-User Refresh

Mount opts each connection in via ctx.Subscribe(ctx.SelfTopic()); Increment updates the shared counter, then explicitly calls ctx.Publish(ctx.SelfTopic(), "RefreshCounter", nil) so peer connections that subscribed reload from the controller's mutex-protected state.

Counter: 0

Try: Open this page in a second tab. Click Increment in either tab — both stay in sync because the action explicitly publishes a peer refresh via ctx.Publish(ctx.SelfTopic(), "RefreshCounter", nil).

Template

One button and one rendered counter — all the synchronization happens server-side.

{{define "content"}}
<article>
    <h3>Multi-User Refresh</h3>
    <p><small>Mount opts each connection in via <code>ctx.Subscribe(ctx.SelfTopic())</code>; <code>Increment</code> updates the shared counter, then explicitly calls <code>ctx.Publish(ctx.SelfTopic(), "RefreshCounter", nil)</code> so peer connections that subscribed reload from the controller's mutex-protected state.</small></p>

    <p>Counter: <strong>{{.Counter}}</strong></p>

    <form method="POST">
        <button name="increment">Increment</button>
    </form>

    <p><small><strong>Try:</strong> Open this page in a second tab. Click Increment in either tab — both stay in sync because the action explicitly publishes a peer refresh via <code>ctx.Publish(ctx.SelfTopic(), "RefreshCounter", nil)</code>.</small></p>
</article>
{{end}}

multi-user-sync.tmpl

Handler & state

Mount subscribes and seeds the initial count; Increment mutates and publishes; RefreshCounter is the action peers run to converge.

type MultiUserSyncController struct {
	mu      sync.RWMutex
	counter int
}

// Mount runs on every initial render. Subscribing the self-topic wires this
// connection to receive RefreshCounter from peer Publishes. Without the
// initial-render counter read, a tab that opens AFTER other tabs have
// incremented would render Counter:0 and only converge on the next peer
// publish. Same fix as PresenceController.
func (c *MultiUserSyncController) Mount(state MultiUserSyncState, ctx *livetemplate.Context) (MultiUserSyncState, error) {
	if err := ctx.Subscribe(ctx.SelfTopic()); err != nil {
		return state, err
	}
	c.mu.RLock()
	state.Counter = c.counter
	c.mu.RUnlock()
	return state, nil
}

// RefreshCounter is the action peer connections run when Increment publishes
// to SelfTopic(). The state arg is the peer's local state; we replace its
// Counter from the shared controller value so all tabs converge.
func (c *MultiUserSyncController) RefreshCounter(state MultiUserSyncState, ctx *livetemplate.Context) (MultiUserSyncState, error) {
	c.mu.RLock()
	state.Counter = c.counter
	c.mu.RUnlock()
	return state, nil
}

func (c *MultiUserSyncController) Increment(state MultiUserSyncState, ctx *livetemplate.Context) (MultiUserSyncState, error) {
	c.mu.Lock()
	c.counter++
	state.Counter = c.counter
	c.mu.Unlock()
	if err := ctx.Publish(ctx.SelfTopic(), "RefreshCounter", nil); err != nil {
		return state, err
	}
	return state, nil
}

func multiUserSyncHandler() http.Handler {
	tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/realtime/multi-user-sync.tmpl")
	return tmpl.Handle(&MultiUserSyncController{}, livetemplate.AsState(&MultiUserSyncState{
		Title:    "Multi-User Sync",
		Category: "Real-Time & Multi-User",
	}))
}

handlers_realtime.go:16-64

type MultiUserSyncState struct {
	Title    string
	Category string
	Counter  int
}

state_realtime.go:10-15

When to use

Reach for Broadcasting when peers need to share a growing log rather than a single value.

source: livetemplate/docs · path: examples/patterns/templates/realtime/multi-user-sync.tmpl