codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)

overhaul dioxus fullstack using a new implementation of server_fn

This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-03 18:26:19 -07:00
1 parent 8421c3aef0
commit f2b1c6d97f
424 files changed
+17284 -6624

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ dioxus-signals = { workspace = true }
futures-channel = { workspace = true }
tracing = { workspace = true }
slab = { workspace = true }
futures-util = { workspace = true}
futures-util = { workspace = true, features = ["std"] }
generational-box = { workspace = true }
rustversion = { workspace = true }
warnings = { workspace = true }
+6
View File
@@ -98,3 +98,9 @@ pub use use_set_compare::*;
mod use_after_suspense_resolved;
pub use use_after_suspense_resolved::*;
mod use_action;
pub use use_action::*;
mod use_waker;
pub use use_waker::*;
+288
View File
@@ -0,0 +1,288 @@
use crate::{use_callback, use_signal};
use dioxus_core::{use_hook, Callback, CapturedError, Result, Task};
use dioxus_signals::{ReadSignal, ReadableBoxExt, ReadableExt, Signal, WritableExt};
use futures_channel::oneshot::Receiver;
use futures_util::{future::Shared, FutureExt};
use std::{marker::PhantomData, prelude::rust_2024::Future};
pub fn use_action<E, C, M>(mut user_fn: C) -> Action<C::Input, C::Output>
where
E: Into<CapturedError> + 'static,
C: ActionCallback<M, E>,
M: 'static,
C::Input: 'static,
C::Output: 'static,
C: 'static,
{
let mut value = use_signal(|| None as Option<C::Output>);
let mut error = use_signal(|| None as Option<CapturedError>);
let mut task = use_signal(|| None as Option<Task>);
let mut state = use_signal(|| ActionState::Unset);
let callback = use_callback(move |input: C::Input| {
// Cancel any existing task
if let Some(task) = task.take() {
task.cancel();
}
let (tx, rx) = futures_channel::oneshot::channel();
let rx = rx.shared();
// Spawn a new task, and *then* fire off the async
let result = user_fn.call(input);
let new_task = dioxus_core::spawn(async move {
// Set the state to pending
state.set(ActionState::Pending);
// Create a new task
let result = result.await;
match result {
Ok(res) => {
error.set(None);
value.set(Some(res));
state.set(ActionState::Ready);
}
Err(err) => {
error.set(Some(err.into()));
value.set(None);
state.set(ActionState::Errored);
}
}
tx.send(()).ok();
});
task.set(Some(new_task));
rx
});
// Create a reader that maps the Option<T> to T, unwrapping the Option
// This should only be handed out if we know the value is Some. We never set the value back to None, only modify the state of the action
let reader = use_hook(|| value.boxed().map(|v| v.as_ref().unwrap()).boxed());
Action {
value,
error,
task,
callback,
reader,
_phantom: PhantomData,
state,
}
}
pub struct Action<I, T> {
reader: ReadSignal<T>,
error: Signal<Option<CapturedError>>,
value: Signal<Option<T>>,
task: Signal<Option<Task>>,
callback: Callback<I, Shared<Receiver<()>>>,
state: Signal<ActionState>,
_phantom: PhantomData<*const I>,
}
impl<I: 'static, O: 'static> Action<I, O> {
pub fn value(&self) -> Option<ReadSignal<O>> {
if self.value.read().is_none() {
return None;
}
Some(self.reader)
}
pub fn result(&self) -> Option<Result<ReadSignal<O>, CapturedError>> {
if !matches!(
*self.state.read(),
ActionState::Ready | ActionState::Errored
) {
return None;
}
if let Some(err) = self.error.cloned() {
return Some(Err(err));
}
if self.value.read().is_none() {
return None;
}
Some(Ok(self.reader))
}
pub fn is_pending(&self) -> bool {
*self.state.read() == ActionState::Pending
}
/// Clear the current value and error, setting the state to Reset
pub fn reset(&mut self) {
self.state.set(ActionState::Reset);
if let Some(t) = self.task.take() {
t.cancel()
}
}
pub fn cancel(&mut self) {
if let Some(t) = self.task.take() {
t.cancel()
}
self.state.set(ActionState::Reset);
}
}
impl<I, T> std::fmt::Debug for Action<I, T>
where
T: std::fmt::Debug + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
f.debug_struct("Action")
.field("state", &self.state.read())
.field("value", &self.value.read())
.field("error", &self.error.read())
.finish()
} else {
std::fmt::Debug::fmt(&self.value.read().as_ref(), f)
}
}
}
pub struct Dispatching<I> {
_phantom: PhantomData<*const I>,
receiver: Shared<Receiver<()>>,
}
impl<T> Dispatching<T> {
pub(crate) fn new(receiver: Shared<Receiver<()>>) -> Self {
Self {
_phantom: PhantomData,
receiver,
}
}
}
impl<T> std::future::Future for Dispatching<T> {
type Output = ();
fn poll(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.receiver.poll_unpin(_cx) {
std::task::Poll::Ready(_) => std::task::Poll::Ready(()),
std::task::Poll::Pending => std::task::Poll::Pending,
}
}
}
impl<I, T> Copy for Action<I, T> {}
impl<I, T> Clone for Action<I, T> {
fn clone(&self) -> Self {
*self
}
}
/// The state of an action
///
/// We can never reset the state to Unset, only to Reset, otherwise the value reader would panic.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
enum ActionState {
Unset,
Pending,
Ready,
Errored,
Reset,
}
pub trait ActionCallback<M, E> {
type Input;
type Output;
fn call(
&mut self,
input: Self::Input,
) -> impl Future<Output = Result<Self::Output, E>> + 'static;
}
impl<F, O, G, E> ActionCallback<(O,), E> for F
where
F: FnMut() -> G,
G: Future<Output = Result<O, E>> + 'static,
{
type Input = ();
type Output = O;
fn call(
&mut self,
_input: Self::Input,
) -> impl Future<Output = Result<Self::Output, E>> + 'static {
(self)()
}
}
impl<F, O, A, G, E> ActionCallback<(A, O), E> for F
where
F: FnMut(A) -> G,
G: Future<Output = Result<O, E>> + 'static,
{
type Input = (A,);
type Output = O;
fn call(
&mut self,
input: Self::Input,
) -> impl Future<Output = Result<Self::Output, E>> + 'static {
let (a,) = input;
(self)(a)
}
}
impl<O, A, B, F, G, E> ActionCallback<(A, B, O), E> for F
where
F: FnMut(A, B) -> G,
G: Future<Output = Result<O, E>> + 'static,
{
type Input = (A, B);
type Output = O;
fn call(
&mut self,
input: Self::Input,
) -> impl Future<Output = Result<Self::Output, E>> + 'static {
let (a, b) = input;
(self)(a, b)
}
}
impl<O, A, B, C, F, G, E> ActionCallback<(A, B, C, O), E> for F
where
F: FnMut(A, B, C) -> G,
G: Future<Output = Result<O, E>> + 'static,
{
type Input = (A, B, C);
type Output = O;
fn call(
&mut self,
input: Self::Input,
) -> impl Future<Output = Result<Self::Output, E>> + 'static {
let (a, b, c) = input;
(self)(a, b, c)
}
}
impl<O> Action<(), O> {
pub fn call(&mut self) -> Dispatching<()> {
Dispatching::new((self.callback).call(()))
}
}
impl<A: 'static, O> Action<(A,), O> {
pub fn call(&mut self, _a: A) -> Dispatching<()> {
Dispatching::new((self.callback).call((_a,)))
}
}
impl<A: 'static, B: 'static, O> Action<(A, B), O> {
pub fn call(&mut self, _a: A, _b: B) -> Dispatching<()> {
Dispatching::new((self.callback).call((_a, _b)))
}
}
impl<A: 'static, B: 'static, C: 'static, O> Action<(A, B, C), O> {
pub fn call(&mut self, _a: A, _b: B, _c: C) -> Dispatching<()> {
Dispatching::new((self.callback).call((_a, _b, _c)))
}
}
+7 -1
View File
@@ -11,7 +11,13 @@ pub fn use_callback<T: 'static, O: 'static>(f: impl FnMut(T) -> O + 'static) ->
// Create a copyvalue with no contents
// This copyvalue is generic over F so that it can be sized properly
let mut inner = use_hook(|| Callback::new(callback.take().unwrap()));
let mut inner = use_hook(|| {
Callback::new(
callback
.take()
.expect("Callback cannot be None on first call"),
)
});
if let Some(callback) = callback.take() {
// Every time this hook is called replace the inner callback with the new callback
+26 -2
View File
@@ -1,13 +1,16 @@
#![allow(missing_docs)]
use crate::{use_callback, use_signal};
use crate::{use_callback, use_signal, use_waker, UseWaker};
use dioxus_core::{
spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, RenderError,
Subscribers, SuspendedFuture, Task,
};
use dioxus_signals::*;
use futures_util::{future, pin_mut, FutureExt, StreamExt};
use futures_util::{
future::{self},
pin_mut, FutureExt, StreamExt,
};
use std::ops::Deref;
use std::{cell::Cell, future::Future, rc::Rc};
@@ -31,6 +34,8 @@ where
(rc, Rc::new(Cell::new(Some(changed))))
});
let mut waker = use_waker::<()>();
let cb = use_callback(move |_| {
// Set the state to Pending when the task is restarted
state.set(UseResourceState::Pending);
@@ -57,6 +62,9 @@ where
// Set the value and state
state.set(UseResourceState::Ready);
value.set(Some(res));
// Notify that the value has changed
waker.wake(());
})
});
@@ -82,6 +90,7 @@ where
task,
value,
state,
waker,
callback: cb,
}
}
@@ -113,6 +122,7 @@ where
/// ```
#[derive(Debug)]
pub struct Resource<T: 'static> {
waker: UseWaker<()>,
value: Signal<Option<T>>,
task: Signal<Task>,
state: Signal<UseResourceState>,
@@ -486,3 +496,17 @@ impl<T: Clone> Deref for Resource<T> {
unsafe { ReadableExt::deref_impl(self) }
}
}
impl<T> std::future::Future for Resource<T> {
type Output = ();
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.waker.clone().poll_unpin(cx) {
std::task::Poll::Ready(_) => std::task::Poll::Ready(()),
std::task::Poll::Pending => std::task::Poll::Pending,
}
}
}
+72
View File
@@ -0,0 +1,72 @@
use dioxus_core::use_hook;
use dioxus_signals::{ReadableExt, Signal, WritableExt};
use futures_channel::oneshot::{Canceled, Receiver, Sender};
use futures_util::{future::Shared, FutureExt};
/// A hook that provides a waker for other hooks to provide async/await capabilities.
///
/// This hook is a reactive wrapper over the `Shared<T>` future from the `futures` crate.
/// It allows multiple awaiters to wait on the same value, similar to a broadcast channel from Tokio.
///
/// Calling `.await` on the waker will consume the waker, so you'll need to call `.wait()` on the
/// source to get a new waker.
pub fn use_waker<T: Clone + 'static>() -> UseWaker<T> {
// We use a oneshot channel to send the value to the awaiter.
// The shared future allows multiple awaiters to wait on the same value.
let (task_tx, task_rx) = use_hook(|| {
let (tx, rx) = futures_channel::oneshot::channel::<T>();
let shared = rx.shared();
(Signal::new(tx), Signal::new(shared))
});
UseWaker { task_tx, task_rx }
}
#[derive(Debug)]
pub struct UseWaker<T: 'static> {
task_tx: Signal<Sender<T>>,
task_rx: Signal<Shared<Receiver<T>>>,
}
impl<T: Clone + 'static> UseWaker<T> {
/// Wake the current task with the provided value.
/// All awaiters will receive a clone of the value.
pub fn wake(&mut self, value: T) {
// We ignore the error because it means the task has already been woken.
let (tx, rx) = futures_channel::oneshot::channel::<T>();
let shared = rx.shared();
// Swap out the old sender and receiver with the new ones.
let tx = self.task_tx.replace(tx);
let _rx = self.task_rx.replace(shared);
// And then send out the oneshot value, waking up all awaiters.
let _ = tx.send(value);
}
/// Returns a future that resolves when the task is woken.
pub async fn wait(&self) -> Result<T, Canceled> {
self.task_rx.cloned().await
}
}
// Can await the waker to be woken.
// We use `.peek()` here to avoid reacting to changes in the underlying task_rx which could lead
// to an effect/future loop.
impl<T: Clone + 'static> std::future::Future for UseWaker<T> {
type Output = Result<T, Canceled>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
self.task_rx.peek().clone().poll_unpin(cx)
}
}
impl<T> Copy for UseWaker<T> {}
impl<T> Clone for UseWaker<T> {
fn clone(&self) -> Self {
*self
}
}