Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions services/shared_theme_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import logging
import re
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple

from services.theme_parser_service import (
Expand Down Expand Up @@ -55,6 +55,38 @@ def __init__(self, db):
"""
self.db = db
self.collection = getattr(db, "shared_themes", None)
# ==========================
# In-memory cache (active themes list)
# ==========================
self._active_themes_cache: Optional[List[Dict[str, Any]]] = None
self._active_themes_expires_at: Optional[datetime] = None
self._cache_ttl_seconds = 300 # 5 minutes
# מונה "גרסה" כדי למנוע דריסת invalidate ע"י כתיבה מאוחרת מ-thread אחר
self._active_themes_cache_version = 0

# Optional: best-effort index creation (safe if unsupported)
self.ensure_indexes()

def invalidate_cache(self) -> None:
"""איפוס cache (יש לקרוא אחרי create/update/delete)."""
# קודם מעלים גרסה כדי שכל fetch שמתקדם במקביל לא יכתוב cache "ישן"
self._active_themes_cache_version += 1
self._active_themes_cache = None
self._active_themes_expires_at = None

def ensure_indexes(self) -> None:
"""יוצר אינדקסים בסיסיים לשיפור ביצועים (best-effort)."""
if self.collection is None:
return
try:
create_index = getattr(self.collection, "create_index", None)
if not callable(create_index):
return
# Compound index for is_active + order + created_at
create_index([("is_active", 1), ("order", 1), ("created_at", -1)])
except Exception as e:
# לא שוברים את האפליקציה/טסטים בגלל אינדקסים
logger.debug("SharedThemeService.ensure_indexes failed: %s", e)

# ============= Validation =============

Expand Down Expand Up @@ -119,6 +151,21 @@ def get_all_active(self) -> List[Dict[str, Any]]:
"""קבלת כל הערכות הפעילות (מטא-דאטה בלבד)."""
if self.collection is None:
return []
now = datetime.now(timezone.utc)
# העתקה למשתנים מקומיים כדי להימנע מקריסה אם invalidate מתרחש באמצע (race)
cached_themes = self._active_themes_cache
cached_expires_at = self._active_themes_expires_at
# Cache hit
if (
cached_themes is not None
and cached_expires_at is not None
and cached_expires_at > now
):
# מחזירים עותק כדי למנוע "השחתה" של ה-cache ע"י קוראים שמשנים את הרשימה/מילונים
return [t.copy() for t in cached_themes]

# Cache miss: זוכרים את הגרסה הנוכחית כדי לא לדרוס invalidate שהתרחש בזמן ה-fetch
version_at_start = self._active_themes_cache_version
try:
cursor = self.collection.find(
{"is_active": True},
Expand All @@ -135,17 +182,24 @@ def get_all_active(self) -> List[Dict[str, Any]]:
themes: List[Dict[str, Any]] = []
for doc in cursor:
created_at = doc.get("created_at")
theme_id = doc.get("_id")
themes.append(
{
"id": doc.get("_id"),
# ⚠️ JSON safety: ObjectId לא תמיד סיריאליזבילי, אז ממירים ל-str
"id": str(theme_id) if theme_id is not None else None,
"name": doc.get("name"),
"description": doc.get("description", ""),
"is_featured": bool(doc.get("is_featured", False)),
"created_at": created_at.isoformat() if isinstance(created_at, datetime) else None,
"type": "shared",
}
)
return themes
# Save to cache
if version_at_start == self._active_themes_cache_version:
self._active_themes_cache = themes
self._active_themes_expires_at = now + timedelta(seconds=self._cache_ttl_seconds)
# מחזירים עותק כדי לשמור על התנהגות עקבית (גם ב-cache miss)
return [t.copy() for t in themes]
except Exception as e:
logger.exception("SharedThemeService.get_all_active failed: %s", e)
return []
Expand Down Expand Up @@ -245,6 +299,7 @@ def create(

try:
self.collection.insert_one(doc)
self.invalidate_cache()
logger.info("Created shared theme %s by user %s", slug, created_by)
return True, slug
except Exception as e:
Expand Down Expand Up @@ -313,6 +368,7 @@ def update(
result = self.collection.update_one({"_id": str(theme_id)}, {"$set": update_fields})
if getattr(result, "modified_count", 0) == 0:
return False, "no_changes"
self.invalidate_cache()
return True, "ok"
except Exception as e:
logger.exception("SharedThemeService.update failed: %s", e)
Expand All @@ -326,6 +382,7 @@ def delete(self, theme_id: str) -> Tuple[bool, str]:
result = self.collection.delete_one({"_id": str(theme_id)})
if getattr(result, "deleted_count", 0) == 0:
return False, "theme_not_found"
self.invalidate_cache()
return True, "ok"
except Exception as e:
logger.exception("SharedThemeService.delete failed: %s", e)
Expand Down
Loading