111 lines
3.8 KiB
Python
111 lines
3.8 KiB
Python
"""In-memory translation session store with 24h TTL."""
|
|
|
|
import uuid
|
|
from datetime import datetime, timezone, timedelta
|
|
from typing import Any
|
|
|
|
from backend.models import TranslationSession
|
|
|
|
|
|
class SessionConflictError(Exception):
|
|
"""Raised when a long-running phase tries to update a changed session."""
|
|
|
|
|
|
class SessionStore:
|
|
"""Process-local in-memory store for user-owned translation sessions.
|
|
|
|
Each session is keyed by a UUID and has a 24-hour TTL.
|
|
Expired sessions are lazily cleaned on access.
|
|
"""
|
|
|
|
def __init__(self, ttl_hours: int = 24):
|
|
self._store: dict[str, dict[str, Any]] = {}
|
|
self.ttl_hours = ttl_hours
|
|
|
|
def create(self, session_data: TranslationSession, owner_id: str) -> str:
|
|
session_id = uuid.uuid4().hex[:16]
|
|
self._store[session_id] = {
|
|
"data": session_data.model_dump(),
|
|
"owner_id": owner_id,
|
|
"version": 0,
|
|
"created_at": datetime.now(timezone.utc),
|
|
"updated_at": datetime.now(timezone.utc),
|
|
}
|
|
return session_id
|
|
|
|
def get(self, session_id: str, owner_id: str) -> TranslationSession | None:
|
|
result = self.get_with_version(session_id, owner_id)
|
|
return result[0] if result else None
|
|
|
|
def get_with_version(
|
|
self, session_id: str, owner_id: str
|
|
) -> tuple[TranslationSession, int] | None:
|
|
entry = self._store.get(session_id)
|
|
if entry is None or entry["owner_id"] != owner_id:
|
|
return None
|
|
# Check TTL
|
|
now = datetime.now(timezone.utc)
|
|
age = now - entry["updated_at"]
|
|
if age > timedelta(hours=self.ttl_hours):
|
|
del self._store[session_id]
|
|
return None
|
|
# Touch (refresh TTL)
|
|
entry["updated_at"] = now
|
|
return TranslationSession(**entry["data"]), entry["version"]
|
|
|
|
def update(
|
|
self,
|
|
session_id: str,
|
|
data: dict[str, Any],
|
|
owner_id: str,
|
|
expected_version: int | None = None,
|
|
) -> TranslationSession | None:
|
|
entry = self._store.get(session_id)
|
|
if entry is None or entry["owner_id"] != owner_id:
|
|
return None
|
|
now = datetime.now(timezone.utc)
|
|
if now - entry["updated_at"] > timedelta(hours=self.ttl_hours):
|
|
del self._store[session_id]
|
|
return None
|
|
if expected_version is not None and entry["version"] != expected_version:
|
|
raise SessionConflictError
|
|
entry["data"].update(data)
|
|
entry["version"] += 1
|
|
entry["updated_at"] = now
|
|
return TranslationSession(**entry["data"])
|
|
|
|
def delete(self, session_id: str, owner_id: str) -> bool:
|
|
entry = self._store.get(session_id)
|
|
if entry is not None and entry["owner_id"] == owner_id:
|
|
del self._store[session_id]
|
|
return True
|
|
return False
|
|
|
|
def set_progress(
|
|
self, session_id: str, progress: dict[str, Any] | None, owner_id: str
|
|
) -> bool:
|
|
"""Update transient progress without changing the semantic session version."""
|
|
entry = self._store.get(session_id)
|
|
if entry is None or entry["owner_id"] != owner_id:
|
|
return False
|
|
now = datetime.now(timezone.utc)
|
|
if now - entry["updated_at"] > timedelta(hours=self.ttl_hours):
|
|
del self._store[session_id]
|
|
return False
|
|
entry["data"]["progress"] = progress
|
|
entry["updated_at"] = now
|
|
return True
|
|
|
|
def cleanup_expired(self):
|
|
"""Remove all expired sessions."""
|
|
now = datetime.now(timezone.utc)
|
|
expired_keys = [
|
|
k for k, v in self._store.items()
|
|
if (now - v["updated_at"]) > timedelta(hours=self.ttl_hours)
|
|
]
|
|
for k in expired_keys:
|
|
del self._store[k]
|
|
|
|
|
|
# Singleton instance
|
|
session_store = SessionStore(ttl_hours=24)
|