use crate::r#async::ActionState;
use crate::store::Store;
use leptos::prelude::*;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use thiserror::Error;
#[derive(Debug, Error, Clone)]
pub enum ServerActionError {
#[error("Server function error: {0}")]
ServerFn(String),
#[error("Network error: {0}")]
Network(String),
#[error("Request timed out after {0}ms")]
Timeout(u64),
#[error("Store update failed: {0}")]
StoreUpdate(String),
#[error("Action was cancelled")]
Cancelled,
}
pub type ServerActionResult<T> = Result<T, ServerActionError>;
pub trait ServerAction<S: Store>: Send + Sync {
type Input: Clone + Send + Sync + 'static;
type Output: Clone + Send + Sync + 'static;
fn path() -> &'static str;
fn execute(
input: Self::Input,
) -> Pin<Box<dyn Future<Output = ServerActionResult<Self::Output>> + Send>>;
fn on_success(store: &S, output: Self::Output);
fn on_error(_store: &S, _error: &ServerActionError) {
}
fn description() -> &'static str {
std::any::type_name::<Self>()
}
}
pub struct ServerActionHandle<A, S>
where
A: ServerAction<S>,
S: Store,
{
store: S,
input: RwSignal<Option<A::Input>>,
value: RwSignal<Option<A::Output>>,
error: RwSignal<Option<ServerActionError>>,
pending: RwSignal<bool>,
version: RwSignal<u64>,
_marker: PhantomData<A>,
}
impl<A, S> Clone for ServerActionHandle<A, S>
where
A: ServerAction<S>,
S: Store + Clone,
{
fn clone(&self) -> Self {
Self {
store: self.store.clone(),
input: self.input,
value: self.value,
error: self.error,
pending: self.pending,
version: self.version,
_marker: PhantomData,
}
}
}
impl<A, S> ServerActionHandle<A, S>
where
A: ServerAction<S> + 'static,
S: Store + Clone + 'static,
{
pub fn new(store: S) -> Self {
Self {
store,
input: RwSignal::new(None),
value: RwSignal::new(None),
error: RwSignal::new(None),
pending: RwSignal::new(false),
version: RwSignal::new(0),
_marker: PhantomData,
}
}
pub fn store(&self) -> &S {
&self.store
}
pub fn input(&self) -> Option<A::Input> {
self.input.get()
}
pub fn value(&self) -> Option<A::Output> {
self.value.get()
}
pub fn error(&self) -> Option<ServerActionError> {
self.error.get()
}
pub fn pending(&self) -> bool {
self.pending.get()
}
pub fn version(&self) -> u64 {
self.version.get()
}
pub fn state(&self) -> ActionState {
if self.pending.get() {
ActionState::Pending
} else if self.error.get().is_some() {
ActionState::Error
} else if self.value.get().is_some() {
ActionState::Success
} else {
ActionState::Idle
}
}
pub fn clear(&self) {
self.input.set(None);
self.value.set(None);
self.error.set(None);
self.pending.set(false);
}
pub fn dispatch(&self, input: A::Input) {
let handle = self.clone();
let input_clone = input.clone();
self.input.set(Some(input.clone()));
self.error.set(None);
self.pending.set(true);
self.version.update(|v| *v += 1);
leptos::task::spawn_local(async move {
let result = A::execute(input_clone).await;
match result {
Ok(output) => {
A::on_success(&handle.store, output.clone());
handle.value.set(Some(output));
handle.error.set(None);
}
Err(err) => {
A::on_error(&handle.store, &err);
handle.error.set(Some(err));
}
}
handle.pending.set(false);
});
}
}
#[derive(Debug, Clone)]
pub struct OptimisticConfig<T> {
pub optimistic_value: T,
pub rollback_on_error: bool,
pub delay_ms: u64,
}
impl<T: Default> Default for OptimisticConfig<T> {
fn default() -> Self {
Self {
optimistic_value: T::default(),
rollback_on_error: true,
delay_ms: 0,
}
}
}
pub struct OptimisticActionHandle<A, S>
where
A: ServerAction<S>,
S: Store,
{
inner: ServerActionHandle<A, S>,
rollback_value: RwSignal<Option<S::State>>,
}
impl<A, S> Clone for OptimisticActionHandle<A, S>
where
A: ServerAction<S>,
S: Store + Clone,
{
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
rollback_value: self.rollback_value,
}
}
}
impl<A, S> OptimisticActionHandle<A, S>
where
A: ServerAction<S> + 'static,
S: Store + Clone + 'static,
{
pub fn new(store: S) -> Self {
Self {
inner: ServerActionHandle::new(store),
rollback_value: RwSignal::new(None),
}
}
pub fn dispatch_optimistic<F>(&self, input: A::Input, optimistic_update: F)
where
F: FnOnce(&S) + 'static,
{
let current_state = self.inner.store.state().get();
self.rollback_value.set(Some(current_state));
optimistic_update(&self.inner.store);
self.inner.dispatch(input);
}
pub fn inner(&self) -> &ServerActionHandle<A, S> {
&self.inner
}
pub fn pending(&self) -> bool {
self.inner.pending()
}
pub fn error(&self) -> Option<ServerActionError> {
self.inner.error()
}
}
pub struct ServerActionBuilder<S: Store> {
timeout_ms: Option<u64>,
retry_count: u32,
retry_delay_ms: u64,
_marker: PhantomData<S>,
}
impl<S: Store> Default for ServerActionBuilder<S> {
fn default() -> Self {
Self::new()
}
}
impl<S: Store> ServerActionBuilder<S> {
pub fn new() -> Self {
Self {
timeout_ms: None,
retry_count: 0,
retry_delay_ms: 1000,
_marker: PhantomData,
}
}
pub fn with_timeout(mut self, ms: u64) -> Self {
self.timeout_ms = Some(ms);
self
}
pub fn with_retry(mut self, count: u32) -> Self {
self.retry_count = count;
self
}
pub fn with_retry_delay(mut self, ms: u64) -> Self {
self.retry_delay_ms = ms;
self
}
pub fn timeout_ms(&self) -> Option<u64> {
self.timeout_ms
}
pub fn retry_count(&self) -> u32 {
self.retry_count
}
}
pub fn use_server_action<A, S>() -> ServerActionHandle<A, S>
where
A: ServerAction<S> + 'static,
S: Store + Clone + 'static,
{
let store = crate::context::use_store::<S>();
ServerActionHandle::new(store)
}
pub fn create_server_action<A, S>(store: S) -> ServerActionHandle<A, S>
where
A: ServerAction<S> + 'static,
S: Store + Clone + 'static,
{
ServerActionHandle::new(store)
}
pub fn use_optimistic_action<A, S>() -> OptimisticActionHandle<A, S>
where
A: ServerAction<S> + 'static,
S: Store + Clone + 'static,
{
let store = crate::context::use_store::<S>();
OptimisticActionHandle::new(store)
}
pub async fn execute_server_action<A, S>(
store: &S,
input: A::Input,
) -> ServerActionResult<A::Output>
where
A: ServerAction<S>,
S: Store,
{
let result = A::execute(input).await;
match &result {
Ok(output) => A::on_success(store, output.clone()),
Err(err) => A::on_error(store, err),
}
result
}
#[derive(Debug, Clone)]
pub struct ActionHistoryEntry {
pub action: String,
pub timestamp: u64,
pub duration_ms: Option<u64>,
pub success: bool,
pub error: Option<String>,
}
#[derive(Clone)]
pub struct ActionHistory {
entries: RwSignal<Vec<ActionHistoryEntry>>,
max_entries: usize,
}
impl Default for ActionHistory {
fn default() -> Self {
Self::new(100)
}
}
impl ActionHistory {
pub fn new(max_entries: usize) -> Self {
Self {
entries: RwSignal::new(Vec::new()),
max_entries,
}
}
pub fn record(&self, entry: ActionHistoryEntry) {
self.entries.update(|entries| {
entries.push(entry);
if entries.len() > self.max_entries {
entries.remove(0);
}
});
}
pub fn entries(&self) -> Vec<ActionHistoryEntry> {
self.entries.get()
}
pub fn recent(&self, count: usize) -> Vec<ActionHistoryEntry> {
self.entries
.with(|entries| entries.iter().rev().take(count).cloned().collect())
}
pub fn clear(&self) {
self.entries.set(Vec::new());
}
pub fn len(&self) -> usize {
self.entries.with(|e| e.len())
}
pub fn is_empty(&self) -> bool {
self.entries.with(|e| e.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_action_error_display() {
assert!(
ServerActionError::ServerFn("test".to_string())
.to_string()
.contains("Server function")
);
assert!(
ServerActionError::Timeout(5000)
.to_string()
.contains("5000")
);
assert!(
ServerActionError::Cancelled
.to_string()
.contains("cancelled")
);
}
#[test]
fn test_action_history() {
let history = ActionHistory::new(10);
assert!(history.is_empty());
history.record(ActionHistoryEntry {
action: "test".to_string(),
timestamp: 12345,
duration_ms: Some(100),
success: true,
error: None,
});
assert_eq!(history.len(), 1);
let entries = history.entries();
assert_eq!(entries[0].action, "test");
assert!(entries[0].success);
history.clear();
assert!(history.is_empty());
}
#[test]
fn test_action_history_max_entries() {
let history = ActionHistory::new(3);
for i in 0..5 {
history.record(ActionHistoryEntry {
action: format!("action_{}", i),
timestamp: i as u64,
duration_ms: None,
success: true,
error: None,
});
}
assert_eq!(history.len(), 3);
let entries = history.entries();
assert_eq!(entries[0].action, "action_2");
assert_eq!(entries[2].action, "action_4");
}
#[test]
fn test_optimistic_config_default() {
let config: OptimisticConfig<i32> = OptimisticConfig::default();
assert!(config.rollback_on_error);
assert_eq!(config.delay_ms, 0);
}
}