freya_hooks/
use_asset_cacher.rsuse std::{
collections::{
HashMap,
HashSet,
},
sync::{
Arc,
Mutex,
},
time::Duration,
};
use bytes::Bytes;
use dioxus_core::prelude::{
spawn_forever,
use_drop,
ReactiveContext,
Task,
};
use dioxus_hooks::{
use_context,
use_context_provider,
use_effect,
use_reactive,
use_signal,
};
use dioxus_signals::{
Readable,
Signal,
Writable,
};
use tokio::time::sleep;
use tracing::info;
#[derive(Hash, PartialEq, Eq, Clone)]
pub enum AssetAge {
Duration(Duration),
Unspecified,
}
impl Default for AssetAge {
fn default() -> Self {
Self::Duration(Duration::from_secs(3600)) }
}
impl From<Duration> for AssetAge {
fn from(value: Duration) -> Self {
Self::Duration(value)
}
}
#[derive(Hash, PartialEq, Eq, Clone)]
pub struct AssetConfiguration {
pub age: AssetAge,
pub id: String,
}
enum AssetUsers {
Listeners(Arc<Mutex<HashSet<ReactiveContext>>>),
ClearTask(Task),
}
#[derive(Clone)]
pub enum AssetBytes {
Cached(Bytes),
Loading,
Pending,
Error(String),
}
impl AssetBytes {
pub fn try_as_bytes(&self) -> Option<&Bytes> {
match self {
Self::Cached(bytes) => Some(bytes),
_ => None,
}
}
}
struct AssetState {
users: AssetUsers,
asset_bytes: AssetBytes,
}
#[derive(Clone, Copy, Default, PartialEq)]
pub struct AssetCacher {
registry: Signal<HashMap<AssetConfiguration, AssetState>>,
}
impl AssetCacher {
pub fn read_asset(&self, asset_config: &AssetConfiguration) -> Option<AssetBytes> {
self.registry
.peek_unchecked()
.get(asset_config)
.map(|a| a.asset_bytes.clone())
}
pub fn update_asset(&mut self, asset_config: AssetConfiguration, asset_bytes: AssetBytes) {
let mut registry = self.registry.write();
let asset = registry
.entry(asset_config.clone())
.or_insert_with(|| AssetState {
asset_bytes: AssetBytes::Pending,
users: AssetUsers::Listeners(Arc::default()),
});
asset.asset_bytes = asset_bytes;
if let AssetUsers::Listeners(listeners) = &asset.users {
for sub in listeners.lock().unwrap().iter() {
sub.mark_dirty();
}
info!(
"Marked as dirty {} reactive contexts listening to asset with id '{}'",
listeners.lock().unwrap().len(),
asset_config.id
);
}
}
pub fn try_clean(&mut self, asset_config: &AssetConfiguration) {
let mut registry = self.registry;
let spawn_clear_task = {
let mut registry = registry.write();
let entry = registry.get_mut(asset_config);
if let Some(asset_state) = entry {
match &mut asset_state.users {
AssetUsers::Listeners(listeners) => {
listeners.lock().unwrap().is_empty()
}
AssetUsers::ClearTask(task) => {
task.cancel();
true
}
}
} else {
false
}
};
if spawn_clear_task {
if let AssetAge::Duration(duration) = asset_config.age {
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;
registry.write().remove(&asset_config);
info!("Cleared asset with ID '{}'", asset_config.id);
}
})
.unwrap();
let mut registry = registry.write();
let entry = registry.get_mut(asset_config).unwrap();
entry.users = AssetUsers::ClearTask(clear_task);
}
}
}
pub(crate) fn listen(&self, rc: ReactiveContext, asset_config: AssetConfiguration) {
let mut registry = self.registry.write_unchecked();
registry
.entry(asset_config.clone())
.or_insert_with(|| AssetState {
asset_bytes: AssetBytes::Pending,
users: AssetUsers::Listeners(Arc::default()),
});
if let Some(asset) = registry.get(&asset_config) {
match &asset.users {
AssetUsers::Listeners(users) => {
rc.subscribe(users.clone());
}
AssetUsers::ClearTask(clear_task) => {
clear_task.cancel();
info!(
"Clear task of asset with ID '{}' has been cancelled",
asset_config.id
);
}
}
}
}
pub fn size(&self) -> usize {
self.registry.read().len()
}
}
pub fn use_asset(asset_config: AssetConfiguration) -> AssetBytes {
let mut asset_cacher = use_asset_cacher();
if let Some(rc) = ReactiveContext::current() {
asset_cacher.listen(rc, asset_config.clone());
}
use_drop({
let asset_config = asset_config.clone();
move || {
spawn_forever(async move {
asset_cacher.try_clean(&asset_config);
});
}
});
let mut prev = use_signal(|| None);
use_effect(use_reactive!(|asset_config| {
if let Some(prev) = &*prev.peek_unchecked() {
if prev != &asset_config {
asset_cacher.try_clean(&asset_config);
}
}
prev.write().replace(asset_config.clone());
}));
asset_cacher.read_asset(&asset_config).unwrap()
}
pub fn use_asset_cacher() -> AssetCacher {
use_context()
}
pub(crate) fn use_init_asset_cacher() {
use_context_provider(AssetCacher::default);
}