use std::fmt::Debug;
use std::rc::Rc;
use crate::{
computed::{context::Context, Value},
struct_mut::ValueMut,
Instant, Resource, get_driver, Computed, transaction,
};
use super::request_builder::{RequestBuilder, RequestBody};
type MapResponse<T> = Option<Result<T, String>>;
fn get_unique_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
enum ApiResponse<T> {
Notinit,
Data {
value: Resource<Rc<T>>,
expiry: Option<Instant>,
}
}
impl<T> ApiResponse<T> {
pub fn new(value: Resource<Rc<T>>, expiry: Option<Instant>) -> Self {
Self::Data {
value,
expiry
}
}
pub fn new_loading() -> Self {
ApiResponse::Data { value: Resource::Loading, expiry: None }
}
pub fn get_value(&self) -> Resource<Rc<T>> {
match self {
Self::Notinit => Resource::Loading,
Self::Data { value, expiry: _ } => value.clone()
}
}
pub fn needs_update(&self) -> bool {
match self {
ApiResponse::Notinit => true,
ApiResponse::Data { value: _, expiry } => {
let Some(expiry) = expiry else {
return false;
};
expiry.is_expire()
}
}
}
}
impl<T> Clone for ApiResponse<T> {
fn clone(&self) -> Self {
match self {
ApiResponse::Notinit => ApiResponse::Notinit,
ApiResponse::Data { value, expiry } => {
ApiResponse::Data {
value: value.clone(),
expiry: expiry.clone(),
}
}
}
}
}
pub struct LazyCache<T: 'static> {
id: u64,
value: Value<ApiResponse<T>>,
queued: Rc<ValueMut<bool>>,
request: Rc<RequestBuilder>,
map_response: Rc<dyn Fn(u32, RequestBody) -> MapResponse<T>>,
}
impl<T: 'static> Debug for LazyCache<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct ("LazyCache")
.field("queued", &self.queued)
.finish()
}
}
impl<T> Clone for LazyCache<T> {
fn clone(&self) -> Self {
LazyCache {
id: self.id,
value: self.value.clone(),
queued: self.queued.clone(),
request: self.request.clone(),
map_response: self.map_response.clone(),
}
}
}
impl<T> LazyCache<T> {
pub fn new(
request: RequestBuilder,
map_response: impl Fn(u32, RequestBody) -> MapResponse<T> + 'static
) -> Self {
Self {
id: get_unique_id(),
value: Value::new(ApiResponse::Notinit),
queued: Rc::new(ValueMut::new(false)),
request: Rc::new(request),
map_response: Rc::new(map_response),
}
}
pub fn get(&self, context: &Context) -> Resource<Rc<T>> {
let api_response = self.value.get(context);
if !self.queued.get() && api_response.needs_update() {
self.force_update(false);
}
api_response.get_value()
}
pub fn force_update(&self, with_loading: bool) {
if self.queued.get() {
return;
}
self.queued.set(true); get_driver().inner.api.on_fetch_start.trigger(());
get_driver().spawn({
let queued = self.queued.clone();
let value = self.value.clone();
let request = self.request.clone();
let map_response = self.map_response.clone();
async move {
if !queued.get() {
log::error!("force_update_spawn: queued.get() in spawn -> expected false");
return;
}
let api_response = transaction(|context| {
value.get(context)
});
if api_response.needs_update() {
if with_loading {
value.set(ApiResponse::new_loading());
}
let ttl = request.get_ttl();
let map_response = &(*map_response);
let new_value = request.call().await.into(map_response);
let expiry = ttl.map(|ttl| get_driver().now().add_duration(ttl));
let new_value = match new_value {
Ok(value) => {
Resource::Ready(Rc::new(value))
},
Err(message) => {
Resource::Error(message)
}
};
value.set(ApiResponse::new(new_value, expiry));
}
queued.set(false);
get_driver().inner.api.on_fetch_stop.trigger(());
}
});
}
pub fn to_computed(&self) -> Computed<Resource<Rc<T>>> {
Computed::from({
let state = self.clone();
move |context| {
state.get(context)
}
})
}
}
impl<T> PartialEq for LazyCache<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}