Skip to content

docs: expand server-actions.md with pubsub package details#160

Merged
adnaan merged 2 commits intomainfrom
docs/update-server-actions-md
Mar 9, 2026
Merged

docs: expand server-actions.md with pubsub package details#160
adnaan merged 2 commits intomainfrom
docs/update-server-actions-md

Conversation

@adnaan
Copy link
Contributor

@adnaan adnaan commented Mar 8, 2026

Summary

  • Expand Distributed Deployments section with full pubsub.Broadcaster interface docs
  • Add Redis setup example with NewRedisBroadcaster
  • Document broadcast scopes (Global, Group, User, ServerAction)
  • Add deprecation note for old Broadcaster/BroadcastAware interfaces in mount.go
  • Fix broken link: ../SCALING.md../guides/SCALING.md
  • Remove stale examples repository link

Test plan

  • Verified pubsub.Broadcaster interface matches pubsub/types.go
  • Verified NewRedisBroadcaster API matches pubsub/redis.go
  • All tests pass

🤖 Generated with Claude Code

- 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>
Copilot AI review requested due to automatic review settings March 8, 2026 06:25
@claude
Copy link

claude bot commented Mar 8, 2026

PR Review

This is a clean documentation-only PR. I verified accuracy against the actual source files.

Verified Accurate

  • pubsub.Broadcaster interface exactly matches pubsub/types.go
  • NewRedisBroadcaster and WithReconnectDelay match pubsub/redis.go
  • WithPubSubBroadcaster exists at template.go:571
  • docs/guides/SCALING.md exists at the corrected path
  • Deprecation notice for Broadcaster/BroadcastAware in mount.go is accurate
  • Broadcast scopes table is accurate

Issues

Missing time import in code example

The Redis setup snippet uses 5*time.Second but does not include "time" in the import block. Developers who copy-paste the example will get a compile error. Add "time" to the imports shown.

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

  • The ### Deprecated Interfaces subsection sits under ## Distributed Deployments, but those interfaces predate the pubsub package and are not specific to distributed deployments. A better home might be a top-level note or in the migration docs.
  • The inline interface snippet duplicates the source of truth. A note pointing to pubsub/types.go would keep the docs lighter and always up-to-date.

Summary

The factual content is correct and the link fix (../SCALING.md to ../guides/SCALING.md) is valuable. The main issue to address before merging is the missing "time" import in the code example.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.Broadcaster interface 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 WithPubSubBroadcaster is configured, Session.TriggerAction() “automatically publishes to Redis” and works transparently across instances. In the current codebase there are subscriptions wired up (Template.Handle calls PubSubBroadcaster.Subscribe*), but there are no corresponding publish calls (no uses of PubSubBroadcaster.Publish* / PublishServerAction outside 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>.

Comment on lines +448 to +458
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),
)
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

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 uses AI. Check for mistakes.
Copilot noted the example uses time.Second but doesn't import time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Mar 8, 2026

LiveTemplate - Development Guidelines

Project Overview

LiveTemplate is a high-performance Go library for building reactive web applications. It provides an API similar to html/template but with the additional capability of generating minimal, tree-based updates that can be efficiently transmitted to clients.

Related repositories:

  • CLI Tool (lvt): Code generator and development server — maintained at github.com/livetemplate/lvt
  • TypeScript Client: Browser-side update handler — maintained at github.com/livetemplate/client

Version 0.7.0 - Controller+State Pattern

Breaking Change Notice: v0.7.0 introduces the Controller+State pattern, separating dependencies from session data.

Why This Change?

The previous cloneStore() approach copied ALL exported fields, causing:

  • Security issues: Session-specific data (OAuth tokens, caches) accidentally shared across users
  • Architectural ambiguity: No clear contract for what gets cloned vs shared
  • Developer footguns: Easy to accidentally put dependencies in cloned state

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

Concept Description
Controller Singleton holding dependencies (DB, Logger, API clients). Never cloned.
State Pure data struct cloned per session. Must be serializable (no pointers to dependencies).
AsStateT Generic wrapper that marks a struct as session state. Handles serialization automatically.
Context Unified context for all lifecycle and action methods. Replaces ActionContext.

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

Old Pattern New Pattern
type Store struct { DB *sql.DB; Items []Todo } Separate into Controller (DB) and State (Items)
func (s *Store) Action(ctx *ActionContext) error func (c *Controller) Action(state State, ctx *Context) (State, error)
tmpl.Handle(&Store{}) tmpl.Handle(&Controller{}, AsState(&State{}))
ctx.Action ctx.Action()
ctx.Data ctx.GetString(), ctx.GetInt(), ctx.BindAndValidate()

Testing Helper

Use AssertPureState[T]() in tests to catch common mistakes:

func TestState(t *testing.T) {
    // Fails if State contains dependency types (DB, Logger, etc.)
    livetemplate.AssertPureState[TodoState](t)
}

Performance Considerations

  • State serialization: Every session clone involves JSON marshal/unmarshal. Keep state small.
  • Method caching: Reflection-based dispatch caches method lookups per type.
  • First invocation: Slightly slower due to cache population; subsequent calls are fast.

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:

  • internal/parse/ - Template parsing into tree structures
  • internal/build/ - Tree construction, fingerprinting, operations
  • internal/diff/ - Tree comparison and update generation
  • internal/render/ - HTML rendering utilities
  • internal/send/ - Message formatting and serialization

Additional supporting packages:

  • internal/keys/ - Key generation for range items (Generator type)
  • internal/session/ - Connection registry and async WebSocket handling
  • internal/observe/ - Logging and metrics

Core Architecture

Main Package Files (Public API)

The main livetemplate package provides a clean, minimal public API:

  1. Template Engine (template.go):

    • Main entry point providing html/template compatible API
    • Manages template parsing, execution, and update generation
    • Handles wrapper ID injection for update targeting
    • Orchestrates internal packages for parsing, building, and diffing
  2. Mount Handler (mount.go):

    • LiveHandler interface for HTTP/WebSocket handling
    • Broadcaster and BroadcastAware interfaces for server-initiated updates
    • WebSocket connection lifecycle management
    • Auto-broadcasting to session groups
  3. Session Stores (session_stores.go):

    • SessionStore interface for session group management
    • MemorySessionStore for single-instance deployments
    • RedisSessionStore for distributed deployments
    • Automatic cleanup and TTL management
  4. Health Checks (health.go):

    • HealthHandler for liveness and readiness probes
    • HealthChecker interface for custom health checks
    • Built-in session store and Redis health checkers
    • Kubernetes-ready health endpoints
  5. Types (types.go):

    • TreeNode, RangeData, TreeMetadata type re-exports
    • Clean public API for tree-based operations
    • Backward-compatible type aliases
  6. Context (context.go):

    • Unified Context for all lifecycle and action methods
    • ActionData for form/JSON data handling
    • FieldError and MultiError for validation
    • Methods: Action(), UserID(), GetString(), GetInt(), BindAndValidate()
  7. Authentication (auth.go):

    • Authenticator interface for user identification
    • DefaultAuthenticator with cookie-based sessions
  8. Configuration (config.go):

    • TemplateConfig for template customization
    • DevMode, CompressHTML, and other options

Internal Packages (5-Phase Architecture)

Phase 1: Parse (internal/parse/)

  • Parses Go templates into tree structures
  • Handles template constructs (fields, conditionals, ranges, with, template invokes)
  • Components: parse.go, conditional.go, field.go, range.go, with.go, flatten.go, types.go, var_context.go
  • Each construct type has a dedicated file following single-responsibility design

Phase 2: Build (internal/build/)

  • Tree construction and operations
  • Components: fingerprint.go, html_diff.go, html_segmentation.go, types.go, wrapper.go
  • Handles tree creation, HTML diffing, segmentation, and change detection
  • Wrapper div injection and HTML content extraction

Phase 3: Diff (internal/diff/)

  • Tree comparison and update generation
  • Components: tree_compare.go, range_ops.go, prepare.go, helpers.go, types.go
  • Architecture: Hierarchical delegation pattern
    • Orchestrator: CompareTreesAndGetChangesWithPath() - entry point
    • Delegators: handle*() functions for specialized cases
    • Coordinator: GenerateRangeDifferentialOperations() for range ops
    • Helpers: ~70 utility functions in helpers.go

Phase 4: Render (internal/render/)

  • HTML rendering utilities
  • Components: html.go, minify.go
  • Functions: Node(), TreeToHTML(), IsVoidElement()
  • Converts tree structures to HTML strings; includes HTML minification

Phase 5: Send (internal/send/)

  • Message formatting and serialization
  • Components: json.go, message.go, response.go
  • Action message parsing (HTTP/WebSocket)
  • Update response wrapping, JSON serialization, and custom JSON encoding
  • Functions: ParseActionFromHTTP(), PrepareUpdate(), SerializeUpdate()

Supporting Packages:

  1. Key Generation (internal/keys/):

    • Sequential key generation for range items
    • Components: generator.go, loader.go
    • Type: Generator (renamed from KeyGenerator)
    • Thread-safe counter with overflow protection; loader for key persistence
  2. Observability (internal/observe/):

    • Operational metrics with Prometheus export
    • Components: metrics.go, prometheus.go
    • Exposed via public handler.MetricsHandler() API
  3. Session Management (internal/session/): ⚡ NEW: Async WebSocket Architecture

    • Async Sending Infrastructure: Channel-based message queuing with background writePump goroutines
    • Connection Types: Connection, ConnectionRegistry, ConnectionLimits
    • WebSocket Concurrency: Each connection has dedicated writePump goroutine for async sends
    • Dual Indexing: By groupID (session groups) and userID (multi-device)
    • Graceful Shutdown: 5-second drain timeout with sync.Once protection
    • Performance: 165M concurrent sends/sec, 54.7M queued sends/sec
    • Memory: ~980 bytes per connection (measured)
    • Backpressure: Closes slow clients when buffer full (fail-fast)
    • Thread-safety: Lock-free sends, mutex-protected registry operations

    Async Send Flow:

    1. Send(messageType, data) → Queue message to buffered channel
    2. writePump goroutine → Dequeue and write to WebSocket
    3. Close() → Signal done → Wait for pump exit → Close WebSocket
    

    Configuration:

    • Buffer size: LVT_WS_BUFFER_SIZE env var (default: 50)
    • Config option: WithWebSocketBufferSize(int)
    • Metrics: wsBufferFull, wsSlowClientCloses, wsWriteErrors, wsSendBufferSize
  4. Execution Context (internal/context/):

    • TemplateContext for error handling and dev mode
    • Template execution utilities
    • Error propagation to client
  5. Compatibility (internal/compat/):

    • Compatibility layer for tree structures
    • Components: tree.go
  6. Template Discovery (internal/discovery/):

    • Automatic template file discovery
    • Components: discovery.go
  7. File Uploads (internal/upload/):

    • File upload handling infrastructure
    • Components: accessor.go, multipart.go, protocol.go, registry.go, tempfile.go, validate.go
  8. Upload Types (internal/uploadtypes/):

    • Upload type definitions shared across packages
    • Components: types.go
  9. Test Utilities (internal/testutil/):

    • Shared test helpers (e.g., Redis test setup)
    • Components: redis.go
  10. String Utilities (internal/util/):

    • General-purpose string utilities
    • Components: strings.go
  11. Fuzz Testing (internal/fuzz/):

    • Fuzz testing infrastructure for tree invariant validation
    • Subpackages: app/ (mutations, templates, types), generators/ (state, template), invariants/ (verifier, helpers, TS oracle), mutations/ (apply, types)

Key Data Structures

TreeNode

type 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")
}
  • Core structure for representing static/dynamic content
  • Custom JSON marshaling maintains wire format compatibility (numeric keys for dynamics)
  • Can be nested for complex templates

Template

type 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

  • FieldConstruct: Simple field replacement {{.Field}}
  • ConditionalConstruct: If/else branches {{if .Cond}}...{{else}}...{{end}}
  • RangeConstruct: Iteration {{range .Items}}...{{end}}
  • WithConstruct: Context switching {{with .Item}}...{{end}}
  • TemplateInvokeConstruct: Template invocation {{template "name" .}}

Testing Strategy

Test Files Structure

  • template_test.go: Core template functionality tests (includes key injection tests)
  • tree_test.go: Tree structure invariant validation
  • e2e_update_spec_test.go: Tree update specification compliance tests
  • Internal package tests: internal/*/

Browser-based E2E Tests:
Browser-based chromedp E2E tests are maintained in the lvt repository:

  • Location: github.com/livetemplate/lvt/e2e/livetemplate_core_test.go
  • These tests validate the library from a black-box perspective using real browser automation
  • Tests include: complete rendering sequences, loading indicators, focus preservation, etc.

Test Data

  • testdata/fixtures/: Template fixtures for unit tests
  • testdata/golden/: Golden files for snapshot testing
  • testdata/fuzz/: Fuzz test corpus

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=30s

Development Conventions

Release Process

  • Never create git tags manually - Always use release.sh script for releases

Code Style

  1. No unnecessary comments - Code should be self-documenting
  2. Follow existing patterns - Check neighboring code for conventions
  3. Use existing utilities - Don't reinvent the wheel
  4. Maintain idiomatic Go - Follow Go best practices

Template Processing Flow

  1. Parse: Template string → Compiled template structure
  2. Execute: First render generates initial tree with statics and dynamics
  3. Update: Subsequent renders generate minimal update trees
  4. Diff: Compare trees to produce update operations

Key Generation Strategy

  • Uses wrapper-based approach with sequential key generation
  • Keys are stable within a single render
  • Supports any data type without special handling
  • Keys reset between renders for consistency

Important Implementation Details

Wire Format Optimization

The library implements a critical optimization per the tree-update-specification.md:

  • First Render: Tree includes complete static structure ("s" arrays) + all dynamics
  • Updates: Tree includes ONLY changed dynamics, NO statics (client has them cached)

This is implemented by PrepareTreeForClient(node, clientHasStatics) which:

  1. Takes a fully-built tree (WITH statics, needed for comparison consistency)
  2. Returns a wire-format tree (WITHOUT statics if client has cached them)
  3. Implements spec requirement: "Updates MUST include ONLY changed dynamics"

Key Architectural Points:

  • Trees are ALWAYS generated WITH statics (ensures consistent comparison)
  • Comparison logic (compareTreesAndGetChanges) determines what changed
  • PrepareTreeForClient removes statics before wire transmission
  • This is NOT a "reactive fix" - it's the correct implementation of specification
  • Result: Updates are ~10% the size of full renders (statics are largest part)

Fingerprint-Based Structure Comparison

The system uses MD5 structure fingerprints to decide whether statics need to be resent. This replaced an earlier per-path ClientStructureRegistry approach (removed in PR #86) that was more complex and harder to debug.

What gets fingerprinted (internal/build/fingerprint.go):

  • Statics arrays (the HTML template parts between dynamic slots)
  • Dynamic key positions (e.g., "there's a dynamic at position 0, 1, 2")
  • Nested TreeNode structure (recursively)
  • Range statics (item template structure)
  • NOT dynamic values — two trees with identical structure but different content produce the same fingerprint

Decision flow (internal/diff/tree_compare.go):

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:

  • CalculateStructureFingerprint(tree) — Computes MD5 truncated to 64 bits (16 hex chars) of static structure (internal/build/fingerprint.go)
  • TreeNode.GetStructureFingerprint() — Lazy-computes and caches fingerprint on first access (internal/build/types.go)
  • ClientNeedsStatics(oldTree, newTree) — Returns true if fingerprints differ (internal/diff/tree_compare.go)
  • PrepareTreeForClient(tree, clientHasStatics) — Strips statics from wire format when cached (internal/diff/prepare.go)

Wrapper ID Injection

  • All templates get a wrapper div with unique ID (lvt-[random])
  • Full HTML documents: Wrapper injected around body content
  • Fragments: Entire content wrapped
  • Used for targeting updates on client side

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 "s" arrays. Subsequent updates omit them:

{
  "0": "Updated content"         // Only changed dynamic, no statics
}

Range Operations

For list updates, special operations are used:

  • ["u", "item-id", updates]: Update existing item
  • ["i", "after-id", "position", data]: Insert new item
  • ["r", "item-id"]: Remove item
  • ["o", ["id1", "id2", ...]]: Reorder items

Pre-commit Hook

The repository has a pre-commit hook that:

  1. Auto-formats Go code using go fmt
  2. Runs all tests with 30-second timeout
  3. Blocks commits if tests fail
  4. Automatically stages formatted files

Common Tasks

Adding New Template Construct

  1. Add handling logic in the appropriate internal/parse/ file (e.g., field.go, conditional.go, range.go, with.go)
  2. For new construct types, create a new file in internal/parse/ following existing patterns
  3. Add parser logic in internal/parse/parse.go
  4. Update type definitions in internal/parse/types.go if needed
  5. Write tests in appropriate test files
  6. Ensure backward compatibility if modifying existing constructs

Debugging Tree Generation

  1. Use TreeNode.GetStructureFingerprint() to track structural changes
  2. Check lastTree vs current tree in Template
  3. Validate tree structure with validateTreeStructure()
  4. Use golden files for regression testing

Updating Client Library

The TypeScript client is maintained in a separate repository at github.com/livetemplate/client (locally ../client).

  1. Make changes in the client repository
  2. Ensure compatibility with tree format
  3. Test with browser test suite
  4. Update TypeScript types if needed

Performance Considerations

  1. Tree Diffing: O(n) complexity for most operations
  2. Memory: Trees are kept in memory for diffing
  3. Fingerprinting: MD5 hashing for change detection
  4. Key Generation: Sequential integers for minimal overhead

Security Notes

  1. HTML Escaping: Uses html/template for automatic escaping
  2. No Direct HTML: All content goes through template engine
  3. Wrapper IDs: Random generation prevents conflicts

Troubleshooting

Test Failures

  • Check golden files in testdata/golden/ and testdata/fixtures/
  • Verify tree structure matches expected format
  • Ensure key generation is consistent
  • Check for HTML escaping issues
  • For browser E2E test failures, see lvt repository

Tree Generation Issues

  • Validate template syntax
  • Check construct parsing order
  • Verify hydration logic matches compilation
  • Test with simpler templates first

Async WebSocket Issues

Connection Closes Unexpectedly:

  • Check metrics for wsSlowClientCloses counter
  • Client may be too slow (buffer full)
  • Increase buffer size with LVT_WS_BUFFER_SIZE or WithWebSocketBufferSize()
  • Default buffer: 50 messages

Goroutine Leaks:

  • Ensure all connections are unregistered via registry.Unregister(conn)
  • Unregister() calls Close() which signals writePump to exit
  • Check pumpExited channel closes (5-second timeout)
  • Run tests with go test -race to detect race conditions

High Memory Usage:

  • Each connection: ~980 bytes base + (buffer size × avg message size)
  • Default 50-buffer: ~1KB per connection + message overhead
  • Reduce buffer size for memory-constrained environments
  • Monitor wsSendBufferSize gauge metric

Messages Not Delivered:

  • Check wsWriteErrors metric for write failures
  • Verify client WebSocket is connected and reading
  • Check for ErrConnectionClosed or ErrClientTooSlow errors
  • Review server logs for "WebSocket write failed" warnings

Performance Tuning:

  • Benchmark results: 165M concurrent sends/sec, 54.7M queued sends/sec
  • For high-throughput: Increase buffer size (100-1000)
  • For low-latency: Decrease buffer size (10-20)
  • Monitor wsBufferFull metric to detect backpressure
  • Use Prometheus metrics at /metrics endpoint for observability

Future Improvements

  • Consider adding more sophisticated diffing algorithms
  • Optimize memory usage for large trees
  • Add metrics and profiling hooks
  • Enhance client-side caching strategies

@claude
Copy link

claude bot commented Mar 8, 2026

PR Review: docs: expand server-actions.md with pubsub package details

Overall this is a solid documentation improvement. The API surface documented matches the actual implementation.


Accuracy checks

Interface matches code
The Broadcaster interface block matches pubsub/types.go exactly.

NewRedisBroadcaster signature
The constructor call and WithReconnectDelay option match pubsub/redis.go.

Deprecation note
mount.go lines 101 and 107 already carry // Deprecated: godoc comments for Broadcaster and BroadcastAware, so the note is accurate.

Link fix
server-actions.md lives at docs/references/, so ../guides/SCALING.md resolves correctly to docs/guides/SCALING.md which exists.


Issues

1. Broadcast scopes table — PublishServerAction does not belong here

The table mixes two conceptually different things. PublishGlobal, PublishToGroup, and PublishToUser all deal with raw payload []byte fan-out to a MessageHandler. PublishServerAction is a different kind of message — it dispatches to a controller action handler via ServerActionHandler, not a message handler. Grouping them in the same Broadcast Scopes table implies they work the same way, which they do not.

Suggestion: move PublishServerAction to a separate subsection (e.g. "Server-initiated actions across instances") with a brief note that it routes to action handlers, not raw payload handlers.

2. Subscribe() is never shown in the example — needs clarification

redis.go requires Subscribe() and SubscribeServerActions() to be called before messages are received. The setup example only shows NewRedisBroadcaster + WithPubSubBroadcaster. If the library calls Subscribe() automatically (which it appears to), a brief note to that effect would prevent users from wondering whether they need to call it manually.

3. Reconnect loses dynamic subscriptions — worth a warning

In redis.go, reconnect() only re-subscribes to channelGlobal. Dynamic channels added via SubscribeToGroup(), SubscribeToUser(), or SubscribeToServerAction() are all lost after a Redis connection drop, meaning active user sessions will silently stop receiving targeted messages. This is worth a documentation warning, and is likely also a bug in the implementation (reconnect should restore all previously subscribed channels).

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

  • import "time" in the Redis setup block is correct and necessary — keep it.
  • defer broadcaster.Close() implies the snippet lives inside a function. A short inline comment like // in main() or server initialization would prevent this from being accidentally copied into a per-request handler.

@adnaan adnaan merged commit e21d650 into main Mar 9, 2026
1 check passed
@adnaan adnaan deleted the docs/update-server-actions-md branch March 9, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants