diff --git a/content/contributing/livetemplate.md b/content/contributing/livetemplate.md index fc8cde2..fd5f194 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Contributing to LiveTemplate Core Library diff --git a/content/guides/ephemeral-components.md b/content/guides/ephemeral-components.md index bf00a7d..7ceab5b 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Ephemeral Components Guide diff --git a/content/guides/observability.md b/content/guides/observability.md index b5f1c54..4ae359c 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # LiveTemplate Observability Guide diff --git a/content/guides/progressive-complexity.md b/content/guides/progressive-complexity.md index 4d7ef10..f349a1a 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Progressive Complexity Guide diff --git a/content/guides/scaling.md b/content/guides/scaling.md index d7b8444..27947ce 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # LiveTemplate Scaling Guide diff --git a/content/guides/standard-html-reactivity.md b/content/guides/standard-html-reactivity.md index abbd332..f8a66de 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Standard HTML Reactivity diff --git a/content/reference/api.md b/content/reference/api.md index d992824..6842dc8 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Go Library API Reference diff --git a/content/reference/authentication.md b/content/reference/authentication.md index 002365e..3ebe740 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Authentication Reference diff --git a/content/reference/client-attributes.md b/content/reference/client-attributes.md index 50827d1..e03dcfd 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Client Attributes Reference diff --git a/content/reference/configuration.md b/content/reference/configuration.md index b65c4a0..08796b6 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # LiveTemplate Configuration Guide diff --git a/content/reference/controller-pattern.md b/content/reference/controller-pattern.md index c7bd732..21b5942 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Controller+State Pattern Reference @@ -147,6 +147,32 @@ func (c *TodoController) Mount(state TodoState, ctx *livetemplate.Context) (Todo - Set up computed fields - Initialize state based on user context +**Guarding side effects per connect kind:** Mount runs on HTTP GET, HTTP POST actions, WebSocket new-connect, and WebSocket reconnect. Use `ctx.IsInitialMount()` to limit one-time setup (e.g. starting a background goroutine) to initial page loads, and `ctx.IsReconnect()` to detect a WebSocket reconnect that restored persisted state: + +```go +func (c *TodoController) Mount(state TodoState, ctx *livetemplate.Context) (TodoState, error) { + if ctx.IsInitialMount() { + // Only on initial HTTP GET — not POST actions, not WS connects. + state.Loading = true + go c.warmCacheFor(ctx.UserID()) + } + return state, nil +} +``` + +```go +func (c *ChatController) OnConnect(state ChatState, ctx *livetemplate.Context) (ChatState, error) { + if ctx.IsReconnect() { + // Network blipped — re-announce presence, skip re-fetches the prior + // connection already completed. + state.SystemMessages = append(state.SystemMessages, "[reconnected]") + } + return state, nil +} +``` + +The older `ctx.Action() == ""` idiom still works (it returns true for GET, internal navigate POSTs, and WS connects/reconnects), but the new helpers disambiguate the four lifecycle paths and make intent obvious to readers. + ### OnConnect Called when a WebSocket connection is established: @@ -170,6 +196,8 @@ func (c *TodoController) OnConnect(state TodoState, ctx *livetemplate.Context) ( - Start background jobs - Subscribe to real-time data feeds +**Heads-up — `ctx.IsReconnect()` on the first WS:** When a browser does the normal flow (HTTP GET → server-renders → WebSocket connects), the framework persists state at the end of the HTTP-path Mount, then *restores* that state when the WebSocket opens. The WS-path `OnConnect` therefore sees `ctx.IsReconnect() == true` even though no prior WebSocket connection ever existed. If your `OnConnect` needs to distinguish "brand-new session" from "WS resumed after a blip", pair `IsReconnect()` with `IsNewConnect()` or gate on per-session state. See the `IsReconnect` godoc for the full semantics. + ### OnDisconnect Called when a WebSocket connection is closed: diff --git a/content/reference/error-handling.md b/content/reference/error-handling.md index 01bbfac..2f0c9c1 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Error Handling Reference diff --git a/content/reference/limitations.md b/content/reference/limitations.md index 8e222d4..6ac43ec 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Current Limitations diff --git a/content/reference/navigate.md b/content/reference/navigate.md index d9b995c..f55a05d 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Navigate Action Reference @@ -50,20 +50,36 @@ If the WebSocket is not OPEN, the client falls through to fetch-based navigation Mount runs on every HTTP request, every WS connect, AND every `__navigate__`. Crucially, **inside Mount, a `__navigate__` re-run is indistinguishable from a connect-time Mount** — the dispatch loop deliberately rebinds `ctx.Action()` to `""` for navigate so handlers don't have to special-case it. (Grep `mount.go` for `WithAction("") // ctx.Action()=="" matches connect-time Mount`.) -That means the standard `if ctx.Action() == "" { ... }` guard from the controller-pattern docs filters out form POSTs but does **not** filter out navigate re-mounts. If you have side effects that must fire exactly once per session (analytics page-view, audit log, expensive bootstrap), gate them on per-session state, not on `ctx.Action()`: +That means the standard `if ctx.Action() == "" { ... }` guard from the controller-pattern docs filters out form POSTs but does **not** filter out navigate re-mounts — it still fires on each `__navigate__`. There are two ways to handle one-time side effects (analytics page-view, audit log, expensive bootstrap): + +**Preferred — `ctx.IsInitialMount()`:** Returns true only for the initial HTTP GET, false for WS new-connects, reconnects, *and* `__navigate__` re-mounts (which dispatch through the WS event loop as an action, not as a GET). Side effects fire exactly once per initial page load: ```go func (c *Controller) Mount(state State, ctx *livetemplate.Context) (State, error) { - if ctx.Action() == "" && !state.PageViewTracked { + if ctx.IsInitialMount() { c.analytics.TrackPageView(ctx.UserID()) - state.PageViewTracked = true } state.Filter = ctx.GetString("filter") return state, nil } ``` -State persists across navigate re-mounts within the same session, so the `PageViewTracked` flag survives. This pattern is the recommended workaround until a first-class "is this the initial mount" signal lands; see issue [#346](https://github.com/livetemplate/livetemplate/issues/346) for the related discussion on `BroadcastAction` semantics inside navigate Mount. +**Fallback — `ctx.Action() == ""` + persist flag:** If you still use the older idiom (which is true for GETs, WS connects, *and* navigate re-mounts), gate side effects on per-session state so they don't fire repeatedly: + +```go +if ctx.Action() == "" && !state.PageViewTracked { + c.analytics.TrackPageView(ctx.UserID()) + state.PageViewTracked = true +} +``` + +**`ConnectKind` behavior during navigate re-mounts:** The dispatch loop applies `WithAction("")` to the WS connection's lifecycle Context for `__navigate__`, which shallow-copies the Context and preserves `connectKind`. So inside a navigate re-mount: + +- `ctx.IsInitialMount()` is always **false** (the GET fired earlier, with a different Context). +- `ctx.IsNewConnect()` reflects the *original* WS connect-time classification — true if the underlying WS was the first connect for this group, false otherwise. +- `ctx.IsReconnect()` likewise reflects the original WS classification — true if state was restored when the WS first connected. + +Only `IsInitialMount()` is guaranteed false inside a navigate re-mount; the other two helpers report the underlying WS's connect-kind, not a navigate-specific value. ### Read query params from `ctx` diff --git a/content/reference/progressive-complexity.md b/content/reference/progressive-complexity.md index 54dc6e6..4fc88b9 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Progressive Complexity Reference diff --git a/content/reference/pubsub.md b/content/reference/pubsub.md index 27aac90..9016174 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # PubSub Reference diff --git a/content/reference/server-actions.md b/content/reference/server-actions.md index 82260bd..8c95cef 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Server Actions Reference diff --git a/content/reference/session.md b/content/reference/session.md index e75d3b6..16aaee7 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Session Reference @@ -45,7 +45,7 @@ In this example, `Filter` and `Page` survive page refreshes because they are per **Mount() lifecycle:** `Mount()` is called on every HTTP request (GET and POST) and every WebSocket connect (new and reconnect). It receives the current state (with persisted fields restored, ephemeral fields at zero value) and returns refreshed state. This ensures data is always fresh from the database. Keep Mount cheap -- it runs on every request. -**Mount on POST:** Since Mount runs before actions on HTTP POST, it must populate ephemeral fields (e.g., `state.Items = db.GetItems(state.Filter)`). Persisted fields like `Filter` and `Page` are already restored from the SessionStore before Mount receives the state. Guard side effects with `ctx.Action() == ""` to restrict them to page loads. +**Mount on POST:** Since Mount runs before actions on HTTP POST, it must populate ephemeral fields (e.g., `state.Items = db.GetItems(state.Filter)`). Persisted fields like `Filter` and `Page` are already restored from the SessionStore before Mount receives the state. Guard side effects with `ctx.IsInitialMount()` (preferred — true only on HTTP GET) or the older `ctx.Action() == ""` (true on GET, internal navigate POSTs, and WS connects/reconnects). ### State Persistence Matrix diff --git a/content/reference/template-support-matrix.md b/content/reference/template-support-matrix.md index 8ccf66b..e5b7763 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # LiveTemplate Go Template Support Matrix diff --git a/content/reference/uploads.md b/content/reference/uploads.md index f2b6ca6..6f9c4ac 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.9.0" -source_commit: "5b9a7cb8cb53d0ad75119ff54f70b6fdd85e05bd" +source_ref: "v0.9.1" +source_commit: "e9a44d16e52d68472e399a5a68ad8713179e9c7f" --- # Upload Reference