docs: expand server-actions.md with pubsub package details#160
Conversation
- Replace brief distributed deployments section with full pubsub docs - Add Broadcaster interface, Redis setup example, broadcast scopes - Add deprecation note for old Broadcaster/BroadcastAware interfaces - Fix broken link: ../SCALING.md → ../guides/SCALING.md - Remove stale examples repository link Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR ReviewThis is a clean documentation-only PR. I verified accuracy against the actual source files. Verified Accurate
IssuesMissing The Redis setup snippet uses Removal of the Examples section Dropping all three example links (login, chat, counter) with no replacement leaves readers with nowhere to go for complete, working reference implementations. Consider either updating the links if the examples repo still exists, or pointing to another location such as the integration tests or a guide. Minor Suggestions
SummaryThe factual content is correct and the link fix ( |
There was a problem hiding this comment.
Pull request overview
Updates the Server Actions reference documentation to better describe running server actions in distributed (multi-instance) deployments, including Redis-backed PubSub setup and broadcaster API details.
Changes:
- Expanded “Distributed Deployments” documentation with the
pubsub.Broadcasterinterface and Redis configuration example (NewRedisBroadcaster). - Added a “Broadcast Scopes” table describing the intended targeting semantics for broadcaster methods.
- Updated/cleaned related docs references (scaling guide link fix, removed stale examples repo link) and added a deprecation note referencing older interfaces in
mount.go.
Comments suppressed due to low confidence (1)
docs/references/server-actions.md:474
- This section claims that, once
WithPubSubBroadcasteris configured,Session.TriggerAction()“automatically publishes to Redis” and works transparently across instances. In the current codebase there are subscriptions wired up (Template.Handle callsPubSubBroadcaster.Subscribe*), but there are no corresponding publish calls (no uses ofPubSubBroadcaster.Publish*/PublishServerActionoutside the pubsub package tests), so this behavior doesn’t appear to be implemented. Either adjust the docs to describe the actual current behavior/required manual publishing, or add the missing publishing integration in code.
With PubSub configured, `TriggerAction()` automatically publishes to Redis so all instances can update their local connections for the user:
```go
// Instance 1: User connects here
session.TriggerAction("update", nil)
// Instance 2: If user has tabs here, they also receive the update
// (Happens transparently via Redis PubSub)
</details>
---
💡 <a href="/livetemplate/livetemplate/new/main?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
| import ( | ||
| "github.com/livetemplate/livetemplate" | ||
| "github.com/livetemplate/livetemplate/pubsub" | ||
| "github.com/redis/go-redis/v9" | ||
| ) | ||
|
|
||
| client := redis.NewClient(&redis.Options{Addr: "localhost:6379"}) | ||
|
|
||
| broadcaster := pubsub.NewRedisBroadcaster(client, | ||
| pubsub.WithReconnectDelay(5*time.Second), | ||
| ) |
There was a problem hiding this comment.
The Redis setup snippet uses time.Second in pubsub.WithReconnectDelay(5*time.Second) but the example’s import block doesn’t import time, so the snippet won’t compile as written. Add the missing time import (or remove the option from the example).
Copilot noted the example uses time.Second but doesn't import time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LiveTemplate - Development GuidelinesProject OverviewLiveTemplate is a high-performance Go library for building reactive web applications. It provides an API similar to Related repositories:
Version 0.7.0 - Controller+State PatternBreaking Change Notice: v0.7.0 introduces the Controller+State pattern, separating dependencies from session data. Why This Change?The previous
New Pattern// CONTROLLER: Singleton, holds dependencies, NEVER cloned
type TodoController struct {
DB *sql.DB
Logger *slog.Logger
}
// STATE: Pure data, cloned per session, serializable
type TodoState struct {
Items []Todo
Filter string
}
// Action methods receive state and return modified state
func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
todo := c.DB.InsertTodo(ctx.GetString("title"))
state.Items = append(state.Items, todo)
return state, nil
}
// Mount handler with explicit separation
handler := tmpl.Handle(controller, livetemplate.AsState(&TodoState{}))Key Concepts
Lifecycle Methods// Called once when session is created
func (c *Controller) Mount(state State, ctx *Context) (State, error)
// Called on each WebSocket connect (optional)
func (c *Controller) OnConnect(state State, ctx *Context) (State, error)
// Called on disconnect (optional)
func (c *Controller) OnDisconnect()Migration from Old API
Testing HelperUse func TestState(t *testing.T) {
// Fails if State contains dependency types (DB, Logger, etc.)
livetemplate.AssertPureState[TodoState](t)
}Performance Considerations
5-Phase Architecture (Current)The library is organized into 5 operational phases: Parse → Build → Diff → Render → Send Each phase has its own internal package with clear responsibilities:
Additional supporting packages:
Core ArchitectureMain Package Files (Public API)The main
Internal Packages (5-Phase Architecture)Phase 1: Parse (
Phase 2: Build (
Phase 3: Diff (
Phase 4: Render (
Phase 5: Send (
Supporting Packages:
Key Data StructuresTreeNodetype TreeNode struct {
Statics []string // Static HTML parts (key: "s")
Dynamics map[string]interface{} // Dynamic content (keys: "0", "1", etc.)
Fingerprint string // Structure hash (key: "f")
Range *RangeData // Range operation data (key: "d")
Metadata *TreeMetadata // Additional metadata (key: "m")
}
Templatetype Template struct {
name string
templateStr string
tmpl *template.Template
wrapperID string
funcs template.FuncMap
mu sync.RWMutex
lastData interface{}
lastHTML string
lastTree *treeNode
initialTree *treeNode
hasInitialTree bool
keyGen *keyGenerator
config Config
uploadRegistry interface{}
}Key Constructs
Testing StrategyTest Files Structure
Browser-based E2E Tests:
Test Data
Running Tests# Run all tests
go test -v ./...
# Run specific test categories
go test -run TestTemplate -v # Template engine tests
go test -run TestTreeInvariant -v # Tree invariant tests
go test -run TestKeyInjection -v # Key injection tests
go test -run TestE2EUpdateSpec -v # Update spec compliance tests
# Run with timeout
go test -v ./... -timeout=30sDevelopment ConventionsRelease Process
Code Style
Template Processing Flow
Key Generation Strategy
Important Implementation DetailsWire Format OptimizationThe library implements a critical optimization per the tree-update-specification.md:
This is implemented by
Key Architectural Points:
Fingerprint-Based Structure ComparisonThe system uses MD5 structure fingerprints to decide whether statics need to be resent. This replaced an earlier per-path What gets fingerprinted (
Decision flow ( func ClientNeedsStatics(oldTree, newTree *TreeNode) bool {
// First render: no previous tree → must send statics
if oldTree == nil { return true }
// Removal: new tree is gone → no statics needed
if newTree == nil { return false }
// Compare structure fingerprints for non-nil trees
oldFP := oldTree.GetStructureFingerprint()
newFP := newTree.GetStructureFingerprint()
// Same fingerprint → client has statics cached → send dynamics only
// Different fingerprint → structure changed → send full tree with statics
return oldFP != newFP
}The lazy-cached fingerprint enables O(1) structure comparison, avoiding re-computation on subsequent comparisons. Key functions:
Wrapper ID Injection
Tree Update Format{
"s": ["<div>", "</div>"], // Static parts (cached client-side)
"0": "Dynamic content", // Dynamic value at position 0
"1": { // Nested tree for complex structures
"s": ["<span>", "</span>"],
"0": "Nested dynamic"
}
}After first render, client caches the {
"0": "Updated content" // Only changed dynamic, no statics
}Range OperationsFor list updates, special operations are used:
Pre-commit HookThe repository has a pre-commit hook that:
Common TasksAdding New Template Construct
Debugging Tree Generation
Updating Client LibraryThe TypeScript client is maintained in a separate repository at
Performance Considerations
Security Notes
TroubleshootingTest Failures
Tree Generation Issues
Async WebSocket IssuesConnection Closes Unexpectedly:
Goroutine Leaks:
High Memory Usage:
Messages Not Delivered:
Performance Tuning:
Future Improvements
|
PR Review: docs: expand server-actions.md with pubsub package detailsOverall this is a solid documentation improvement. The API surface documented matches the actual implementation. Accuracy checksInterface matches code ✓
Deprecation note ✓ Link fix ✓ Issues1. Broadcast scopes table — The table mixes two conceptually different things. Suggestion: move 2.
3. Reconnect loses dynamic subscriptions — worth a warning In 4. Removed examples link with no replacement Dropping the examples repository link without pointing to any alternative leaves users with no concrete starting point. Even a short inline snippet showing an end-to-end flow would be more useful than the current state. Minor
|
Summary
pubsub.Broadcasterinterface docsNewRedisBroadcasterBroadcaster/BroadcastAwareinterfaces inmount.go../SCALING.md→../guides/SCALING.mdTest plan
pubsub.Broadcasterinterface matchespubsub/types.goNewRedisBroadcasterAPI matchespubsub/redis.go🤖 Generated with Claude Code