Skip to content

Fix StatelessLiveComponent commands returning 410 on stateless-only pages#35

Merged
imankulov merged 5 commits into
mainfrom
fix-stateless-livecomponent
Apr 1, 2026
Merged

Fix StatelessLiveComponent commands returning 410 on stateless-only pages#35
imankulov merged 5 commits into
mainfrom
fix-stateless-livecomponent

Conversation

@imankulov
Copy link
Copy Markdown
Collaborator

@imankulov imankulov commented Mar 29, 2026

Summary

Fixes #33. Pages with only stateless components returned HTTP 410 on every command call because StatelessLiveComponent never writes to the state store, so session_exists() returned False.

  • Add ensure_session() to IStateStore / StateManager — writes an idempotent __session__ sentinel via HSETNX so the session becomes discoverable without storing real state
  • Call it from StatelessLiveComponent.get_or_create_state() only — stateful components are unaffected
  • Add statelesscounter example: a counter storing its value in a global variable (simulating external storage)
  • Add unit, Redis store, and Playwright E2E tests
  • Expand docs to cover standalone stateless components with externally-managed state

Test plan

  • All 59 unit tests pass (poetry run pytest tests/ --ignore=tests/test_example.py)
  • Playwright E2E test passes (test_stateless_counter)
  • Manual: visit /statelesscounter/, click +1/-1, verify count updates (no 410 in network tab)
  • Manual: visit a stateful page (e.g. /simplecounter/), verify no __session__ key appears in hgetall lc:states:{session_id}

The decorator docstring and parameter names used @classmethod and cls,
but LiveComponent methods are instance methods (self). Fix the docstring
and rename cls to self in the wrapper functions.
Fix bugs:
- quickstart.md: broken </html> closing tag, typo, placeholder install URL
- nested_components.md: component_attrs on button (belongs on root only)
- component_ids.md: id= claim corrected to data-livecomponent-id
- templates.md: stale / and . separators updated to | and :
- uploads.md: dead link to example/uploads
- management_commands.md: "Statless" typo
- livecomponents.md: stale "|message.0" separator

Add new content:
- index.md: "Why use it?" section explaining the problem and approach
- livecomponents.md: request lifecycle section, LiveComponentsModel vs
  BaseModel note, own_id explanation, mkdocstrings directives for
  InitStateContext, UpdateStateContext, ExtraContextRequest, CallContext,
  and StateManager
- context.md: document outer_context as alternative to save_context

Add docstrings to CallContext, InitStateContext, UpdateStateContext,
ExtraContextRequest, and StateManager so mkdocstrings can render them.
…ages (#33)

StatelessLiveComponent.get_or_create_state() bypasses the state store
entirely, so on pages with only stateless components, no Redis key was
ever written. The call_command view checks session_exists() before
dispatching, which looks for that key — causing every command to fail
with HTTP 410.

**Core Fix**

- Add ensure_session() to IStateStore, MemoryStateStore, and
  RedisStateStore. Uses HSETNX to write an idempotent "__session__"
  sentinel entry that makes the session discoverable without storing
  real state
- Expose ensure_session() on StateManager
- Call ensure_session() from StatelessLiveComponent.get_or_create_state()
  so only stateless components write the sentinel; stateful components
  are unaffected

**Demo Application**

- Add statelesscounter example: a counter that stores its value in a
  global variable (simulating external storage like a database or file)
- Wire up view, URL, and nav entry (after Simple counter)

**Tests**

- Unit test: ensure_session() makes session_exists() return True
- Redis store tests: discoverability, idempotency, no overwrite of
  existing state, TTL behavior
- Playwright E2E test: stateless counter increment/decrement

**Documentation**

- Expand "Stateless components" section with externally-managed state
  use case and standalone usage note
- Soften nested_components.md claim that stateless children "don't
  have any actions"
Copy link
Copy Markdown
Collaborator

@Lowizer Lowizer left a comment

Choose a reason for hiding this comment

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

Beware: This PR's base branch is update-documentation and thus is also targeting the latter. This should probably be corrected to base these changes off the main branch and also target the latter.

Comment thread tests/test_views.py Outdated
@imankulov imankulov changed the base branch from update-documentation to main March 30, 2026 11:02
@imankulov imankulov changed the base branch from main to update-documentation March 30, 2026 11:04
The test name and docstring claimed it tested call_command not returning
410, but it only asserted session_exists(). Rename to reflect the actual
behavior under test and drop the unused client fixture.
@imankulov imankulov requested a review from Lowizer March 30, 2026 11:25
Copy link
Copy Markdown
Collaborator

@Lowizer Lowizer left a comment

Choose a reason for hiding this comment

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

Apart from this PR (yet again) targeting the wrong branch, this now looks good to me. In the meantime I tested this fix and it did indeed resolve my issues.

@imankulov
Copy link
Copy Markdown
Collaborator Author

Apart from this PR (yet again) targeting the wrong branch, this now looks good to me

This is intentional. Both PRs modify the same docs/livecomponents.md file and I decided that it would be easier to stack PRs instead of messing up with merge conflicts in the second PR after merging the first one.

Copy link
Copy Markdown

@atrautsch atrautsch left a comment

Choose a reason for hiding this comment

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

Tested the stateless counter and session sentinel is created in the redis 🚀

I am sorry I did not really make the time to look into the initial issue. I thought that we should have a session because of the template and context initially saved from the templatetag, but it seems like the error was exclusively regarding the state.

Base automatically changed from update-documentation to main April 1, 2026 07:33
@imankulov imankulov merged commit d47b54b into main Apr 1, 2026
6 checks passed
@imankulov imankulov deleted the fix-stateless-livecomponent branch April 1, 2026 07:34
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.

StatelessLiveComponent renders don't register a session, causing call_command to return 410

3 participants