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:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -0,0 +1,365 @@
|
||||
use dioxus_core::{
|
||||
spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, Subscribers, Task,
|
||||
};
|
||||
use dioxus_core::{CapturedError, RenderError, Result, SuspendedFuture};
|
||||
use dioxus_hooks::{use_callback, use_signal};
|
||||
use dioxus_signals::{
|
||||
read_impls, ReadSignal, Readable, ReadableBoxExt, ReadableExt, ReadableRef, Signal, Writable,
|
||||
WritableExt, WriteLock,
|
||||
};
|
||||
use futures_util::{future, pin_mut, FutureExt, StreamExt};
|
||||
use generational_box::{BorrowResult, UnsyncStorage};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::future::Future;
|
||||
use std::{cell::Cell, ops::Deref, rc::Rc};
|
||||
|
||||
/// A hook to create a resource that loads data asynchronously.
|
||||
///
|
||||
/// This hook takes a closure that returns a future. This future will be executed on both the client
|
||||
/// and the server. The loader will return `Loading` until the future resolves, at which point it will
|
||||
/// return a `Loader<T>`. If the future fails, it will return `Loading::Failed`.
|
||||
///
|
||||
/// After the loader has successfully loaded once, it will never suspend the component again, but will
|
||||
/// instead re-load the value in the background whenever any of its dependencies change.
|
||||
///
|
||||
/// If an error occurs while re-loading, `use_loader` will once again emit a `Loading::Failed` value.
|
||||
/// The `use_loader` hook will never return a suspended state after the initial load.
|
||||
///
|
||||
/// # On the server
|
||||
///
|
||||
/// On the server, this hook will block the rendering of the component (and therefore, the page) until
|
||||
/// the future resolves. Any server futures called by `use_loader` will receive the same request context
|
||||
/// as the component that called `use_loader`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
#[track_caller]
|
||||
pub fn use_loader<F, T, E>(mut future: impl FnMut() -> F + 'static) -> Result<Loader<T>, Loading>
|
||||
where
|
||||
F: Future<Output = Result<T, E>> + 'static,
|
||||
T: 'static + std::cmp::PartialEq + Serialize + DeserializeOwned,
|
||||
E: Into<dioxus_core::Error> + 'static,
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
|
||||
// todo: wire up serialize context into loader
|
||||
// let serialize_context = use_hook(crate::transport::serialize_context);
|
||||
// // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||
// let storage_entry: SerializeContextEntry<T> = use_hook(|| serialize_context.create_entry());
|
||||
// use crate::{transport::SerializeContextEntry, use_server_future, use_server_future_unsuspended};
|
||||
|
||||
let mut err = use_signal(|| None as Option<CapturedError>);
|
||||
let mut value = use_signal(|| None as Option<T>);
|
||||
let mut state = use_signal(|| LoaderState::Pending);
|
||||
let (rc, changed) = use_hook(|| {
|
||||
let (rc, changed) = ReactiveContext::new_with_origin(location);
|
||||
(rc, Rc::new(Cell::new(Some(changed))))
|
||||
});
|
||||
|
||||
let callback = use_callback(move |_| {
|
||||
// Set the state to Pending when the task is restarted
|
||||
state.set(LoaderState::Pending);
|
||||
|
||||
// Create the user's task
|
||||
let fut = rc.reset_and_run_in(&mut future);
|
||||
|
||||
// Spawn a wrapper task that polls the inner future and watches its dependencies
|
||||
spawn(async move {
|
||||
// Move the future here and pin it so we can poll it
|
||||
let fut = fut;
|
||||
pin_mut!(fut);
|
||||
|
||||
// Run each poll in the context of the reactive scope
|
||||
// This ensures the scope is properly subscribed to the future's dependencies
|
||||
let res = future::poll_fn(|cx| {
|
||||
rc.run_in(|| {
|
||||
tracing::trace_span!("polling resource", location = %location)
|
||||
.in_scope(|| fut.poll_unpin(cx))
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
// Map the error to the captured error type so it's cheap to clone and pass out
|
||||
// We go through the anyhow::Error first since it's more useful.
|
||||
let res: Result<T, CapturedError> = res.map_err(|e| {
|
||||
let res: dioxus_core::Error = e.into();
|
||||
res.into()
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(v) => {
|
||||
err.set(None);
|
||||
value.set(Some(v));
|
||||
state.set(LoaderState::Ready);
|
||||
}
|
||||
Err(e) => {
|
||||
err.set(Some(e));
|
||||
state.set(LoaderState::Failed);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let mut task = use_hook(|| Signal::new(callback(())));
|
||||
|
||||
use_hook(|| {
|
||||
let mut changed = changed.take().unwrap();
|
||||
spawn(async move {
|
||||
loop {
|
||||
// Wait for the dependencies to change
|
||||
let _ = changed.next().await;
|
||||
|
||||
// Stop the old task
|
||||
task.write().cancel();
|
||||
|
||||
// Start a new task
|
||||
task.set(callback(()));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let read_value = use_hook(|| value.map(|f| f.as_ref().unwrap()).boxed());
|
||||
|
||||
let handle = LoaderHandle {
|
||||
task,
|
||||
err,
|
||||
callback,
|
||||
state,
|
||||
};
|
||||
|
||||
match &*state.read_unchecked() {
|
||||
LoaderState::Pending => Err(Loading::Pending(handle)),
|
||||
LoaderState::Failed => Err(Loading::Failed(handle)),
|
||||
LoaderState::Ready => Ok(Loader {
|
||||
real_value: value,
|
||||
read_value,
|
||||
error: err,
|
||||
state,
|
||||
task,
|
||||
handle,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// A Loader is a signal that represents a value that is loaded asynchronously.
|
||||
///
|
||||
/// Once a `Loader<T>` has been successfully created from `use_loader`, it can be use like a normal signal of type `T`.
|
||||
///
|
||||
/// When the loader is re-reloading its values, it will no longer suspend its component, making it
|
||||
/// very useful for server-side-rendering.
|
||||
pub struct Loader<T> {
|
||||
/// This is a signal that unwraps the inner value. We can't give it out unless we know the inner value is Some(T)!
|
||||
read_value: ReadSignal<T>,
|
||||
|
||||
/// This is the actual signal. We let the user set this value if they want to, but we can't let them set it to `None`.
|
||||
real_value: Signal<Option<T>>,
|
||||
error: Signal<Option<CapturedError>>,
|
||||
state: Signal<LoaderState>,
|
||||
task: Signal<Task>,
|
||||
handle: LoaderHandle,
|
||||
}
|
||||
|
||||
impl<T: 'static> Loader<T> {
|
||||
/// Get the error that occurred during loading, if any.
|
||||
///
|
||||
/// After initial load, this will return `None` until the next reload fails.
|
||||
pub fn error(&self) -> Option<CapturedError> {
|
||||
self.error.read().as_ref().cloned()
|
||||
}
|
||||
|
||||
/// Restart the loading task.
|
||||
///
|
||||
/// After initial load, this won't suspend the component, but will reload in the background.
|
||||
pub fn restart(&mut self) {
|
||||
self.handle.restart();
|
||||
}
|
||||
|
||||
/// Check if the loader has failed.
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.error.read().is_some() && matches!(*self.state.read(), LoaderState::Failed)
|
||||
}
|
||||
|
||||
/// Cancel the current loading task.
|
||||
pub fn cancel(&self) {
|
||||
self.task.read().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Writable for Loader<T> {
|
||||
type WriteMetadata = <Signal<Option<T>> as Writable>::WriteMetadata;
|
||||
|
||||
fn try_write_unchecked(
|
||||
&self,
|
||||
) -> std::result::Result<
|
||||
dioxus_signals::WritableRef<'static, Self>,
|
||||
generational_box::BorrowMutError,
|
||||
>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
let writer = self.real_value.try_write_unchecked()?;
|
||||
Ok(WriteLock::map(writer, |f: &mut Option<T>| {
|
||||
f.as_mut()
|
||||
.expect("Loader value should be set if the `Loader<T>` exists")
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Readable for Loader<T> {
|
||||
type Target = T;
|
||||
type Storage = UnsyncStorage;
|
||||
|
||||
#[track_caller]
|
||||
fn try_read_unchecked(
|
||||
&self,
|
||||
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Ok(self.read_value.read_unchecked())
|
||||
}
|
||||
|
||||
/// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
|
||||
///
|
||||
/// If the signal has been dropped, this will panic.
|
||||
#[track_caller]
|
||||
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Ok(self.peek_unchecked())
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> Subscribers
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.read_value.subscribers()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoAttributeValue for Loader<T>
|
||||
where
|
||||
T: Clone + IntoAttributeValue + PartialEq + 'static,
|
||||
{
|
||||
fn into_value(self) -> dioxus_core::AttributeValue {
|
||||
self.with(|f| f.clone().into_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoDynNode for Loader<T>
|
||||
where
|
||||
T: Clone + IntoDynNode + PartialEq + 'static,
|
||||
{
|
||||
fn into_dyn_node(self) -> dioxus_core::DynamicNode {
|
||||
self().into_dyn_node()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> PartialEq for Loader<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.read_value == other.read_value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Deref for Loader<T>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
{
|
||||
type Target = dyn Fn() -> T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
unsafe { ReadableExt::deref_impl(self) }
|
||||
}
|
||||
}
|
||||
|
||||
read_impls!(Loader<T> where T: PartialEq);
|
||||
|
||||
impl<T> Clone for Loader<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for Loader<T> {}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
|
||||
pub enum LoaderState {
|
||||
/// The loader's future is still running
|
||||
Pending,
|
||||
|
||||
/// The loader's future has completed successfully
|
||||
Ready,
|
||||
|
||||
/// The loader's future has failed and now the loader is in an error state.
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct LoaderHandle {
|
||||
callback: Callback<(), Task>,
|
||||
task: Signal<Task>,
|
||||
err: Signal<Option<CapturedError>>,
|
||||
state: Signal<LoaderState>,
|
||||
}
|
||||
|
||||
impl LoaderHandle {
|
||||
/// Restart the loading task.
|
||||
pub fn restart(&mut self) {
|
||||
self.task.write().cancel();
|
||||
let new_task = self.callback.call(());
|
||||
self.task.set(new_task);
|
||||
}
|
||||
|
||||
/// Get the current state of the loader.
|
||||
pub fn state(&self) -> LoaderState {
|
||||
*self.state.read()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LoaderHandle {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl Copy for LoaderHandle {}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Loading {
|
||||
/// The loader is still pending and the component should suspend.
|
||||
Pending(LoaderHandle),
|
||||
|
||||
/// The loader has failed and an error will be returned up the tree.
|
||||
Failed(LoaderHandle),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Loading {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Loading::Pending(_) => write!(f, "Loading::Pending"),
|
||||
Loading::Failed(_) => write!(f, "Loading::Failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Loading {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Loading::Pending(_) => write!(f, "Loading is still pending"),
|
||||
Loading::Failed(_) => write!(f, "Loading has failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Loading into a RenderError for use with the `?` operator in components
|
||||
impl From<Loading> for RenderError {
|
||||
fn from(val: Loading) -> Self {
|
||||
match val {
|
||||
Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.task.cloned())),
|
||||
Loading::Failed(err) => RenderError::Error(
|
||||
err.err
|
||||
.cloned()
|
||||
.expect("LoaderHandle in Failed state should always have an error"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user