Skip to content

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

Description

@Lowizer

Problem

When a page contains only stateless livecomponents (i.e. no LiveComponent subclass), calling a command on any of them always returns HTTP 410 Gone.

The root cause is a gap between how sessions are established and how they are checked:

  1. StatelessLiveComponent.get_or_create_state() returns a StatelessModel() directly without writing anything to the state store.
  2. StatelessLiveComponent.set_state() is a no-op for the same reason — stateless components intentionally have no persistent state.
  3. The call_command view guards every request with state_manager.session_exists(session_id), which checks for the presence of the Redis hash key lc:states:{session_id}.
  4. Because no state was ever written, that key does not exist → session_exists() returns False → the view returns 410 before the command is ever dispatched.

This means stateless components that use commands are entirely broken in isolation — they only work incidentally when a stateful component happens to be on the same page (and therefore creates the session key).

Proposed fix

Add a get_or_create_session mechanism that writes a lightweight sentinel entry to the state store during get_or_create_state of StatelessLiveComponent. This makes the session discoverable without storing any real state:

livecomponents/manager/stores.py — add to IStateStore and both implementations:

# RedisStateStore
def get_or_create_session(self, session_id: str) -> None:
    key_name = self._get_key_name(self.key_prefix, session_id)
    with self.client.pipeline() as pipe:
        pipe.hsetnx(key_name, "__stateless__", b"1")
        pipe.expire(key_name, self.ttl)
        pipe.execute()

# MemoryStateStore
def get_or_create_session(self, session_id: str) -> None:
    sentinel = StateAddress(session_id=session_id, component_id="__stateless__")
    self._store.setdefault(sentinel, b"")

livecomponents/manager/manager.py:

def get_or_create_session(self, session_id: str) -> None:
    self.store.get_or_create_session(session_id)

livecomponents/component.py — override in StatelessLiveComponent:

def get_or_create_state(self, state_manager, state_addr, request, component_kwargs):
    state_manager.get_or_create_session(state_addr.session_id)
    return StatelessModel()

This approach keeps TTL enforcement intact, requires no changes to the view layer, and has no effect on purely stateful pages (the sentinel write is idempotent via hsetnx).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions