Skip to content

Commit 1ecfccc

Browse files
authored
Create session_store.py
1 parent b09c568 commit 1ecfccc

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

src/ohip_runtime/session_store.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""
2+
In-memory runtime session store for IX-HapticSight.
3+
4+
This module provides a small, explicit registry for InteractionSession objects.
5+
It is intentionally simple and backend-agnostic so it can be reused by:
6+
7+
- local runtime coordinators
8+
- future ROS 2 wrappers
9+
- replay tools
10+
- benchmark harnesses
11+
- integration tests
12+
13+
The store does not replace a real database or distributed runtime state system.
14+
Its purpose is to keep session ownership visible and deterministic during the
15+
repository's runtime buildout phase.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from dataclasses import replace
21+
from threading import RLock
22+
from typing import Dict, Iterable, Optional
23+
24+
from .state import InteractionSession
25+
26+
27+
class SessionStore:
28+
"""
29+
Thread-safe in-memory registry for interaction sessions.
30+
31+
Design goals:
32+
- explicit session lifecycle
33+
- no hidden globals
34+
- safe-by-default duplicate handling
35+
- snapshots returned to callers to reduce accidental shared mutation
36+
"""
37+
38+
def __init__(self) -> None:
39+
self._lock = RLock()
40+
self._sessions: Dict[str, InteractionSession] = {}
41+
42+
def exists(self, session_id: str) -> bool:
43+
with self._lock:
44+
return session_id in self._sessions
45+
46+
def create(self, session: InteractionSession, *, overwrite: bool = False) -> InteractionSession:
47+
"""
48+
Insert a new session.
49+
50+
By default, duplicate session IDs are rejected to avoid silent state
51+
replacement. Set overwrite=True only when the caller has an explicit
52+
reason to replace existing state.
53+
"""
54+
with self._lock:
55+
if not overwrite and session.session_id in self._sessions:
56+
raise ValueError(f"session already exists: {session.session_id}")
57+
self._sessions[session.session_id] = replace(session)
58+
return replace(self._sessions[session.session_id])
59+
60+
def get(self, session_id: str) -> Optional[InteractionSession]:
61+
"""
62+
Return a detached snapshot of the session, or None if not found.
63+
"""
64+
with self._lock:
65+
session = self._sessions.get(session_id)
66+
return replace(session) if session is not None else None
67+
68+
def require(self, session_id: str) -> InteractionSession:
69+
"""
70+
Return a detached snapshot of the session or raise KeyError.
71+
"""
72+
session = self.get(session_id)
73+
if session is None:
74+
raise KeyError(f"unknown session_id: {session_id}")
75+
return session
76+
77+
def update(self, session: InteractionSession) -> InteractionSession:
78+
"""
79+
Replace an existing session with a new snapshot.
80+
81+
This method is explicit on purpose. Callers should construct the next
82+
session state and then commit it rather than mutating shared global
83+
state through hidden references.
84+
"""
85+
with self._lock:
86+
if session.session_id not in self._sessions:
87+
raise KeyError(f"unknown session_id: {session.session_id}")
88+
self._sessions[session.session_id] = replace(session)
89+
return replace(self._sessions[session.session_id])
90+
91+
def upsert(self, session: InteractionSession) -> InteractionSession:
92+
"""
93+
Insert or replace a session snapshot.
94+
"""
95+
with self._lock:
96+
self._sessions[session.session_id] = replace(session)
97+
return replace(self._sessions[session.session_id])
98+
99+
def delete(self, session_id: str) -> bool:
100+
"""
101+
Remove a session if present. Returns True when something was deleted.
102+
"""
103+
with self._lock:
104+
return self._sessions.pop(session_id, None) is not None
105+
106+
def clear(self) -> None:
107+
with self._lock:
108+
self._sessions.clear()
109+
110+
def list_ids(self) -> list[str]:
111+
with self._lock:
112+
return sorted(self._sessions.keys())
113+
114+
def list_sessions(self) -> list[InteractionSession]:
115+
with self._lock:
116+
return [replace(self._sessions[sid]) for sid in sorted(self._sessions.keys())]
117+
118+
def count(self) -> int:
119+
with self._lock:
120+
return len(self._sessions)
121+
122+
def bulk_upsert(self, sessions: Iterable[InteractionSession]) -> int:
123+
"""
124+
Upsert multiple sessions and return the number written.
125+
"""
126+
written = 0
127+
with self._lock:
128+
for session in sessions:
129+
self._sessions[session.session_id] = replace(session)
130+
written += 1
131+
return written
132+
133+
134+
__all__ = [
135+
"SessionStore",
136+
]

0 commit comments

Comments
 (0)