diff --git a/content/contributing/livetemplate.md b/content/contributing/livetemplate.md index 2510cf3..89d0e42 100644 --- a/content/contributing/livetemplate.md +++ b/content/contributing/livetemplate.md @@ -2,8 +2,8 @@ title: "Contributing to LiveTemplate Core Library" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "CONTRIBUTING.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Contributing to LiveTemplate Core Library diff --git a/content/guides/ephemeral-components.md b/content/guides/ephemeral-components.md index d582769..67d286a 100644 --- a/content/guides/ephemeral-components.md +++ b/content/guides/ephemeral-components.md @@ -2,8 +2,8 @@ title: "Ephemeral Components Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/ephemeral-components.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Ephemeral Components Guide diff --git a/content/guides/observability.md b/content/guides/observability.md index 5e4bfd6..919ac9d 100644 --- a/content/guides/observability.md +++ b/content/guides/observability.md @@ -2,8 +2,8 @@ title: "LiveTemplate Observability Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/OBSERVABILITY.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # LiveTemplate Observability Guide diff --git a/content/guides/progressive-complexity.md b/content/guides/progressive-complexity.md index a5f3ffe..e9c38c0 100644 --- a/content/guides/progressive-complexity.md +++ b/content/guides/progressive-complexity.md @@ -2,8 +2,8 @@ title: "Progressive Complexity Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/progressive-complexity.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Progressive Complexity Guide diff --git a/content/guides/scaling.md b/content/guides/scaling.md index 2055034..ec5d5f3 100644 --- a/content/guides/scaling.md +++ b/content/guides/scaling.md @@ -2,8 +2,8 @@ title: "LiveTemplate Scaling Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/SCALING.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # LiveTemplate Scaling Guide diff --git a/content/guides/standard-html-reactivity.md b/content/guides/standard-html-reactivity.md index ff35f7d..0d18549 100644 --- a/content/guides/standard-html-reactivity.md +++ b/content/guides/standard-html-reactivity.md @@ -2,8 +2,8 @@ title: "Standard HTML Reactivity" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/guides/standard-html-reactivity.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Standard HTML Reactivity diff --git a/content/reference/api.md b/content/reference/api.md index 31a7bb7..d631cbd 100644 --- a/content/reference/api.md +++ b/content/reference/api.md @@ -2,8 +2,8 @@ title: "Go Library API Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/api-reference.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Go Library API Reference @@ -437,6 +437,7 @@ Options passed to `New()`: | `WithCookieMaxAge` | `(maxAge time.Duration)` | Session cookie max age | | `WithUpgrader` | `(upgrader *websocket.Upgrader)` | Custom WebSocket upgrader | | `WithParseFiles` | `(files ...string)` | Explicit template files | +| `WithParseFS` | `(fsys fs.FS, patterns ...string)` | Templates from an `fs.FS` (e.g. `embed.FS`); precedence over `WithParseFiles` | | `WithTemplateBaseDir` | `(dir string)` | Template discovery base dir | | `WithIgnoreTemplateDirs` | `(dirs ...string)` | Skip directories during discovery | | `WithUpload` | `(name string, config UploadConfig)` | Configure upload field | diff --git a/content/reference/authentication.md b/content/reference/authentication.md index abb3cae..90c2a41 100644 --- a/content/reference/authentication.md +++ b/content/reference/authentication.md @@ -2,8 +2,8 @@ title: "Authentication Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/authentication.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Authentication Reference diff --git a/content/reference/client-attributes.md b/content/reference/client-attributes.md index 88c7a4f..8354c7d 100644 --- a/content/reference/client-attributes.md +++ b/content/reference/client-attributes.md @@ -2,8 +2,8 @@ title: "Client Attributes Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/client-attributes.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Client Attributes Reference diff --git a/content/reference/configuration.md b/content/reference/configuration.md index a3fd3e5..6aae06a 100644 --- a/content/reference/configuration.md +++ b/content/reference/configuration.md @@ -2,8 +2,8 @@ title: "LiveTemplate Configuration Guide" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/CONFIGURATION.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # LiveTemplate Configuration Guide diff --git a/content/reference/controller-pattern.md b/content/reference/controller-pattern.md index 54ef9cd..66dfe9a 100644 --- a/content/reference/controller-pattern.md +++ b/content/reference/controller-pattern.md @@ -2,8 +2,8 @@ title: "Controller+State Pattern Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/controller-pattern.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Controller+State Pattern Reference @@ -213,10 +213,59 @@ func (c *TodoController) OnDisconnect() { - Cancel background jobs - Unsubscribe from data feeds +## State methods in templates + +Exported zero-arg methods on your State type are callable from templates just like +fields — `{{.ActiveCount}}` resolves to `ActiveCount()`'s return value. Methods +returning `(T, error)` are omitted (with a warning) when the error is non-nil. + +Because LiveTemplate converts State to a map before rendering, these methods are +*precomputed* — but only for methods your templates actually reference. A method that +appears nowhere in any template is never called, so an expensive or side-effecting +helper you keep on State but don't render costs nothing. + +Two caveats: + +- **Conditional references still run.** The scoping is by name, not by reachability: + `{{if .Show}}{{.Expensive}}{{end}}` still calls `Expensive()` on every render even + when `.Show` is false, because the name appears in the template text. Keep genuinely + expensive work in an action method, not a render-time State method. +- **Prefer methods without side effects.** Precompute timing is an implementation + detail; a State method should compute a view of the state, not mutate anything or + perform I/O. + ## Context API For the complete Context API (data extraction, HTTP operations, struct binding), see [API Reference — Context](api-reference.md#context). +### Request context + +`*livetemplate.Context` **embeds `context.Context`** — it carries the request/connection +context (cancellation, deadline, request-scoped values). So `ctx` *is* a `context.Context`: +pass it directly to any context-aware call (database queries, outbound HTTP, tracing) instead +of `context.Background()`. + +```go +func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) { + // ✅ ctx is a context.Context — propagates cancellation when the request/connection ends + if err := c.DB.InsertTodo(ctx, todo); err != nil { + return state, err + } + return state, nil +} +``` + +Reaching for `context.Background()` inside an action instead silently discards that +cancellation and any deadline or trace attached to the request: + +```go +// ❌ throws away request-scoped cancellation, deadline, and tracing +if err := c.DB.InsertTodo(context.Background(), todo); err != nil { ... } +``` + +Use `context.Background()` only for work that must deliberately **outlive** the request (e.g. +a fire-and-forget goroutine you start from the action) — not for the action's own calls. + ## Error Handling For validation errors, field errors, and template error display, see [Error Handling Reference](error-handling.md). @@ -266,7 +315,8 @@ type TodoController struct { } func (c *TodoController) Mount(state TodoState, ctx *livetemplate.Context) (TodoState, error) { - items, err := c.DB.GetTodos() + // Pass ctx (a context.Context) so the query is cancelled if the request ends. + items, err := c.DB.GetTodos(ctx) if err != nil { return state, fmt.Errorf("failed to load todos: %w", err) } @@ -285,7 +335,7 @@ func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoSt Title: title, } - if err := c.DB.InsertTodo(todo); err != nil { + if err := c.DB.InsertTodo(ctx, todo); err != nil { return state, fmt.Errorf("database error") } diff --git a/content/reference/error-handling.md b/content/reference/error-handling.md index becc502..909cfeb 100644 --- a/content/reference/error-handling.md +++ b/content/reference/error-handling.md @@ -2,8 +2,8 @@ title: "Error Handling Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/error-handling.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Error Handling Reference diff --git a/content/reference/limitations.md b/content/reference/limitations.md index 2e900ee..d6acaa1 100644 --- a/content/reference/limitations.md +++ b/content/reference/limitations.md @@ -2,8 +2,8 @@ title: "Current Limitations" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/current-limitations.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Current Limitations diff --git a/content/reference/navigate.md b/content/reference/navigate.md index b586386..d6e24a7 100644 --- a/content/reference/navigate.md +++ b/content/reference/navigate.md @@ -2,8 +2,8 @@ title: "Navigate Action Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/navigate.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Navigate Action Reference diff --git a/content/reference/progressive-complexity.md b/content/reference/progressive-complexity.md index f69c763..9c33304 100644 --- a/content/reference/progressive-complexity.md +++ b/content/reference/progressive-complexity.md @@ -2,8 +2,8 @@ title: "Progressive Complexity Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/progressive-complexity-reference.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Progressive Complexity Reference diff --git a/content/reference/pubsub.md b/content/reference/pubsub.md index d811619..0e22db5 100644 --- a/content/reference/pubsub.md +++ b/content/reference/pubsub.md @@ -2,8 +2,8 @@ title: "PubSub Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/pubsub.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # PubSub Reference diff --git a/content/reference/server-actions.md b/content/reference/server-actions.md index f2253b8..ad778ed 100644 --- a/content/reference/server-actions.md +++ b/content/reference/server-actions.md @@ -2,8 +2,8 @@ title: "Server Actions Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/server-actions.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Server Actions Reference @@ -12,14 +12,16 @@ Server actions let you push updates from server-side code to connected clients. ## Overview -LiveTemplate supports two types of updates: +LiveTemplate supports three types of updates: | Type | Trigger | Scope | Use Case | |------|---------|-------|----------| | **Client Action** | User interaction (click, submit) | Same session group | Form submissions, button clicks | | **Server Action** | Server-side code | Same session group | Timers, webhooks, background jobs | +| **Topic Fan-out** | Server-side code | Many sessions across groups | Refreshing every viewer of a shared dashboard | -Server actions use the `Session` interface to trigger updates: +Server actions use the `Session` interface to trigger updates for **one session +group**: ```go // From any goroutine - timer, webhook handler, background job @@ -28,6 +30,11 @@ session.TriggerAction("notification", map[string]interface{}{ }) ``` +To reach **many sessions at once** from a background goroutine — every tab of a +user, or every viewer of a shared dashboard — without stashing a registry of +`Session` handles, use out-of-band topic fan-out instead. See +[Fanning out to many sessions](#fanning-out-to-many-sessions-without-a-handle-registry). + ## Session Interface ```go @@ -325,9 +332,17 @@ func (c *ExportController) ExportFailed(state ExportState, ctx *livetemplate.Con } ``` -### Real-time Notifications +### Real-time Notifications (legacy `sync.Map` pattern) + +Push notifications from any part of your application. -Push notifications from any part of your application: +> **Prefer topic fan-out for this.** The hand-rolled `sync.Map` of `Session` +> handles below predates the topic API and is kept only for the case where you +> genuinely need a specific per-session handle (e.g. to cancel a per-session +> goroutine). To notify a user's connections by ID, subscribe to the user's +> topic in `Mount` and call `handler.Publish(livetemplate.UserTopic(userID), …)` +> — no registry, no `OnConnect`/`OnDisconnect` bookkeeping, no pruning of dead +> handles. See [Fanning out to many sessions](#fanning-out-to-many-sessions-without-a-handle-registry). ```go // Global session registry (thread-safe) @@ -354,6 +369,84 @@ func NotifyUser(userID string, message string) { } ``` +### Fanning out to many sessions (without a handle registry) + +`Session.TriggerAction` targets **one** session group, so refreshing many +connections at once tempts you to keep a registry of `Session` handles and +iterate it (the pattern above) — which then needs `OnConnect`/`OnDisconnect` +bookkeeping and dead-handle pruning. You don't need any of that. Two primitives +compose into a registry-free fan-out: + +1. **Join in `Mount`** with `ctx.Subscribe(topic)`. Because it runs in `Mount`, + the subscription is re-established automatically on reconnect. +2. **Fan out from anywhere** with the `LiveHandler.Publish(topic, action, data)` + returned by `Handle()` — out-of-band (no `Context`), safe from any goroutine. + Every subscriber re-runs `action` against its own state, exactly like + `TriggerAction`, and re-renders. + +The `action` you publish is a normal controller method — typically a `Refresh` +that reloads shared data into state: + +```go +func (c *DashboardController) Refresh(s State, ctx *livetemplate.Context) (State, error) { + s.Stats = c.store.Snapshot() // re-read the shared source + return s, nil +} +``` + +**Per-user (all of one user's tabs) — no configuration:** + +```go +func (c *DashboardController) Mount(s State, ctx *livetemplate.Context) (State, error) { + // ctx.SelfTopic() is the ACL-exempt self-identity topic. For an + // authenticated user it is livetemplate.UserTopic(ctx.UserID()). + if err := ctx.Subscribe(ctx.SelfTopic()); err != nil { + return s, err + } + s.Stats = c.store.Snapshot() + return s, nil +} + +// Background goroutine — refresh every tab of one user: +handler.Publish(livetemplate.UserTopic("alice"), "Refresh", nil) +``` + +**Shared across all viewers (a developer topic) — requires an ACL.** Developer +topics are **deny-all by default**: a connection may only subscribe if you +configure [`WithTopicACL`](pubsub.md#topic-subscribe--publish-api) (or +`WithOpenTopics` in trusted single-tenant tools). This is deliberate — a +developer topic is cross-user, so its ACL is the only boundary. + +```go +tmpl := livetemplate.New("dash", + livetemplate.WithTopicACL(func(topic, userID string, r *http.Request) (bool, error) { + return topic == "dashboard", nil // authorize the shared topic + }), +) + +func (c *DashboardController) Mount(s State, ctx *livetemplate.Context) (State, error) { + if err := ctx.Subscribe("dashboard"); err != nil { + return s, err // surfaces an lvt:error envelope to the client if denied + } + s.Stats = c.store.Snapshot() + return s, nil +} + +// Background goroutine — refresh every viewer, in every group: +handler.Publish("dashboard", "Refresh", nil) +``` + +**Notes:** + +- The out-of-band dispatch response is a pure state update: the client receives + the re-rendered tree with no `meta.action` echo (unlike a client-initiated + action). +- `Publish` is scoped to one `LiveHandler`'s subscribers. If a shared group + spans two separate handlers (e.g. a `/home` and a `/board` page backed by + different controllers), publish on each handler. +- For horizontally scaled (multi-instance) deployments, configure + [`WithPubSubBroadcaster`](pubsub.md#setup) so topic fan-out crosses instances. + ## Thread Safety Session methods are thread-safe and can be called from any goroutine: @@ -410,7 +503,11 @@ func (c *Controller) triggerFromBackground() { - Simpler mental model - "push to myself" - No accidental cross-user data leaks - No authorization checks needed in controller code -- For admin broadcasts, use database + polling or dedicated admin endpoints + +For cross-user broadcasts (admin announcements, a shared dashboard), use a +developer topic with [`WithTopicACL`](#fanning-out-to-many-sessions-without-a-handle-registry) +— the ACL is the explicit authorization boundary `TriggerAction` deliberately +lacks. ## Multi-Tab/Multi-Device Behavior @@ -623,4 +720,5 @@ In multi-instance deployments, `TriggerAction()` automatically publishes to Redi - [Controller+State Pattern](controller-pattern.md) - Core architecture pattern - [Session Reference](session.md) - Session stores and connection management - [Authentication Reference](authentication.md) - User identification and custom authenticators +- [PubSub Reference](pubsub.md#topic-subscribe--publish-api) - Topic grammar, ACL, and out-of-band `handler.Publish` - [Scaling Guide](../guides/SCALING.md) - Horizontal scaling with Redis diff --git a/content/reference/session.md b/content/reference/session.md index dbfe5c8..49bf81d 100644 --- a/content/reference/session.md +++ b/content/reference/session.md @@ -2,8 +2,8 @@ title: "Session Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/session.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Session Reference diff --git a/content/reference/template-support-matrix.md b/content/reference/template-support-matrix.md index 722a1cc..b245c7e 100644 --- a/content/reference/template-support-matrix.md +++ b/content/reference/template-support-matrix.md @@ -2,8 +2,8 @@ title: "LiveTemplate Go Template Support Matrix" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/template-support-matrix.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # LiveTemplate Go Template Support Matrix diff --git a/content/reference/uploads.md b/content/reference/uploads.md index 21f2e10..0ff9323 100644 --- a/content/reference/uploads.md +++ b/content/reference/uploads.md @@ -2,8 +2,8 @@ title: "Upload Reference" source_repo: "https://github.com/livetemplate/livetemplate" source_path: "docs/references/uploads.md" -source_ref: "v0.16.0" -source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a" +source_ref: "v0.18.0" +source_commit: "c96981964c226221cf2a34848e4350a494c08e8c" --- # Upload Reference