33import asyncio
44import json
55import logging
6- from dataclasses import dataclass
6+ from dataclasses import dataclass , replace
77from typing import Any , Dict , Tuple , Union
88from urllib import parse as urllib_parse
99from uuid import uuid4
1414from sanic_cors import CORS
1515from websockets .legacy .protocol import WebSocketCommonProtocol
1616
17- from idom .backend .types import Location
17+ from idom .backend .types import Location , SessionState
1818from idom .core .hooks import Context , create_context , use_context
1919from idom .core .layout import Layout , LayoutEvent
2020from idom .core .serve import (
2727from idom .core .types import RootComponentConstructor
2828
2929from ._asgi import serve_development_asgi
30- from .utils import safe_client_build_dir_path , safe_web_modules_dir_path
30+ from .utils import (
31+ SESSION_COOKIE_NAME ,
32+ SessionManager ,
33+ safe_client_build_dir_path ,
34+ safe_web_modules_dir_path ,
35+ )
3136
3237
3338logger = logging .getLogger (__name__ )
@@ -47,7 +52,7 @@ def configure(
4752 _setup_common_routes (blueprint , options )
4853
4954 # this route should take priority so set up it up first
50- _setup_single_view_dispatcher_route (blueprint , component )
55+ _setup_single_view_dispatcher_route (blueprint , component , options )
5156
5257 app .blueprint (blueprint )
5358
@@ -129,6 +134,9 @@ class Options:
129134 url_prefix : str = ""
130135 """The URL prefix where IDOM resources will be served from"""
131136
137+ session : SessionManager [Any ] | None = None
138+ """Used to create session cookies to perserve client state"""
139+
132140
133141def _setup_common_routes (blueprint : Blueprint , options : Options ) -> None :
134142 cors_options = options .cors
@@ -164,22 +172,51 @@ async def web_module_files(
164172
165173
166174def _setup_single_view_dispatcher_route (
167- blueprint : Blueprint , constructor : RootComponentConstructor
175+ blueprint : Blueprint ,
176+ constructor : RootComponentConstructor ,
177+ options : Options ,
168178) -> None :
169179 async def model_stream (
170180 request : request .Request , socket : WebSocketCommonProtocol , path : str = ""
171181 ) -> None :
182+ root = ConnectionContext (constructor (), value = Connection (request , socket , path ))
183+
184+ if options .session :
185+ root = options .session .context (root , value = request .ctx .idom_sesssion_state )
186+
172187 send , recv = _make_send_recv_callbacks (socket )
173- conn = Connection (request , socket , path )
174- await serve_json_patch (
175- Layout (ConnectionContext (constructor (), value = conn )),
176- send ,
177- recv ,
178- )
188+ await serve_json_patch (Layout (root ), send , recv )
179189
180190 blueprint .add_websocket_route (model_stream , "/_api/stream" )
181191 blueprint .add_websocket_route (model_stream , "/<path:path>/_api/stream" )
182192
193+ if options .session :
194+ session = options .session
195+
196+ @blueprint .on_request
197+ async def set_session_on_request (request : request .Request ) -> None :
198+ if request .scheme not in ("http" , "https" ):
199+ return
200+ session_id = request .cookies .get (SESSION_COOKIE_NAME )
201+ request .ctx .idom_session_state = await session .get_state (session_id )
202+
203+ @blueprint .on_response
204+ async def set_session_cookie_header (
205+ request : request .Request , response : response .ResponseStream
206+ ):
207+ session_state : SessionState [Any ] = request .ctx .idom_session_state
208+ # only set cookie if it has not been set before
209+ if session_state .fresh :
210+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
211+ response .cookies [SESSION_COOKIE_NAME ] = session .sid
212+ response .cookies [SESSION_COOKIE_NAME ]["secure" ] = True
213+ response .cookies [SESSION_COOKIE_NAME ]["httponly" ] = True
214+ response .cookies [SESSION_COOKIE_NAME ]["samesite" ] = "strict"
215+ response .cookies [SESSION_COOKIE_NAME ]["expires" ] = session .expiry_date
216+
217+ await session .update_state (replace (session_state , fresh = False ))
218+ logger .info (f"Setting cookie { response .cookies [SESSION_COOKIE_NAME ]} " )
219+
183220
184221def _make_send_recv_callbacks (
185222 socket : WebSocketCommonProtocol ,
0 commit comments