Support RSocket
Skip to main content

User session

Let's add a server side session to store the logged-in user's state. Later on it will be used to temporarily store the messages which will be delivered to the client.

See resulting code on GitHub

Server side

Data-classes

First we will add some dataclasses to represent a single user session, and a lookup dictionary for all user sessions by their id.

from dataclasses import dataclass, field
from typing import Dict
from weakref import WeakValueDictionary

class SessionId(str):
pass

@dataclass()
class UserSessionData:
username: str
session_id: SessionId

@dataclass(frozen=True)
class ChatData:
user_session_by_id: Dict[SessionId, UserSessionData] = field(default_factory=WeakValueDictionary)

chat_data = ChatData()

The SessionId defined in Lines 5-6 is required in order to store the string session-id as a weak reference later on.

Lines 8-11 define the UserSessionData dataclass which represents the user's session. It contains two fields:

  • username - Human readable name specified in the login payload.
  • session_id - unique id (e.g. UUID4) generated to identify the session.

Lines 13-15 define the ChatData dataclass which represents the application's data. For now, it contains only a dict for looking up user sessions by their id. It is initialized as a WeakValueDictionary in order for the session to be removed when the user disconnects.

Line 17 instantiates a global instance of this class.

Login endpoint

Next we will modify the login endpoint to create a user session:

import logging
import uuid
from typing import Optional, Awaitable

from rsocket.frame_helpers import ensure_bytes
from rsocket.helpers import utf8_decode, create_response
from rsocket.payload import Payload
from rsocket.routing.request_router import RequestRouter

class ChatUserSession:

def __init__(self):
self._session: Optional[UserSessionData] = None

def router_factory(self):
router = RequestRouter()

@router.response('login')
async def login(payload: Payload) -> Awaitable[Payload]:
username = utf8_decode(payload.data)

logging.info(f'New user: {username}')

session_id = SessionId(uuid.uuid4())
self._session = UserSessionData(username, session_id)
chat_data.user_session_by_id[session_id] = self._session

return create_response(ensure_bytes(session_id))

return router

In order to keep a reference to the UserSessionData we will modify the request handler factory. The ChatUserSession class will keep the reference to the session data, and define the request routes.

Below is the modified handler_factory:

class CustomRoutingRequestHandler(RoutingRequestHandler):
def __init__(self, session: ChatUserSession):
super().__init__(session.router_factory())
self._session = session

def handler_factory():
return CustomRoutingRequestHandler(ChatUserSession())

The CustomRoutingRequestHandler class (Lines 1-4) is the actual request handler which will wrap the ChatUserSession instance.

Finally, we modify the handler_factory (Lines 6-7) to instantiate the handler and the session.

Client side

Below is the modified ChatClient:

from rsocket.extensions.helpers import composite, route
from rsocket.frame_helpers import ensure_bytes
from rsocket.payload import Payload
from rsocket.rsocket_client import RSocketClient

class ChatClient:
def __init__(self, rsocket: RSocketClient):
self._rsocket = rsocket
self._session_id = None
self._username: Optional[str] = None

async def login(self, username: str):
payload = Payload(ensure_bytes(username), composite(route('login')))
response = await self._rsocket.request_response(payload)
self._session_id = response.data
self._username = username
return self

Instead of a greeting from the server, we now receive a session id in the response payload. Line 14 stores this session on the client class.

We also store the username on the client for later printing as part of incoming messages.