use std::future::Future;
use std::marker::PhantomData;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::spawn_local;
use crate::core::{
MessageContext, MessageResult, Mut, NoElement, View, ViewId, ViewMarker, ViewPathTracker,
};
use crate::{OptionalAction, ViewCtx};
pub struct MemoizedAwait<State, Action, OA, InitFuture, Data, Callback, F, FOut> {
init_future: InitFuture,
data: Data,
callback: Callback,
debounce_ms: usize,
reset_debounce_on_update: bool,
phantom: PhantomData<fn() -> (State, Action, OA, F, FOut)>,
}
impl<State, Action, OA, InitFuture, Data, Callback, F, FOut>
MemoizedAwait<State, Action, OA, InitFuture, Data, Callback, F, FOut>
where
FOut: std::fmt::Debug + 'static,
F: Future<Output = FOut> + 'static,
InitFuture: Fn(&Data) -> F,
{
pub fn debounce_ms(mut self, milliseconds: usize) -> Self {
self.debounce_ms = milliseconds;
self
}
pub fn reset_debounce_on_update(mut self, reset: bool) -> Self {
self.reset_debounce_on_update = reset;
self
}
fn init_future(&self, ctx: &mut ViewCtx, generation: u64) {
ctx.with_id(ViewId::new(generation), |ctx| {
let thunk = ctx.message_thunk();
let future = (self.init_future)(&self.data);
spawn_local(async move {
thunk.push_message(MemoizedAwaitMessage::<FOut>::Output(future.await));
});
});
}
}
pub fn memoized_await<State, Action, OA, InitFuture, Data, Callback, F, FOut>(
data: Data,
init_future: InitFuture,
callback: Callback,
) -> MemoizedAwait<State, Action, OA, InitFuture, Data, Callback, F, FOut>
where
State: 'static,
Action: 'static,
Data: PartialEq + 'static,
FOut: std::fmt::Debug + 'static,
F: Future<Output = FOut> + 'static,
InitFuture: Fn(&Data) -> F + 'static,
OA: OptionalAction<Action> + 'static,
Callback: Fn(&mut State, FOut) -> OA + 'static,
{
MemoizedAwait {
init_future,
data,
callback,
debounce_ms: 0,
reset_debounce_on_update: true,
phantom: PhantomData,
}
}
#[derive(Default)]
#[expect(
unnameable_types,
reason = "Implementation detail, public because of trait visibility rules"
)]
pub struct MemoizedAwaitState {
generation: u64,
schedule_update: bool,
schedule_update_fn: Option<Closure<dyn FnMut()>>,
schedule_update_timeout_handle: Option<i32>,
update: bool,
}
impl MemoizedAwaitState {
fn clear_update_timeout(&mut self) {
if let Some(handle) = self.schedule_update_timeout_handle {
web_sys::window()
.unwrap_throw()
.clear_timeout_with_handle(handle);
}
self.schedule_update_timeout_handle = None;
self.schedule_update_fn = None;
}
fn reset_debounce_timeout_and_schedule_update<FOut: std::fmt::Debug + 'static>(
&mut self,
ctx: &mut ViewCtx,
debounce_duration: usize,
) {
ctx.with_id(ViewId::new(self.generation), |ctx| {
self.clear_update_timeout();
let thunk = ctx.message_thunk();
let schedule_update_fn = Closure::new(move || {
thunk.push_message(MemoizedAwaitMessage::<FOut>::ScheduleUpdate);
});
let handle = web_sys::window()
.unwrap_throw()
.set_timeout_with_callback_and_timeout_and_arguments_0(
schedule_update_fn.as_ref().unchecked_ref(),
debounce_duration.try_into().unwrap_throw(),
)
.unwrap_throw();
self.schedule_update_fn = Some(schedule_update_fn);
self.schedule_update_timeout_handle = Some(handle);
self.schedule_update = true;
});
}
}
#[derive(Debug)]
enum MemoizedAwaitMessage<Output: std::fmt::Debug> {
Output(Output),
ScheduleUpdate,
}
impl<State, Action, OA, InitFuture, Data, CB, F, FOut> ViewMarker
for MemoizedAwait<State, Action, OA, InitFuture, Data, CB, F, FOut>
{
}
impl<State, Action, InitFuture, F, FOut, Data, CB, OA> View<State, Action, ViewCtx>
for MemoizedAwait<State, Action, OA, InitFuture, Data, CB, F, FOut>
where
State: 'static,
Action: 'static,
OA: OptionalAction<Action> + 'static,
InitFuture: Fn(&Data) -> F + 'static,
FOut: std::fmt::Debug + 'static,
Data: PartialEq + 'static,
F: Future<Output = FOut> + 'static,
CB: Fn(&mut State, FOut) -> OA + 'static,
{
type Element = NoElement;
type ViewState = MemoizedAwaitState;
fn build(&self, ctx: &mut ViewCtx, _: &mut State) -> (Self::Element, Self::ViewState) {
let mut state = MemoizedAwaitState::default();
if self.debounce_ms > 0 {
state.reset_debounce_timeout_and_schedule_update::<FOut>(ctx, self.debounce_ms);
} else {
self.init_future(ctx, state.generation);
}
(NoElement, state)
}
fn rebuild(
&self,
prev: &Self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
(): Mut<'_, Self::Element>,
_: &mut State,
) {
let debounce_has_changed_and_update_is_scheduled = view_state.schedule_update
&& (prev.reset_debounce_on_update != self.reset_debounce_on_update
|| prev.debounce_ms != self.debounce_ms);
if debounce_has_changed_and_update_is_scheduled {
if self.debounce_ms == 0 {
if view_state.schedule_update_timeout_handle.is_some() {
view_state.clear_update_timeout();
view_state.schedule_update = false;
view_state.update = true;
}
} else {
view_state
.reset_debounce_timeout_and_schedule_update::<FOut>(ctx, self.debounce_ms);
return; }
}
if view_state.update
|| (prev.data != self.data
&& (!view_state.schedule_update || self.reset_debounce_on_update))
{
if !view_state.update && self.debounce_ms > 0 {
view_state
.reset_debounce_timeout_and_schedule_update::<FOut>(ctx, self.debounce_ms);
} else {
view_state.generation += 1;
view_state.update = false;
self.init_future(ctx, view_state.generation);
}
}
}
fn teardown(&self, state: &mut Self::ViewState, _: &mut ViewCtx, (): Mut<'_, Self::Element>) {
state.clear_update_timeout();
}
fn message(
&self,
view_state: &mut Self::ViewState,
message: &mut MessageContext,
(): Mut<'_, Self::Element>,
app_state: &mut State,
) -> MessageResult<Action> {
assert_eq!(
message.remaining_path().len(),
1,
"MemoizedAwait doesn't have children but controls a single path element, got {message:?}."
);
let my_id = message.take_first().unwrap();
if my_id.routing_id() == view_state.generation {
match *message.take_message().unwrap_throw() {
MemoizedAwaitMessage::Output(future_output) => {
match (self.callback)(app_state, future_output).action() {
Some(action) => MessageResult::Action(action),
None => MessageResult::Nop,
}
}
MemoizedAwaitMessage::ScheduleUpdate => {
view_state.update = true;
view_state.schedule_update = false;
MessageResult::RequestRebuild
}
}
} else {
MessageResult::Stale
}
}
}