Fix StatelessLiveComponent commands returning 410 on stateless-only pages#35
Conversation
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"
Lowizer
left a comment
There was a problem hiding this comment.
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.
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.
Lowizer
left a comment
There was a problem hiding this comment.
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.
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. |
atrautsch
left a comment
There was a problem hiding this comment.
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.
Summary
Fixes #33. Pages with only stateless components returned HTTP 410 on every command call because
StatelessLiveComponentnever writes to the state store, sosession_exists()returnedFalse.ensure_session()toIStateStore/StateManager— writes an idempotent__session__sentinel viaHSETNXso the session becomes discoverable without storing real stateStatelessLiveComponent.get_or_create_state()only — stateful components are unaffectedstatelesscounterexample: a counter storing its value in a global variable (simulating external storage)Test plan
poetry run pytest tests/ --ignore=tests/test_example.py)test_stateless_counter)/statelesscounter/, click +1/-1, verify count updates (no 410 in network tab)/simplecounter/), verify no__session__key appears inhgetall lc:states:{session_id}