use std::{
any::Any,
cell::RefCell,
collections::HashMap,
hash::{
DefaultHasher,
Hash,
Hasher,
},
rc::Rc,
time::Duration,
};
use async_io::Timer;
use freya_core::{
integration::FxHashSet,
prelude::*,
};
#[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: u64,
}
impl AssetConfiguration {
pub fn new(id: impl Hash, age: AssetAge) -> Self {
let mut state = DefaultHasher::default();
id.hash(&mut state);
let id = state.finish();
Self { id, age }
}
}
enum AssetUsers {
Listeners(Rc<RefCell<FxHashSet<ReactiveContext>>>),
ClearTask(TaskHandle),
}
#[derive(Clone)]
pub enum Asset {
Cached(Rc<dyn Any>),
Loading,
Pending,
Error(String),
}
impl Asset {
pub fn try_get(&self) -> Option<&Rc<dyn Any>> {
match self {
Self::Cached(asset) => Some(asset),
_ => None,
}
}
}
struct AssetState {
users: AssetUsers,
asset: Asset,
}
#[derive(Clone, Copy, PartialEq)]
pub struct AssetCacher {
registry: State<HashMap<AssetConfiguration, AssetState>>,
}
impl AssetCacher {
pub fn create() -> Self {
Self {
registry: State::create(HashMap::new()),
}
}
pub fn try_get() -> Option<Self> {
try_consume_root_context()
}
pub fn get() -> Self {
consume_root_context()
}
pub fn read_asset(&self, asset_config: &AssetConfiguration) -> Option<Asset> {
self.registry
.peek()
.get(asset_config)
.map(|a| a.asset.clone())
}
pub fn subscribe_asset(&self, asset_config: &AssetConfiguration) -> Option<Asset> {
self.listen(ReactiveContext::current(), asset_config.clone());
self.registry
.peek()
.get(asset_config)
.map(|a| a.asset.clone())
}
pub fn update_asset(&mut self, asset_config: AssetConfiguration, new_asset: Asset) {
let mut registry = self.registry.write();
let asset = registry
.entry(asset_config.clone())
.or_insert_with(|| AssetState {
asset: Asset::Pending,
users: AssetUsers::Listeners(Rc::default()),
});
asset.asset = new_asset;
if let AssetUsers::Listeners(listeners) = &asset.users {
for sub in listeners.borrow().iter() {
sub.notify();
}
}
}
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.borrow().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 {
Timer::after(duration).await;
registry.write().remove(&asset_config);
}
});
let mut registry = registry.write();
if let Some(entry) = registry.get_mut(asset_config) {
entry.users = AssetUsers::ClearTask(clear_task);
} else {
#[cfg(debug_assertions)]
tracing::info!(
"Failed to spawn clear task to remove cache of {}",
asset_config.id
)
}
}
}
}
pub(crate) fn listen(&self, mut rc: ReactiveContext, asset_config: AssetConfiguration) {
let mut registry = self.registry.write_unchecked();
registry
.entry(asset_config.clone())
.or_insert_with(|| AssetState {
asset: Asset::Pending,
users: AssetUsers::Listeners(Rc::default()),
});
if let Some(asset) = registry.get(&asset_config) {
match &asset.users {
AssetUsers::Listeners(users) => {
rc.subscribe(users);
}
AssetUsers::ClearTask(clear_task) => {
clear_task.cancel();
}
}
}
}
pub fn size(&self) -> usize {
self.registry.read().len()
}
}
pub fn use_asset(asset_config: &AssetConfiguration) -> Asset {
let mut asset_cacher = use_hook(AssetCacher::get);
use_drop({
let asset_config = asset_config.clone();
move || {
spawn_forever(async move {
asset_cacher.try_clean(&asset_config);
});
}
});
let mut prev = use_state::<Option<AssetConfiguration>>(|| None);
{
let mut prev = prev.write();
if prev.as_ref() != Some(asset_config) {
if let Some(prev) = &*prev
&& prev != asset_config
{
asset_cacher.try_clean(asset_config);
}
prev.replace(asset_config.clone());
}
asset_cacher.listen(ReactiveContext::current(), asset_config.clone());
}
asset_cacher
.read_asset(asset_config)
.expect("Asset should be be cached by now.")
}