Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Simplify use_asset_cacher hook #1032

Merged
merged 2 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tracing.workspace = true
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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert to 3600

}
}

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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this too

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
Loading