Skip to content

Commit

Permalink
feat: Simplify use_asset_cacher hook
Browse files Browse the repository at this point in the history
  • Loading branch information
marc2332 committed Jan 1, 2025
1 parent 05b7d57 commit 705ba2f
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 85 deletions.
91 changes: 50 additions & 41 deletions crates/components/src/network_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub enum ImageState {
Errored,

/// Image has been fetched.
Loaded(Signal<Bytes>),
Loaded(Bytes),
}

/// Image component that automatically fetches and caches remote (HTTP) images.
Expand Down Expand Up @@ -74,7 +74,7 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element {
let NetworkImageTheme { width, height } = use_applied_theme!(&props.theme, network_image);
let alt = props.alt.as_deref();

use_memo(move || {
use_effect(move || {
let url = props.url.read().clone();
// Cancel previous asset fetching requests
for asset_task in assets_tasks.write().drain(..) {
Expand All @@ -101,10 +101,13 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element {
let asset_task = spawn(async move {
let asset = fetch_image(url).await;
if let Ok(asset_bytes) = asset {
let asset_signal =
asset_cacher.cache(asset_configuration.clone(), asset_bytes, true);
asset_cacher.cache_asset(
asset_configuration.clone(),
asset_bytes.clone(),
true,
);
// Image loaded
status.set(ImageState::Loaded(asset_signal));
status.set(ImageState::Loaded(asset_bytes));
cached_assets.write().push(asset_configuration);
} else if let Err(_err) = asset {
// Image errored
Expand All @@ -116,45 +119,51 @@ pub fn NetworkImage(props: NetworkImageProps) -> Element {
}
});

if let ImageState::Loaded(bytes) = &*status.read_unchecked() {
let image_data = dynamic_bytes(bytes.read().clone());
rsx!(image {
height: "{height}",
width: "{width}",
a11y_id,
image_data,
a11y_role: "image",
a11y_name: alt
})
} else if *status.read() == ImageState::Loading {
if let Some(loading_element) = &props.loading {
rsx!({ loading_element })
} else {
rsx!(
rect {
height: "{height}",
width: "{width}",
main_align: "center",
cross_align: "center",
Loader {}
}
)
}
} else if let Some(fallback_element) = &props.fallback {
rsx!({ fallback_element })
} else {
rsx!(
rect {
match &*status.read_unchecked() {
ImageState::Loaded(bytes) => {
let image_data = dynamic_bytes(bytes.clone());
rsx!(image {
height: "{height}",
width: "{width}",
main_align: "center",
cross_align: "center",
label {
text_align: "center",
"Error"
}
a11y_id,
image_data,
a11y_role: "image",
a11y_name: alt
})
}
ImageState::Loading => {
if let Some(loading_element) = props.loading {
rsx!({ loading_element })
} else {
rsx!(
rect {
height: "{height}",
width: "{width}",
main_align: "center",
cross_align: "center",
Loader {}
}
)
}
}
_ => {
if let Some(fallback_element) = props.fallback {
rsx!({ fallback_element })
} else {
rsx!(
rect {
height: "{height}",
width: "{width}",
main_align: "center",
cross_align: "center",
label {
text_align: "center",
"Error"
}
}
)
}
)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/hooks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ nokhwa = { version = "0.10.7", features = ["input-native"], optional = true }
paste = "1.0.14"
bitflags = "2.4.1"
bytes = "1.5.0"
tracing.workspace = true

[dev-dependencies]
dioxus = { workspace = true }
Expand Down
84 changes: 47 additions & 37 deletions crates/hooks/src/use_asset_cacher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ use std::{
};

use bytes::Bytes;
use dioxus_core::prelude::{
current_scope_id,
spawn_forever,
ScopeId,
Task,
use dioxus_core::{
prelude::{
current_scope_id,
spawn_forever,
ScopeId,
Task,
},
schedule_update_any,
};
use dioxus_hooks::{
use_context,
Expand All @@ -23,6 +26,7 @@ use dioxus_signals::{
Writable,
};
use tokio::time::sleep;
use tracing::info;

/// Defines the duration for which an Asset will remain cached after it's user has stopped using it.
/// The default is 1h (3600s).
Expand All @@ -36,7 +40,7 @@ pub enum AssetAge {

impl Default for AssetAge {
fn default() -> Self {
Self::Duration(Duration::from_secs(3600)) // 1h
Self::Duration(Duration::from_secs(10)) // 1h
}
}

Expand All @@ -62,7 +66,7 @@ enum AssetUsers {

struct AssetState {
users: AssetUsers,
asset_bytes: Signal<Bytes>,
asset_bytes: Bytes,
}

#[derive(Clone, Copy, Default)]
Expand All @@ -71,38 +75,37 @@ pub struct AssetCacher {
}

impl AssetCacher {
/// Cache the given [`AssetConfiguration`]
pub fn cache(
/// Cache the given [`AssetConfiguration`]. If it already exists and has a pending clear-task, it will get cancelled.
pub fn cache_asset(
&mut self,
asset_config: AssetConfiguration,
asset_bytes: Bytes,
subscribe: bool,
) -> Signal<Bytes> {
// Cancel previous caches
) {
// Invalidate previous caches
if let Some(asset_state) = self.registry.write().remove(&asset_config) {
if let AssetUsers::ClearTask(task) = asset_state.users {
task.cancel();
asset_state.asset_bytes.manually_drop();
info!("Clear task of asset with ID '{}' has been cancelled as the asset has been revalidated", asset_config.id);
}
}

// Insert the asset into the cache
let value = ScopeId::ROOT.in_runtime(|| asset_bytes);
let asset_bytes = Signal::new_in_scope(value, ScopeId::ROOT);
let current_scope_id = current_scope_id().unwrap();

self.registry.write().insert(
asset_config.clone(),
AssetState {
asset_bytes,
users: AssetUsers::Scopes(if subscribe {
HashSet::from([current_scope_id().unwrap()])
HashSet::from([current_scope_id])
} else {
HashSet::default()
}),
},
);

asset_bytes
schedule_update_any()(current_scope_id);
}

/// Stop using an asset. It will get removed after the specified duration if it's not used until then.
Expand Down Expand Up @@ -136,15 +139,13 @@ impl AssetCacher {
if spawn_clear_task {
// Only clear the asset if a duration was specified
if let AssetAge::Duration(duration) = asset_config.age {
// Why not use `spawn_forever`? Reason: https://github.com/DioxusLabs/dioxus/issues/2215
let clear_task = spawn_forever({
let asset_config = asset_config.clone();
async move {
info!("Waiting asset with ID '{}' to be cleared", asset_config.id);
sleep(duration).await;
if let Some(asset_state) = registry.write().remove(&asset_config) {
// Clear the asset
asset_state.asset_bytes.manually_drop();
}
registry.write().remove(&asset_config);
info!("Cleared asset with ID '{}'", asset_config.id);
}
})
.unwrap();
Expand All @@ -158,14 +159,17 @@ impl AssetCacher {
}

/// Start using an Asset. Your scope will get subscribed, to stop using an asset use [`Self::unuse_asset`]
pub fn use_asset(&mut self, config: &AssetConfiguration) -> Option<Signal<Bytes>> {
pub fn use_asset(&mut self, asset_config: &AssetConfiguration) -> Option<Bytes> {
let mut registry = self.registry.write();
if let Some(asset_state) = registry.get_mut(config) {
if let Some(asset_state) = registry.get_mut(asset_config) {
match &mut asset_state.users {
AssetUsers::ClearTask(task) => {
// Cancel clear-tasks
// Cancel clear-task
task.cancel();
asset_state.asset_bytes.manually_drop();
info!(
"Clear task of asset with ID '{}' has been cancelled",
asset_config.id
);

// Start using this asset
asset_state.users =
Expand All @@ -176,32 +180,38 @@ impl AssetCacher {
scopes.insert(current_scope_id().unwrap());
}
}

// Reruns those subscribed components
if let AssetUsers::Scopes(scopes) = &asset_state.users {
let schedule = schedule_update_any();
for scope in scopes {
schedule(*scope);
}
info!(
"Reran {} scopes subscribed to asset with id '{}'",
scopes.len(),
asset_config.id
);
}
}

registry.get(config).map(|s| s.asset_bytes)
registry.get(asset_config).map(|s| s.asset_bytes.clone())
}

/// Get the size of the cache registry.
/// Read the size of the cache registry.
pub fn size(&self) -> usize {
self.registry.read().len()
}

/// Clear all the assets from the cache registry.
pub fn clear(&mut self) {
self.registry.try_write().unwrap().clear();
}
}

/// Global caching system for assets.
///
/// This is a "low level" hook, so you probably won't need it.
/// Get access to the global cache of assets.
pub fn use_asset_cacher() -> AssetCacher {
use_context()
}

/// Initialize the global caching system for assets.
/// Initialize the global cache of assets.
///
/// This is a "low level" hook, so you probably won't need it.
/// This is a **low level** hook that **runs by default** in all Freya apps, you don't need it.
pub fn use_init_asset_cacher() {
use_context_provider(AssetCacher::default);
}
4 changes: 2 additions & 2 deletions crates/hooks/tests/use_asset_cacher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async fn asset_cacher() {
cacher.unuse_asset(asset_config.clone());
});

rsx!(label { "{asset.read()[2]}" })
rsx!(label { "{asset[2]}" })
}

fn asset_cacher_app() -> Element {
Expand All @@ -39,7 +39,7 @@ async fn asset_cacher() {
id: "test-asset".to_string(),
};

cacher.cache(asset_config.clone(), vec![9, 8, 7, 6].into(), false);
cacher.cache_asset(asset_config.clone(), vec![9, 8, 7, 6].into(), false);
});

rsx!(
Expand Down
21 changes: 16 additions & 5 deletions examples/app_dog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
)]

use freya::prelude::*;
use rand::seq::SliceRandom;
use reqwest::Url;
use serde::Deserialize;

Expand All @@ -17,11 +18,21 @@ struct DogApiResponse {
}

async fn fetch_random_dog() -> Option<Url> {
let res = reqwest::get("https://dog.ceo/api/breeds/image/random")
.await
.ok()?;
let data = res.json::<DogApiResponse>().await.ok()?;
data.message.parse().ok()
// let res = reqwest::get("https://dog.ceo/api/breeds/image/random")
// .await
// .ok()?;
// let data = res.json::<DogApiResponse>().await.ok()?;
// data.message.parse().ok()
vec![
"https://images.dog.ceo/breeds/terrier-norwich/n02094258_2617.jpg"
.parse::<Url>()
.unwrap(),
"https://images.dog.ceo/breeds/weimaraner/n02092339_2157.jpg"
.parse::<Url>()
.unwrap(),
]
.choose(&mut rand::thread_rng())
.map(|e| e.clone())
}

fn app() -> Element {
Expand Down

0 comments on commit 705ba2f

Please sign in to comment.