Skip to content

Commit ee047a5

Browse files
committed
Refactor session management: integrate SessionManager for user session persistence and simplify state handling
1 parent 04d620f commit ee047a5

1 file changed

Lines changed: 31 additions & 107 deletions

File tree

examples/ex_tsdataui.py

Lines changed: 31 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -55,66 +55,25 @@
5555
# example must be run as `python examples/ex_tsdataui.py`, not
5656
# `panel serve examples/ex_tsdataui.py`.
5757
#
58-
# Two-layer approach:
59-
# Layer 1 — manager registry (in-memory): UUID cookie → _MANAGER_REGISTRY
60-
# lookup → reuse existing param.Parameterized manager → fresh DataUI wraps
61-
# it. All param state (time_range, catalog, math refs) is already live.
62-
# Layer 2 — diskcache (disk): After a server restart the registry is empty;
63-
# diskcache restores picklable params into a newly created manager.
58+
# install_session_handler() patches Bokeh's per_app_patterns to set a
59+
# persistent UUID cookie on first visit. SessionManager provides a two-layer
60+
# store: in-memory registry (Layer 1) + diskcache (Layer 2, server-restart).
6461

6562
import panel as pn
66-
from uuid import uuid4
67-
from bokeh.server.urls import per_app_patterns
68-
from panel.io.server import DocHandler
63+
from dvue.session_persistence import (
64+
install_session_handler,
65+
SessionManager,
66+
snapshot as _snapshot,
67+
restore as _restore,
68+
)
6969

70+
install_session_handler()
7071

71-
class _SessionAwareDocHandler(DocHandler):
72-
"""Sets a persistent 'dvue_user_id' UUID cookie on first visit and injects
73-
it into the current request so session_key_func sees it immediately."""
74-
75-
_COOKIE_NAME = "dvue_user_id"
76-
77-
async def get(self, *args, **kwargs):
78-
user_id = self.get_cookie(self._COOKIE_NAME)
79-
if not user_id:
80-
user_id = uuid4().hex
81-
self.set_cookie(self._COOKIE_NAME, user_id, expires_days=365, path="/")
82-
# Inject into request.cookies (Tornado SimpleCookie) so
83-
# session_key_func can read it on the very first visit.
84-
self.request.cookies[self._COOKIE_NAME] = user_id
85-
await super().get(*args, **kwargs)
86-
87-
88-
per_app_patterns[0] = (r"/?", _SessionAwareDocHandler)
89-
90-
# -- diskcache state store (server-restart fallback) -------------------------
91-
#
92-
# Panel already depends on diskcache (used by pn.state.as_cached), so it is
93-
# always available in the Panel conda/pip environment.
94-
#
95-
# diskcache advantages over hand-rolled JSON:
96-
# - file-locking: safe for concurrent multi-user writes
97-
# - built-in TTL: entries expire automatically (no manual eviction needed)
98-
# - cleaner API: no tmp-file dance
99-
#
100-
# IMPORTANT: diskcache stores plain picklable Python objects (dicts, lists,
101-
# datetimes). It CANNOT store live Panel/HoloViews objects (pn.Tabs, hv.Overlay
102-
# etc.) — those are not meaningfully picklable. Only the *inputs* that produce
103-
# plots (selection indices, time range) are stored here.
104-
105-
import diskcache
106-
107-
_CACHE_DIR = Path(__file__).parent.parent / ".session_cache"
108-
_SESSION_CACHE = diskcache.Cache(str(_CACHE_DIR))
109-
_TTL = 30 * 24 * 3600 # 30 days
110-
111-
112-
def _load_state(user_id: str) -> dict:
113-
return _SESSION_CACHE.get(user_id, default={})
114-
115-
116-
def _save_state(user_id: str, state: dict) -> None:
117-
_SESSION_CACHE.set(user_id, state, expire=_TTL)
72+
_session_mgr = SessionManager(
73+
cookie_name="dvue_user_id",
74+
cache_dir=Path(__file__).parent.parent / ".session_cache",
75+
persist=True,
76+
)
11877

11978
# %% -- [2] Station metadata and synthetic data generator ---------------------
12079
STATIONS = [
@@ -372,20 +331,11 @@ def _make_plot_action(self):
372331
MATH_REFS_SEARCH_MAP_FILE = Path(__file__).parent / "data" / "math_refs_search_map.yaml"
373332

374333

375-
# %% -- [6] App registry ------------------------------------------------------
376-
#
377-
# user_id → {"mgr": manager, "ui": DataUI, "template": VanillaTemplate}
334+
# %% -- [6] Per-user catalog factory ------------------------------------------
378335
#
379-
# Panel widgets (pn.Tabs, pn.Row, hv.HoloViews panes …) maintain their Python-
380-
# level state (.objects, param values) independently of any Bokeh Document.
381-
# When template.servable() is called in a new session, Panel creates fresh
382-
# Bokeh models for the new Document that mirror the current Python state —
383-
# including all plot tabs already in _display_panel. No replay needed.
384-
#
385-
# Per-session work (URL/location sync) is re-registered via pn.state.onload
386-
# because pn.state.location is bound to the current Bokeh Document.
387-
388-
_APP_REGISTRY: dict = {} # user_id → {mgr, ui, template}
336+
# Each user gets an independent DataCatalog instance so math refs added by one
337+
# user do not affect others. Session registry and persistence are handled by
338+
# _session_mgr (dvue.SessionManager).
389339

390340

391341
def _make_catalog():
@@ -396,32 +346,6 @@ def _make_catalog():
396346
return cat
397347

398348

399-
def _snapshot(mgr, ui) -> dict:
400-
"""Picklable snapshot for diskcache (server-restart fallback only)."""
401-
tr = getattr(mgr, "time_range", None)
402-
tbl = getattr(ui, "display_table", None)
403-
return {
404-
"time_range": (
405-
[pd.Timestamp(tr[0]).isoformat(), pd.Timestamp(tr[1]).isoformat()]
406-
if tr else None
407-
),
408-
"selection": list(tbl.selection or []) if tbl is not None else [],
409-
}
410-
411-
412-
def _restore(mgr, saved: dict) -> None:
413-
"""Apply diskcache params to a freshly created manager."""
414-
tr = saved.get("time_range")
415-
if tr:
416-
try:
417-
mgr.time_range = (
418-
pd.Timestamp(tr[0]).to_pydatetime(),
419-
pd.Timestamp(tr[1]).to_pydatetime(),
420-
)
421-
except Exception:
422-
pass
423-
424-
425349
# %% -- [7] App factory -------------------------------------------------------
426350
#
427351
# Called once per Bokeh session (every browser tab open / page load).
@@ -437,22 +361,24 @@ def _restore(mgr, saved: dict) -> None:
437361
# Re-trigger plot for the saved selection (diskcache case only).
438362

439363
def make_app():
440-
user_id = pn.state.cookies.get("dvue_user_id", "")
364+
user_id = _session_mgr.current_user_id
365+
reg_key = _session_mgr.make_reg_key(user_id, "tsdataui")
366+
entry = _session_mgr.get_entry(reg_key)
441367

442-
if user_id and user_id in _APP_REGISTRY:
368+
if entry:
443369
# --- Registry hit: reuse existing objects --------------------------
444-
entry = _APP_REGISTRY[user_id]
445-
mgr = entry["mgr"]
446-
ui = entry["ui"]
447-
tmpl = entry["template"]
370+
mgr = entry["mgr"]
371+
ui = entry["ui"]
372+
tmpl = entry["template"]
448373
# Re-register per-Document setup (location/URL sync binds to curdoc).
449374
pn.state.onload(lambda: (ui.setup_location_sync(), ui.setup_url_sync()))
450375
tmpl.servable()
451376
return
452377

453378
# --- No registry: new user or server restart ---------------------------
379+
saved = _session_mgr.load_state(user_id)
454380
mgr = ExampleTimeSeriesDataUIManager(_make_catalog())
455-
saved = _load_state(user_id) if user_id else {}
381+
mgr.show_reset_session_button = True # reset button appears in DataUI action row
456382
if saved:
457383
_restore(mgr, saved)
458384

@@ -462,12 +388,11 @@ def make_app():
462388
)
463389
tmpl.servable()
464390

465-
if user_id:
466-
_APP_REGISTRY[user_id] = {"mgr": mgr, "ui": ui, "template": tmpl}
391+
_session_mgr.set_entry(reg_key, {"mgr": mgr, "ui": ui, "template": tmpl})
467392

468393
# Restore selection + re-trigger plot (only meaningful after server restart
469394
# when diskcache had a saved selection; fresh users have no saved state).
470-
sel = saved.get("selection", [])
395+
sel = saved.get("selection", []) if saved else []
471396

472397
def _on_load():
473398
if sel and hasattr(ui, "display_table") and hasattr(ui, "_registered_actions"):
@@ -481,8 +406,7 @@ def _on_load():
481406

482407
# Wire live-persistence watchers.
483408
def _save(event=None):
484-
if user_id:
485-
_save_state(user_id, _snapshot(mgr, ui))
409+
_session_mgr.save_state(user_id, _snapshot(mgr, ui))
486410

487411
mgr.param.watch(_save, "time_range")
488412
if hasattr(ui, "display_table"):

0 commit comments

Comments
 (0)