use futures::future::BoxFuture;
use leptos::prelude::*;
use pin_project_lite::pin_project;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use thiserror::Error;
use crate::store::Store;
#[derive(Debug, Error)]
pub enum ActionError {
#[error("Action cancelled")]
Cancelled,
#[error("Action timed out after {0}ms")]
Timeout(u64),
#[error("Action failed: {0}")]
Failed(String),
#[error("Network error: {0}")]
Network(String),
#[error("Validation error: {0}")]
Validation(String),
}
impl ActionError {
pub fn failed(msg: impl Into<String>) -> Self {
Self::Failed(msg.into())
}
pub fn network(msg: impl Into<String>) -> Self {
Self::Network(msg.into())
}
pub fn validation(msg: impl Into<String>) -> Self {
Self::Validation(msg.into())
}
}
pub type ActionResult<T, E = ActionError> = Result<T, E>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum ActionState {
#[default]
Idle,
Pending,
Success,
Error,
}
impl ActionState {
pub fn is_idle(&self) -> bool {
matches!(self, Self::Idle)
}
pub fn is_pending(&self) -> bool {
matches!(self, Self::Pending)
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
pub fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
pub fn is_finished(&self) -> bool {
matches!(self, Self::Success | Self::Error)
}
}
pub trait Action<S: Store> {
type Output;
fn execute(&self, store: &S) -> Self::Output;
}
pub trait AsyncAction<S: Store>: Send + Sync {
type Output: Send;
type Error: Send + std::error::Error;
fn execute(
&self,
store: &S,
) -> impl Future<Output = ActionResult<Self::Output, Self::Error>> + Send;
}
pub type BoxedAsyncAction<S, O, E> =
Box<dyn Fn(&S) -> BoxFuture<'static, ActionResult<O, E>> + Send + Sync>;
pub struct AsyncActionBuilder<S: Store, O, E> {
timeout_ms: Option<u64>,
retry_count: u32,
_marker: PhantomData<(S, O, E)>,
}
impl<S: Store, O, E> Default for AsyncActionBuilder<S, O, E> {
fn default() -> Self {
Self::new()
}
}
impl<S: Store, O, E> AsyncActionBuilder<S, O, E> {
pub fn new() -> Self {
Self {
timeout_ms: None,
retry_count: 0,
_marker: PhantomData,
}
}
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.timeout_ms = Some(timeout_ms);
self
}
pub fn with_retry(mut self, count: u32) -> Self {
self.retry_count = count;
self
}
pub fn timeout_ms(&self) -> Option<u64> {
self.timeout_ms
}
pub fn retry_count(&self) -> u32 {
self.retry_count
}
}
pin_project! {
pub struct ActionFuture<F> {
#[pin]
inner: F,
state: ActionState,
}
}
impl<F> ActionFuture<F> {
pub fn new(inner: F) -> Self {
Self {
inner,
state: ActionState::Pending,
}
}
pub fn state(&self) -> &ActionState {
&self.state
}
}
impl<F, T, E> Future for ActionFuture<F>
where
F: Future<Output = ActionResult<T, E>>,
{
type Output = ActionResult<T, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.inner.poll(cx) {
Poll::Ready(Ok(value)) => {
*this.state = ActionState::Success;
Poll::Ready(Ok(value))
}
Poll::Ready(Err(err)) => {
*this.state = ActionState::Error;
Poll::Ready(Err(err))
}
Poll::Pending => Poll::Pending,
}
}
}
#[derive(Clone)]
pub struct ReactiveAction<I, O>
where
I: Clone + Send + Sync + 'static,
O: Clone + Send + Sync + 'static,
{
input: RwSignal<Option<I>>,
value: RwSignal<Option<O>>,
pending: RwSignal<bool>,
version: RwSignal<usize>,
}
impl<I, O> Default for ReactiveAction<I, O>
where
I: Clone + Send + Sync + 'static,
O: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<I, O> ReactiveAction<I, O>
where
I: Clone + Send + Sync + 'static,
O: Clone + Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
input: RwSignal::new(None),
value: RwSignal::new(None),
pending: RwSignal::new(false),
version: RwSignal::new(0),
}
}
pub fn input(&self) -> Option<I> {
self.input.get()
}
pub fn value(&self) -> Option<O> {
self.value.get()
}
pub fn pending(&self) -> bool {
self.pending.get()
}
pub fn version(&self) -> usize {
self.version.get()
}
#[allow(dead_code)]
fn set_input(&self, input: I) {
self.input.set(Some(input));
}
#[allow(dead_code)]
fn set_value(&self, value: O) {
self.value.set(Some(value));
self.pending.set(false);
}
fn set_pending(&self) {
self.pending.set(true);
self.version.update(|v| *v += 1);
}
fn clear_internal(&self) {
self.input.set(None);
self.value.set(None);
self.pending.set(false);
}
pub fn dispatch(&self, input: I) -> ActionHandle<O> {
self.set_input(input);
self.set_pending();
ActionHandle {
value: self.value,
pending: self.pending,
}
}
pub fn clear(&self) {
self.clear_internal();
}
}
#[derive(Clone)]
pub struct ActionHandle<O: Clone + Send + Sync + 'static> {
value: RwSignal<Option<O>>,
pending: RwSignal<bool>,
}
impl<O: Clone + Send + Sync + 'static> ActionHandle<O> {
pub fn complete(self, value: O) {
self.value.set(Some(value));
self.pending.set(false);
}
pub fn set_value(self, value: O) {
self.complete(value);
}
pub fn cancel(self) {
self.pending.set(false);
}
}
pub trait StoreActionExt: Store + Sized {
fn dispatch<A>(&self, action: A) -> A::Output
where
A: Action<Self>,
{
action.execute(self)
}
}
impl<S: Store> StoreActionExt for S {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_state_default() {
let state = ActionState::default();
assert!(state.is_idle());
}
#[test]
fn test_action_state_transitions() {
assert!(ActionState::Idle.is_idle());
assert!(!ActionState::Idle.is_pending());
assert!(!ActionState::Idle.is_finished());
assert!(ActionState::Pending.is_pending());
assert!(!ActionState::Pending.is_idle());
assert!(!ActionState::Pending.is_finished());
assert!(ActionState::Success.is_success());
assert!(ActionState::Success.is_finished());
assert!(ActionState::Error.is_error());
assert!(ActionState::Error.is_finished());
}
#[test]
fn test_action_error_display() {
let err = ActionError::Cancelled;
assert_eq!(err.to_string(), "Action cancelled");
let err = ActionError::Timeout(5000);
assert_eq!(err.to_string(), "Action timed out after 5000ms");
let err = ActionError::failed("Something went wrong");
assert_eq!(err.to_string(), "Action failed: Something went wrong");
let err = ActionError::network("Connection refused");
assert_eq!(err.to_string(), "Network error: Connection refused");
let err = ActionError::validation("Invalid email");
assert_eq!(err.to_string(), "Validation error: Invalid email");
}
#[test]
fn test_reactive_action_creation() {
let action: ReactiveAction<String, i32> = ReactiveAction::new();
assert!(action.input().is_none());
assert!(action.value().is_none());
assert!(!action.pending());
assert_eq!(action.version(), 0);
}
#[test]
fn test_reactive_action_state_changes() {
let action: ReactiveAction<String, i32> = ReactiveAction::new();
let handle = action.dispatch("test".to_string());
assert_eq!(action.input(), Some("test".to_string()));
assert!(action.pending());
assert_eq!(action.version(), 1);
handle.complete(42);
assert_eq!(action.value(), Some(42));
assert!(!action.pending());
action.clear();
assert!(action.input().is_none());
assert!(action.value().is_none());
}
#[test]
fn test_action_handle_complete() {
let action: ReactiveAction<String, i32> = ReactiveAction::new();
let handle = action.dispatch("query".to_string());
assert!(action.pending());
handle.complete(100);
assert!(!action.pending());
assert_eq!(action.value(), Some(100));
}
#[test]
fn test_action_handle_cancel() {
let action: ReactiveAction<String, i32> = ReactiveAction::new();
let handle = action.dispatch("query".to_string());
assert!(action.pending());
handle.cancel();
assert!(!action.pending());
assert!(action.value().is_none()); }
}