From 62e2045b35820be671ad06d48a32ba1e94187d84 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 9 Jun 2026 16:47:17 +0800 Subject: [PATCH] feat: add cache module with moka-backed post/tag/stats caching - Create src/cache.rs with CacheKey enum, moka cache instances, getter/setter functions, and invalidation helpers. - TTLs: post list 60s, tags 300s, single post 600s, stats 60s, tag posts 120s. - Register mod cache in src/main.rs. - All cache internals gated behind #[cfg(feature = "server")] --- src/cache.rs | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 2 files changed, 211 insertions(+) create mode 100644 src/cache.rs diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..0b852d7 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,210 @@ +#[cfg(feature = "server")] +use moka::future::Cache; +#[cfg(feature = "server")] +use std::sync::LazyLock; +#[cfg(feature = "server")] +use std::time::Duration; + +use crate::models::post::{Post, PostStats, Tag}; + +// ============================================================================ +// Cache TTL Configuration +// ============================================================================ + +#[cfg(feature = "server")] +const TTL_POST_LIST: Duration = Duration::from_secs(60); +#[cfg(feature = "server")] +const TTL_TAG_LIST: Duration = Duration::from_secs(300); +#[cfg(feature = "server")] +const TTL_SINGLE_POST: Duration = Duration::from_secs(600); +#[cfg(feature = "server")] +const TTL_POST_STATS: Duration = Duration::from_secs(60); +#[cfg(feature = "server")] +const TTL_TAG_POSTS: Duration = Duration::from_secs(120); + +// ============================================================================ +// Cache Key Types +// ============================================================================ + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum CacheKey { + PublishedPosts { page: i32, per_page: i32 }, + AllTags, + PostBySlug(String), + PostsByTag(String), + PostStats, +} + +impl CacheKey { + pub fn as_string(&self) -> String { + match self { + CacheKey::PublishedPosts { page, per_page } => { + format!("posts:list:{}:{}", page, per_page) + } + CacheKey::AllTags => "tags:all".to_string(), + CacheKey::PostBySlug(slug) => format!("post:slug:{}", slug), + CacheKey::PostsByTag(tag) => format!("posts:tag:{}", tag), + CacheKey::PostStats => "posts:stats".to_string(), + } + } +} + +// ============================================================================ +// Cache Instances +// ============================================================================ + +#[cfg(feature = "server")] +pub type PostListCache = Cache>; + +#[cfg(feature = "server")] +pub type TagListCache = Cache>; + +#[cfg(feature = "server")] +pub type SinglePostCache = Cache>; + +#[cfg(feature = "server")] +pub type PostStatsCache = Cache; + +#[cfg(feature = "server")] +static POST_LIST_CACHE: LazyLock = LazyLock::new(|| { + Cache::builder() + .max_capacity(100) + .time_to_live(TTL_POST_LIST) + .build() +}); + +#[cfg(feature = "server")] +static TAG_LIST_CACHE: LazyLock = LazyLock::new(|| { + Cache::builder() + .max_capacity(50) + .time_to_live(TTL_TAG_LIST) + .build() +}); + +#[cfg(feature = "server")] +static SINGLE_POST_CACHE: LazyLock = LazyLock::new(|| { + Cache::builder() + .max_capacity(200) + .time_to_live(TTL_SINGLE_POST) + .build() +}); + +#[cfg(feature = "server")] +static POST_STATS_CACHE: LazyLock = LazyLock::new(|| { + Cache::builder() + .max_capacity(10) + .time_to_live(TTL_POST_STATS) + .build() +}); + +#[cfg(feature = "server")] +static TAG_POSTS_CACHE: LazyLock = LazyLock::new(|| { + Cache::builder() + .max_capacity(100) + .time_to_live(TTL_TAG_POSTS) + .build() +}); + +// ============================================================================ +// Public Cache API +// ============================================================================ + +#[cfg(feature = "server")] +pub async fn get_post_list(key: &CacheKey) -> Option> { + POST_LIST_CACHE.get(&key.as_string()).await +} + +#[cfg(feature = "server")] +pub async fn set_post_list(key: &CacheKey, posts: Vec) { + let _ = POST_LIST_CACHE.insert(key.as_string(), posts).await; +} + +#[cfg(feature = "server")] +pub async fn get_tag_list() -> Option> { + TAG_LIST_CACHE.get(&CacheKey::AllTags.as_string()).await +} + +#[cfg(feature = "server")] +pub async fn set_tag_list(tags: Vec) { + let _ = TAG_LIST_CACHE.insert(CacheKey::AllTags.as_string(), tags).await; +} + +#[cfg(feature = "server")] +pub async fn get_post_by_slug(slug: &str) -> Option> { + SINGLE_POST_CACHE.get(&CacheKey::PostBySlug(slug.to_string()).as_string()).await +} + +#[cfg(feature = "server")] +pub async fn set_post_by_slug(slug: &str, post: Option) { + let _ = SINGLE_POST_CACHE + .insert(CacheKey::PostBySlug(slug.to_string()).as_string(), post) + .await; +} + +#[cfg(feature = "server")] +pub async fn get_posts_by_tag(tag: &str) -> Option> { + TAG_POSTS_CACHE + .get(&CacheKey::PostsByTag(tag.to_string()).as_string()) + .await +} + +#[cfg(feature = "server")] +pub async fn set_posts_by_tag(tag: &str, posts: Vec) { + let _ = TAG_POSTS_CACHE + .insert(CacheKey::PostsByTag(tag.to_string()).as_string(), posts) + .await; +} + +#[cfg(feature = "server")] +pub async fn get_post_stats() -> Option { + POST_STATS_CACHE.get(&CacheKey::PostStats.as_string()).await +} + +#[cfg(feature = "server")] +pub async fn set_post_stats(stats: PostStats) { + let _ = POST_STATS_CACHE + .insert(CacheKey::PostStats.as_string(), stats) + .await; +} + +// ============================================================================ +// Cache Invalidation +// ============================================================================ + +#[cfg(feature = "server")] +pub async fn invalidate_post_lists() { + POST_LIST_CACHE.invalidate_all(); +} + +#[cfg(feature = "server")] +pub async fn invalidate_all_tags() { + TAG_LIST_CACHE.invalidate_all(); +} + +#[cfg(feature = "server")] +pub async fn invalidate_post_by_slug(slug: &str) { + SINGLE_POST_CACHE + .invalidate(&CacheKey::PostBySlug(slug.to_string()).as_string()) + .await; +} + +#[cfg(feature = "server")] +pub async fn invalidate_posts_by_tag(tag: &str) { + TAG_POSTS_CACHE + .invalidate(&CacheKey::PostsByTag(tag.to_string()).as_string()) + .await; +} + +#[cfg(feature = "server")] +pub async fn invalidate_post_stats() { + POST_STATS_CACHE.invalidate_all(); +} + +#[cfg(feature = "server")] +pub async fn invalidate_all_post_caches() { + POST_LIST_CACHE.invalidate_all(); + TAG_LIST_CACHE.invalidate_all(); + SINGLE_POST_CACHE.invalidate_all(); + POST_STATS_CACHE.invalidate_all(); + TAG_POSTS_CACHE.invalidate_all(); +} diff --git a/src/main.rs b/src/main.rs index c888091..1a56f6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod api; +mod cache; mod auth; mod components; mod context;