diff --git a/Cargo.lock b/Cargo.lock index 73c149d2..075352fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4892,6 +4892,7 @@ name = "upub-web" version = "0.1.0" dependencies = [ "apb", + "async-trait", "chrono", "console_error_panic_hook", "dashmap", diff --git a/web/Cargo.toml b/web/Cargo.toml index eacdf4ea..8d968ae8 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -12,6 +12,8 @@ repository = "https://git.alemi.dev/upub.git" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = "0.1" +lazy_static = "1.4" tracing = "0.1" tracing-subscriber = "0.3" tracing-subscriber-wasm = "0.1" @@ -29,7 +31,6 @@ apb = { path = "../apb", features = ["unstructured", "activitypub-fe", "activity uriproxy = { path = "../utils/uriproxy/" } mdhtml = { path = "../utils/mdhtml/" } futures = "0.3.30" -lazy_static = "1.4" chrono = { version = "0.4", features = ["serde"] } jrd = "0.1" tld = "2.35" diff --git a/web/src/actors/header.rs b/web/src/actors/header.rs index 0f0b4252..4baf4330 100644 --- a/web/src/actors/header.rs +++ b/web/src/actors/header.rs @@ -19,7 +19,7 @@ pub fn ActorHeader() -> impl IntoView { .await .map_err(|e| e.to_string())?; let user = std::sync::Arc::new(user); - cache::OBJECTS.put(Uri::full(U::Actor, &id), user.clone()); + cache::OBJECTS.store(&Uri::full(U::Actor, &id), user.clone()); Ok(user) }, } diff --git a/web/src/components/object.rs b/web/src/components/object.rs index 72ed9ba7..e9ac3de0 100644 --- a/web/src/components/object.rs +++ b/web/src/components/object.rs @@ -272,7 +272,7 @@ pub fn LikeButton( new = new.set_likes(apb::Node::object(likes.clone().set_total_items(Some(count + 1)))); } } - cache::OBJECTS.put(target, Arc::new(new)); + cache::OBJECTS.store(&target, Arc::new(new)); } }, Err(e) => tracing::error!("failed sending like: {e}"), diff --git a/web/src/config.rs b/web/src/config.rs index b0f2a1da..c87ce442 100644 --- a/web/src/config.rs +++ b/web/src/config.rs @@ -46,6 +46,7 @@ pub struct FiltersConfig { impl FiltersConfig { pub fn visible(&self, item: &crate::Object) -> bool { use apb::{Object, Activity}; + use crate::Cache; let type_filter = match item.object_type().unwrap_or(apb::ObjectType::Object) { apb::ObjectType::Note | apb::ObjectType::Document(_) => self.orphans, diff --git a/web/src/lib.rs b/web/src/lib.rs index 7918a629..56d3e970 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -23,38 +23,73 @@ pub const DEFAULT_AVATAR_URL: &str = "https://cdn.alemi.dev/social/gradient.png" pub const NAME: &str = "μ"; pub const DEFAULT_COLOR: &str = "#BF616A"; -use std::sync::Arc; +use std::{ops::Deref, sync::Arc}; use uriproxy::UriClass; -pub mod cache { - use super::{ObjectCache, WebfingerCache}; - lazy_static::lazy_static! { - pub static ref OBJECTS: ObjectCache = ObjectCache::default(); - pub static ref WEBFINGER: WebfingerCache = WebfingerCache::default(); - } -} - - -} - pub type Object = Arc; -#[derive(Debug, Clone, Default)] -pub struct ObjectCache(Arc>); +pub mod cache { + use super::DashmapCache; + lazy_static::lazy_static! { + pub static ref OBJECTS: DashmapCache = DashmapCache::default(); + pub static ref WEBFINGER: DashmapCache = DashmapCache::default(); + } +} -impl ObjectCache { - pub fn get(&self, k: &str) -> Option { - self.0.get(k).map(|x| x.clone()) +#[derive(Debug)] +pub enum LookupStatus { + Resolving, + Found(T), + NotFound, +} + +impl LookupStatus { + fn inner(&self) -> Option<&T> { + if let Self::Found(x) = self { + return Some(x); + } + None + } +} + +pub trait Cache { + type Item; + + fn lookup(&self, key: &str) -> Option>>; + fn store(&self, key: &str, value: Self::Item) -> Option; + + fn get(&self, key: &str) -> Option where Self::Item : Clone { + Some(self.lookup(key)?.deref().inner()?.clone()) } - pub fn get_or(&self, k: &str, or: Object) -> Object { - self.get(k).unwrap_or(or) + fn get_or(&self, key: &str, or: Self::Item) -> Self::Item where Self::Item : Clone { + self.get(key).unwrap_or(or) } - pub fn put(&self, k: String, v: Object) { - self.0.insert(k, v); + fn get_or_default(&self, key: &str) -> Self::Item where Self::Item : Clone + Default { + self.get(key).unwrap_or_default() + } +} + +#[derive(Default, Clone)] +pub struct DashmapCache(Arc>>); + +impl Cache for DashmapCache { + type Item = T; + + fn lookup(&self, key: &str) -> Option>> { + self.0.get(key) } + fn store(&self, key: &str, value: Self::Item) -> Option { + self.0.insert(key.to_string(), LookupStatus::Found(value)) + .and_then(|x| if let LookupStatus::Found(x) = x { Some(x) } else { None } ) + } +} + +// TODO would be cool unifying a bit the fetch code too + +impl DashmapCache { pub async fn fetch(&self, k: &str, kind: UriClass) -> reqwest::Result { match self.get(k) { Some(x) => Ok(x), @@ -63,43 +98,30 @@ impl ObjectCache { .await? .json::() .await?; - self.put(k.to_string(), Arc::new(obj)); + self.store(k, Arc::new(obj)); Ok(self.get(k).expect("not found in cache after insertion")) } } } } -#[derive(Debug, Clone)] -enum LookupStatus { - Resolving, - Found(String), - NotFound, -} - -#[derive(Debug, Clone, Default)] -pub struct WebfingerCache(Arc>); - -impl WebfingerCache { +impl DashmapCache { pub async fn blocking_resolve(&self, user: &str, domain: &str) -> Option { - if let Some(x) = self.get(user, domain) { return Some(x); } + if let Some(x) = self.resource(user, domain) { return Some(x); } self.fetch(user, domain).await; - self.get(user, domain) + self.resource(user, domain) } pub fn resolve(&self, user: &str, domain: &str) -> Option { - if let Some(x) = self.get(user, domain) { return Some(x); } + if let Some(x) = self.resource(user, domain) { return Some(x); } let (_self, user, domain) = (self.clone(), user.to_string(), domain.to_string()); leptos::spawn_local(async move { _self.fetch(&user, &domain).await }); None } - fn get(&self, user: &str, domain: &str) -> Option { + fn resource(&self, user: &str, domain: &str) -> Option { let query = format!("{user}@{domain}"); - match self.0.get(&query).map(|x| (*x).clone())? { - LookupStatus::Resolving | LookupStatus::NotFound => None, - LookupStatus::Found(x) => Some(x), - } + self.get(&query) } async fn fetch(&self, user: &str, domain: &str) { diff --git a/web/src/objects/view.rs b/web/src/objects/view.rs index ddf7d162..662c126f 100644 --- a/web/src/objects/view.rs +++ b/web/src/objects/view.rs @@ -28,10 +28,10 @@ pub fn ObjectView() -> impl IntoView { if let Ok(user) = Http::fetch::( &Uri::api(U::Actor, author, true), auth ).await { - cache::OBJECTS.put(Uri::full(U::Actor, author), Arc::new(user)); + cache::OBJECTS.store(&Uri::full(U::Actor, author), Arc::new(user)); } } - cache::OBJECTS.put(Uri::full(U::Object, &oid), obj.clone()); + cache::OBJECTS.store(&Uri::full(U::Object, &oid), obj.clone()); obj } }; diff --git a/web/src/prelude.rs b/web/src/prelude.rs index 070eced5..0c0186a4 100644 --- a/web/src/prelude.rs +++ b/web/src/prelude.rs @@ -1,7 +1,7 @@ pub use crate::{ URL_BASE, Http, Uri, - cache, + Cache, cache, // TODO move Cache under cache app::{Feeds, Loader}, auth::Auth, page::*, diff --git a/web/src/timeline/mod.rs b/web/src/timeline/mod.rs index 4ca20b61..ce5025a7 100644 --- a/web/src/timeline/mod.rs +++ b/web/src/timeline/mod.rs @@ -115,7 +115,7 @@ async fn process_activities(activities: Vec, auth: Auth) -> V actors_seen.insert(attributed_to); } if let Ok(object_uri) = object.id() { - cache::OBJECTS.put(object_uri.to_string(), Arc::new(object.clone())); + cache::OBJECTS.store(object_uri, Arc::new(object.clone())); } else { tracing::warn!("embedded object without id: {object:?}"); } @@ -136,8 +136,8 @@ async fn process_activities(activities: Vec, auth: Auth) -> V let object_id = activity.object().id().str(); if let Some(activity_id) = activity.id().str() { out.push(activity_id.to_string()); - cache::OBJECTS.put( - activity_id.to_string(), + cache::OBJECTS.store( + &activity_id, Arc::new(activity.clone().set_object(apb::Node::maybe_link(object_id))) ); } else if let Some(object_id) = activity.object().id().str() { @@ -170,7 +170,7 @@ async fn process_activities(activities: Vec, auth: Auth) -> V async fn fetch_and_update(kind: U, id: String, auth: Auth) { match Http::fetch(&Uri::api(kind, &id, false), auth).await { - Ok(data) => cache::OBJECTS.put(id, Arc::new(data)), + Ok(data) => { cache::OBJECTS.store(&id, Arc::new(data)); }, Err(e) => console_warn(&format!("could not fetch '{id}': {e}")), } }