diff --git a/content/contributing/livetemplate.md b/content/contributing/livetemplate.md index 2510cf3..fd70ffa 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Contributing to LiveTemplate Core Library diff --git a/content/guides/ephemeral-components.md b/content/guides/ephemeral-components.md index d582769..8f8824f 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Ephemeral Components Guide diff --git a/content/guides/observability.md b/content/guides/observability.md index 5e4bfd6..c4fde1d 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # LiveTemplate Observability Guide diff --git a/content/guides/progressive-complexity.md b/content/guides/progressive-complexity.md index a5f3ffe..5916d14 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Progressive Complexity Guide diff --git a/content/guides/scaling.md b/content/guides/scaling.md index 2055034..ce9deb4 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # LiveTemplate Scaling Guide diff --git a/content/guides/standard-html-reactivity.md b/content/guides/standard-html-reactivity.md index ff35f7d..67525b8 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Standard HTML Reactivity diff --git a/content/reference/api.md b/content/reference/api.md index 31a7bb7..b20d368 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Go Library API Reference @@ -111,7 +111,9 @@ handler := tmpl.Handle(&TodoController{DB: db}, livetemplate.AsState(&TodoState{ | Option | Description | |--------|-------------| -| `WithStore(store SessionStore)` | Override the session store for this handler | +| `WithEphemeralSweepTTL(ttl time.Duration)` | How long an idle ephemeral HTTP cache entry survives before eviction (default 30m; no effect in persistent mode) | + +> The session store is set at construction via `WithSessionStore` (a `New` option), not per-handler. --- @@ -423,11 +425,11 @@ Options passed to `New()`: | Option | Signature | Description | |--------|-----------|-------------| -| `WithDevMode` | `(enabled bool)` | Enable development mode | +| `WithDevMode` | `(enabled bool)` | Enable dev mode: allows all WS origins, verbose logging, `{{.lvt.DevMode}}` | | `WithSessionStore` | `(store SessionStore)` | Set session store | | `WithAuthenticator` | `(auth Authenticator)` | Set authenticator | | `WithAllowedOrigins` | `(origins []string)` | Allowed WebSocket origins | -| `WithPermissiveOriginCheck` | `()` | Bypass origin check (dev only) | +| `WithPermissiveOriginCheck` | `()` | Bypass origin check without dev mode (`WithDevMode` already relaxes origins) | | `WithMaxConnections` | `(max int64)` | Max WebSocket connections | | `WithMaxConnectionsPerGroup` | `(max int64)` | Max connections per group | | `WithWebSocketDisabled` | `()` | HTTP-only mode | @@ -437,6 +439,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..24b9da9 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Authentication Reference diff --git a/content/reference/client-attributes.md b/content/reference/client-attributes.md index 88c7a4f..afcab42 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Client Attributes Reference diff --git a/content/reference/configuration.md b/content/reference/configuration.md index a3fd3e5..f6c9657 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # LiveTemplate Configuration Guide @@ -140,9 +140,9 @@ Enable development mode. - **Accepted values**: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off` (case-insensitive) **Features when enabled**: -- Uses local client library instead of CDN +- Allows **all** WebSocket origins — disables the same-origin/CSRF check, so `WithPermissiveOriginCheck()` is redundant in dev (**never enable in production**) - More verbose logging -- Less strict origin checking +- Exposes `{{.lvt.DevMode}}` to templates **Use case**: Local development and debugging. @@ -181,6 +181,18 @@ Base directory for template auto-discovery. **Use case**: Override automatic template directory detection. Useful in containerized deployments where `runtime.Caller` may resolve to an unexpected path. +#### `LVT_MAX_TEMPLATE_DEPTH` + +Maximum nesting depth for recursive `{{template}}` invocations while building the reactive tree. + +- **Type**: Integer +- **Default**: `128` +- **Example**: `LVT_MAX_TEMPLATE_DEPTH=256` +- **Validation**: Must be a positive integer; an invalid value is a hard startup error +- **Option**: `WithMaxTemplateDepth(n)` + +**Use case**: Recursive templates (file trees, comment threads, nested navigation) render self-referential data. This cap stops a *cycle in the data* — a node whose children contain itself — from overflowing the stack, surfacing an error instead. Raise it only when your data is legitimately deeper than the default; the cap is a safety net, not a tuning knob. See [Template Support Matrix — Recursion depth](template-support-matrix.md#recursion-depth) for the first-render versus update behavior. + ### Graceful Shutdown #### `LVT_SHUTDOWN_TIMEOUT` @@ -345,7 +357,7 @@ tmpl := livetemplate.New("app", | `WithPubSubBroadcaster(broadcaster)` | Redis pub/sub for distributed deployments | | `WithComponentTemplates(sets...)` | Register component template sets | | `WithIgnoreTemplateDirs(dirs...)` | Skip directories during template discovery | -| `WithPermissiveOriginCheck()` | Bypass origin check (dev only) | +| `WithPermissiveOriginCheck()` | Bypass origin check without dev mode (`WithDevMode` already relaxes origins) | | `WithProgressiveEnhancement(enabled)` | Non-JS form submission support (default: true) | | `WithCookieMaxAge(duration)` | Session cookie max age (default: 365 days) | diff --git a/content/reference/controller-pattern.md b/content/reference/controller-pattern.md index 54ef9cd..ab50a73 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.19.1" +source_commit: "fe690899b1400a0c3886206038c0b958b40554be" --- # Controller+State Pattern Reference @@ -213,10 +213,100 @@ 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. + +### Methods that take arguments (view helpers) + +Sometimes you want a method that takes a key and returns a per-item view — e.g. a +CSS class for a given row, or a formatted label for a given id. **Put it on a struct +field, not on the top-level State**, and call it through that field: + +```go +type RowViews struct{ selected map[string]bool } + +func (v RowViews) Class(id string) string { + if v.selected[id] { + return "row selected" + } + return "row" +} + +type BoardState struct { + Rows []Row + Views RowViews // a "view helper" sub-struct +} +``` + +```html +{{range .Rows}} +
` resolving to `/diagram.png` — those paths can collide with app routes, and `ServeMux` can't disambiguate them by prefix. That case needs a fall-through wrapper: *serve the file if it exists under a safe root, otherwise defer to the app handler.* Keep it a small wrapper in your own `main` rather than reaching for a framework primitive — the security-relevant policy (which extensions to serve, embedded vs. disk, symlink resolution) is app-specific, and `http.Dir` / `http.FileServer` already supply the traversal-safe file access it builds on.
+
## Upload Access
For file upload configuration and handling, see [Upload Reference](uploads.md).
diff --git a/content/reference/error-handling.md b/content/reference/error-handling.md
index becc502..54b603b 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# Error Handling Reference
diff --git a/content/reference/limitations.md b/content/reference/limitations.md
index 2e900ee..83434d5 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# Current Limitations
diff --git a/content/reference/navigate.md b/content/reference/navigate.md
index b586386..5803838 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# Navigate Action Reference
diff --git a/content/reference/progressive-complexity.md b/content/reference/progressive-complexity.md
index f69c763..a8b7b69 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# Progressive Complexity Reference
diff --git a/content/reference/pubsub.md b/content/reference/pubsub.md
index d811619..32afa77 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# PubSub Reference
diff --git a/content/reference/server-actions.md b/content/reference/server-actions.md
index f2253b8..c9cac29 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# 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..a98d323 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# Session Reference
@@ -74,6 +74,15 @@ Fields tagged with `lvt:"persist"` follow this persistence schedule. Untagged fi
This matches the pre-`lvt:"persist"` behavior where a path change always meant fresh state. Treat `lvt:"persist"` as "survives refresh," not "survives navigation."
+### What the session store is not for
+
+`lvt:"persist"` and the `SessionStore` provide **session continuity** — carrying UI state across a refresh or reconnect within a session group. They are not a durable application datastore:
+
+- The default `MemorySessionStore` is in-process. A **server relaunch loses every session**; the browser's cookie survives but now misses the fresh empty store, so persist fields fall back to their zero values and `Mount()` repopulates them. `RedisSessionStore` survives a relaunch, but its purpose is shared continuity across instances, not per-user durable storage.
+- Persistence is keyed by the cookie-derived `groupID`. State stored there is scoped to *that browser session*, not to a user — the same person on a second device (a different cookie) gets a different group.
+
+So data that must **outlive a process relaunch** or mean the same thing **across a user's sessions and devices** — durable user preferences, saved documents, anything you'd call "app data" — belongs in your own store (a file, your database), loaded in `Mount()`. Keep only genuine session-continuity state (filters, pagination, an in-progress draft) on `lvt:"persist"`. Mixing the two is the trap: a preference parked in the session silently resets on the next relaunch. The framework deliberately owns session continuity and leaves durable app data to the app.
+
### Explicit Peer Refresh
```go
diff --git a/content/reference/template-support-matrix.md b/content/reference/template-support-matrix.md
index 722a1cc..b9dd44e 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# LiveTemplate Go Template Support Matrix
@@ -181,10 +181,29 @@ Stripping uses the HTML tokenizer (not a regex), so it is context-aware:
|---------|--------|-------|
| `{{define}}` / `{{template}}` | ✅ | Automatically flattened via `FlattenTemplate()` in `internal/parse/flatten.go` |
| `{{block}}` | ✅ | Resolved during flattening; equivalent to `{{define}}` + `{{template}}` |
-| Circular template references | ❌ | Not supported, would cause infinite loop |
+| Recursive / circular template references | ✅ | Supported since v0.19.0 — direct self-recursion (`node -> node`), mutual recursion (`a -> b -> a`), longer cycles, and a self-referential entry point all render. `{{template}}` calls are normally inlined during flattening, which a cycle cannot survive, so `FlattenTemplate` detects templates reachable from themselves and leaves those invocations un-inlined, evaluating them at build time as nested `TreeNode`s. The recursive region stays inside the reactive tree, so updates diff per-leaf rather than re-sending the branch. Depth is capped — see [Recursion depth](#recursion-depth). |
| Undefined template invocation | ❌ | Returns error from Go template engine |
-Template composition is fully supported through automatic AST flattening. `FlattenTemplate()` walks the parsed template tree, identifies the entry point, and inlines all `{{template}}` invocations into a single flat template before tree generation.
+Template composition is fully supported through automatic AST flattening. `FlattenTemplate()` walks the parsed template tree, identifies the entry point, and inlines all `{{template}}` invocations into a single flat template before tree generation. Templates that are reachable from themselves are the exception: they are left un-inlined and invoked at build time instead.
+
+### Recursion depth
+
+Recursive invocation is capped so that self-referential *data* (a node whose child list contains itself) surfaces an error instead of overflowing the stack. The cap defaults to **128** and is configurable:
+
+```go
+lt.New("tree", lt.WithMaxTemplateDepth(256))
+```
+
+```bash
+export LVT_MAX_TEMPLATE_DEPTH=256 # must be a positive integer
+```
+
+Exceeding the cap on a **reactive update** returns a build-phase `ParseError` naming the template and the limit. Raise the cap only if your data is legitimately deeper — the default exists to catch cycles in data, not to constrain real nesting.
+
+Two behaviours differ from that and are worth knowing:
+
+- **First render with finite data deeper than the cap does not error.** The build degrades and still produces a usable, keyed page, so a too-low cap can go unnoticed until the first update fails.
+- **If the initial tree build errors outright** — genuinely self-referential data, where a node's children contain the node itself — the initial render falls back to an HTML-structure-based tree and logs `Template parsing failed, falling back to HTML structure-based tree`. That region then stays on HTML-string diffing for the life of the template rather than the reactive path. A recursive region that renders but never updates reactively is the symptom; that warning in the server log is the confirmation.
## Custom Functions
@@ -192,7 +211,8 @@ Template composition is fully supported through automatic AST flattening. `Flatt
|---------|--------|-------|
| Built-in Go functions | ✅ | All standard functions supported |
| User-defined functions | ⚠️ | Must be registered with Go template engine |
-| Method calls on data | ✅ | Works if methods are public |
+| Zero-arg method calls on data | ✅ | Public methods; precomputed from State (`{{.Count}}`) |
+| Arg-accepting method calls | ⚠️ | Only on a struct **field**, not top-level State — `{{.Views.Class .ID}}`, not `{{.Class .ID}}` (see [Controller Pattern — view helpers](controller-pattern.md#methods-that-take-arguments-view-helpers)) |
## Performance Considerations
diff --git a/content/reference/uploads.md b/content/reference/uploads.md
index 21f2e10..eb0f02d 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.19.1"
+source_commit: "fe690899b1400a0c3886206038c0b958b40554be"
---
# Upload Reference