from __future__ import annotations import os import sqlite3 import stat from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from urllib.parse import urlencode DEFAULT_READ_CACHE_PATH = Path( os.environ.get("STATEHUB_READ_CACHE_PATH", "~/.statehub/edge-read-cache.sqlite3") ).expanduser() MAX_CACHE_ENTRY_BYTES = 1024 * 1024 CACHEABLE_GET_PREFIXES = ( "/state/", "/workplans/", "/messages/", "/decisions/", "/tasks/", "/progress/", "/sbom/", "/repos", "/legacy-meter/", ) @dataclass(frozen=True) class ReadCacheEntry: cache_key: str status_code: int content: bytes content_type: str | None cached_at: str def default_read_cache_path() -> Path: return DEFAULT_READ_CACHE_PATH def utcnow() -> str: return datetime.now(tz=timezone.utc).isoformat() def build_cache_key(method: str, path: str, query_items: list[tuple[str, str]]) -> str: normalized_path = path if path.startswith("/") else f"/{path}" query = urlencode(sorted(query_items)) if query: return f"{method.upper()}:{normalized_path}?{query}" return f"{method.upper()}:{normalized_path}" def is_cacheable_get(path: str) -> bool: normalized = path if path.startswith("/") else f"/{path}" if normalized.startswith("/edge/"): return False return any(normalized.startswith(prefix) for prefix in CACHEABLE_GET_PREFIXES) class ReadCacheStore: def __init__(self, path: str | Path | None = None) -> None: self.path = Path(path).expanduser() if path is not None else default_read_cache_path() self.path.parent.mkdir(parents=True, exist_ok=True) self._init_db() self._chmod_private() def _connect(self) -> sqlite3.Connection: conn = sqlite3.connect(self.path) conn.row_factory = sqlite3.Row return conn def _init_db(self) -> None: with self._connect() as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS read_cache_entries ( cache_key TEXT PRIMARY KEY, status_code INTEGER NOT NULL, content BLOB NOT NULL, content_type TEXT, cached_at TEXT NOT NULL ) """ ) conn.execute( "CREATE INDEX IF NOT EXISTS ix_read_cache_cached_at ON read_cache_entries(cached_at)" ) conn.commit() def _chmod_private(self) -> None: try: os.chmod(self.path, stat.S_IRUSR | stat.S_IWUSR) except OSError: pass def put( self, *, cache_key: str, status_code: int, content: bytes, content_type: str | None, ) -> None: if len(content) > MAX_CACHE_ENTRY_BYTES: return now = utcnow() with self._connect() as conn: conn.execute( """ INSERT INTO read_cache_entries ( cache_key, status_code, content, content_type, cached_at ) VALUES (?, ?, ?, ?, ?) ON CONFLICT(cache_key) DO UPDATE SET status_code = excluded.status_code, content = excluded.content, content_type = excluded.content_type, cached_at = excluded.cached_at """, (cache_key, status_code, content, content_type, now), ) conn.commit() def get(self, cache_key: str) -> ReadCacheEntry | None: with self._connect() as conn: row = conn.execute( "SELECT * FROM read_cache_entries WHERE cache_key = ?", (cache_key,), ).fetchone() if row is None: return None return ReadCacheEntry( cache_key=row["cache_key"], status_code=row["status_code"], content=row["content"], content_type=row["content_type"], cached_at=row["cached_at"], ) def summary(self) -> dict[str, int | str | None]: with self._connect() as conn: row = conn.execute( """ SELECT COUNT(*) AS entry_count, MIN(cached_at) AS oldest_cached_at FROM read_cache_entries """ ).fetchone() return { "entry_count": int(row["entry_count"] or 0), "oldest_cached_at": row["oldest_cached_at"], }