Compare commits

..

52 Commits

Author SHA1 Message Date
Greg Johnston
faa73ff4db fmt 2024-10-18 16:21:02 -04:00
Penguinwithatie
7291efc077 add tachys::view::fragment::Fragment to prelude 2024-10-16 12:07:42 -06:00
stefnotch
ee66f6c395 Add support for user-supplied executors (#3091) 2024-10-16 06:24:07 -07:00
Greg Johnston
eba08ad592 fix: don't render empty string as a space in unescaped elements (closes #3120) (#3122) 2024-10-15 18:57:09 -04:00
Greg Johnston
4833b4e287 fix: avoid double-polling synchronously-available Suspend (closes #3113) (#3114) 2024-10-15 08:49:40 -04:00
Greg Johnston
9d1be64e4d chore: publish stores (#3110) 2024-10-14 10:18:38 -04:00
benwis
d6e6cd3be0 v0.7.0gamma3 2024-10-14 05:01:19 -07:00
stefnotch
70476f9277 feat: add support for async-executor from smol-rs (#3090) 2024-10-14 07:57:19 -04:00
zakstucke
d8ddfc26e9 perf: use the Track trait for the Signal wrapper. (#3076) 2024-10-12 20:29:03 -04:00
stefnotch
c8acc3e8bd fix: correctly support local pools for futures-executor (#3089) 2024-10-12 20:13:50 -04:00
zakstucke
547442243b impl IntoClass for Option<impl IntoClass> (#3104) 2024-10-12 05:03:53 -07:00
Greg Johnston
6e58266f54 feat: support set_is_routing/RoutingProgress for nested routes (#3101) 2024-10-11 19:05:33 -04:00
Greg Johnston
f0cd0fb41d feat: condense Router/Routes base prop into one (#3100) 2024-10-11 14:06:11 -04:00
Daniil Polyakov
7585faf57e fix: use full path to Result in Params derive (#3096) 2024-10-10 15:20:38 -04:00
zakstucke
da7f6a34e8 chore: expose AnyView in prelude (#3099) 2024-10-10 15:20:24 -04:00
Greg Johnston
4f7fa41262 fix: don't on WASM server targets unless you actually try to generate static routes (closes #3094) (#3097) 2024-10-10 15:20:04 -04:00
Greg Johnston
4becfa39ca correct version number 2024-10-10 09:13:39 -04:00
Greg Johnston
f8388b122d fix: avoid reentering lock when initializing nested keyed store fields (closes #3086) (#3087) 2024-10-10 08:53:28 -04:00
Greg Johnston
f57a57b92b feat: restore AnimatedShow for 0.7 (#3084) 2024-10-10 08:53:05 -04:00
vsuryamurthy
f0bcbd9cfe remove unused dependencies leptos_axum and leptos_router (#2960)
* remove unused dependencies leptos_axum and leptos_router

* cargo fmt

* Restore http::Uri under default feature

* use axum re-exported headers instead of http directly

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2024-10-10 04:29:11 -07:00
Greg Johnston
115477ef1d chore: remove unused code from leptos package (#3085) 2024-10-10 04:23:37 -07:00
Greg Johnston
832b9cb321 chore: pin wasm-bindgen to 0.2.93 to fix example builds (#3088) 2024-10-09 22:56:05 -04:00
Greg Johnston
b0150ceeec fix: missing Copy/Clone implementations for OnceResource (#3080) 2024-10-09 19:33:33 -04:00
dependabot[bot]
af8df34360 chore(deps): bump denoland/setup-deno from 1 to 2 (#3081)
Bumps [denoland/setup-deno](https://github.com/denoland/setup-deno) from 1 to 2.
- [Release notes](https://github.com/denoland/setup-deno/releases)
- [Commits](https://github.com/denoland/setup-deno/compare/v1...v2)

---
updated-dependencies:
- dependency-name: denoland/setup-deno
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 18:50:31 -04:00
Greg Johnston
b2e6185b22 fix: don't escape script/style/textarea in InertHtml (closes #3078) (#3079) 2024-10-09 10:07:40 -04:00
Greg Johnston
d2bfb3080b Merge pull request #3077 from leptos-rs/router-fixes
Router fixes
2024-10-09 07:33:24 -04:00
Greg Johnston
72ebd17042 fix: only set browser URL if it matches current router URL (closes #2979( 2024-10-08 22:12:18 -04:00
Greg Johnston
e2f0b4deeb fix: prevent simultaneous \query_signal\ writes from canceling each other (closes #2369) 2024-10-08 22:12:02 -04:00
Greg Johnston
57c07e9aec feat: enable faster compile times with RUSTFLAGS="--cfg erase_components (#2905) 2024-10-08 17:03:40 -04:00
Greg Johnston
0835066bc0 chore: re-add regression tests from #2639 (#3073) 2024-10-08 17:02:18 -04:00
webmstk
656e83fe24 docs: fix comment for set_interval helper (#3074) 2024-10-08 13:30:17 -04:00
Greg Johnston
ad0252ecfd fix: inconsistencies in check for latest version in actions (#3070) 2024-10-07 21:02:02 -04:00
Greg Johnston
77f05c6f4e fix: add HEAD support for Actix in leptos_routes (closes #2885) (#3069) 2024-10-07 21:01:46 -04:00
zakstucke
a4ea491dc0 feat: add Read/ReadUntracked/Track for Signal/MaybeSignal/MaybeProp (#3031) 2024-10-07 19:55:07 -04:00
Greg Johnston
3c89b9c930 feat: add an optimized OnceResource and use it to rebuild Await (#3064) 2024-10-06 20:47:22 -04:00
Greg Johnston
93d7ba0d5f fix: add SVG <use> (closes #3065) (#3067) 2024-10-06 20:47:06 -04:00
Greg Johnston
e188993800 fix: remove unnecessary Send/Sync bounds on LocalResource (#3061) 2024-10-04 16:16:24 -04:00
Greg Johnston
c1dc8c7629 Merge pull request #3062 from leptos-rs/into-render
feat: add `IntoRender` for rendering custom data
2024-10-04 14:43:55 -04:00
Greg Johnston
ab9de1b8c0 chore: remove unused variable 2024-10-04 13:56:38 -04:00
Greg Johnston
b39985d9b8 fix: only use IntoAttributeValue for parts of view that are actually attribute values 2024-10-04 13:38:09 -04:00
Greg Johnston
5e8e93001d docs: IntoRender and IntoAttributeValue 2024-10-04 13:25:57 -04:00
Greg Johnston
a4ed0cbe5b feat: add IntoAttributeValue for rendering arbitrary attribute values 2024-10-04 13:24:39 -04:00
Greg Johnston
422fe9f43b feat: add IntoRender for rendering arbitrary types 2024-10-04 13:13:23 -04:00
kczimm
36df36e16c feat: allow ParamsMap to support multiple values per key (closes #2882) (#2966)
Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2024-10-03 18:35:50 -04:00
Chris
95fc79034b chore: dead router::router module from 0.6 (#2943) 2024-10-02 19:35:49 -04:00
Greg Johnston
7403e4084f Merge pull request #3040 from mahdi739/double-ended-iterator-for-stores
Double-ended-iterator-for-stores
2024-10-02 19:19:40 -04:00
jk
8feee5e5d7 Migrate rkyv 0.8.x (#3054) 2024-10-02 10:03:20 -07:00
Greg Johnston
e6da266b4f Merge pull request #3050 from leptos-rs/2086
Module restructuring and docs cleanup
2024-10-01 21:23:47 -04:00
Greg Johnston
dcc7865989 fix: remove r# from raw attribute names in InertHtml (closes #3049) (#3058) 2024-10-01 20:18:29 -04:00
Greg Johnston
c47893ad60 fix: <textarea> does not parse its children as HTML, like <script> and <style> (#3052) 2024-10-01 19:39:10 -04:00
Mahdi
4c3bcaa68d implement DoubleEndedIterator for StoreFieldKeyedIter 2024-09-28 17:15:43 +03:30
Mahdi
fe060617d2 implement DoubleEndedIterator for StoreFieldIter 2024-09-28 17:14:45 +03:30
92 changed files with 3076 additions and 3004 deletions

View File

@@ -94,7 +94,7 @@ jobs:
fi
done
- name: Install Deno
uses: denoland/setup-deno@v1
uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: Maybe install gtk-rs dependencies

View File

@@ -40,36 +40,36 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.7.0-gamma"
version = "0.7.0-gamma3"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-gamma" }
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
either_of = { path = "./either_of/", version = "0.1.0" }
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma" }
leptos = { path = "./leptos", version = "0.7.0-gamma" }
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma" }
leptos_router = { path = "./router", version = "0.7.0-gamma" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma" }
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma" }
leptos_meta = { path = "./meta", version = "0.7.0-gamma" }
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma" }
hydration_context = { path = "./hydration_context", version = "0.2.0-gamma3" }
leptos = { path = "./leptos", version = "0.7.0-gamma3" }
leptos_config = { path = "./leptos_config", version = "0.7.0-gamma3" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-gamma3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-gamma3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-gamma3" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-gamma3" }
leptos_router = { path = "./router", version = "0.7.0-gamma3" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-gamma3" }
leptos_server = { path = "./leptos_server", version = "0.7.0-gamma3" }
leptos_meta = { path = "./meta", version = "0.7.0-gamma3" }
next_tuple = { path = "./next_tuple", version = "0.1.0-gamma3" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma" }
server_fn = { path = "./server_fn", version = "0.7.0-gamma" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma" }
tachys = { path = "./tachys", version = "0.1.0-gamma" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-gamma3" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-gamma3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-gamma3" }
server_fn = { path = "./server_fn", version = "0.7.0-gamma3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-gamma3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-gamma3" }
tachys = { path = "./tachys", version = "0.1.0-gamma3" }
[profile.release]
codegen-units = 1

View File

@@ -1,6 +1,6 @@
[package]
name = "throw_error"
version = "0.2.0-gamma"
version = "0.2.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -9,6 +9,7 @@ description = "Spawn asynchronous tasks in an executor-independent way."
edition.workspace = true
[dependencies]
async-executor = { version = "1.13.1", optional = true }
futures = "0.3.30"
glib = { version = "0.20.0", optional = true }
thiserror = "1.0"
@@ -19,12 +20,14 @@ tracing = { version = "0.1.40", optional = true }
wasm-bindgen-futures = { version = "0.4.42", optional = true }
[features]
async-executor = ["dep:async-executor"]
tracing = ["dep:tracing"]
tokio = ["dep:tokio"]
glib = ["dep:glib"]
wasm-bindgen = ["dep:wasm-bindgen-futures"]
futures-executor = ["futures/thread-pool", "futures/executor"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -32,11 +32,14 @@
use std::{future::Future, pin::Pin, sync::OnceLock};
use thiserror::Error;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub(crate) type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
/// A future that has been pinned.
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
/// A future that has been pinned.
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
static POLL_LOCAL: OnceLock<fn()> = OnceLock::new();
/// Errors that can occur when using the executor.
#[derive(Error, Debug)]
@@ -115,6 +118,14 @@ impl Executor {
});
_ = rx.await;
}
/// Polls the current async executor.
/// Not all async executors support polling, so this function may not do anything.
pub fn poll_local() {
if let Some(poller) = POLL_LOCAL.get() {
poller()
}
}
}
impl Executor {
@@ -193,13 +204,15 @@ impl Executor {
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
pub fn init_futures_executor() -> Result<(), ExecutorError> {
use futures::{
executor::{LocalPool, ThreadPool},
executor::{LocalPool, LocalSpawner, ThreadPool},
task::{LocalSpawnExt, SpawnExt},
};
use std::cell::RefCell;
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
thread_local! {
static LOCAL_POOL: LocalPool = LocalPool::new();
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
}
fn get_thread_pool() -> &'static ThreadPool {
@@ -218,28 +231,97 @@ impl Executor {
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
LOCAL_POOL.with(|pool| {
let spawner = pool.spawner();
SPAWNER.with(|spawner| {
spawner.spawn_local(fut).expect("failed to spawn future");
});
})
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| {
LOCAL_POOL.with(|pool| {
if let Ok(mut pool) = pool.try_borrow_mut() {
pool.run_until_stalled();
}
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
});
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "futures-executor")]
#[test]
fn can_spawn_local_future() {
use crate::Executor;
use std::rc::Rc;
Executor::init_futures_executor().expect("couldn't set executor");
let rc = Rc::new(());
Executor::spawn_local(async {
_ = rc;
});
Executor::spawn(async {});
/// Globally sets the [`async_executor`] executor as the executor used to spawn tasks,
/// lazily creating a thread pool to spawn tasks into.
///
/// Returns `Err(_)` if an executor has already been set.
///
/// Requires the `async-executor` feature to be activated on this crate.
#[cfg(feature = "async-executor")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-executor")))]
pub fn init_async_executor() -> Result<(), ExecutorError> {
use async_executor::{Executor, LocalExecutor};
static THREAD_POOL: OnceLock<Executor> = OnceLock::new();
thread_local! {
static LOCAL_POOL: LocalExecutor<'static> = const { LocalExecutor::new() };
}
fn get_thread_pool() -> &'static Executor<'static> {
THREAD_POOL.get_or_init(Executor::new)
}
SPAWN
.set(|fut| {
get_thread_pool().spawn(fut).detach();
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
LOCAL_POOL.with(|pool| pool.spawn(fut).detach());
})
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| {
LOCAL_POOL.with(|pool| pool.try_tick());
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
/// Globally sets a custom executor as the executor used to spawn tasks.
///
/// Returns `Err(_)` if an executor has already been set.
pub fn init_custom_executor(
custom_executor: impl CustomExecutor + 'static,
) -> Result<(), ExecutorError> {
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
EXECUTOR
.set(Box::new(custom_executor))
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN
.set(|fut| {
EXECUTOR.get().unwrap().spawn(fut);
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| EXECUTOR.get().unwrap().spawn_local(fut))
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| EXECUTOR.get().unwrap().poll_local())
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
}
/// A trait for custom executors.
/// Custom executors can be used to integrate with any executor that supports spawning futures.
///
/// All methods can be called recursively.
pub trait CustomExecutor: Send + Sync {
/// Spawns a future, usually on a thread pool.
fn spawn(&self, fut: PinnedFuture<()>);
/// Spawns a local future. May require calling `poll_local` to make progress.
fn spawn_local(&self, fut: PinnedLocalFuture<()>);
/// Polls the executor, if it supports polling.
fn poll_local(&self);
}

View File

@@ -0,0 +1,55 @@
#[cfg(feature = "futures-executor")]
use any_spawner::{CustomExecutor, Executor, PinnedFuture, PinnedLocalFuture};
#[cfg(feature = "futures-executor")]
#[test]
fn can_create_custom_executor() {
use futures::{
executor::{LocalPool, LocalSpawner},
task::LocalSpawnExt,
};
use std::{
cell::RefCell,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
thread_local! {
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
}
struct CustomFutureExecutor;
impl CustomExecutor for CustomFutureExecutor {
fn spawn(&self, _fut: PinnedFuture<()>) {
panic!("not supported in this test");
}
fn spawn_local(&self, fut: PinnedLocalFuture<()>) {
SPAWNER.with(|spawner| {
spawner.spawn_local(fut).expect("failed to spawn future");
});
}
fn poll_local(&self) {
LOCAL_POOL.with(|pool| {
if let Ok(mut pool) = pool.try_borrow_mut() {
pool.run_until_stalled();
}
// If we couldn't borrow_mut, we're in a nested call to poll, so we don't need to do anything.
});
}
}
Executor::init_custom_executor(CustomFutureExecutor)
.expect("couldn't set executor");
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
Executor::spawn_local(async move {
counter_clone.store(1, Ordering::Release);
});
Executor::poll_local();
assert_eq!(counter.load(Ordering::Acquire), 1);
}

View File

@@ -0,0 +1,38 @@
#[cfg(feature = "futures-executor")]
use any_spawner::Executor;
// All tests in this file use the same executor.
#[cfg(feature = "futures-executor")]
#[test]
fn can_spawn_local_future() {
use std::rc::Rc;
let _ = Executor::init_futures_executor();
let rc = Rc::new(());
Executor::spawn_local(async {
_ = rc;
});
Executor::spawn(async {});
}
#[cfg(feature = "futures-executor")]
#[test]
fn can_make_local_progress() {
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
let _ = Executor::init_futures_executor();
let counter = Arc::new(AtomicUsize::new(0));
Executor::spawn_local({
let counter = Arc::clone(&counter);
async move {
assert_eq!(counter.fetch_add(1, Ordering::AcqRel), 0);
Executor::spawn_local(async {
// Should not crash
});
}
});
Executor::poll_local();
assert_eq!(counter.load(Ordering::Acquire), 1);
}

View File

@@ -5,13 +5,14 @@ use leptos::prelude::*;
use leptos_router::{
components::{
Form, Outlet, ParentRoute, ProtectedRoute, Redirect, Route, Router,
Routes, A,
Routes, RoutingProgress, A,
},
hooks::{use_navigate, use_params, use_query_map},
params::Params,
MatchNestedRoutes,
};
use leptos_router_macro::path;
use std::time::Duration;
use tracing::info;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -26,9 +27,14 @@ pub fn RouterExample() -> impl IntoView {
// this signal will be ued to set whether we are allowed to access a protected route
let (logged_in, set_logged_in) = signal(true);
let (is_routing, set_is_routing) = signal(false);
view! {
<Router>
<Router set_is_routing>
// shows a progress bar while async data are loading
<div class="routing-progress">
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
</div>
<nav>
// ordinary <a> elements can be used for client-side navigation
// using <A> has two effects:
@@ -64,7 +70,7 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component]
#[component(transparent)]
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>

View File

@@ -40,6 +40,8 @@ pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
once_cell = { version = "1.19", optional = true }
async-broadcast = { version = "0.7.1", optional = true }
bytecheck = "0.8.0"
rkyv = { version = "0.8.8" }
[features]
hydrate = ["leptos/hydrate"]

View File

@@ -417,7 +417,6 @@ pub fn FileUploadWithProgress() -> impl IntoView {
/// This requires us to store some global state of all the uploads. In a real app, you probably
/// shouldn't do exactly what I'm doing here in the demo. For example, this map just
/// distinguishes between files by filename, not by user.
#[cfg(feature = "ssr")]
mod progress {
use async_broadcast::{broadcast, Receiver, Sender};

View File

@@ -4,6 +4,8 @@ use leptos::{config::get_configuration, logging};
use leptos_axum::{generate_route_list, LeptosRoutes};
use server_fns_axum::*;
// cargo make cli: error: unneeded `return` statement
#[allow(clippy::needless_return)]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Error)

View File

@@ -3,7 +3,6 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use chrono::{Local, NaiveDate};
use leptos::prelude::*;
use reactive_stores::{Field, Patch, Store};
use reactive_stores_macro::{Patch, Store};
use serde::{Deserialize, Serialize};
// ID starts higher than 0 because we have a few starting todos by default

58
flake.lock generated
View File

@@ -5,29 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
@@ -38,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1703961334,
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
"lastModified": 1727634051,
"narHash": "sha256-S5kVU7U82LfpEukbn/ihcyNt2+EvG7Z5unsKW9H/yFA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
"rev": "06cf0e1da4208d3766d898b7fdab6513366d45b9",
"type": "github"
},
"original": {
@@ -54,11 +36,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
},
"original": {
@@ -77,15 +59,14 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1704075545,
"narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
"lastModified": 1727749966,
"narHash": "sha256-DUS8ehzqB1DQzfZ4bRXVSollJhu+y7cvh1DJ9mbWebE=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
"rev": "00decf1b4f9886d25030b9ee4aed7bfddccb5f66",
"type": "github"
},
"original": {
@@ -108,21 +89,6 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -1,6 +1,6 @@
[package]
name = "hydration_context"
version = "0.2.0-gamma"
version = "0.2.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1381,39 +1381,41 @@ where
),
)
} else {
router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
router
.route(path, web::head().to(HttpResponse::Ok))
.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
}
}

View File

@@ -16,8 +16,6 @@ axum = { version = "0.7.5", default-features = false, features = [
] }
dashmap = "6"
futures = "0.3.30"
http = "1.1"
http-body-util = "0.1.2"
leptos = { workspace = true, features = ["nonce", "ssr"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
@@ -26,9 +24,8 @@ leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
once_cell = "1"
parking_lot = "0.12.3"
serde_json = "1.0"
tokio = { version = "1.39", default-features = false }
tower = "0.4.13"
tower = { version = "0.4.13", features = ["util"] }
tower-http = "0.5.2"
tracing = { version = "0.1.40", optional = true }

View File

@@ -76,7 +76,7 @@ use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
use std::path::Path;
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::ServiceExt;
use tower::util::ServiceExt;
#[cfg(feature = "default")]
use tower_http::services::ServeDir;
// use tracing::Instrument; // TODO check tracing span -- was this used in 0.6 for a missing link?
@@ -606,9 +606,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::{config::LeptosOptions, context::provide_context, prelude::*};
///
/// async fn custom_handler(
@@ -806,9 +806,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
@@ -1025,9 +1025,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
@@ -1093,9 +1093,9 @@ where
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
@@ -1342,8 +1342,7 @@ where
.with(|| {
// stub out a path for now
provide_context(RequestUrl::new(""));
let (mock_parts, _) =
http::Request::new(Body::from("")).into_parts();
let (mock_parts, _) = Request::new(Body::from("")).into_parts();
let (mock_meta, _) = ServerMetaContext::new();
provide_contexts("", &mock_meta, mock_parts, Default::default());
additional_context();
@@ -1402,8 +1401,8 @@ impl StaticRouteGenerator {
let add_context = additional_context.clone();
move || {
let full_path = format!("http://leptos.dev{path}");
let mock_req = http::Request::builder()
.method(http::Method::GET)
let mock_req = Request::builder()
.method(Method::GET)
.header("Accept", "text/html")
.body(Body::empty())
.unwrap();
@@ -1495,10 +1494,12 @@ impl StaticRouteGenerator {
_ = routes;
_ = app_fn;
_ = additional_context;
panic!(
"Static routes are not currently supported on WASM32 server \
targets."
);
Self(Box::new(|_| {
panic!(
"Static routes are not currently supported on WASM32 \
server targets."
);
}))
}
}
@@ -1933,7 +1934,7 @@ where
///
/// #[server]
/// pub async fn request_method() -> Result<String, ServerFnError> {
/// use http::Method;
/// use axum::http::Method;
/// use leptos_axum::extract;
///
/// // you can extract anything that a regular Axum extractor can extract

View File

@@ -45,7 +45,7 @@ web-sys = { version = "0.3.70", features = [
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = "0.2.93"
wasm-bindgen = "=0.2.93"
serde_qs = "0.13.0"
slotmap = "1.0"
futures = "0.3.30"

View File

@@ -1,59 +0,0 @@
#![allow(deprecated)]
use crate::TextProp;
use std::rc::Rc;
/// A collection of additional HTML attributes to be applied to an element,
/// each of which may or may not be reactive.
#[derive(Clone)]
#[repr(transparent)]
#[deprecated = "Most uses of `AdditionalAttributes` can be replaced with `#[prop(attrs)]` \
and the `attr:` syntax. If you have a use case that still requires `AdditionalAttributes`, please \
open a GitHub issue here and share it: https://github.com/leptos-rs/leptos"]
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
impl<I, T, U> From<I> for AdditionalAttributes
where
I: IntoIterator<Item = (T, U)>,
T: Into<String>,
U: Into<TextProp>,
{
fn from(value: I) -> Self {
Self(
value
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
impl Default for AdditionalAttributes {
fn default() -> Self {
Self([].into_iter().collect())
}
}
/// Iterator over additional HTML attributes.
#[repr(transparent)]
pub struct AdditionalAttributesIter<'a>(
std::slice::Iter<'a, (String, TextProp)>,
);
impl<'a> Iterator for AdditionalAttributesIter<'a> {
type Item = &'a (String, TextProp);
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl<'a> IntoIterator for &'a AdditionalAttributes {
type Item = &'a (String, TextProp);
type IntoIter = AdditionalAttributesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
AdditionalAttributesIter(self.0.iter())
}
}

View File

@@ -1,12 +1,15 @@
use crate::{ChildrenFn, Show};
use crate::{children::ChildrenFn, component, control_flow::Show, IntoView};
use core::time::Duration;
use leptos::component;
use leptos_dom::{helpers::TimeoutHandle, IntoView};
use leptos_dom::helpers::TimeoutHandle;
use leptos_macro::view;
use leptos_reactive::{
create_render_effect, on_cleanup, signal_prelude::*, store_value,
StoredValue,
use reactive_graph::{
effect::RenderEffect,
owner::{on_cleanup, StoredValue},
signal::RwSignal,
traits::{Get, GetUntracked, GetValue, Set, SetValue},
wrappers::read::Signal,
};
use tachys::prelude::*;
/// A component that will show its children when the `when` condition is `true`.
/// Additionally, you need to specify a `hide_delay`. If the `when` condition changes to `false`,
@@ -16,10 +19,10 @@ use leptos_reactive::{
///
/// ```rust
/// # use core::time::Duration;
/// # use leptos::*;
/// # use leptos::prelude::*;
/// # #[component]
/// # pub fn App() -> impl IntoView {
/// let show = create_rw_signal(false);
/// let show = RwSignal::new(false);
///
/// view! {
/// <div
@@ -50,7 +53,7 @@ pub fn AnimatedShow(
children: ChildrenFn,
/// If the component should show or not
#[prop(into)]
when: MaybeSignal<bool>,
when: Signal<bool>,
/// Optional CSS class to apply if `when == true`
#[prop(optional)]
show_class: &'static str,
@@ -60,15 +63,15 @@ pub fn AnimatedShow(
/// The timeout after which the component will be unmounted if `when == false`
hide_delay: Duration,
) -> impl IntoView {
let handle: StoredValue<Option<TimeoutHandle>> = store_value(None);
let cls = create_rw_signal(if when.get_untracked() {
let handle: StoredValue<Option<TimeoutHandle>> = StoredValue::new(None);
let cls = RwSignal::new(if when.get_untracked() {
show_class
} else {
hide_class
});
let show = create_rw_signal(when.get_untracked());
let show = RwSignal::new(when.get_untracked());
create_render_effect(move |_| {
let eff = RenderEffect::new(move |_| {
if when.get() {
// clear any possibly active timer
if let Some(h) = handle.get_value() {
@@ -93,6 +96,7 @@ pub fn AnimatedShow(
if let Some(Some(h)) = handle.try_get_value() {
h.clear();
}
drop(eff);
});
view! {

View File

@@ -1,10 +1,8 @@
use crate::Suspense;
use leptos_dom::IntoView;
use crate::{prelude::Suspend, suspense_component::Suspense, IntoView};
use leptos_macro::{component, view};
use leptos_reactive::{
create_blocking_resource, create_local_resource, create_resource,
store_value, Serializable,
};
use leptos_server::ArcOnceResource;
use reactive_graph::prelude::ReadUntracked;
use serde::{de::DeserializeOwned, Serialize};
#[component]
/// Allows you to inline the data loading for an `async` block or
@@ -15,11 +13,8 @@ use leptos_reactive::{
/// Adding `let:{variable name}` to the props makes the data available in the children
/// that variable name, when resolved.
/// ```
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # use leptos::prelude::*;
/// # if false {
/// # let runtime = create_runtime();
/// async fn fetch_monkeys(monkey: i32) -> i32 {
/// // do some expensive work
/// 3
@@ -27,29 +22,23 @@ use leptos_reactive::{
///
/// view! {
/// <Await
/// future=|| fetch_monkeys(3)
/// future=fetch_monkeys(3)
/// let:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
pub fn Await<T, Fut, FF, VF, V>(
/// A function that returns the [`Future`](std::future::Future) that
/// will the component will `.await` before rendering.
future: FF,
/// If `true`, the component will use [`create_blocking_resource`], preventing
pub fn Await<T, Fut, Chil, V>(
/// A [`Future`](std::future::Future) that will the component will `.await`
/// before rendering.
future: Fut,
/// If `true`, the component will create a blocking resource, preventing
/// the HTML stream from returning anything before `future` has resolved.
#[prop(optional)]
blocking: bool,
/// If `true`, the component will use [`create_local_resource`], this will
/// always run on the local system and therefore its result type does not
/// need to be `Serializable`.
#[prop(optional)]
local: bool,
/// A function that takes a reference to the resolved data from the `future`
/// renders a view.
///
@@ -58,65 +47,58 @@ pub fn Await<T, Fut, FF, VF, V>(
/// `let:` syntax to specify the name for the data variable.
///
/// ```rust
/// # use leptos::*;
/// # use leptos::prelude::*;
/// # if false {
/// # let runtime = create_runtime();
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
/// # 3
/// # }
/// view! {
/// <Await
/// future=|| fetch_monkeys(3)
/// future=fetch_monkeys(3)
/// let:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
/// is the same as
/// ```rust
/// # use leptos::*;
/// # use leptos::prelude::*;
/// # if false {
/// # let runtime = create_runtime();
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
/// # 3
/// # }
/// view! {
/// <Await
/// future=|| fetch_monkeys(3)
/// future=fetch_monkeys(3)
/// children=|data| view! {
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// }
/// />
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
children: VF,
children: Chil,
) -> impl IntoView
where
Fut: std::future::Future<Output = T> + 'static,
FF: Fn() -> Fut + 'static,
V: IntoView,
VF: Fn(&T) -> V + 'static,
T: Serializable + 'static,
T: Send + Sync + Serialize + DeserializeOwned + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
Chil: FnOnce(&T) -> V + Send + 'static,
V: IntoView + 'static,
{
let res = if blocking {
create_blocking_resource(|| (), move |_| future())
} else if local {
create_local_resource(|| (), move |_| future())
} else {
create_resource(|| (), move |_| future())
};
let view = store_value(children);
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
let ready = res.ready();
view! {
<Suspense fallback=|| ()>
{move || res.map(|data| view.with_value(|view| view(data)))}
{Suspend::new(async move {
ready.await;
children(res.read_untracked().as_ref().unwrap())
})}
</Suspense>
}
}

View File

@@ -41,7 +41,10 @@
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use reactive_graph::owner::{LocalStorage, StoredValue};
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::WithValue,
};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.

View File

@@ -174,7 +174,9 @@ pub mod prelude {
pub use server_fn::{self, ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::template::ViewTemplate,
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
};
}
pub use export_types::*;
@@ -200,10 +202,12 @@ pub mod error {
pub use throw_error::*;
}
/// Control-flow components like `<Show>` and `<For>`.
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
pub mod control_flow {
pub use crate::{for_loop::*, show::*};
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
}
mod animated_show;
mod await_;
mod for_loop;
mod show;
@@ -325,234 +329,3 @@ pub use tracing;
pub use wasm_bindgen;
#[doc(hidden)]
pub use web_sys;
/*mod additional_attributes;
pub use additional_attributes::*;
mod await_;
pub use await_::*;
pub use leptos_config::{self, get_configuration, LeptosOptions};
#[cfg(not(all(
target_arch = "wasm32",
any(feature = "csr", feature = "hydrate")
)))]
/// Utilities for server-side rendering HTML.
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
pub use leptos_dom::{
self, create_node_ref, document, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped,
},
html,
html::Binding,
math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class,
CollectView, Errors, EventHandlerFn, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
};
/// Types to make it easier to handle errors in your application.
pub mod error {
pub use server_fn::error::{Error, Result};
}
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
pub use leptos_macro::template;
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
pub use leptos_macro::view as template;
pub use leptos_macro::{component, island, slice, slot, view, Params};
cfg_if::cfg_if!(
if #[cfg(feature="spin")] {
pub use leptos_spin_macro::server;
} else {
pub use leptos_macro::server;
}
);
pub use leptos_reactive::*;
pub use leptos_server::{
self, create_action, create_multi_action, create_server_action,
create_server_multi_action, Action, MultiAction, ServerFnError,
ServerFnErrorErr,
};
pub use server_fn::{self, ServerFn as _};
mod error_boundary;
pub use error_boundary::*;
mod animated_show;
mod for_loop;
mod provider;
mod show;
pub use animated_show::*;
pub use for_loop::*;
pub use provider::*;
#[cfg(feature = "experimental-islands")]
pub use serde;
#[cfg(feature = "experimental-islands")]
pub use serde_json;
pub use show::*;
//pub use suspense_component::*;
mod suspense_component;
//mod transition;
#[cfg(feature = "tracing")]
#[doc(hidden)]
pub use tracing;
pub use transition::*;
#[doc(hidden)]
pub use typed_builder;
#[doc(hidden)]
pub use typed_builder::Optional;
#[doc(hidden)]
pub use typed_builder_macro;
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use wasm_bindgen; // used in islands
#[doc(hidden)]
#[cfg(any(
feature = "csr",
feature = "hydrate",
feature = "template_macro"
))]
pub use web_sys; // used in islands
mod children;
mod portal;
mod view_fn;
pub use children::*;
pub use portal::*;
pub use view_fn::*;
extern crate self as leptos;
/// A type for taking anything that implements [`IntoAttribute`].
///
/// ```rust
/// use leptos::*;
///
/// #[component]
/// pub fn MyHeading(
/// text: String,
/// #[prop(optional, into)] class: Option<AttributeValue>,
/// ) -> impl IntoView {
/// view! {
/// <h1 class=class>{text}</h1>
/// }
/// }
/// ```
pub type AttributeValue = Box<dyn IntoAttribute>;
#[doc(hidden)]
pub trait Component<P> {}
#[doc(hidden)]
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
#[doc(hidden)]
pub trait DynAttrs {
fn dyn_attrs(self, _args: Vec<(&'static str, Attribute)>) -> Self
where
Self: Sized,
{
self
}
}
impl DynAttrs for () {}
#[doc(hidden)]
pub trait DynBindings {
fn dyn_bindings<B: Into<Binding>>(
self,
_args: impl IntoIterator<Item = B>,
) -> Self
where
Self: Sized,
{
self
}
}
impl DynBindings for () {}
#[doc(hidden)]
pub trait PropsOrNoPropsBuilder {
type Builder;
fn builder_or_not() -> Self::Builder;
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Default)]
pub struct EmptyPropsBuilder {}
impl EmptyPropsBuilder {
pub fn build(self) {}
}
impl<P: Props> PropsOrNoPropsBuilder for P {
type Builder = <P as Props>::Builder;
fn builder_or_not() -> Self::Builder {
Self::builder()
}
}
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
type Builder = EmptyPropsBuilder;
fn builder_or_not() -> Self::Builder {
EmptyPropsBuilder {}
}
}
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
impl<P, F, R> Component<P> for F
where
F: FnOnce(P) -> R,
P: Props,
{
}
#[doc(hidden)]
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
_f: &impl Component<P>,
) -> <P as PropsOrNoPropsBuilder>::Builder {
<P as PropsOrNoPropsBuilder>::builder_or_not()
}
#[doc(hidden)]
pub fn component_view<P>(f: impl ComponentConstructor<P>, props: P) -> View {
f.construct(props)
}
#[doc(hidden)]
pub trait ComponentConstructor<P> {
fn construct(self, props: P) -> View;
}
impl<Func, V> ComponentConstructor<()> for Func
where
Func: FnOnce() -> V,
V: IntoView,
{
fn construct(self, (): ()) -> View {
(self)().into_view()
}
}
impl<Func, V, P> ComponentConstructor<P> for Func
where
Func: FnOnce(P) -> V,
V: IntoView,
P: PropsOrNoPropsBuilder,
{
fn construct(self, props: P) -> View {
(self)(props).into_view()
}
}*/

View File

@@ -35,7 +35,7 @@ pub fn Provider<T, Chil>(
) -> impl IntoView
where
T: Send + Sync + 'static,
Chil: IntoView,
Chil: IntoView + 'static,
{
let owner = Owner::current()
.expect("no current reactive Owner found")

View File

@@ -1,30 +0,0 @@
use leptos_dom::{IntoView, View};
use std::rc::Rc;
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
pub struct ViewFn(Rc<dyn Fn() -> View>);
impl Default for ViewFn {
fn default() -> Self {
Self(Rc::new(|| ().into_view()))
}
}
impl<F, IV> From<F> for ViewFn
where
F: Fn() -> IV + 'static,
IV: IntoView,
{
fn from(value: F) -> Self {
Self(Rc::new(move || value().into_view()))
}
}
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> View {
(self.0)()
}
}

View File

@@ -1,5 +1,7 @@
#[cfg(feature = "ssr")]
use leptos::html::HtmlElement;
#[cfg(feature = "ssr")]
#[test]
fn simple_ssr_test() {
use leptos::prelude::*;
@@ -20,6 +22,7 @@ fn simple_ssr_test() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_components() {
use leptos::prelude::*;
@@ -51,6 +54,7 @@ fn ssr_test_with_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_snake_case_components() {
use leptos::prelude::*;
@@ -81,6 +85,7 @@ fn ssr_test_with_snake_case_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn test_classes() {
use leptos::prelude::*;
@@ -98,6 +103,7 @@ fn test_classes() {
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_with_styles() {
use leptos::prelude::*;
@@ -119,6 +125,7 @@ fn ssr_with_styles() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_option() {
use leptos::prelude::*;

View File

@@ -396,8 +396,7 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
feature = "tracing",

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0-gamma"
version = "0.7.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -51,7 +51,27 @@ axum = ["server_fn_macro/axum"]
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
skip_feature_sets = [
[
"csr",
"hydrate",
],
[
"hydrate",
"csr",
],
[
"hydrate",
"ssr",
],
[
"actix",
"axum",
],
]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }

View File

@@ -18,6 +18,7 @@ use syn::{
};
pub struct Model {
is_transparent: bool,
island: Option<String>,
docs: Docs,
unknown_attrs: UnknownAttrs,
@@ -62,6 +63,7 @@ impl Parse for Model {
});
Ok(Self {
is_transparent: false,
island: None,
docs,
unknown_attrs,
@@ -102,6 +104,7 @@ pub fn convert_from_snake_case(name: &Ident) -> Ident {
impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
is_transparent,
island,
docs,
unknown_attrs,
@@ -116,20 +119,22 @@ impl ToTokens for Model {
let no_props = props.is_empty();
// check for components that end ;
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error2::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually an \
accident, not intentional, so we prevent it. If youd like \
to return (), you can do it it explicitly by returning () as \
the last item from the component."
);
if !is_transparent {
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error2::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually \
an accident, not intentional, so we prevent it. If youd \
like to return (), you can do it it explicitly by \
returning () as the last item from the component."
);
}
}
//body.sig.ident = format_ident!("__{}", body.sig.ident);
@@ -265,14 +270,30 @@ impl ToTokens for Model {
}
};
let component = quote! {
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
let component = if *is_transparent {
body_expr
} else if cfg!(erase_components) {
quote! {
::leptos::prelude::IntoAny::into_any(
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
)
}
} else {
quote! {
::leptos::prelude::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
}
};
// add island wrapper if island
@@ -520,6 +541,13 @@ impl ToTokens for Model {
}
impl Model {
#[allow(clippy::wrong_self_convention)]
pub fn is_transparent(mut self, is_transparent: bool) -> Self {
self.is_transparent = is_transparent;
self
}
#[allow(clippy::wrong_self_convention)]
pub fn with_island(mut self, island: Option<String>) -> Self {
self.island = island;

View File

@@ -535,11 +535,24 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
pub fn component(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
component_macro(s, None)
pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
);
}
true
} else {
false
};
component_macro(s, is_transparent, None)
}
/// Defines a component as an interactive island when you are using the
@@ -615,17 +628,37 @@ pub fn component(
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
);
}
true
} else {
false
};
let island_src = s.to_string();
component_macro(s, Some(island_src))
component_macro(s, is_transparent, Some(island_src))
}
fn component_macro(s: TokenStream, island: Option<String>) -> TokenStream {
fn component_macro(
s: TokenStream,
is_transparent: bool,
island: Option<String>,
) -> TokenStream {
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s);
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.with_island(island).into_token_stream();
let expanded = model.is_transparent(is_transparent).with_island(island).into_token_stream();
if !matches!(unexpanded.vis, Visibility::Public(_)) {
unexpanded.vis = Visibility::Public(Pub {
span: unexpanded.vis.span(),

View File

@@ -32,7 +32,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::params::ParamsMap) -> Result<Self, ::leptos_router::params::ParamsError> {
fn from_map(map: &::leptos_router::params::ParamsMap) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
Ok(Self {
#(#fields,)*
})

View File

@@ -179,7 +179,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
}
enum Item<'a, T> {
Node(&'a Node<T>),
Node(&'a Node<T>, bool),
ClosingTag(String),
}
@@ -290,10 +290,11 @@ impl<'a> InertElementBuilder<'a> {
fn inert_element_to_tokens(
node: &Node<impl CustomNode>,
escape_text: bool,
global_class: Option<&TokenTree>,
) -> Option<TokenStream> {
let mut html = InertElementBuilder::new(global_class);
let mut nodes = VecDeque::from([Item::Node(node)]);
let mut nodes = VecDeque::from([Item::Node(node, escape_text)]);
while let Some(current) = nodes.pop_front() {
match current {
@@ -303,21 +304,32 @@ fn inert_element_to_tokens(
html.push_str(&tag);
html.push('>');
}
Item::Node(current) => {
Item::Node(current, escape) => {
match current {
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = html_escape::encode_text(&text);
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
let text = html_escape::encode_text(&text);
let text = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
html.push_str(&text);
}
Node::Element(node) => {
let self_closing = is_self_closing(node);
let el_name = node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
// opening tag
html.push('<');
@@ -326,9 +338,12 @@ fn inert_element_to_tokens(
for attr in node.attributes() {
if let NodeAttribute::Attribute(attr) = attr {
let attr_name = attr.key.to_string();
// trim r# from raw identifiers like r#as
let attr_name =
attr_name.trim_start_matches("r#");
if attr_name != "class" {
html.push(' ');
html.push_str(&attr_name);
html.push_str(attr_name);
}
if let Some(value) =
@@ -361,7 +376,7 @@ fn inert_element_to_tokens(
nodes.push_front(Item::ClosingTag(el_name));
let children = node.children.iter().rev();
for child in children {
nodes.push_front(Item::Node(child));
nodes.push_front(Item::Node(child, escape));
}
}
}
@@ -545,7 +560,9 @@ fn node_to_tokens(
view_marker,
disable_inert_html,
),
Node::Block(block) => Some(quote! { #block }),
Node::Block(block) => {
Some(quote! { ::leptos::prelude::IntoRender::into_render(#block) })
}
Node::Text(text) => Some(text_to_tokens(&text.value)),
Node::RawText(raw) => {
let text = raw.to_string_best();
@@ -554,7 +571,11 @@ fn node_to_tokens(
}
Node::Element(el_node) => {
if !top_level && is_inert {
inert_element_to_tokens(node, global_class)
let el_name = el_node.name().to_string();
let escape = el_name != "script"
&& el_name != "style"
&& el_name != "textarea";
inert_element_to_tokens(node, escape, global_class)
} else {
element_to_tokens(
el_node,
@@ -722,6 +743,11 @@ pub(crate) fn element_to_tokens(
quote! { ::leptos::tachys::html::element::#custom(#name) }
} else if is_svg_element(&tag) {
parent_type = TagType::Svg;
let name = if tag == "use" || tag == "use_" {
Ident::new_raw("use", name.span()).to_token_stream()
} else {
name.to_token_stream()
};
quote! { ::leptos::tachys::svg::#name() }
} else if is_math_ml_element(&tag) {
parent_type = TagType::Math;
@@ -875,7 +901,7 @@ fn attribute_to_tokens(
NodeName::Path(path) => path.path.get_ident(),
_ => unreachable!(),
};
let value = attribute_value(node);
let value = attribute_value(node, false);
quote! {
.#node_ref(#value)
}
@@ -925,13 +951,13 @@ fn attribute_to_tokens(
// we don't provide statically-checked methods for SVG attributes
|| (tag_type == TagType::Svg && name != "inner_html")
{
let value = attribute_value(node);
let value = attribute_value(node, true);
quote! {
.attr(#name, #value)
}
} else {
let key = attribute_name(&node.key);
let value = attribute_value(node);
let value = attribute_value(node, true);
// special case of global_class and class attribute
if &node.key.to_string() == "class"
@@ -968,11 +994,11 @@ pub(crate) fn attribute_absolute(
let id = &parts[0];
match id {
NodeNameFragment::Ident(id) => {
let value = attribute_value(node);
// ignore `let:` and `clone:`
if id == "let" || id == "clone" {
None
} else if id == "attr" {
let value = attribute_value(node, true);
let key = &parts[1];
let key_name = key.to_string();
if key_name == "class" || key_name == "style" {
@@ -980,6 +1006,7 @@ pub(crate) fn attribute_absolute(
quote! { ::leptos::tachys::html::#key::#key(#value) },
)
} else if key_name == "aria" {
let value = attribute_value(node, true);
let mut parts_iter = parts.iter();
parts_iter.next();
let fn_name = parts_iter.map(|p| p.to_string()).collect::<Vec<String>>().join("_");
@@ -1008,6 +1035,7 @@ pub(crate) fn attribute_absolute(
},
)
} else if id == "style" || id == "class" {
let value = attribute_value(node, false);
let key = &node.key.to_string();
let key = key
.replacen("style:", "", 1)
@@ -1016,6 +1044,7 @@ pub(crate) fn attribute_absolute(
quote! { ::leptos::tachys::html::#id::#id((#key, #value)) },
)
} else if id == "prop" {
let value = attribute_value(node, false);
let key = &node.key.to_string();
let key = key.replacen("prop:", "", 1);
Some(
@@ -1072,7 +1101,7 @@ pub(crate) fn two_way_binding_to_tokens(
name: &str,
node: &KeyedAttribute,
) -> TokenStream {
let value = attribute_value(node);
let value = attribute_value(node, false);
let ident =
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
@@ -1097,7 +1126,7 @@ pub(crate) fn event_type_and_handler(
name: &str,
node: &KeyedAttribute,
) -> (TokenStream, TokenStream, TokenStream) {
let handler = attribute_value(node);
let handler = attribute_value(node, false);
let (event_type, is_custom, is_force_undelegated, is_targeted) =
parse_event_name(name);
@@ -1154,7 +1183,7 @@ fn class_to_tokens(
class: TokenStream,
class_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node);
let value = attribute_value(node, false);
if let Some(class_name) = class_name {
quote! {
.#class((#class_name, #value))
@@ -1171,7 +1200,7 @@ fn style_to_tokens(
style: TokenStream,
style_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node);
let value = attribute_value(node, false);
if let Some(style_name) = style_name {
quote! {
.#style((#style_name, #value))
@@ -1188,7 +1217,7 @@ fn prop_to_tokens(
prop: TokenStream,
key: &str,
) -> TokenStream {
let value = attribute_value(node);
let value = attribute_value(node, false);
quote! {
.#prop(#key, #value)
}
@@ -1345,7 +1374,10 @@ fn attribute_name(name: &NodeName) -> TokenStream {
}
}
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
fn attribute_value(
attr: &KeyedAttribute,
is_attribute_proper: bool,
) -> TokenStream {
match attr.possible_value.to_value() {
None => quote! { true },
Some(value) => match &value.value {
@@ -1360,14 +1392,26 @@ fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
}
}
quote! {
{#expr}
if matches!(expr, Expr::Lit(_)) || !is_attribute_proper {
quote! {
#expr
}
} else {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#expr)
}
}
}
// any value in braces: expand as-is to give proper r-a support
KVAttributeValue::InvalidBraced(block) => {
quote! {
#block
if is_attribute_proper {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#block)
}
} else {
quote! {
#block
}
}
}
},

View File

@@ -1,3 +1,4 @@
#[cfg(not(erase_components))]
#[test]
fn ui() {
let t = trybuild::TestCases::new();

View File

@@ -19,6 +19,7 @@ tracing = { version = "0.1.40", optional = true }
futures = "0.3.30"
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
send_wrapper = "0.6"

View File

@@ -8,6 +8,8 @@ mod local_resource;
pub use local_resource::*;
mod multi_action;
pub use multi_action::*;
mod once_resource;
pub use once_resource::*;
mod resource;
pub use resource::*;
mod shared;
@@ -183,7 +185,7 @@ impl FromEncodedStr for [u8] {
mod view_implementations {
use crate::Resource;
use reactive_graph::traits::Read;
use std::{future::Future, pin::Pin};
use std::future::Future;
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
@@ -217,16 +219,11 @@ mod view_implementations {
{
type Output<SomeNewAttr: Attribute> = Box<
dyn FnMut() -> Suspend<
Pin<
Box<
dyn Future<
Output = <T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
> + Send,
>,
>,
> + Send,
<T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
>
+ Send
>;
fn add_any_attr<NewAttr: Attribute>(

View File

@@ -35,10 +35,10 @@ impl<T> Clone for ArcLocalResource<T> {
impl<T> ArcLocalResource<T> {
#[track_caller]
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
where
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
T: 'static,
Fut: Future<Output = T> + 'static,
{
let fetcher = move || {
let fut = fetcher();
@@ -60,7 +60,7 @@ impl<T> ArcLocalResource<T> {
}
};
Self {
data: ArcAsyncDerived::new(fetcher),
data: ArcAsyncDerived::new_unsync(fetcher),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
@@ -103,7 +103,7 @@ impl<T> DefinedAt for ArcLocalResource<T> {
impl<T> ReadUntracked for ArcLocalResource<T>
where
T: Send + Sync + 'static,
T: 'static,
{
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
@@ -201,7 +201,7 @@ impl<T> LocalResource<T> {
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
where
T: 'static,
Fut: Future<Output = T> + Send + 'static,
Fut: Future<Output = T> + 'static,
{
let fetcher = move || {
let fut = fetcher();
@@ -230,7 +230,7 @@ impl<T> LocalResource<T> {
let fetcher = SendWrapper::new(fetcher);
AsyncDerived::new(move || {
let fut = fetcher();
async move { SendWrapper::new(fut.await) }
SendWrapper::new(async move { SendWrapper::new(fut.await) })
})
},
#[cfg(debug_assertions)]
@@ -280,7 +280,7 @@ impl<T> DefinedAt for LocalResource<T> {
impl<T> ReadUntracked for LocalResource<T>
where
T: Send + Sync + 'static,
T: 'static,
{
type Value =
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
@@ -307,7 +307,7 @@ impl<T: 'static> IsDisposed for LocalResource<T> {
impl<T: 'static> ToAnySource for LocalResource<T>
where
T: Send + Sync + 'static,
T: 'static,
{
fn to_any_source(&self) -> AnySource {
self.data.to_any_source()
@@ -316,7 +316,7 @@ where
impl<T: 'static> ToAnySubscriber for LocalResource<T>
where
T: Send + Sync + 'static,
T: 'static,
{
fn to_any_subscriber(&self) -> AnySubscriber {
self.data.to_any_subscriber()
@@ -325,7 +325,7 @@ where
impl<T> Source for LocalResource<T>
where
T: Send + Sync + 'static,
T: 'static,
{
fn add_subscriber(&self, subscriber: AnySubscriber) {
self.data.add_subscriber(subscriber)
@@ -342,7 +342,7 @@ where
impl<T> ReactiveNode for LocalResource<T>
where
T: Send + Sync + 'static,
T: 'static,
{
fn mark_dirty(&self) {
self.data.mark_dirty();
@@ -363,7 +363,7 @@ where
impl<T> Subscriber for LocalResource<T>
where
T: Send + Sync + 'static,
T: 'static,
{
fn add_source(&self, source: AnySource) {
self.data.add_source(source);

View File

@@ -0,0 +1,708 @@
use crate::{
initial_value, FromEncodedStr, IntoEncodedString,
IS_SUPPRESSING_RESOURCE_LOAD,
};
#[cfg(feature = "rkyv")]
use codee::binary::RkyvCodec;
#[cfg(feature = "serde-wasm-bindgen")]
use codee::string::JsonSerdeWasmCodec;
#[cfg(feature = "miniserde")]
use codee::string::MiniserdeCodec;
#[cfg(feature = "serde-lite")]
use codee::SerdeLite;
use codee::{
string::{FromToStringCodec, JsonSerdeCodec},
Decoder, Encoder,
};
use core::{fmt::Debug, marker::PhantomData};
use futures::Future;
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
suspense::SuspenseContext, AsyncDerivedReadyFuture, ScopedFuture,
},
diagnostics::{SpecialNonReactiveFuture, SpecialNonReactiveZone},
graph::{AnySource, ToAnySource},
owner::{use_context, ArenaItem, Owner},
prelude::*,
signal::{
guards::{Plain, ReadGuard},
ArcTrigger,
},
unwrap_signal,
};
use std::{
future::IntoFuture,
mem,
panic::Location,
pin::Pin,
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
},
task::{Context, Poll, Waker},
};
#[derive(Debug)]
pub struct ArcOnceResource<T, Ser = JsonSerdeCodec> {
trigger: ArcTrigger,
value: Arc<RwLock<Option<T>>>,
wakers: Arc<RwLock<Vec<Waker>>>,
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
loading: Arc<AtomicBool>,
ser: PhantomData<fn() -> Ser>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<T, Ser> Clone for ArcOnceResource<T, Ser> {
fn clone(&self) -> Self {
Self {
trigger: self.trigger.clone(),
value: self.value.clone(),
wakers: self.wakers.clone(),
suspenses: self.suspenses.clone(),
loading: self.loading.clone(),
ser: self.ser,
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
}
impl<T, Ser> ArcOnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: Encoder<T> + Decoder<T>,
<Ser as Encoder<T>>::Error: Debug,
<Ser as Decoder<T>>::Error: Debug,
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_with_options(
fut: impl Future<Output = T> + Send + 'static,
#[allow(unused)] // this is used with `feature = "ssr"`
blocking: bool,
) -> Self {
let shared_context = Owner::current_shared_context();
let id = shared_context
.as_ref()
.map(|sc| sc.next_id())
.unwrap_or_default();
let initial = initial_value::<T, Ser>(&id, shared_context.as_ref());
let is_ready = initial.is_some();
let value = Arc::new(RwLock::new(initial));
let wakers = Arc::new(RwLock::new(Vec::<Waker>::new()));
let suspenses = Arc::new(RwLock::new(Vec::<SuspenseContext>::new()));
let loading = Arc::new(AtomicBool::new(!is_ready));
let trigger = ArcTrigger::new();
let fut = ScopedFuture::new(fut);
if !is_ready && !IS_SUPPRESSING_RESOURCE_LOAD.load(Ordering::Relaxed) {
let value = Arc::clone(&value);
let wakers = Arc::clone(&wakers);
let loading = Arc::clone(&loading);
let trigger = trigger.clone();
reactive_graph::spawn(async move {
let loaded = fut.await;
*value.write().or_poisoned() = Some(loaded);
loading.store(false, Ordering::Relaxed);
for waker in mem::take(&mut *wakers.write().or_poisoned()) {
waker.wake();
}
trigger.notify();
});
}
let data = Self {
trigger,
value: value.clone(),
loading,
wakers,
suspenses,
ser: PhantomData,
#[cfg(debug_assertions)]
defined_at: Location::caller(),
};
#[cfg(feature = "ssr")]
if let Some(shared_context) = shared_context {
let value = Arc::clone(&value);
let ready_fut = data.ready();
if blocking {
shared_context.defer_stream(Box::pin(data.ready()));
}
if shared_context.get_is_hydrating() {
shared_context.write_async(
id,
Box::pin(async move {
ready_fut.await;
let value = value.read().or_poisoned();
let value = value.as_ref().unwrap();
Ser::encode(value).unwrap().into_encoded_string()
}),
);
}
}
data
}
}
impl<T, Ser> ArcOnceResource<T, Ser> {
/// Returns a `Future` that is ready when this resource has next finished loading.
pub fn ready(&self) -> AsyncDerivedReadyFuture {
AsyncDerivedReadyFuture::new(
self.to_any_source(),
&self.loading,
&self.wakers,
)
}
}
impl<T, Ser> DefinedAt for ArcOnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
{
None
}
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
}
}
impl<T, Ser> IsDisposed for ArcOnceResource<T, Ser> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T, Ser> ToAnySource for ArcOnceResource<T, Ser> {
fn to_any_source(&self) -> AnySource {
self.trigger.to_any_source()
}
}
impl<T, Ser> Track for ArcOnceResource<T, Ser> {
fn track(&self) {
self.trigger.track();
}
}
impl<T, Ser> ReadUntracked for ArcOnceResource<T, Ser>
where
T: 'static,
{
type Value = ReadGuard<Option<T>, Plain<Option<T>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(suspense_context) = use_context::<SuspenseContext>() {
if self.value.read().or_poisoned().is_none() {
let handle = suspense_context.task_id();
let ready = SpecialNonReactiveFuture::new(self.ready());
reactive_graph::spawn(async move {
ready.await;
drop(handle);
});
self.suspenses.write().or_poisoned().push(suspense_context);
}
}
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
}
}
impl<T, Ser> IntoFuture for ArcOnceResource<T, Ser>
where
T: Clone + 'static,
{
type Output = T;
type IntoFuture = OnceResourceFuture<T>;
fn into_future(self) -> Self::IntoFuture {
OnceResourceFuture {
source: self.to_any_source(),
value: Arc::clone(&self.value),
loading: Arc::clone(&self.loading),
wakers: Arc::clone(&self.wakers),
suspenses: Arc::clone(&self.suspenses),
}
}
}
/// A [`Future`] that is ready when an [`ArcAsyncDerived`] is finished loading or reloading,
/// and contains its value. `.await`ing this clones the value `T`.
pub struct OnceResourceFuture<T> {
source: AnySource,
value: Arc<RwLock<Option<T>>>,
loading: Arc<AtomicBool>,
wakers: Arc<RwLock<Vec<Waker>>>,
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
}
impl<T> Future for OnceResourceFuture<T>
where
T: Clone + 'static,
{
type Output = T;
#[track_caller]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(debug_assertions)]
let _guard = SpecialNonReactiveZone::enter();
let waker = cx.waker();
self.source.track();
if let Some(suspense_context) = use_context::<SuspenseContext>() {
self.suspenses.write().or_poisoned().push(suspense_context);
}
if self.loading.load(Ordering::Relaxed) {
self.wakers.write().or_poisoned().push(waker.clone());
Poll::Pending
} else {
Poll::Ready(
self.value.read().or_poisoned().as_ref().unwrap().clone(),
)
}
}
}
impl<T> ArcOnceResource<T, JsonSerdeCodec>
where
T: Send + Sync + 'static,
JsonSerdeCodec: Encoder<T> + Decoder<T>,
<JsonSerdeCodec as Encoder<T>>::Error: Debug,
<JsonSerdeCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, true)
}
}
impl<T> ArcOnceResource<T, FromToStringCodec>
where
T: Send + Sync + 'static,
FromToStringCodec: Encoder<T> + Decoder<T>,
<FromToStringCodec as Encoder<T>>::Error: Debug, <FromToStringCodec as Decoder<T>>::Error: Debug,
<<FromToStringCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
pub fn new_str(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, false)
}
pub fn new_str_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-wasm-bindgen")]
impl<T> ArcOnceResource<T, JsonSerdeWasmCodec>
where
T: Send + Sync + 'static,
JsonSerdeWasmCodec: Encoder<T> + Decoder<T>,
<JsonSerdeWasmCodec as Encoder<T>>::Error: Debug, <JsonSerdeWasmCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeWasmCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_wb(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_wb_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "miniserde")]
impl<T> ArcOnceResource<T, MiniserdeCodec>
where
T: Send + Sync + 'static,
MiniserdeCodec: Encoder<T> + Decoder<T>,
<MiniserdeCodec as Encoder<T>>::Error: Debug,
<MiniserdeCodec as Decoder<T>>::Error: Debug,
<<MiniserdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_miniserde(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_miniserde_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-lite")]
impl<T> ArcOnceResource<T, SerdeLite<JsonSerdeCodec>>
where
T: Send + Sync + 'static,
SerdeLite<JsonSerdeCodec>: Encoder<T> + Decoder<T>,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Error: Debug, <SerdeLite<JsonSerdeCodec> as Decoder<T>>::Error: Debug,
<<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_lite(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_lite_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "rkyv")]
impl<T> ArcOnceResource<T, RkyvCodec>
where
T: Send + Sync + 'static,
RkyvCodec: Encoder<T> + Decoder<T>,
<RkyvCodec as Encoder<T>>::Error: Debug,
<RkyvCodec as Decoder<T>>::Error: Debug,
<<RkyvCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_rkyv_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
ArcOnceResource::new_with_options(fut, true)
}
}
#[derive(Debug)]
pub struct OnceResource<T, Ser = JsonSerdeCodec> {
inner: ArenaItem<ArcOnceResource<T, Ser>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<T, Ser> Clone for OnceResource<T, Ser> {
fn clone(&self) -> Self {
*self
}
}
impl<T, Ser> Copy for OnceResource<T, Ser> {}
impl<T, Ser> OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: Encoder<T> + Decoder<T>,
<Ser as Encoder<T>>::Error: Debug,
<Ser as Decoder<T>>::Error: Debug,
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_with_options(
fut: impl Future<Output = T> + Send + 'static,
blocking: bool,
) -> Self {
#[cfg(debug_assertions)]
let defined_at = Location::caller();
Self {
inner: ArenaItem::new(ArcOnceResource::new_with_options(
fut, blocking,
)),
#[cfg(debug_assertions)]
defined_at,
}
}
}
impl<T, Ser> OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
/// Returns a `Future` that is ready when this resource has next finished loading.
pub fn ready(&self) -> AsyncDerivedReadyFuture {
self.inner
.try_with_value(|inner| inner.ready())
.unwrap_or_else(unwrap_signal!(self))
}
}
impl<T, Ser> DefinedAt for OnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
{
None
}
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
}
}
impl<T, Ser> IsDisposed for OnceResource<T, Ser> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T, Ser> ToAnySource for OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
fn to_any_source(&self) -> AnySource {
self.inner
.try_with_value(|inner| inner.to_any_source())
.unwrap_or_else(unwrap_signal!(self))
}
}
impl<T, Ser> Track for OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
fn track(&self) {
if let Some(inner) = self.inner.try_get_value() {
inner.track();
}
}
}
impl<T, Ser> ReadUntracked for OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
type Value = ReadGuard<Option<T>, Plain<Option<T>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner
.try_with_value(|inner| inner.try_read_untracked())
.flatten()
}
}
impl<T, Ser> IntoFuture for OnceResource<T, Ser>
where
T: Clone + Send + Sync + 'static,
Ser: 'static,
{
type Output = T;
type IntoFuture = OnceResourceFuture<T>;
fn into_future(self) -> Self::IntoFuture {
self.inner
.try_get_value()
.unwrap_or_else(unwrap_signal!(self))
.into_future()
}
}
impl<T> OnceResource<T, JsonSerdeCodec>
where
T: Send + Sync + 'static,
JsonSerdeCodec: Encoder<T> + Decoder<T>,
<JsonSerdeCodec as Encoder<T>>::Error: Debug,
<JsonSerdeCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, true)
}
}
impl<T> OnceResource<T, FromToStringCodec>
where
T: Send + Sync + 'static,
FromToStringCodec: Encoder<T> + Decoder<T>,
<FromToStringCodec as Encoder<T>>::Error: Debug, <FromToStringCodec as Decoder<T>>::Error: Debug,
<<FromToStringCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
pub fn new_str(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, false)
}
pub fn new_str_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-wasm-bindgen")]
impl<T> OnceResource<T, JsonSerdeWasmCodec>
where
T: Send + Sync + 'static,
JsonSerdeWasmCodec: Encoder<T> + Decoder<T>,
<JsonSerdeWasmCodec as Encoder<T>>::Error: Debug, <JsonSerdeWasmCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeWasmCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_wb(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_wb_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "miniserde")]
impl<T> OnceResource<T, MiniserdeCodec>
where
T: Send + Sync + 'static,
MiniserdeCodec: Encoder<T> + Decoder<T>,
<MiniserdeCodec as Encoder<T>>::Error: Debug,
<MiniserdeCodec as Decoder<T>>::Error: Debug,
<<MiniserdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_miniserde(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_miniserde_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-lite")]
impl<T> OnceResource<T, SerdeLite<JsonSerdeCodec>>
where
T: Send + Sync + 'static,
SerdeLite<JsonSerdeCodec>: Encoder<T> + Decoder<T>,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Error: Debug, <SerdeLite<JsonSerdeCodec> as Decoder<T>>::Error: Debug,
<<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_lite(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_lite_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "rkyv")]
impl<T> OnceResource<T, RkyvCodec>
where
T: Send + Sync + 'static,
RkyvCodec: Encoder<T> + Decoder<T>,
<RkyvCodec as Encoder<T>>::Error: Debug,
<RkyvCodec as Decoder<T>>::Error: Debug,
<<RkyvCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_rkyv_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
OnceResource::new_with_options(fut, true)
}
}

View File

@@ -13,7 +13,7 @@ use codee::{
};
use core::{fmt::Debug, marker::PhantomData};
use futures::Future;
use hydration_context::SerializedDataId;
use hydration_context::{SerializedDataId, SharedContext};
use reactive_graph::{
computed::{
ArcAsyncDerived, ArcMemo, AsyncDerived, AsyncDerivedFuture,
@@ -28,10 +28,14 @@ use std::{
future::{pending, IntoFuture},
ops::Deref,
panic::Location,
sync::atomic::{AtomicBool, Ordering},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool = AtomicBool::new(false);
pub(crate) static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool =
AtomicBool::new(false);
pub struct SuppressResourceLoad;
@@ -175,7 +179,7 @@ where
.map(|sc| sc.next_id())
.unwrap_or_default();
let initial = Self::initial_value(&id);
let initial = initial_value::<T, Ser>(&id, shared_context.as_ref());
let is_ready = initial.is_some();
let refetch = ArcRwSignal::new(0);
@@ -253,43 +257,53 @@ where
pub fn refetch(&self) {
*self.refetch.write() += 1;
}
}
#[inline(always)]
#[allow(unused)]
fn initial_value(id: &SerializedDataId) -> Option<T> {
#[cfg(feature = "hydration")]
{
use std::borrow::Borrow;
#[inline(always)]
#[allow(unused)]
pub(crate) fn initial_value<T, Ser>(
id: &SerializedDataId,
shared_context: Option<&Arc<dyn SharedContext + Send + Sync>>,
) -> Option<T>
where
Ser: Encoder<T> + Decoder<T>,
<Ser as Encoder<T>>::Error: Debug,
<Ser as Decoder<T>>::Error: Debug,
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
#[cfg(feature = "hydration")]
{
use std::borrow::Borrow;
let shared_context = Owner::current_shared_context();
if let Some(shared_context) = shared_context {
let value = shared_context.read_data(id);
if let Some(value) = value {
let encoded =
match <Ser as Decoder<T>>::Encoded::from_encoded_str(
&value,
) {
Ok(value) => value,
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("couldn't deserialize: {e:?}");
return None;
}
};
let encoded = encoded.borrow();
match Ser::decode(encoded) {
Ok(value) => return Some(value),
#[allow(unused)]
let shared_context = Owner::current_shared_context();
if let Some(shared_context) = shared_context {
let value = shared_context.read_data(id);
if let Some(value) = value {
let encoded =
match <Ser as Decoder<T>>::Encoded::from_encoded_str(&value)
{
Ok(value) => value,
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("couldn't deserialize: {e:?}");
return None;
}
};
let encoded = encoded.borrow();
match Ser::decode(encoded) {
Ok(value) => return Some(value),
#[allow(unused)]
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("couldn't deserialize: {e:?}");
}
}
}
}
None
}
None
}
impl<T, E, Ser> ArcResource<Result<T, E>, Ser>

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.0-gamma"
version = "0.7.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -1,6 +1,6 @@
[package]
name = "next_tuple"
version = "0.1.0-gamma"
version = "0.1.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.1.0-gamma"
version = "0.1.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,9 +1,12 @@
use crate::{
computed::{ArcMemo, Memo},
diagnostics::is_suppressing_resource_load,
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
SyncStorage,
},
signal::{ArcRwSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, GetUntracked, Update},
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update},
unwrap_signal,
};
use any_spawner::Executor;
@@ -93,6 +96,7 @@ pub struct ArcAction<I, O> {
input: ArcRwSignal<Option<I>>,
value: ArcRwSignal<Option<O>>,
version: ArcRwSignal<usize>,
dispatched: ArcStoredValue<usize>,
#[allow(clippy::complexity)]
action_fn: Arc<
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
@@ -108,6 +112,7 @@ impl<I, O> Clone for ArcAction<I, O> {
input: self.input.clone(),
value: self.value.clone(),
version: self.version.clone(),
dispatched: self.dispatched.clone(),
action_fn: self.action_fn.clone(),
#[cfg(debug_assertions)]
defined_at: self.defined_at,
@@ -191,6 +196,7 @@ where
input: Default::default(),
value: ArcRwSignal::new(value),
version: Default::default(),
dispatched: Default::default(),
action_fn: Arc::new(move |input| Box::pin(action_fn(input))),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
@@ -230,14 +236,14 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version =
self.version.try_get_untracked().unwrap_or_default();
let current_version = self.dispatched.get_value();
self.input.try_update(|inp| *inp = Some(input));
// Spawn the task
crate::spawn({
let input = self.input.clone();
let version = self.version.clone();
let dispatched = self.dispatched.clone();
let value = self.value.clone();
let in_flight = self.in_flight.clone();
async move {
@@ -249,7 +255,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = version.get_untracked() <= current_version;
let is_latest = dispatched.get_value() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -282,8 +288,7 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version =
self.version.try_get_untracked().unwrap_or_default();
let current_version = self.dispatched.get_value();
self.input.try_update(|inp| *inp = Some(input));
// Spawn the task
@@ -291,6 +296,7 @@ where
let input = self.input.clone();
let version = self.version.clone();
let value = self.value.clone();
let dispatched = self.dispatched.clone();
let in_flight = self.in_flight.clone();
async move {
select! {
@@ -301,7 +307,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = version.get_untracked() <= current_version;
let is_latest = dispatched.get_value() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -351,6 +357,7 @@ where
input: Default::default(),
value: ArcRwSignal::new(value),
version: Default::default(),
dispatched: Default::default(),
action_fn: Arc::new(move |input| {
Box::pin(SendWrapper::new(action_fn(input)))
}),

View File

@@ -15,6 +15,7 @@ use crate::{
};
pub use arc_memo::*;
pub use async_derived::*;
pub(crate) use inner::MemoInner;
pub use memo::*;
pub use selector::*;

View File

@@ -534,11 +534,11 @@ impl<T: 'static> ArcAsyncDerived<T> {
/// Returns a `Future` that is ready when this resource has next finished loading.
pub fn ready(&self) -> AsyncDerivedReadyFuture {
AsyncDerivedReadyFuture {
source: self.to_any_source(),
loading: Arc::clone(&self.loading),
wakers: Arc::clone(&self.wakers),
}
AsyncDerivedReadyFuture::new(
self.to_any_source(),
&self.loading,
&self.wakers,
)
}
}

View File

@@ -34,6 +34,21 @@ pub struct AsyncDerivedReadyFuture {
pub(crate) wakers: Arc<RwLock<Vec<Waker>>>,
}
impl AsyncDerivedReadyFuture {
/// Creates a new [`Future`] that will be ready when the given resource is ready.
pub fn new(
source: AnySource,
loading: &Arc<AtomicBool>,
wakers: &Arc<RwLock<Vec<Waker>>>,
) -> Self {
AsyncDerivedReadyFuture {
source,
loading: Arc::clone(loading),
wakers: Arc::clone(wakers),
}
}
}
impl Future for AsyncDerivedReadyFuture {
type Output = ();

View File

@@ -53,7 +53,8 @@ impl Drop for SpecialNonReactiveZoneGuard {
}
pin_project! {
pub(crate) struct SpecialNonReactiveFuture<Fut> {
#[doc(hidden)]
pub struct SpecialNonReactiveFuture<Fut> {
#[pin]
inner: Fut
}

View File

@@ -125,7 +125,7 @@ pub fn log_warning(text: Arguments) {
/// Calls [`Executor::spawn`], but ensures that the task also runs in the current arena, if
/// multithreaded arena sandboxing is enabled.
pub(crate) fn spawn(task: impl Future<Output = ()> + Send + 'static) {
pub fn spawn(task: impl Future<Output = ()> + Send + 'static) {
#[cfg(feature = "sandboxed-arenas")]
let task = owner::Sandboxed::new(task);

View File

@@ -12,12 +12,14 @@ use std::{
sync::{Arc, RwLock, Weak},
};
mod arc_stored_value;
mod arena;
mod arena_item;
mod context;
mod storage;
mod stored_value;
use self::arena::Arena;
pub use arc_stored_value::ArcStoredValue;
#[cfg(feature = "sandboxed-arenas")]
pub use arena::sandboxed::Sandboxed;
#[cfg(feature = "sandboxed-arenas")]

View File

@@ -0,0 +1,126 @@
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, IsDisposed, ReadValue, WriteValue},
};
use std::{
fmt::{Debug, Formatter},
hash::Hash,
panic::Location,
sync::{Arc, RwLock},
};
/// A reference-counted getter for any value non-reactively.
///
/// This is a reference-counted value, which is `Clone` but not `Copy`.
/// For arena-allocated `Copy` values, use [`StoredValue`](super::StoredValue).
///
/// This allows you to create a stable reference for any value by storing it within
/// the reactive system. Unlike e.g. [`ArcRwSignal`](crate::signal::ArcRwSignal), it is not reactive;
/// accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
pub struct ArcStoredValue<T> {
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
value: Arc<RwLock<T>>,
}
impl<T> Clone for ArcStoredValue<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
}
}
}
impl<T> Debug for ArcStoredValue<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ArcStoredValue")
.field("type", &std::any::type_name::<T>())
.field("value", &Arc::as_ptr(&self.value))
.finish()
}
}
impl<T: Default> Default for ArcStoredValue<T> {
#[track_caller]
fn default() -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(T::default())),
}
}
}
impl<T> PartialEq for ArcStoredValue<T> {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.value, &other.value)
}
}
impl<T> Eq for ArcStoredValue<T> {}
impl<T> Hash for ArcStoredValue<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(&Arc::as_ptr(&self.value), state);
}
}
impl<T> DefinedAt for ArcStoredValue<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<T> ArcStoredValue<T> {
/// Creates a new stored value, taking the initial value as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
#[track_caller]
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
}
}
}
impl<T> ReadValue for ArcStoredValue<T>
where
T: 'static,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
}
}
impl<T> WriteValue for ArcStoredValue<T>
where
T: 'static,
{
type Value = T;
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
UntrackedWriteGuard::try_new(self.value.clone())
}
}
impl<T> IsDisposed for ArcStoredValue<T> {
fn is_disposed(&self) -> bool {
false
}
}

View File

@@ -1,14 +1,16 @@
use super::{ArenaItem, LocalStorage, Storage, SyncStorage};
use super::{
arc_stored_value::ArcStoredValue, ArenaItem, LocalStorage, Storage,
SyncStorage,
};
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, Dispose, IsDisposed},
traits::{DefinedAt, Dispose, IsDisposed, ReadValue, WriteValue},
unwrap_signal,
};
use or_poisoned::OrPoisoned;
use std::{
fmt::{Debug, Formatter},
hash::Hash,
panic::Location,
sync::{Arc, RwLock},
};
/// A **non-reactive**, `Copy` handle for any value.
@@ -18,9 +20,8 @@ use std::{
/// and [`RwSignal`](crate::signal::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
#[derive(Debug)]
pub struct StoredValue<T, S = SyncStorage> {
value: ArenaItem<Arc<RwLock<T>>, S>,
value: ArenaItem<ArcStoredValue<T>, S>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -33,6 +34,18 @@ impl<T, S> Clone for StoredValue<T, S> {
}
}
impl<T, S> Debug for StoredValue<T, S>
where
S: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StoredValue")
.field("type", &std::any::type_name::<T>())
.field("value", &self.value)
.finish()
}
}
impl<T, S> PartialEq for StoredValue<T, S> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
@@ -63,13 +76,13 @@ impl<T, S> DefinedAt for StoredValue<T, S> {
impl<T, S> StoredValue<T, S>
where
T: 'static,
S: Storage<Arc<RwLock<T>>>,
S: Storage<ArcStoredValue<T>>,
{
/// Stores the given value in the arena allocator.
#[track_caller]
pub fn new_with_storage(value: T) -> Self {
Self {
value: ArenaItem::new_with_storage(Arc::new(RwLock::new(value))),
value: ArenaItem::new_with_storage(ArcStoredValue::new(value)),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
@@ -79,7 +92,7 @@ where
impl<T, S> Default for StoredValue<T, S>
where
T: Default + 'static,
S: Storage<Arc<RwLock<T>>>,
S: Storage<ArcStoredValue<T>>,
{
#[track_caller] // Default trait is not annotated with #[track_caller]
fn default() -> Self {
@@ -109,268 +122,31 @@ where
}
}
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S> {
/// Returns an [`Option`] of applying a function to the value within the [`StoredValue`].
///
/// If the owner of the reactive node has not been disposed [`Some`] is returned. Calling this
/// function after the owner has been disposed will always return [`None`].
///
/// See [`StoredValue::with_value`] for a version that panics in the case of the owner being
/// disposed.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data {
/// rows: vec![0, 1, 2, 3, 4],
/// });
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let length_fn = move || data.try_with_value(|inner| inner.rows.len());
///
/// let sum = data.try_with_value(|inner| inner.rows.iter().sum::<u8>());
///
/// assert_eq!(sum, Some(10));
/// assert_eq!(length_fn(), Some(5));
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// let last = data.try_with_value(|inner| inner.rows.last().cloned());
///
/// assert_eq!(last, None);
/// assert_eq!(length_fn(), None);
/// ```
#[track_caller]
pub fn try_with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> Option<U> {
impl<T, S> ReadValue for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
self.value
.try_get_value()
.map(|inner| fun(&*inner.read().or_poisoned()))
.and_then(|inner| inner.try_read_value())
}
}
/// Returns the output of applying a function to the value within the [`StoredValue`].
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_with_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data {
/// rows: vec![1, 2, 3],
/// });
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let length_fn = move || data.with_value(|inner| inner.rows.len());
///
/// let sum = data.with_value(|inner| inner.rows.iter().sum::<u8>());
///
/// assert_eq!(sum, 6);
/// assert_eq!(length_fn(), 3);
/// ```
#[track_caller]
pub fn with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> U {
self.try_with_value(fun)
.unwrap_or_else(unwrap_signal!(self))
}
impl<T, S> WriteValue for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = T;
/// Returns a read guard to the stored data, or `None` if the owner of the reactive node has been disposed.
#[track_caller]
pub fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
self.value
.try_get_value()
.and_then(|inner| Plain::try_new(inner).map(ReadGuard::new))
}
/// Returns a read guard to the stored data.
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_read_value`] for a version without panic.
#[track_caller]
pub fn read_value(&self) -> ReadGuard<T, Plain<T>> {
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
}
/// Returns a write guard to the stored data, or `None` if the owner of the reactive node has been disposed.
#[track_caller]
pub fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
self.value
.try_get_value()
.and_then(|inner| UntrackedWriteGuard::try_new(inner))
}
/// Returns a write guard to the stored data.
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_write_value`] for a version without panic.
#[track_caller]
pub fn write_value(&self) -> UntrackedWriteGuard<T> {
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
}
/// Updates the current value by applying the given closure, returning the return value of the
/// closure, or `None` if the value has already been disposed.
pub fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.value
.try_get_value()
.map(|inner| fun(&mut *inner.write().or_poisoned()))
}
/// Updates the value within [`StoredValue`] by applying a function to it.
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_update_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// #[derive(Default)] // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data::default());
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let push_next = move || {
/// data.update_value(|inner| match inner.rows.last().as_deref() {
/// Some(n) => inner.rows.push(n + 1),
/// None => inner.rows.push(0),
/// })
/// };
///
/// data.update_value(|inner| inner.rows = vec![5, 6, 7]);
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&7)));
///
/// push_next();
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&8)));
///
/// data.update_value(|inner| {
/// std::mem::take(inner) // sets Data back to default
/// });
/// data.with_value(|inner| assert!(inner.rows.is_empty()));
/// ```
pub fn update_value<U>(&self, fun: impl FnOnce(&mut T) -> U) {
self.try_update_value(fun);
}
/// Sets the value within [`StoredValue`].
///
/// Returns [`Some`] containing the passed value if the owner of the reactive node has been
/// disposed.
///
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
/// would be too expensive, prefer [`StoredValue::try_update_value`].
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// let data = StoredValue::new(String::default());
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let say_hello = move || {
/// // Note that using the `update` methods would be more efficient here.
/// data.try_set_value("Hello, World!".into())
/// };
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let reset = move || {
/// // Note that using the `update` methods would be more efficient here.
/// data.try_set_value(Default::default())
/// };
/// assert_eq!(data.get_value(), "");
///
/// // None is returned because the value was able to be updated
/// assert_eq!(say_hello(), None);
///
/// assert_eq!(data.get_value(), "Hello, World!");
///
/// reset();
/// assert_eq!(data.get_value(), "");
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// // The reactive owner is disposed, so the value we intended to set is now
/// // returned as some.
/// assert_eq!(say_hello().as_deref(), Some("Hello, World!"));
/// assert_eq!(reset().as_deref(), Some(""));
/// ```
pub fn try_set_value(&self, value: T) -> Option<T> {
match self.value.try_get_value() {
Some(inner) => {
*inner.write().or_poisoned() = value;
None
}
None => Some(value),
}
}
/// Sets the value within [`StoredValue`].
///
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
/// would be too expensive, prefer [`StoredValue::update_value`].
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_set_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// let data = StoredValue::new(10);
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.get_value(), u8::MAX);
///
/// zero();
/// assert_eq!(data.get_value(), u8::MIN);
/// ```
pub fn set_value(&self, value: T) {
self.update_value(|n| *n = value);
.and_then(|inner| inner.try_write_value())
}
}
@@ -380,90 +156,39 @@ impl<T, S> IsDisposed for StoredValue<T, S> {
}
}
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S>
where
T: Clone + 'static,
{
/// Returns the value within [`StoredValue`] by cloning.
///
/// Returns [`Some`] containing the value if the owner of the reactive node has not been
/// disposed. When disposed, returns [`None`].
///
/// See [`StoredValue::try_with_value`] for a version that avoids cloning. See
/// [`StoredValue::get_value`] for a version that clones, but panics if the node is disposed.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// // u8 is practically free to clone.
/// let data: StoredValue<u8> = StoredValue::new(10);
///
/// // Larger data structures can become very expensive to clone.
/// // You may prefer to use StoredValue::try_with_value.
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
///
/// // Easy to move into closures because StoredValue is Copy + 'static
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.try_get_value(), Some(u8::MAX));
///
/// zero();
/// assert_eq!(data.try_get_value(), Some(u8::MIN));
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// assert_eq!(data.try_get_value(), None);
/// ```
pub fn try_get_value(&self) -> Option<T> {
self.try_with_value(T::clone)
}
/// Returns the value within [`StoredValue`] by cloning.
///
/// See [`StoredValue::with_value`] for a version that avoids cloning.
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_get_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// // u8 is practically free to clone.
/// let data: StoredValue<u8> = StoredValue::new(10);
///
/// // Larger data structures can become very expensive to clone.
/// // You may prefer to use StoredValue::try_with_value.
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
///
/// // Easy to move into closures because StoredValue is Copy + 'static
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.get_value(), u8::MAX);
///
/// zero();
/// assert_eq!(data.get_value(), u8::MIN);
/// ```
pub fn get_value(&self) -> T {
self.with_value(T::clone)
}
}
impl<T, S> Dispose for StoredValue<T, S> {
fn dispose(self) {
self.value.dispose();
}
}
impl<T> From<ArcStoredValue<T>> for StoredValue<T>
where
T: Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcStoredValue<T>) -> Self {
StoredValue {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: ArenaItem::new(value),
}
}
}
impl<T, S> From<StoredValue<T, S>> for ArcStoredValue<T>
where
S: Storage<ArcStoredValue<T>>,
{
#[track_caller]
fn from(value: StoredValue<T, S>) -> Self {
value
.value
.try_get_value()
.unwrap_or_else(unwrap_signal!(value))
}
}
/// Creates a new [`StoredValue`].
#[inline(always)]
#[track_caller]

View File

@@ -75,7 +75,7 @@ impl<T: Serialize + 'static, St: Storage<T>> Serialize for ArcMemo<T, St> {
impl<T, St> Serialize for MaybeSignal<T, St>
where
T: Send + Sync + Serialize,
T: Clone + Send + Sync + Serialize,
St: Storage<SignalTypes<T, St>> + Storage<T>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>

View File

@@ -623,3 +623,46 @@ where
Display::fmt(&**self, f)
}
}
/// A wrapper that implements [`Deref`] and [`Borrow`] for itself.
pub struct Derefable<T>(pub T);
impl<T> Clone for Derefable<T>
where
T: Clone,
{
fn clone(&self) -> Self {
Derefable(self.0.clone())
}
}
impl<T> std::ops::Deref for Derefable<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> Borrow<T> for Derefable<T> {
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T> PartialEq<T> for Derefable<T>
where
T: PartialEq,
{
fn eq(&self, other: &T) -> bool {
self.deref() == other
}
}
impl<T> Display for Derefable<T>
where
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&**self, f)
}
}

View File

@@ -52,7 +52,7 @@ use crate::{
effect::Effect,
graph::{Observer, Source, Subscriber, ToAnySource},
owner::Owner,
signal::{arc_signal, ArcReadSignal},
signal::{arc_signal, guards::UntrackedWriteGuard, ArcReadSignal},
};
use any_spawner::Executor;
use futures::{Stream, StreamExt};
@@ -168,11 +168,20 @@ pub trait ReadUntracked: Sized + DefinedAt {
self.try_read_untracked()
.unwrap_or_else(unwrap_signal!(self))
}
/// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
///
/// If your type contains a [`Signal`](crate::wrappers::read::Signal),
/// call it's [`ReadUntracked::custom_try_read`] here, else return `None`.
#[track_caller]
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
None
}
}
/// Give read-only access to a signal's value by reference through a guard type,
/// and subscribes the active reactive observer (an effect or computed) to changes in its value.
pub trait Read {
pub trait Read: DefinedAt {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
@@ -185,7 +194,9 @@ pub trait Read {
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn read(&self) -> Self::Value;
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> Read for T
@@ -195,13 +206,18 @@ where
type Value = T::Value;
fn try_read(&self) -> Option<Self::Value> {
self.track();
self.try_read_untracked()
}
fn read(&self) -> Self::Value {
self.track();
self.read_untracked()
// The [`Read`] trait is auto implemented for types that implement [`ReadUntracked`] + [`Track`]. The [`Read`] trait then auto implements the [`With`] and [`Get`] traits too.
//
// This is a problem for e.g. the [`Signal`](crate::wrappers::read::Signal) type,
// this type must use a custom [`Read::try_read`] implementation to avoid an unnecessary clone.
//
// This is a backdoor to allow overriding the [`Read::try_read`] implementation despite it being auto implemented.
if let Some(custom) = self.custom_try_read() {
custom
} else {
self.track();
self.try_read_untracked()
}
}
}
@@ -307,14 +323,13 @@ pub trait With: DefinedAt {
impl<T> With for T
where
T: WithUntracked + Track,
T: Read,
{
type Value = <T as WithUntracked>::Value;
type Value = <<T as Read>::Value as Deref>::Target;
#[track_caller]
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
self.track();
self.try_with_untracked(fun)
self.try_read().map(|val| fun(&val))
}
}
@@ -639,3 +654,189 @@ pub fn panic_getting_disposed_signal(
)
}
}
/// A variation of the [`Read`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait ReadValue: Sized + DefinedAt {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
/// Returns the non-reactive guard, or `None` if the value has already been disposed.
#[track_caller]
fn try_read_value(&self) -> Option<Self::Value>;
/// Returns the non-reactive guard.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn read_value(&self) -> Self::Value {
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
}
}
/// A variation of the [`With`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait WithValue: DefinedAt {
/// The type of the value contained in the value.
type Value: ?Sized;
/// Applies the closure to the value, non-reactively, and returns the result,
/// or `None` if the value has already been disposed.
#[track_caller]
fn try_with_value<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U>;
/// Applies the closure to the value, non-reactively, and returns the result.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn with_value<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
self.try_with_value(fun)
.unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> WithValue for T
where
T: DefinedAt + ReadValue,
{
type Value = <<Self as ReadValue>::Value as Deref>::Target;
fn try_with_value<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
self.try_read_value().map(|value| fun(&value))
}
}
/// A variation of the [`Get`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait GetValue: DefinedAt {
/// The type of the value contained in the value.
type Value: Clone;
/// Clones and returns the value of the value, non-reactively,
/// or `None` if the value has already been disposed.
#[track_caller]
fn try_get_value(&self) -> Option<Self::Value>;
/// Clones and returns the value of the value, non-reactively.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn get_value(&self) -> Self::Value {
self.try_get_value().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> GetValue for T
where
T: WithValue,
T::Value: Clone,
{
type Value = <Self as WithValue>::Value;
fn try_get_value(&self) -> Option<Self::Value> {
self.try_with_value(Self::Value::clone)
}
}
/// A variation of the [`Write`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait WriteValue: Sized + DefinedAt {
/// The type of the value's value.
type Value: Sized + 'static;
/// Returns a non-reactive write guard, or `None` if the value has already been disposed.
#[track_caller]
fn try_write_value(&self) -> Option<UntrackedWriteGuard<Self::Value>>;
/// Returns a non-reactive write guard.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn write_value(&self) -> UntrackedWriteGuard<Self::Value> {
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
}
}
/// A variation of the [`Update`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait UpdateValue: DefinedAt {
/// The type of the value contained in the value.
type Value;
/// Updates the value, returning the value that is
/// returned by the update function, or `None` if the value has already been disposed.
#[track_caller]
fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
) -> Option<U>;
/// Updates the value.
#[track_caller]
fn update_value(&self, fun: impl FnOnce(&mut Self::Value)) {
self.try_update_value(fun);
}
}
impl<T> UpdateValue for T
where
T: WriteValue,
{
type Value = <Self as WriteValue>::Value;
#[track_caller]
fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
) -> Option<U> {
let mut guard = self.try_write_value()?;
Some(fun(&mut *guard))
}
}
/// A variation of the [`Set`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait SetValue: DefinedAt {
/// The type of the value contained in the value.
type Value;
/// Updates the value by replacing it, non-reactively.
///
/// If the value has already been disposed, returns `Some(value)` with the value that was
/// passed in. Otherwise, returns `None`.
#[track_caller]
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value>;
/// Updates the value by replacing it, non-reactively.
#[track_caller]
fn set_value(&self, value: Self::Value) {
self.try_set_value(value);
}
}
impl<T> SetValue for T
where
T: WriteValue,
{
type Value = <Self as WriteValue>::Value;
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value> {
// Unlike most other traits, for these None actually means success:
if let Some(mut guard) = self.try_write_value() {
*guard = value;
None
} else {
Some(value)
}
}
}

View File

@@ -3,15 +3,29 @@
/// Types that abstract over signals with values that can be read.
pub mod read {
use crate::{
computed::{ArcMemo, Memo},
computed::{ArcMemo, Memo, MemoInner},
graph::untrack,
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, With, WithUntracked},
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
SyncStorage,
},
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
},
traits::{
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
},
unwrap_signal,
};
use send_wrapper::SendWrapper;
use std::{panic::Location, sync::Arc};
use std::{
borrow::Borrow,
fmt::Display,
ops::Deref,
panic::Location,
sync::{Arc, RwLock},
};
/// Possibilities for the inner type of a [`Signal`].
pub enum SignalTypes<T, S = SyncStorage>
@@ -24,6 +38,8 @@ pub mod read {
Memo(ArcMemo<T, S>),
/// A derived signal.
DerivedSignal(Arc<dyn Fn() -> T + Send + Sync>),
/// A static, stored value.
Stored(ArcStoredValue<T>),
}
impl<T, S> Clone for SignalTypes<T, S>
@@ -35,6 +51,7 @@ pub mod read {
Self::ReadSignal(arg0) => Self::ReadSignal(arg0.clone()),
Self::Memo(arg0) => Self::Memo(arg0.clone()),
Self::DerivedSignal(arg0) => Self::DerivedSignal(arg0.clone()),
Self::Stored(arg0) => Self::Stored(arg0.clone()),
}
}
}
@@ -52,6 +69,9 @@ pub mod read {
Self::DerivedSignal(_) => {
f.debug_tuple("DerivedSignal").finish()
}
Self::Stored(arg0) => {
f.debug_tuple("Static").field(arg0).finish()
}
}
}
}
@@ -168,6 +188,16 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
#[track_caller]
pub fn stored(value: T) -> Self {
Self {
inner: SignalTypes::Stored(ArcStoredValue::new(value)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Default for ArcSignal<T, SyncStorage>
@@ -175,7 +205,7 @@ pub mod read {
T: Default + Send + Sync + 'static,
{
fn default() -> Self {
Self::derive(|| Default::default())
Self::stored(Default::default())
}
}
@@ -231,40 +261,71 @@ pub mod read {
}
}
impl<T, S> WithUntracked for ArcSignal<T, S>
impl<T, S> Track for ArcSignal<T, S>
where
S: Storage<T>,
{
type Value = T;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn track(&self) {
match &self.inner {
SignalTypes::ReadSignal(i) => i.try_with_untracked(fun),
SignalTypes::Memo(i) => i.try_with_untracked(fun),
SignalTypes::DerivedSignal(i) => Some(untrack(|| fun(&i()))),
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
impl<T, S> With for ArcSignal<T, S>
impl<T, S> ReadUntracked for ArcSignal<T, S>
where
S: Storage<T>,
T: Clone,
{
type Value = T;
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn try_read_untracked(&self) -> Option<Self::Value> {
match &self.inner {
SignalTypes::ReadSignal(i) => i.try_with(fun),
SignalTypes::Memo(i) => i.try_with(fun),
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
SignalTypes::ReadSignal(i) => {
i.try_read_untracked().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read_untracked().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(untrack(|| i())))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
}
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
Some(
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new),
)
}
}
@@ -343,52 +404,92 @@ pub mod read {
}
}
impl<T, S> WithUntracked for Signal<T, S>
impl<T, S> Track for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
S: Storage<T> + Storage<SignalTypes<T, S>>,
{
type Value = T;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
self.inner
fn track(&self) {
let inner = self
.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| match &inner {
SignalTypes::ReadSignal(i) => i.try_with_untracked(fun),
SignalTypes::Memo(i) => i.try_with_untracked(fun),
SignalTypes::DerivedSignal(i) => {
Some(untrack(|| fun(&i())))
}
})
.unwrap_or_else(unwrap_signal!(self));
match inner {
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
impl<T, S> With for Signal<T, S>
impl<T, S> ReadUntracked for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = T;
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| match &inner {
SignalTypes::ReadSignal(i) => i.try_with(fun),
SignalTypes::Memo(i) => i.try_with(fun),
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
.and_then(|inner| {
match &inner {
SignalTypes::ReadSignal(i) => {
i.try_read_untracked().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read_untracked().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(untrack(|| i())))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
})
}
/// Overriding the default auto implemented [`Read::try_read`] to combine read and track,
/// to avoid 2 clones and just have 1 in the [`SignalTypes::DerivedSignal`].
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
Some(
self.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| {
match &inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
}),
)
}
}
impl<T> Signal<T>
@@ -433,6 +534,18 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
#[track_caller]
pub fn stored(value: T) -> Self {
Self {
inner: ArenaItem::new_with_storage(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Signal<T, LocalStorage>
@@ -460,6 +573,19 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
/// Works like [`Signal::stored`] but uses [`LocalStorage`].
#[track_caller]
pub fn stored_local(value: T) -> Self {
Self {
inner: ArenaItem::new_local(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Default for Signal<T>
@@ -467,7 +593,7 @@ pub mod read {
T: Send + Sync + Default + 'static,
{
fn default() -> Self {
Self::derive(|| Default::default())
Self::stored(Default::default())
}
}
@@ -476,34 +602,34 @@ pub mod read {
T: Default + 'static,
{
fn default() -> Self {
Self::derive_local(|| Default::default())
Self::stored_local(Default::default())
}
}
impl<T: Clone + Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
impl<T: Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
#[track_caller]
fn from(value: T) -> Self {
Self::derive(move || value.clone())
ArcSignal::stored(value)
}
}
impl<T> From<T> for Signal<T>
where
T: Clone + Send + Sync + 'static,
T: Send + Sync + 'static,
{
#[track_caller]
fn from(value: T) -> Self {
Self::derive(move || value.clone())
Self::stored(value)
}
}
impl<T> From<T> for Signal<T, LocalStorage>
where
T: Clone + 'static,
T: 'static,
{
#[track_caller]
fn from(value: T) -> Self {
Self::derive_local(move || value.clone())
Self::stored_local(value)
}
}
@@ -642,6 +768,20 @@ pub mod read {
}
}
impl From<&str> for Signal<String> {
#[track_caller]
fn from(value: &str) -> Self {
Signal::stored(value.to_string())
}
}
impl From<&str> for Signal<String, LocalStorage> {
#[track_caller]
fn from(value: &str) -> Self {
Signal::stored_local(value.to_string())
}
}
/// A wrapper for a value that is *either* `T` or [`Signal<T>`].
///
/// This allows you to create APIs that take either a reactive or a non-reactive value
@@ -715,37 +855,38 @@ pub mod read {
}
}
impl<T, S> WithUntracked for MaybeSignal<T, S>
impl<T, S> Track for MaybeSignal<T, S>
where
S: Storage<SignalTypes<T, S>> + Storage<T>,
S: Storage<T> + Storage<SignalTypes<T, S>>,
{
type Value = T;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn track(&self) {
match self {
Self::Static(t) => Some(fun(t)),
Self::Dynamic(s) => s.try_with_untracked(fun),
Self::Static(_) => {}
Self::Dynamic(signal) => signal.track(),
}
}
}
impl<T, S> With for MaybeSignal<T, S>
impl<T, S> ReadUntracked for MaybeSignal<T, S>
where
T: Send + Sync + 'static,
T: Clone,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = T;
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn try_read_untracked(&self) -> Option<Self::Value> {
match self {
Self::Static(t) => Some(fun(t)),
Self::Dynamic(s) => s.try_with(fun),
Self::Static(t) => {
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
}
Self::Dynamic(s) => s.try_read_untracked(),
}
}
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
match self {
Self::Static(_) => None,
Self::Dynamic(s) => s.custom_try_read(),
}
}
}
@@ -893,7 +1034,7 @@ pub mod read {
impl<S> From<&str> for MaybeSignal<String, S>
where
S: Storage<String>,
S: Storage<String> + Storage<Arc<RwLock<String>>>,
{
fn from(value: &str) -> Self {
Self::Static(value.to_string())
@@ -968,37 +1109,36 @@ pub mod read {
}
}
impl<T, S> WithUntracked for MaybeProp<T, S>
impl<T, S> Track for MaybeProp<T, S>
where
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
S: Storage<Option<T>> + Storage<SignalTypes<Option<T>, S>>,
{
type Value = Option<T>;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn track(&self) {
match &self.0 {
None => Some(fun(&None)),
Some(inner) => inner.try_with_untracked(fun),
None => {}
Some(signal) => signal.track(),
}
}
}
impl<T, S> With for MaybeProp<T, S>
impl<T, S> ReadUntracked for MaybeProp<T, S>
where
T: Send + Sync + 'static,
T: Clone,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
type Value = Option<T>;
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
fn try_with<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
fn try_read_untracked(&self) -> Option<Self::Value> {
match &self.0 {
None => Some(fun(&None)),
Some(inner) => inner.try_with(fun),
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
Some(inner) => inner.try_read_untracked(),
}
}
fn custom_try_read(&self) -> Option<Option<Self::Value>> {
match &self.0 {
None => None,
Some(inner) => inner.custom_try_read(),
}
}
}
@@ -1242,6 +1382,76 @@ pub mod read {
Self(Some(MaybeSignal::from_local(Some(value.to_string()))))
}
}
/// The content of a [`Signal`] wrapper read guard, variable depending on the signal type.
#[derive(Debug)]
pub enum SignalReadGuard<T: 'static, S: Storage<T>> {
/// A read signal guard.
Read(ReadGuard<T, Plain<T>>),
/// A memo guard.
Memo(ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>),
/// A fake guard for derived signals, the content had to actually be cloned, so it's not a guard but we pretend it is.
Owned(T),
}
impl<T, S> Clone for SignalReadGuard<T, S>
where
S: Storage<T>,
T: Clone,
Plain<T>: Clone,
Mapped<Plain<MemoInner<T, S>>, T>: Clone,
{
fn clone(&self) -> Self {
match self {
SignalReadGuard::Read(i) => SignalReadGuard::Read(i.clone()),
SignalReadGuard::Memo(i) => SignalReadGuard::Memo(i.clone()),
SignalReadGuard::Owned(i) => SignalReadGuard::Owned(i.clone()),
}
}
}
impl<T, S> Deref for SignalReadGuard<T, S>
where
S: Storage<T>,
{
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
SignalReadGuard::Read(i) => i,
SignalReadGuard::Memo(i) => i,
SignalReadGuard::Owned(i) => i,
}
}
}
impl<T, S> Borrow<T> for SignalReadGuard<T, S>
where
S: Storage<T>,
{
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T, S> PartialEq<T> for SignalReadGuard<T, S>
where
S: Storage<T>,
T: PartialEq,
{
fn eq(&self, other: &T) -> bool {
self.deref() == other
}
}
impl<T, S> Display for SignalReadGuard<T, S>
where
S: Storage<T>,
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&**self, f)
}
}
}
/// Types that abstract over the ability to update a signal.

View File

@@ -0,0 +1,88 @@
use reactive_graph::{
computed::Memo,
owner::{on_cleanup, Owner},
signal::{RwSignal, Trigger},
traits::{Dispose, GetUntracked, Track},
};
use std::sync::Arc;
#[test]
fn cleanup_on_dispose() {
let owner = Owner::new();
owner.set();
struct ExecuteOnDrop(Option<Box<dyn FnOnce() + Send + Sync>>);
impl ExecuteOnDrop {
fn new(f: impl FnOnce() + Send + Sync + 'static) -> Self {
Self(Some(Box::new(f)))
}
}
impl Drop for ExecuteOnDrop {
fn drop(&mut self) {
self.0.take().unwrap()();
}
}
let trigger = Trigger::new();
println!("STARTING");
let memo = Memo::new(move |_| {
trigger.track();
// An example of why you might want to do this is that
// when something goes out of reactive scope you want it to be cleaned up.
// The cleaning up might have side effects, and those side effects might cause
// re-renders where new `on_cleanup` are registered.
let on_drop = ExecuteOnDrop::new(|| {
on_cleanup(|| println!("Nested cleanup in progress."))
});
on_cleanup(move || {
println!("Cleanup in progress.");
drop(on_drop)
});
});
println!("Memo 1: {:?}", memo);
memo.get_untracked(); // First cleanup registered.
memo.dispose(); // Cleanup not run here.
println!("Cleanup should have been executed.");
let memo = Memo::new(move |_| {
// New cleanup registered. It'll panic here.
on_cleanup(move || println!("Test passed."));
});
println!("Memo 2: {:?}", memo);
println!("^ Note how the memos have the same key (different versions).");
memo.get_untracked(); // First cleanup registered.
println!("Test passed.");
memo.dispose();
}
#[test]
fn leak_on_dispose() {
let owner = Owner::new();
owner.set();
let trigger = Trigger::new();
let value = Arc::new(());
let weak = Arc::downgrade(&value);
let memo = Memo::new(move |_| {
trigger.track();
RwSignal::new(value.clone());
});
memo.get_untracked();
memo.dispose();
assert!(weak.upgrade().is_none()); // Should have been dropped.
}

View File

@@ -1,6 +1,11 @@
[package]
name = "reactive_stores"
version = "0.1.0-gamma"
version = "0.1.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
rust-version.workspace = true
edition.workspace = true
@@ -11,10 +16,10 @@ or_poisoned = { workspace = true }
paste = "1.0"
reactive_graph = { workspace = true }
rustc-hash = "2.0"
reactive_stores_macro = { workspace = true }
[dev-dependencies]
tokio = { version = "1.39", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4.4" }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
reactive_stores_macro = { workspace = true }
reactive_graph = { workspace = true, features = ["effects"] }

15
reactive_stores/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Stores
Stores are a data structure for nested reactivity.
The [`reactive_graph`](https://crates.io/crates/reactive_graph) crate provides primitives for fine-grained reactivity
via signals, memos, and effects.
This crate extends that reactivity to support reactive access to nested dested, without the need to create nested signals.
Using the `#[derive(Store)]` macro on a struct creates a series of getters that allow accessing each field. Individual fields
can then be read as if they were signals. Changes to parents will notify their children, but changing one sibling field will
not notify any of the others, nor will it require diffing those sibling fields (unlike earlier solutions using memoized “slices”).
This is published for use with the Leptos framework but can be used in any scenario where `reactive_graph` is being used
for reactivity.

View File

@@ -258,3 +258,20 @@ where
}
}
}
impl<Inner, Prev> DoubleEndedIterator for StoreFieldIter<Inner, Prev>
where
Inner: StoreField<Value = Prev> + Clone + 'static,
Prev: IndexMut<usize> + 'static,
Prev::Output: Sized + 'static,
{
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > self.idx {
self.len -= 1;
let field = AtIndex::new(self.inner.clone(), self.len);
Some(field)
} else {
None
}
}
}

View File

@@ -682,3 +682,18 @@ where
.map(|key| AtKeyed::new(self.inner.clone(), key))
}
}
impl<Inner, Prev, K, T> DoubleEndedIterator
for StoreFieldKeyedIter<Inner, Prev, K, T>
where
Inner: StoreField<Value = Prev> + Clone + 'static,
T: IndexMut<usize> + 'static,
T::Output: Sized + 'static,
for<'a> &'a T: IntoIterator,
{
fn next_back(&mut self) -> Option<Self::Item> {
self.keys
.pop_back()
.map(|key| AtKeyed::new(self.inner.clone(), key))
}
}

View File

@@ -10,6 +10,7 @@ use reactive_graph::{
Write,
},
};
pub use reactive_stores_macro::*;
use rustc_hash::FxHashMap;
use std::{
any::Any,
@@ -171,12 +172,26 @@ impl KeyMap {
where
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
{
// this incredibly defensive mechanism takes the guard twice
// on initialization. unfortunately, this is because `initialize`, on
// a nested keyed field can, when being initialized), can in fact try
// to take the lock again, as we try to insert the keys of the parent
// while inserting the keys on this child.
//
// see here https://github.com/leptos-rs/leptos/issues/3086
let mut guard = self.0.write().or_poisoned();
let entry = guard
.entry(path)
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
if guard.contains_key(&path) {
let entry = guard.get_mut(&path)?;
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
} else {
drop(guard);
let keys = Box::new(FieldKeys::new(initialize()));
let mut guard = self.0.write().or_poisoned();
let entry = guard.entry(path).or_insert(keys);
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
Some(fun(entry))
}
}
}
@@ -430,7 +445,6 @@ mod tests {
effect::Effect,
traits::{Read, ReadUntracked, Set, Update, Write},
};
use reactive_stores_macro::{Patch, Store};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,

View File

@@ -51,7 +51,6 @@ mod tests {
effect::Effect,
traits::{Get, Read, ReadUntracked, Set, Write},
};
use reactive_stores_macro::Store;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,

View File

@@ -1,6 +1,11 @@
[package]
name = "reactive_stores_macro"
version = "0.1.0-gamma"
version = "0.1.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
repository = "https://github.com/leptos-rs/leptos"
description = "Stores for holding deeply-nested reactive state while maintaining fine-grained reactive tracking."
rust-version.workspace = true
edition.workspace = true

View File

@@ -0,0 +1 @@
This crate provides macro that are helpful or required when using the `reactive_stores` crate.

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.7.0-gamma"
version = "0.7.0-gamma3"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"
@@ -22,13 +22,11 @@ url = "2.5"
js-sys = { version = "0.3.69" }
wasm-bindgen = { version = "0.2.93" }
tracing = { version = "0.1.40", optional = true }
paste = "1.0"
once_cell = "1.19"
send_wrapper = "0.6.0"
thiserror = "1.0"
percent-encoding = { version = "2.3", optional = true }
gloo-net = "0.6.0"
serde = { version = "1", features = ["derive"] }
[dependencies.web-sys]
version = "0.3.70"

View File

@@ -24,6 +24,7 @@ use reactive_graph::{
use std::{
borrow::Cow,
fmt::{Debug, Display},
mem,
sync::Arc,
time::Duration,
};
@@ -47,7 +48,7 @@ where
}
}
#[component]
#[component(transparent)]
pub fn Router<Chil>(
/// The base URL for the router. Defaults to `""`.
#[prop(optional, into)]
@@ -101,6 +102,7 @@ where
location,
state,
set_is_routing,
query_mutations: Default::default(),
});
let children = children.into_inner();
@@ -114,6 +116,8 @@ pub(crate) struct RouterContext {
pub location: Location,
pub state: ArcRwSignal<State>,
pub set_is_routing: Option<SignalSetter<bool>>,
pub query_mutations:
ArcStoredValue<Vec<(Oco<'static, str>, Option<String>)>>,
}
impl RouterContext {
@@ -130,7 +134,7 @@ impl RouterContext {
resolve_path("", path, None)
};
let url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
let mut url = match resolved_to.map(|to| BrowserUrl::parse(&to)) {
Some(Ok(url)) => url,
Some(Err(e)) => {
leptos::logging::error!("Error parsing URL: {e:?}");
@@ -141,6 +145,22 @@ impl RouterContext {
return;
}
};
let query_mutations =
mem::take(&mut *self.query_mutations.write_value());
if !query_mutations.is_empty() {
for (key, value) in query_mutations {
if let Some(value) = value {
url.search_params_mut().replace(key, value);
} else {
url.search_params_mut().remove(&key);
}
}
*url.search_mut() = url
.search_params()
.to_query_string()
.trim_start_matches('?')
.into()
}
if url.origin() != current.origin() {
window().location().set_href(path).unwrap();
@@ -153,13 +173,14 @@ impl RouterContext {
}
// update URL signal, if necessary
let value = url.to_full_path();
if current != url {
drop(current);
self.current_url.set(url);
}
BrowserUrl::complete_navigation(&LocationChange {
value: path.to_string(),
value,
replace: options.replace,
scroll: options.scroll,
state: options.state,
@@ -204,7 +225,7 @@ where
}
}*/
#[component]
#[component(transparent)]
pub fn Routes<Defs, FallbackFn, Fallback>(
fallback: FallbackFn,
children: RouteChildren<Defs>,
@@ -227,7 +248,10 @@ where
base.upgrade_inplace();
base
});
let routes = Routes::new(children.into_inner());
let routes = Routes::new_with_base(
children.into_inner(),
base.clone().unwrap_or_default(),
);
let outer_owner =
Owner::current().expect("creating Routes, but no Owner was found");
move || {
@@ -247,7 +271,7 @@ where
}
}
#[component]
#[component(transparent)]
pub fn FlatRoutes<Defs, FallbackFn, Fallback>(
fallback: FallbackFn,
children: RouteChildren<Defs>,
@@ -273,7 +297,10 @@ where
base.upgrade_inplace();
base
});
let routes = Routes::new(children.into_inner());
let routes = Routes::new_with_base(
children.into_inner(),
base.clone().unwrap_or_default(),
);
let outer_owner =
Owner::current().expect("creating Router, but no Owner was found");
@@ -294,7 +321,7 @@ where
}
}
#[component]
#[component(transparent)]
pub fn Route<Segments, View>(
path: Segments,
view: View,
@@ -306,7 +333,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr)
}
#[component]
#[component(transparent)]
pub fn ParentRoute<Segments, View, Children>(
path: Segments,
view: View,
@@ -320,7 +347,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
}
#[component]
#[component(transparent)]
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
path: Segments,
view: ViewFn,
@@ -362,7 +389,7 @@ where
NestedRoute::new(path, view).ssr_mode(ssr)
}
#[component]
#[component(transparent)]
pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
path: Segments,
view: ViewFn,
@@ -424,7 +451,7 @@ where
///
/// [`leptos_actix`]: <https://docs.rs/leptos_actix/>
/// [`leptos_axum`]: <https://docs.rs/leptos_axum/>
#[component]
#[component(transparent)]
pub fn Redirect<P>(
/// The relative path to which the user should be redirected.
path: P,

View File

@@ -4,15 +4,18 @@ use crate::{
navigate::NavigateOptions,
params::{Params, ParamsError, ParamsMap},
};
use leptos::oco::Oco;
use leptos::{leptos_dom::helpers::request_animation_frame, oco::Oco};
use reactive_graph::{
computed::{ArcMemo, Memo},
owner::use_context,
owner::{expect_context, use_context},
signal::{ArcRwSignal, ReadSignal},
traits::{Get, GetUntracked, With},
traits::{Get, GetUntracked, ReadUntracked, With, WriteValue},
wrappers::write::SignalSetter,
};
use std::str::FromStr;
use std::{
str::FromStr,
sync::atomic::{AtomicBool, Ordering},
};
#[track_caller]
#[deprecated = "This has been renamed to `query_signal` to match Rust naming \
@@ -93,34 +96,45 @@ pub fn query_signal_with_options<T>(
where
T: FromStr + ToString + PartialEq + Send + Sync,
{
static IS_NAVIGATING: AtomicBool = AtomicBool::new(false);
let mut key: Oco<'static, str> = key.into();
let query_map = use_query_map();
let navigate = use_navigate();
let location = use_location();
let RouterContext {
query_mutations, ..
} = expect_context();
let get = Memo::new({
let key = key.clone_inplace();
move |_| {
query_map
.with(|map| map.get(&key).and_then(|value| value.parse().ok()))
query_map.with(|map| {
map.get_str(&key).and_then(|value| value.parse().ok())
})
}
});
let set = SignalSetter::map(move |value: Option<T>| {
let mut new_query_map = query_map.get();
match value {
Some(value) => {
new_query_map.insert(key.to_string(), value.to_string());
}
None => {
new_query_map.remove(&key);
}
}
let qs = new_query_map.to_query_string();
let path = location.pathname.get_untracked();
let hash = location.hash.get_untracked();
let qs = location.query.read_untracked().to_query_string();
let new_url = format!("{path}{qs}{hash}");
navigate(&new_url, nav_options.clone());
query_mutations
.write_value()
.push((key.clone(), value.as_ref().map(ToString::to_string)));
if !IS_NAVIGATING.load(Ordering::Relaxed) {
IS_NAVIGATING.store(true, Ordering::Relaxed);
request_animation_frame({
let navigate = navigate.clone();
let nav_options = nav_options.clone();
move || {
navigate(&new_url, nav_options.clone());
IS_NAVIGATING.store(false, Ordering::Relaxed)
}
})
}
});
(get, set)

View File

@@ -14,7 +14,6 @@ mod method;
mod navigate;
pub mod nested_router;
pub mod params;
//mod router;
mod ssr_mode;
pub mod static_routes;
@@ -24,5 +23,4 @@ pub use leptos_router_macro::path;
pub use matching::*;
pub use method::*;
pub use navigate::*;
//pub use router::*;
pub use ssr_mode::*;

View File

@@ -121,7 +121,7 @@ impl LocationProvider for BrowserUrl {
&& curr.path() == new_url.path()
};
url.set(new_url);
url.set(new_url.clone());
if same_path {
Self::complete_navigation(&loc);
}
@@ -130,12 +130,19 @@ impl LocationProvider for BrowserUrl {
if !same_path {
*pending.lock().or_poisoned() = Some(tx);
}
let url = url.clone();
async move {
if !same_path {
// if it has been canceled, ignore
// otherwise, complete navigation -- i.e., set URL in address bar
if rx.await.is_ok() {
Self::complete_navigation(&loc);
// only update the URL in the browser if this is still the current URL
// if we've navigated to another page in the meantime, don't update the
// browser URL
let curr = url.read_untracked();
if curr == new_url {
Self::complete_navigation(&loc);
}
}
}
}

View File

@@ -36,22 +36,42 @@ impl Url {
&self.origin
}
pub fn origin_mut(&mut self) -> &mut String {
&mut self.origin
}
pub fn path(&self) -> &str {
&self.path
}
pub fn path_mut(&mut self) -> &mut str {
&mut self.path
}
pub fn search(&self) -> &str {
&self.search
}
pub fn search_mut(&mut self) -> &mut String {
&mut self.search
}
pub fn search_params(&self) -> &ParamsMap {
&self.search_params
}
pub fn search_params_mut(&mut self) -> &mut ParamsMap {
&mut self.search_params
}
pub fn hash(&self) -> &str {
&self.hash
}
pub fn hash_mut(&mut self) -> &mut String {
&mut self.hash
}
pub fn provide_server_action_error(&self) {
let search_params = self.search_params();
if let (Some(err), Some(path)) = (
@@ -62,6 +82,19 @@ impl Url {
}
}
pub(crate) fn to_full_path(&self) -> String {
let mut path = self.path.to_string();
if !self.search.is_empty() {
path.push('?');
path.push_str(&self.search);
}
if !self.hash.is_empty() {
path.push('#');
path.push_str(&self.hash);
}
path
}
pub fn escape(s: &str) -> String {
#[cfg(not(feature = "ssr"))]
{

View File

@@ -8,7 +8,7 @@ use crate::{
};
use any_spawner::Executor;
use either_of::{Either, EitherOf3};
use futures::{future::join_all, FutureExt};
use futures::{channel::oneshot, future::join_all, FutureExt};
use leptos::{component, oco::Oco};
use or_poisoned::OrPoisoned;
use reactive_graph::{
@@ -16,6 +16,7 @@ use reactive_graph::{
owner::{provide_context, use_context, Owner},
signal::{ArcRwSignal, ArcTrigger},
traits::{Get, GetUntracked, Notify, ReadUntracked, Set, Track},
transition::AsyncTransition,
wrappers::write::SignalSetter,
};
use send_wrapper::SendWrapper;
@@ -47,7 +48,6 @@ pub(crate) struct NestedRoutesView<Loc, Defs, FalFn> {
pub current_url: ArcRwSignal<Url>,
pub base: Option<Oco<'static, str>>,
pub fallback: FalFn,
#[allow(unused)] // TODO
pub set_is_routing: Option<SignalSetter<bool>>,
}
@@ -155,23 +155,37 @@ where
state.outlets.clear();
}
Some(route) => {
let mut loaders = Vec::new();
if let Some(set_is_routing) = self.set_is_routing {
set_is_routing.set(true);
}
let mut preloaders = Vec::new();
let mut full_loaders = Vec::new();
route.rebuild_nested_route(
&self.current_url.read_untracked(),
self.base,
&mut 0,
&mut loaders,
&mut preloaders,
&mut full_loaders,
&mut state.outlets,
&self.outer_owner,
self.set_is_routing.is_some(),
);
let location = self.location.clone();
Executor::spawn_local(async move {
let triggers = join_all(loaders).await;
let triggers = join_all(preloaders).await;
// tell each one of the outlet triggers that it's ready
for trigger in triggers {
trigger.notify();
}
});
Executor::spawn_local(async move {
join_all(full_loaders).await;
if let Some(set_is_routing) = self.set_is_routing {
set_is_routing.set(false);
}
if let Some(loc) = location {
loc.ready_to_complete();
}
@@ -426,9 +440,7 @@ where
}
}
type OutletViewFn = Box<
dyn Fn() -> Suspend<Pin<Box<dyn Future<Output = AnyView> + Send>>> + Send,
>;
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;
pub(crate) struct RouteContext {
id: RouteMatchId,
@@ -486,14 +498,17 @@ trait AddNestedRoute {
parent: &Owner,
);
#[allow(clippy::too_many_arguments)]
fn rebuild_nested_route(
self,
url: &Url,
base: Option<Oco<'static, str>>,
items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
);
}
@@ -629,14 +644,17 @@ where
}
}
#[allow(clippy::too_many_arguments)]
fn rebuild_nested_route(
self,
url: &Url,
base: Option<Oco<'static, str>>,
items: &mut usize,
loaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
preloaders: &mut Vec<Pin<Box<dyn Future<Output = ArcTrigger>>>>,
full_loaders: &mut Vec<oneshot::Receiver<()>>,
outlets: &mut Vec<RouteContext>,
parent: &Owner,
set_is_routing: bool,
) {
let (parent_params, parent_matches): (Vec<_>, Vec<_>) = outlets
.iter()
@@ -647,7 +665,7 @@ where
match current {
// if there's nothing currently in the routes at this point, build from here
None => {
self.build_nested_route(url, base, loaders, outlets, parent);
self.build_nested_route(url, base, preloaders, outlets, parent);
}
Some(current) => {
// a unique ID for each route, which allows us to compare when we get new matches
@@ -716,11 +734,14 @@ where
let old_owner =
mem::replace(&mut current.owner, parent.child());
let owner = current.owner.clone();
let (full_tx, full_rx) = oneshot::channel();
let full_tx = Mutex::new(Some(full_tx));
full_loaders.push(full_rx);
// send the new view, with the new owner, through the channel to the Outlet,
// and notify the trigger so that the reactive view inside the Outlet tracking
// the trigger runs again
loaders.push(Box::pin(owner.with(|| {
preloaders.push(Box::pin(owner.with(|| {
ScopedFuture::new({
let owner = owner.clone();
let trigger = current.trigger.clone();
@@ -736,15 +757,26 @@ where
Box::new(move || {
let owner = owner.clone();
let view = view.clone();
let full_tx =
full_tx.lock().or_poisoned().take();
Suspend::new(Box::pin(async move {
let view = SendWrapper::new(
owner.with(|| {
ScopedFuture::new(
view.choose(),
async move {
if set_is_routing {
AsyncTransition::run(|| view.choose()).await
} else {
view.choose().await
}
}
)
}),
);
let view = view.await;
if let Some(tx) = full_tx {
_ = tx.send(());
}
owner.with(|| {
OwnedView::new(view).into_any()
})
@@ -766,7 +798,7 @@ where
// if this children has matches, then rebuild the lower section of the tree
if let Some(child) = child {
child.build_nested_route(
url, base, loaders, outlets, &owner,
url, base, preloaders, outlets, &owner,
);
}
@@ -782,7 +814,14 @@ where
let owner = current.owner.clone();
*items += 1;
child.rebuild_nested_route(
url, base, items, loaders, outlets, &owner,
url,
base,
items,
preloaders,
full_loaders,
outlets,
&owner,
set_is_routing,
);
}
}

View File

@@ -1,8 +1,8 @@
use crate::location::{unescape, Url};
use std::{borrow::Cow, mem, str::FromStr, sync::Arc};
use std::{borrow::Cow, str::FromStr, sync::Arc};
use thiserror::Error;
type ParamsMapInner = Vec<(Cow<'static, str>, String)>;
type ParamsMapInner = Vec<(Cow<'static, str>, Vec<String>)>;
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct ParamsMap(ParamsMapInner);
@@ -21,34 +21,63 @@ impl ParamsMap {
}
/// Inserts a value into the map.
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
///
/// If a value with that key already exists, the new value will be added to it.
/// To replace the value instead, see [`replace`].
pub fn insert(&mut self, key: impl Into<Cow<'static, str>>, value: String) {
let value = unescape(&value);
if let Some(prev) = self.0.iter().position(|(k, _)| k == &key) {
return Some(mem::replace(&mut self.0[prev].1, value));
let key = key.into();
if let Some(prev) = self.0.iter_mut().find(|(k, _)| k == &key) {
prev.1.push(value);
} else {
self.0.push((key, vec![value]));
}
self.0.push((key.into(), value));
None
}
/// Gets an owned value from the map.
/// Inserts a value into the map, replacing any existing value for that key.
pub fn replace(
&mut self,
key: impl Into<Cow<'static, str>>,
value: String,
) {
let value = unescape(&value);
let key = key.into();
if let Some(prev) = self.0.iter_mut().find(|(k, _)| k == &key) {
prev.1.clear();
prev.1.push(value);
} else {
self.0.push((key, vec![value]));
}
}
/// Gets the most-recently-added value of this param from the map.
pub fn get(&self, key: &str) -> Option<String> {
self.0
.iter()
.find_map(|(k, v)| (k == key).then_some(v.to_owned()))
self.get_str(key).map(ToOwned::to_owned)
}
/// Gets a referenc to a value from the map.
pub fn get_str(&self, key: &str) -> Option<&str> {
/// Gets all references to a param of this name from the map.
pub fn get_all(&self, key: &str) -> Option<Vec<String>> {
self.0
.iter()
.find_map(|(k, v)| (k == key).then_some(v.as_str()))
.find_map(|(k, v)| if k == key { Some(v.clone()) } else { None })
}
/// Gets a reference to the most-recently-added value of this param from the map.
pub fn get_str(&self, key: &str) -> Option<&str> {
self.0.iter().find_map(|(k, v)| {
if k == key {
v.last().map(|i| i.as_str())
} else {
None
}
})
}
/// Removes a value from the map.
#[inline(always)]
pub fn remove(&mut self, key: &str) -> Option<String> {
pub fn remove(&mut self, key: &str) -> Option<Vec<String>> {
for i in 0..self.0.len() {
if self.0[i].0 == key {
return Some(self.0.swap_remove(i).1);
@@ -62,11 +91,13 @@ impl ParamsMap {
let mut buf = String::new();
if !self.0.is_empty() {
buf.push('?');
for (k, v) in &self.0 {
buf.push_str(&Url::escape(k));
buf.push('=');
buf.push_str(&Url::escape(v));
buf.push('&');
for (k, vs) in &self.0 {
for v in vs {
buf.push_str(&Url::escape(k));
buf.push('=');
buf.push_str(&Url::escape(v));
buf.push('&');
}
}
if buf.len() > 1 {
buf.pop();
@@ -82,11 +113,12 @@ where
V: Into<String>,
{
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
Self(
iter.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
let mut map = Self::new();
for (key, value) in iter {
map.insert(key, value.into());
}
map
}
}
@@ -95,13 +127,21 @@ impl IntoIterator for ParamsMap {
type IntoIter = ParamsMapIter;
fn into_iter(self) -> Self::IntoIter {
ParamsMapIter(self.0.into_iter())
let inner = self.0.into_iter().fold(vec![], |mut c, (k, vs)| {
for v in vs {
c.push((k.clone(), v));
}
c
});
ParamsMapIter(inner.into_iter())
}
}
/// An iterator over the keys and values of a [`ParamsMap`].
#[derive(Debug)]
pub struct ParamsMapIter(<ParamsMapInner as IntoIterator>::IntoIter);
pub struct ParamsMapIter(
<Vec<(Cow<'static, str>, String)> as IntoIterator>::IntoIter,
);
impl Iterator for ParamsMapIter {
type Item = (Cow<'static, str>, String);
@@ -201,3 +241,20 @@ impl PartialEq for ParamsError {
}
}
}
#[cfg(all(test, feature = "ssr"))]
mod tests {
use super::*;
#[test]
fn paramsmap_to_query_string() {
let mut map = ParamsMap::new();
let key = "param".to_string();
let value1 = "a".to_string();
let value2 = "b".to_string();
map.insert(key.clone(), value1);
map.insert(key, value2);
let query_string = map.to_query_string();
assert_eq!(&query_string, "?param=a&param=b")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.7.0-gamma"
version = "0.7.0-gamma3"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -25,8 +25,8 @@ inventory = { version = "0.3.15", optional = true }
dashmap = "6.0"
once_cell = "1.19"
## servers
# actix
## servers
# actix
actix-web = { version = "4.8", optional = true }
# axum
@@ -36,12 +36,12 @@ axum = { version = "0.7.5", optional = true, default-features = false, features
tower = { version = "0.4.13", optional = true }
tower-layer = { version = "0.3.2", optional = true }
## input encodings
## input encodings
serde_qs = { version = "0.13.0", optional = true }
multer = { version = "3.1", optional = true }
## output encodings
# serde
## output encodings
# serde
serde_json = "1.0"
serde-lite = { version = "0.5.0", features = ["derive"], optional = true }
futures = "0.3.30"
@@ -51,11 +51,7 @@ postcard = { version = "1", features = ["alloc"], optional = true }
hyper = { version = "1.4", optional = true }
bytes = "1.7"
http-body-util = { version = "0.1.2", optional = true }
rkyv = { version = "0.7.44", features = [
"validation",
"uuid",
"strict",
], optional = true }
rkyv = { version = "0.8.8", optional = true }
rmp-serde = { version = "1.3.0", optional = true }
# client
@@ -72,7 +68,7 @@ web-sys = { version = "0.3.70", optional = true, features = [
"AbortSignal",
] }
# reqwest client
# reqwest client
reqwest = { version = "0.12.5", default-features = false, optional = true, features = [
"multipart",
"stream",

View File

@@ -8,11 +8,19 @@ use bytes::Bytes;
use futures::StreamExt;
use http::Method;
use rkyv::{
de::deserializers::SharedDeserializeMap, ser::serializers::AllocSerializer,
validation::validators::DefaultValidator, AlignedVec, Archive, CheckBytes,
Deserialize, Serialize,
api::high::{HighDeserializer, HighSerializer, HighValidator},
bytecheck::CheckBytes,
rancor,
ser::allocator::ArenaHandle,
util::AlignedVec,
Archive, Deserialize, Serialize,
};
type RkyvSerializer<'a> =
HighSerializer<AlignedVec, ArenaHandle<'a>, rancor::Error>;
type RkyvDeserializer = HighDeserializer<rancor::Error>;
type RkyvValidator<'a> = HighValidator<'a, rancor::Error>;
/// Pass arguments and receive responses using `rkyv` in a `POST` request.
pub struct Rkyv;
@@ -24,17 +32,16 @@ impl Encoding for Rkyv {
impl<CustErr, T, Request> IntoReq<Rkyv, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let encoded = rkyv::to_bytes::<T, 1024>(&self)
let encoded = rkyv::to_bytes::<rancor::Error>(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
let bytes = Bytes::copy_from_slice(encoded.as_ref());
Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes)
@@ -44,13 +51,12 @@ where
impl<CustErr, T, Request> FromReq<Rkyv, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
let mut aligned = AlignedVec::new();
let mut aligned = AlignedVec::<1024>::new();
let mut body_stream = Box::pin(req.try_into_stream()?);
while let Some(chunk) = body_stream.next().await {
match chunk {
@@ -64,7 +70,7 @@ where
}
}
}
rkyv::from_bytes::<T>(aligned.as_ref())
rkyv::from_bytes::<T, rancor::Error>(aligned.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
@@ -72,14 +78,14 @@ where
impl<CustErr, T, Response> IntoRes<Rkyv, Response, CustErr> for T
where
Response: Res<CustErr>,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
T: Send,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let encoded = rkyv::to_bytes::<T, 1024>(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
let encoded = rkyv::to_bytes::<rancor::Error>(&self)
.map_err(|e| ServerFnError::Serialization(format!("{e:?}")))?;
let bytes = Bytes::copy_from_slice(encoded.as_ref());
Response::try_from_bytes(Rkyv::CONTENT_TYPE, bytes)
}
@@ -88,14 +94,13 @@ where
impl<CustErr, T, Response> FromRes<Rkyv, Response, CustErr> for T
where
Response: ClientRes<CustErr> + Send,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
let data = res.try_into_bytes().await?;
rkyv::from_bytes::<T>(&data)
rkyv::from_bytes::<T, rancor::Error>(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}

View File

@@ -505,12 +505,7 @@ pub fn server_macro_impl(
PathInfo::Serde => quote! {
#[serde(crate = #serde_path)]
},
PathInfo::Rkyv => {
let rkyv_path = format!("{server_fn_path}::rkyv");
quote! {
#[archive(crate = #rkyv_path, check_bytes)]
}
}
PathInfo::Rkyv => quote! {},
PathInfo::None => quote! {},
};

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.1.0-gamma"
version = "0.1.0-gamma3"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -180,6 +180,4 @@ tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]
denylist = ["tracing", "sledgehammer"]
skip_feature_sets = [
["ssr", "hydrate"],
]
skip_feature_sets = [["ssr", "hydrate"]]

View File

@@ -67,107 +67,121 @@ where
let value = Box::new(self) as Box<dyn Any + Send>;
#[cfg(feature = "ssr")]
let to_html = |value: Box<dyn Any>,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::to_html could not be downcast");
value.to_html(buf, class, style, inner_html);
};
let build = |value: Box<dyn Any>,
match value.downcast::<AnyAttribute>() {
// if it's already an AnyAttribute, we don't need to double-wrap it
Ok(any_attribute) => *any_attribute,
Err(value) => {
#[cfg(feature = "ssr")]
let to_html =
|value: Box<dyn Any>,
buf: &mut String,
class: &mut String,
style: &mut String,
inner_html: &mut String| {
let value = value.downcast::<T>().expect(
"AnyAttribute::to_html could not be downcast",
);
value.to_html(buf, class, style, inner_html);
};
let build =
|value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::build couldn't downcast");
let state = Box::new(value.build(el));
let value = value
.downcast::<T>()
.expect("AnyAttribute::build couldn't downcast");
let state = Box::new(value.build(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't \
downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_template =
|value: Box<dyn Any>,
el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't \
downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
let rebuild =
|new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyAttributeState| {
let value = value.downcast::<T>().expect(
"AnyAttribute::rebuild couldn't downcast value",
);
if new_type_id == state.type_id {
let state = state.state.downcast_mut().expect(
"AnyAttribute::rebuild couldn't downcast state",
);
value.rebuild(state);
} else {
let new = value.into_any_attr().build(&state.el);
*state = new;
}
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(
async move { value.resolve().await.into_any_attr() },
)
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
};
AnyAttribute {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
html_len,
value,
#[cfg(feature = "ssr")]
to_html,
build,
rebuild,
#[cfg(feature = "hydrate")]
hydrate_from_server,
#[cfg(feature = "hydrate")]
hydrate_from_template,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_template =
|value: Box<dyn Any>, el: &crate::renderer::types::Element| {
let value = value.downcast::<T>().expect(
"AnyAttribute::hydrate_from_server couldn't downcast",
);
let state = Box::new(value.hydrate::<true>(el));
AnyAttributeState {
type_id: TypeId::of::<T>(),
state,
el: el.clone(),
}
};
let rebuild = |new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyAttributeState| {
let value = value
.downcast::<T>()
.expect("AnyAttribute::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state
.state
.downcast_mut()
.expect("AnyAttribute::rebuild couldn't downcast state");
value.rebuild(state);
} else {
let new = value.into_any_attr().build(&state.el);
*state = new;
}
};
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any_attr() })
as Pin<Box<dyn Future<Output = AnyAttribute> + Send>>
};
AnyAttribute {
type_id: TypeId::of::<T>(),
html_len,
value,
#[cfg(feature = "ssr")]
to_html,
build,
rebuild,
#[cfg(feature = "hydrate")]
hydrate_from_server,
#[cfg(feature = "hydrate")]
hydrate_from_template,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
}
}
}

View File

@@ -11,6 +11,26 @@ use std::{
sync::Arc,
};
/// Declares that this type can be converted into some other type, which is a valid attribute value.
pub trait IntoAttributeValue {
/// The attribute value into which this type can be converted.
type Output;
/// Consumes this value, transforming it into an attribute value.
fn into_attribute_value(self) -> Self::Output;
}
impl<T> IntoAttributeValue for T
where
T: AttributeValue,
{
type Output = Self;
fn into_attribute_value(self) -> Self::Output {
self
}
}
/// A possible value for an HTML attribute.
pub trait AttributeValue: Send {
/// The state that should be retained between building and rebuilding.

View File

@@ -180,6 +180,84 @@ pub trait IntoClass: Send {
fn resolve(self) -> impl Future<Output = Self::AsyncOutput> + Send;
}
impl<T: IntoClass> IntoClass for Option<T> {
type AsyncOutput = Option<T::AsyncOutput>;
type State = (crate::renderer::types::Element, Option<T::State>);
type Cloneable = Option<T::Cloneable>;
type CloneableOwned = Option<T::CloneableOwned>;
fn html_len(&self) -> usize {
self.as_ref().map_or(0, IntoClass::html_len)
}
fn to_html(self, class: &mut String) {
if let Some(t) = self {
t.to_html(class);
}
}
fn hydrate<const FROM_SERVER: bool>(
self,
el: &crate::renderer::types::Element,
) -> Self::State {
if let Some(t) = self {
(el.clone(), Some(t.hydrate::<FROM_SERVER>(el)))
} else {
(el.clone(), None)
}
}
fn build(self, el: &crate::renderer::types::Element) -> Self::State {
if let Some(t) = self {
(el.clone(), Some(t.build(el)))
} else {
(el.clone(), None)
}
}
fn rebuild(self, state: &mut Self::State) {
let el = &state.0;
let prev_state = &mut state.1;
let maybe_next_t_state = match (prev_state, self) {
(Some(_prev_t_state), None) => {
Rndr::remove_attribute(el, "class");
Some(None)
}
(None, Some(t)) => Some(Some(t.build(el))),
(Some(prev_t_state), Some(t)) => {
t.rebuild(prev_t_state);
None
}
(None, None) => Some(None),
};
if let Some(next_t_state) = maybe_next_t_state {
state.1 = next_t_state;
}
}
fn into_cloneable(self) -> Self::Cloneable {
self.map(|t| t.into_cloneable())
}
fn into_cloneable_owned(self) -> Self::CloneableOwned {
self.map(|t| t.into_cloneable_owned())
}
fn dry_resolve(&mut self) {
if let Some(t) = self {
t.dry_resolve();
}
}
async fn resolve(self) -> Self::AsyncOutput {
if let Some(t) = self {
Some(t.resolve().await)
} else {
None
}
}
}
impl<'a> IntoClass for &'a str {
type AsyncOutput = Self;
type State = (crate::renderer::types::Element, Self);

View File

@@ -390,7 +390,7 @@ html_elements! {
/// The `<template>` HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.
template HtmlTemplateElement [] true,
/// The `<textarea>` HTML element represents a multi-line plain-text editing control, useful when you want to allow users to enter a sizeable amount of free-form text, for example a comment on a review or feedback form.
textarea HtmlTextAreaElement [autocomplete, cols, dirname, disabled, form, maxlength, minlength, name, placeholder, readonly, required, rows, wrap] true,
textarea HtmlTextAreaElement [autocomplete, cols, dirname, disabled, form, maxlength, minlength, name, placeholder, readonly, required, rows, wrap] false,
/// The `<tfoot>` HTML element defines a set of rows summarizing the columns of the table.
tfoot HtmlTableSectionElement [] true,
/// The `<th>` HTML element defines a cell as header of a group of table cells. The exact nature of this group is defined by the scope and headers attributes.

View File

@@ -4,8 +4,8 @@ use crate::{
renderer::{CastFrom, Rndr},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml, ToTemplate,
add_attr::AddAnyAttr, IntoRender, Mountable, Position, PositionState,
Render, RenderHtml, ToTemplate,
},
};
use const_str_slice_concat::{
@@ -65,11 +65,13 @@ impl<E, At, Ch, NewChild> ElementChild<NewChild> for HtmlElement<E, At, Ch>
where
E: ElementWithChildren,
Ch: Render + NextTuple,
<Ch as NextTuple>::Output<NewChild>: Render,
<Ch as NextTuple>::Output<NewChild::Output>: Render,
NewChild: Render,
NewChild: IntoRender,
NewChild::Output: Render,
{
type Output = HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild>>;
type Output =
HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild::Output>>;
fn child(self, child: NewChild) -> Self::Output {
let HtmlElement {
@@ -82,7 +84,7 @@ where
tag,
attributes,
children: children.next_tuple(child),
children: children.next_tuple(child.into_render()),
}
}
}
@@ -116,7 +118,7 @@ where
/// Adds a child to the element.
pub trait ElementChild<NewChild>
where
NewChild: Render,
NewChild: IntoRender,
{
/// The type of the element, with the child added.
type Output;

View File

@@ -19,6 +19,7 @@ pub mod prelude {
OnAttribute, OnTargetAttribute, PropAttribute,
StyleAttribute,
},
IntoAttributeValue,
},
directive::DirectiveAttribute,
element::{ElementChild, ElementExt, InnerHtmlAttribute},
@@ -26,8 +27,8 @@ pub mod prelude {
},
renderer::{dom::Dom, Renderer},
view::{
add_attr::AddAnyAttr, any_view::IntoAny, Mountable, Render,
RenderHtml,
add_attr::AddAnyAttr, any_view::IntoAny, IntoRender, Mountable,
Render, RenderHtml,
},
};
}

View File

@@ -1,11 +1,7 @@
use super::{ReactiveFunction, SharedReactiveFunction, Suspend};
use super::{ReactiveFunction, SharedReactiveFunction};
use crate::{html::class::IntoClass, renderer::Rndr};
use futures::FutureExt;
use reactive_graph::{effect::RenderEffect, signal::guards::ReadGuard};
use std::{
borrow::Borrow, cell::RefCell, future::Future, ops::Deref, rc::Rc,
sync::Arc,
};
use std::{borrow::Borrow, ops::Deref, sync::Arc};
impl<F, C> IntoClass for F
where
@@ -709,6 +705,7 @@ mod stable {
class_signal!(ArcSignal);
}
/*
impl<Fut> IntoClass for Suspend<Fut>
where
Fut: Clone + Future + Send + 'static,
@@ -789,3 +786,4 @@ where
self.inner.await
}
}
*/

View File

@@ -11,7 +11,6 @@ use crate::{
use reactive_graph::effect::RenderEffect;
use std::{
cell::RefCell,
future::Future,
rc::Rc,
sync::{Arc, Mutex},
};
@@ -361,9 +360,8 @@ where
}
}
impl<Fut, V> AttributeValue for Suspend<Fut>
impl<V> AttributeValue for Suspend<V>
where
Fut: Future<Output = V> + Send + 'static,
V: AttributeValue + 'static,
V::State: 'static,
{

View File

@@ -1,7 +1,10 @@
use crate::html::{element::ElementType, node_ref::NodeRefContainer};
use reactive_graph::{
signal::RwSignal,
traits::{DefinedAt, Set, Track, WithUntracked},
signal::{
guards::{Derefable, ReadGuard},
RwSignal,
},
traits::{DefinedAt, ReadUntracked, Set, Track},
};
use send_wrapper::SendWrapper;
use wasm_bindgen::JsCast;
@@ -75,19 +78,17 @@ where
}
}
impl<E> WithUntracked for NodeRef<E>
impl<E> ReadUntracked for NodeRef<E>
where
E: ElementType,
E::Output: JsCast + Clone + 'static,
{
type Value = Option<E::Output>;
type Value = ReadGuard<Option<E::Output>, Derefable<Option<E::Output>>>;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
self.0
.try_with_untracked(|inner| fun(&inner.as_deref().cloned()))
fn try_read_untracked(&self) -> Option<Self::Value> {
Some(ReadGuard::new(Derefable(
self.0.try_read_untracked()?.as_deref().cloned(),
)))
}
}

View File

@@ -1,8 +1,7 @@
use super::{ReactiveFunction, SharedReactiveFunction, Suspend};
use super::{ReactiveFunction, SharedReactiveFunction};
use crate::{html::style::IntoStyle, renderer::Rndr};
use futures::FutureExt;
use reactive_graph::effect::RenderEffect;
use std::{borrow::Cow, cell::RefCell, future::Future, rc::Rc};
use std::borrow::Cow;
impl<F, S> IntoStyle for (&'static str, F)
where
@@ -438,6 +437,7 @@ mod stable {
style_signal!(ArcSignal);
}
/*
impl<Fut> IntoStyle for Suspend<Fut>
where
Fut: Clone + Future + Send + 'static,
@@ -514,3 +514,4 @@ where
self.inner.await
}
}
*/

View File

@@ -36,10 +36,9 @@ use std::{
use throw_error::ErrorHook;
/// A suspended `Future`, which can be used in the view.
#[derive(Clone)]
pub struct Suspend<Fut> {
pub struct Suspend<T> {
pub(crate) subscriber: SuspendSubscriber,
pub(crate) inner: Pin<Box<ScopedFuture<Fut>>>,
pub(crate) inner: Pin<Box<dyn Future<Output = T> + Send>>,
}
#[derive(Debug, Clone)]
@@ -114,9 +113,9 @@ impl ToAnySubscriber for SuspendSubscriber {
}
}
impl<Fut> Suspend<Fut> {
impl<T> Suspend<T> {
/// Creates a new suspended view.
pub fn new(fut: Fut) -> Self {
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
let subscriber = SuspendSubscriber::new();
let any_subscriber = subscriber.to_any_subscriber();
let inner =
@@ -125,7 +124,7 @@ impl<Fut> Suspend<Fut> {
}
}
impl<Fut> Debug for Suspend<Fut> {
impl<T> Debug for Suspend<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Suspend").finish()
}
@@ -160,12 +159,11 @@ where
}
}
impl<Fut> Render for Suspend<Fut>
impl<T> Render for Suspend<T>
where
Fut: Future + 'static,
Fut::Output: Render,
T: Render + 'static,
{
type State = SuspendState<Fut::Output>;
type State = SuspendState<T>;
fn build(self) -> Self::State {
let Self { subscriber, inner } = self;
@@ -255,22 +253,12 @@ where
}
}
impl<Fut> AddAnyAttr for Suspend<Fut>
impl<T> AddAnyAttr for Suspend<T>
where
Fut: Future + Send + 'static,
Fut::Output: AddAnyAttr,
T: Send + AddAnyAttr + 'static,
{
type Output<SomeNewAttr: Attribute> = Suspend<
Pin<
Box<
dyn Future<
Output = <Fut::Output as AddAnyAttr>::Output<
SomeNewAttr::CloneableOwned,
>,
> + Send,
>,
>,
>;
type Output<SomeNewAttr: Attribute> =
Suspend<<T as AddAnyAttr>::Output<SomeNewAttr::CloneableOwned>>;
fn add_any_attr<NewAttr: Attribute>(
self,
@@ -280,21 +268,20 @@ where
Self::Output<NewAttr>: RenderHtml,
{
let attr = attr.into_cloneable_owned();
Suspend::new(Box::pin(async move {
Suspend::new(async move {
let this = self.inner.await;
this.add_any_attr(attr)
}))
})
}
}
impl<Fut> RenderHtml for Suspend<Fut>
impl<T> RenderHtml for Suspend<T>
where
Fut: Future + Send + 'static,
Fut::Output: RenderHtml,
T: RenderHtml + Sized + 'static,
{
type AsyncOutput = Option<Fut::Output>;
type AsyncOutput = Option<T>;
const MIN_LENGTH: usize = Fut::Output::MIN_LENGTH;
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn to_html_with_buf(
self,
@@ -440,6 +427,23 @@ where
}
fn dry_resolve(&mut self) {
self.inner.as_mut().now_or_never();
// this is a little crazy, but if a Suspend is immediately available, we end up
// with a situation where polling it multiple times (here in dry_resolve and then in
// resolve) causes a runtime panic
// (see https://github.com/leptos-rs/leptos/issues/3113)
//
// at the same time, we do need to dry_resolve Suspend so that we can register synchronous
// resource reads that happen inside them
// (see https://github.com/leptos-rs/leptos/issues/2917)
//
// fuse()-ing the Future doesn't work, because that will cause the subsequent resolve()
// simply to be pending forever
//
// in this case, though, we can simply... discover that the data are already here, and then
// stuff them back into a new Future, which can safely be polled after its completion
if let Some(inner) = self.inner.as_mut().now_or_never() {
self.inner = Box::pin(async move { inner })
as Pin<Box<dyn Future<Output = T> + Send>>;
}
}
}

View File

@@ -11,7 +11,7 @@ macro_rules! svg_elements {
($($tag:ident [$($attr:ty),*]),* $(,)?) => {
paste::paste! {
$(
/// An SVG attribute.
/// An SVG element.
// `tag()` function
#[allow(non_snake_case)]
pub fn $tag() -> HtmlElement<[<$tag:camel>], (), ()>
@@ -151,4 +151,33 @@ svg_elements![
view [],
];
// TODO <use>
/// An SVG element.
#[allow(non_snake_case)]
pub fn r#use() -> HtmlElement<Use, (), ()>
where {
HtmlElement {
tag: Use,
attributes: (),
children: (),
}
}
/// An SVG element.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Use;
impl ElementType for Use {
type Output = web_sys::SvgElement;
const TAG: &'static str = "use";
const SELF_CLOSING: bool = false;
const ESCAPE_CHILDREN: bool = true;
const NAMESPACE: Option<&'static str> = Some("http://www.w3.org/2000/svg");
#[inline(always)]
fn tag(&self) -> &str {
Self::TAG
}
}
impl ElementWithChildren for Use {}

View File

@@ -27,7 +27,8 @@ use std::{future::Future, pin::Pin};
pub struct AnyView {
type_id: TypeId,
value: Box<dyn Any + Send>,
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
// The fields below are cfg-gated so they will not be included in WASM bundles if not needed.
// Ordinarily, the compiler can simply omit this dead code because the methods are not called.
// With this type-erased wrapper, however, the compiler is not *always* able to correctly
@@ -42,8 +43,6 @@ pub struct AnyView {
#[cfg(feature = "ssr")]
to_html_async_ooo:
fn(Box<dyn Any>, &mut StreamBuilder, &mut Position, bool, bool),
build: fn(Box<dyn Any>) -> AnyViewState,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState),
#[cfg(feature = "ssr")]
#[allow(clippy::type_complexity)]
resolve: fn(Box<dyn Any>) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
@@ -138,156 +137,172 @@ where
let value = Box::new(self) as Box<dyn Any + Send>;
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
match value.downcast::<AnyView>() {
// if it's already an AnyView, we don't need to double-wrap it
Ok(any_view) => *any_view,
Err(value) => {
#[cfg(feature = "ssr")]
let dry_resolve = |value: &mut Box<dyn Any + Send>| {
let value = value
.downcast_mut::<T>()
.expect("AnyView::resolve could not be downcast");
value.dry_resolve();
};
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
#[cfg(feature = "ssr")]
let to_html = |value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_with_buf(buf, position, escape, mark_branches);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async = |value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_async_with_buf::<false>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::build couldn't downcast");
let state = Box::new(value.build());
#[cfg(feature = "ssr")]
let resolve = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::resolve could not be downcast");
Box::pin(async move { value.resolve().await.into_any() })
as Pin<Box<dyn Future<Output = AnyView> + Send>>
};
#[cfg(feature = "ssr")]
let to_html =
|value: Box<dyn Any>,
buf: &mut String,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_with_buf(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let type_id = mark_branches
.then(|| format!("{:?}", TypeId::of::<T>()))
.unwrap_or_default();
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
if mark_branches {
buf.open_branch(&type_id);
}
value.to_html_async_with_buf::<false>(
buf,
position,
escape,
mark_branches,
);
if mark_branches {
buf.close_branch(&type_id);
}
};
#[cfg(feature = "ssr")]
let to_html_async_ooo =
|value: Box<dyn Any>,
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
mark_branches: bool| {
let value = value
.downcast::<T>()
.expect("AnyView::to_html could not be downcast");
value.to_html_async_with_buf::<true>(
buf,
position,
escape,
mark_branches,
);
};
let build = |value: Box<dyn Any>| {
let value = value
.downcast::<T>()
.expect("AnyView::build couldn't downcast");
let state = Box::new(value.build());
AnyViewState {
type_id: TypeId::of::<T>(),
state,
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>, cursor: &Cursor, position: &PositionState| {
let value = value
.downcast::<T>()
.expect("AnyView::hydrate_from_server couldn't downcast");
let state = Box::new(value.hydrate::<true>(cursor, position));
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
#[cfg(feature = "hydrate")]
let hydrate_from_server =
|value: Box<dyn Any>,
cursor: &Cursor,
position: &PositionState| {
let value = value.downcast::<T>().expect(
"AnyView::hydrate_from_server couldn't downcast",
);
let state =
Box::new(value.hydrate::<true>(cursor, position));
AnyViewState {
AnyViewState {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
}
};
let rebuild =
|new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyViewState| {
let value = value
.downcast::<T>()
.expect("AnyView::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state.state.downcast_mut().expect(
"AnyView::rebuild couldn't downcast state",
);
value.rebuild(state);
} else {
let mut new = value.into_any().build();
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
};
AnyView {
type_id: TypeId::of::<T>(),
state,
mount: mount_any::<T>,
unmount: unmount_any::<T>,
insert_before_this: insert_before_this::<T>,
value,
build,
rebuild,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
html_len,
#[cfg(feature = "ssr")]
to_html,
#[cfg(feature = "ssr")]
to_html_async,
#[cfg(feature = "ssr")]
to_html_async_ooo,
#[cfg(feature = "hydrate")]
hydrate_from_server,
}
};
let rebuild = |new_type_id: TypeId,
value: Box<dyn Any>,
state: &mut AnyViewState| {
let value = value
.downcast::<T>()
.expect("AnyView::rebuild couldn't downcast value");
if new_type_id == state.type_id {
let state = state
.state
.downcast_mut()
.expect("AnyView::rebuild couldn't downcast state");
value.rebuild(state);
} else {
let mut new = value.into_any().build();
state.insert_before_this(&mut new);
state.unmount();
*state = new;
}
};
AnyView {
type_id: TypeId::of::<T>(),
value,
build,
rebuild,
#[cfg(feature = "ssr")]
resolve,
#[cfg(feature = "ssr")]
dry_resolve,
#[cfg(feature = "ssr")]
html_len,
#[cfg(feature = "ssr")]
to_html,
#[cfg(feature = "ssr")]
to_html_async,
#[cfg(feature = "ssr")]
to_html_async_ooo,
#[cfg(feature = "hydrate")]
hydrate_from_server,
}
}
}
@@ -314,7 +329,7 @@ impl AddAnyAttr for AnyView {
where
Self::Output<NewAttr>: RenderHtml,
{
todo!()
self
}
}

View File

@@ -432,3 +432,23 @@ pub enum Position {
/// This is the last child of its parent.
LastChild,
}
/// Declares that this type can be converted into some other type, which can be renderered.
pub trait IntoRender {
/// The renderable type into which this type can be converted.
type Output;
/// Consumes this value, transforming it into the renderable type.
fn into_render(self) -> Self::Output;
}
impl<T> IntoRender for T
where
T: Render,
{
type Output = Self;
fn into_render(self) -> Self::Output {
self
}
}

View File

@@ -62,7 +62,7 @@ impl<'a> RenderHtml for &'a str {
if matches!(position, Position::NextChildAfterText) {
buf.push_str("<!>")
}
if self.is_empty() {
if self.is_empty() && escape {
buf.push(' ');
} else if escape {
let escaped = html_escape::encode_text(self);