Source code for ascribe_link.cache
"""Result cache for multiplayer synchronization.
Caches processing results per room to ensure all peers receive identical data
without redundant computation.
"""
from __future__ import annotations
import hashlib
import json
import threading
import time
from dataclasses import dataclass
from typing import Any
[docs]
@dataclass
class CachedResult:
"""A cached processing result."""
room_id: str
function_name: str
params_hash: str
result: Any
timestamp: float
access_count: int = 0
[docs]
class RoomResultCache:
"""Cache for processing results, keyed by room ID.
Each room can have one cached result at a time. When a new request comes in
for a room, the old cached result for that room is invalidated (since the new
specimen will replace the old one for all peers).
Thread-safe for concurrent access.
"""
def __init__(self, ttl_seconds: float = 300.0) -> None:
"""Initialize the cache.
Parameters
----------
ttl_seconds : float
Time-to-live for cache entries (default 5 minutes).
Entries older than this are automatically evicted.
"""
self._cache: dict[str, CachedResult] = {} # room_id -> CachedResult
self._lock = threading.RLock()
self._ttl = ttl_seconds
[docs]
def get(
self,
room_id: str,
function_name: str,
params: dict[str, Any],
) -> Any | None:
"""Get a cached result if available.
Parameters
----------
room_id : str
Room identifier (e.g., "ascribe")
function_name : str
Name of the processing function
params : dict
Function parameters (must match exactly)
Returns
-------
Any | None
Cached result if found and valid, otherwise None.
Stored objects may be raw Result instances or dicts;
callers must handle either shape.
"""
params_hash = self._hash_params(params)
with self._lock:
entry = self._cache.get(room_id)
if entry is None:
return None
# Check if function and params match
if entry.function_name != function_name or entry.params_hash != params_hash:
# Different request for this room — invalidate
del self._cache[room_id]
return None
# Check TTL
age = time.time() - entry.timestamp
if age > self._ttl:
del self._cache[room_id]
return None
# Valid cache hit
entry.access_count += 1
return entry.result
[docs]
def put(
self,
room_id: str,
function_name: str,
params: dict[str, Any],
result: Any,
) -> None:
"""Store a processing result in the cache.
Any previous cached result for this room is replaced.
Parameters
----------
room_id : str
Room identifier
function_name : str
Name of the processing function
params : dict
Function parameters
result : Any
Processing result to cache. May be a raw Result
(MeshResult/VolumeResult/...) or a JSON-ready dict;
consumers are responsible for normalizing on read.
"""
params_hash = self._hash_params(params)
with self._lock:
self._cache[room_id] = CachedResult(
room_id=room_id,
function_name=function_name,
params_hash=params_hash,
result=result,
timestamp=time.time(),
access_count=0,
)
[docs]
def invalidate_room(self, room_id: str) -> bool:
"""Explicitly invalidate the cached result for a room.
Parameters
----------
room_id : str
Room identifier
Returns
-------
bool
True if an entry was removed, False if no entry existed
"""
with self._lock:
if room_id in self._cache:
del self._cache[room_id]
return True
return False
[docs]
def clear(self) -> None:
"""Clear all cached results."""
with self._lock:
self._cache.clear()
[docs]
def cleanup_expired(self) -> int:
"""Remove expired entries from the cache.
Returns
-------
int
Number of entries removed
"""
now = time.time()
with self._lock:
expired = [
room_id
for room_id, entry in self._cache.items()
if now - entry.timestamp > self._ttl
]
for room_id in expired:
del self._cache[room_id]
return len(expired)
[docs]
def stats(self) -> dict[str, Any]:
"""Get cache statistics.
Returns
-------
dict
Statistics including entry count, room IDs, access counts
"""
with self._lock:
entries = []
for room_id, entry in self._cache.items():
age = time.time() - entry.timestamp
entries.append({
"room_id": room_id,
"function_name": entry.function_name,
"age_seconds": round(age, 2),
"access_count": entry.access_count,
})
return {
"total_entries": len(self._cache),
"ttl_seconds": self._ttl,
"entries": entries,
}
@staticmethod
def _hash_params(params: dict[str, Any]) -> str:
"""Create a deterministic hash of parameters.
Parameters
----------
params : dict
Function parameters
Returns
-------
str
Hex digest of parameter hash
"""
# Sort keys for deterministic serialization
json_str = json.dumps(params, sort_keys=True, separators=(',', ':'))
return hashlib.sha256(json_str.encode('utf-8')).hexdigest()[:16]