Compare commits

..

73 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
0798e0812d fix: unused import in example 2024-10-01 21:23:22 -04:00
Greg Johnston
03514e68be chore: fix test import 2024-10-01 20:43:46 -04:00
Greg Johnston
4092368581 chore: remove unused import 2024-10-01 20:43:37 -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
896f7de8e1 fix: update import of spawn_local in test examples 2024-10-01 20:06:04 -04:00
Greg Johnston
29b2d3024a fix: update import of tick in test examples 2024-10-01 20:05:22 -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
Greg Johnston
0d4f3b51e9 fix: import of reactive_graph in integrations 2024-10-01 19:38:55 -04:00
Greg Johnston
2431b19cdf chore: reexport reactive_graph as leptos::reactive 2024-10-01 19:36:20 -04:00
Greg Johnston
4801e1ec6d chore: hide unwrap_signal from docs 2024-10-01 19:36:20 -04:00
Greg Johnston
e206b93ba5 chore: inline docs for reexported crates 2024-10-01 19:36:20 -04:00
Greg Johnston
f0675446d8 chore: reexport reactive_graph as leptos::reactive 2024-10-01 19:36:20 -04:00
Greg Johnston
2ac7eaad15 chore: add missing docs for tick 2024-10-01 19:36:20 -04:00
Greg Johnston
8a040fda69 chore: rename Writeable to Write for consistency 2024-10-01 19:36:20 -04:00
Greg Johnston
2f5c966cf4 chore: reexport untrack from leptos::prelude but not from reactive_graph 2024-10-01 19:36:20 -04:00
Greg Johnston
45e2629f0e chore: remove reexport of tachys in leptos::prelude 2024-10-01 19:36:20 -04:00
Greg Johnston
517566d085 change: rename spawn module to task 2024-10-01 19:35:45 -04:00
Greg Johnston
6df8657700 fix: escape text nodes and attributes in InertHtml (closes #3056) (#3057) 2024-10-01 19:19:21 -04:00
Georg Vienna
2a4063a259 projects: port session_auth_axum (#2970) 2024-10-01 09:40:57 -07:00
Marc-Stefan Cassola
013ec4a09d Two-way data binding (#2977)
* added two-way data binding to dom elements

* added two-way data binding to radio groups

* moved bind into reactive_graph mod

* chore: use `::leptos` absolute path in macro

* chore: move `Group` into the reactive module, as that's the only place it has meaning

* feat: use new `bind:` syntax

* chore: update for non-generic rendering

* chore: ignore this test

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2024-10-01 09:39:45 -07:00
Greg Johnston
d10fec08e2 Merge pull request #3037 from leptos-rs/rename-island-router
change: rename island-router feature so people don't use it
2024-09-30 20:55:08 -04:00
Greg Johnston
94f4328586 example: add README that actually explains this example 2024-09-30 20:08:39 -04:00
Greg Johnston
2b70961110 change: rename island-router feature so people don't use it 2024-09-30 20:08:24 -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
132 changed files with 3256 additions and 3212 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

@@ -1,7 +1,7 @@
use counter::*;
use leptos::mount::mount_to;
use leptos::prelude::*;
use leptos::spawn::tick;
use leptos::task::tick;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -1,4 +1,4 @@
use leptos::{prelude::*, reactive_graph::actions::Action};
use leptos::prelude::*;
use leptos_router::{
components::{FlatRoutes, Route, Router, A},
StaticSegment,

View File

@@ -1,5 +1,5 @@
use counter_without_macros::counter;
use leptos::{prelude::*, spawn::tick};
use leptos::{prelude::*, task::tick};
use pretty_assertions::assert_eq;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -4,7 +4,7 @@ use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counters::Counters;
use leptos::prelude::*;
use leptos::spawn::tick;
use leptos::task::tick;
use web_sys::HtmlElement;
#[wasm_bindgen_test]

View File

@@ -1,5 +1,5 @@
use directives::App;
use leptos::{prelude::*, spawn::tick};
use leptos::{prelude::*, task::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;

View File

@@ -17,7 +17,7 @@ leptos = { path = "../../leptos", features = [
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
"islands-router",
"dont-use-islands-router",
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,19 +1,14 @@
# Leptos Todo App Sqlite with Axum
# Work in Progress
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
This example is something I wrote on a long layover in the Orlando airport in July. (It was really hot!)
## Getting Started
It is the culmination of a couple years of thinking and working toward being able to do this, which you can see
described pretty well in the pinned roadmap issue (#1830) and its discussion of different modes of client-side
routing when you use islands.
See the [Examples README](../README.md) for setup and run instructions.
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
new HTML from the client, with extremely minimal diffing.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.

View File

@@ -1,5 +1,5 @@
use js_framework_benchmark_leptos::*;
use leptos::{prelude::*, spawn::tick};
use leptos::{prelude::*, task::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -3,7 +3,7 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::spawn::tick;
use leptos::task::tick;
use leptos::{leptos_dom::helpers::document, mount::mount_to};
use web_sys::HtmlButtonElement;

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

@@ -1,6 +1,6 @@
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, prelude::*, spawn::spawn_local};
use leptos::{html::Input, prelude::*, task::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
client::{browser::BrowserClient, Client},
@@ -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

@@ -33,7 +33,7 @@ once_cell = "1"
rustdoc-args = ["--generate-link-to-definition"]
[features]
islands-router = []
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]

View File

@@ -24,7 +24,7 @@ use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::expect_context,
reactive_graph::{computed::ScopedFuture, owner::Owner},
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
@@ -749,7 +749,7 @@ where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -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 }
@@ -39,7 +36,7 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
islands-router = []
dont-use-islands-router = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]

View File

@@ -53,7 +53,7 @@ use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::*,
reactive_graph::{computed::ScopedFuture, owner::Owner},
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
@@ -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(
@@ -784,7 +784,7 @@ where
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_out_of_order()
@@ -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(
@@ -849,7 +849,7 @@ where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -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(
@@ -1069,7 +1069,7 @@ where
{
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -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(
@@ -1146,7 +1146,7 @@ where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
let app = if cfg!(feature = "dont-use-islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -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

@@ -2,7 +2,7 @@ use futures::{stream::once, Stream, StreamExt};
use hydration_context::{SharedContext, SsrSharedContext};
use leptos::{
nonce::use_nonce,
reactive_graph::owner::{Owner, Sandboxed},
reactive::owner::{Owner, Sandboxed},
IntoView,
};
use leptos_config::LeptosOptions;

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

@@ -168,14 +168,15 @@ pub mod prelude {
pub use leptos_server::*;
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, owner::*, signal::*, untrack,
wrappers::read::*,
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
self,
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::template::ViewTemplate,
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
};
}
pub use export_types::*;
@@ -201,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;
@@ -230,6 +233,7 @@ mod suspense_component;
pub mod text_prop;
mod transition;
pub use leptos_macro::*;
#[doc(inline)]
pub use server_fn;
#[doc(hidden)]
pub use typed_builder;
@@ -237,16 +241,22 @@ pub use typed_builder;
pub use typed_builder_macro;
mod into_view;
pub use into_view::IntoView;
#[doc(inline)]
pub use leptos_dom;
mod provider;
#[doc(inline)]
pub use tachys;
/// Tools to mount an application to the DOM, or to hydrate it from server-rendered HTML.
pub mod mount;
#[doc(inline)]
pub use leptos_config as config;
#[doc(inline)]
pub use oco_ref as oco;
mod from_form_data;
#[doc(inline)]
pub use either_of as either;
pub use reactive_graph;
#[doc(inline)]
pub use reactive_graph as reactive;
/// Provide and access data along the reactive graph, sharing data without directly passing arguments.
pub mod context {
@@ -254,17 +264,22 @@ pub mod context {
pub use reactive_graph::owner::{provide_context, use_context};
}
#[doc(inline)]
pub use leptos_server as server;
/// HTML attribute types.
#[doc(inline)]
pub use tachys::html::attribute as attr;
/// HTML element types.
#[doc(inline)]
pub use tachys::html::element as html;
/// HTML event types.
#[doc(no_inline)]
pub use tachys::html::event as ev;
/// MathML element types.
#[doc(inline)]
pub use tachys::mathml as math;
/// SVG element types.
#[doc(inline)]
pub use tachys::svg;
/// Utilities for simple isomorphic logging to the console or terminal.
@@ -272,7 +287,7 @@ pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
pub mod spawn {
pub mod task {
pub use any_spawner::Executor;
use std::future::Future;
@@ -290,6 +305,7 @@ pub mod spawn {
Executor::spawn_local(fut)
}
/// Waits until the next "tick" of the current async executor.
pub async fn tick() {
Executor::tick().await
}
@@ -313,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

@@ -1,7 +1,7 @@
use crate::{children::TypedChildrenFn, mount, IntoView};
use leptos_dom::helpers::document;
use leptos_macro::component;
use reactive_graph::{effect::Effect, owner::Owner, untrack};
use reactive_graph::{effect::Effect, graph::untrack, owner::Owner};
use std::sync::Arc;
/// Renders components somewhere else in the DOM.

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);
@@ -255,7 +260,7 @@ impl ToTokens for Model {
let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if is_island {
quote! {
::leptos::reactive_graph::owner::Owner::with_hydration(move || {
::leptos::reactive::owner::Owner::with_hydration(move || {
#body_name(#prop_names)
})
}
@@ -265,14 +270,30 @@ impl ToTokens for Model {
}
};
let component = quote! {
::leptos::reactive_graph::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
@@ -280,7 +301,7 @@ impl ToTokens for Model {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::reactive_graph::owner::Owner::current_shared_context()
if ::leptos::reactive::owner::Owner::current_shared_context()
.map(|sc| sc.get_is_hydrating())
.unwrap_or(false) {
::leptos::either::Either::Left(
@@ -316,9 +337,9 @@ impl ToTokens for Model {
quote! {
use leptos::tachys::view::any_view::IntoAny;
let children = Box::new(|| {
let sc = ::leptos::reactive_graph::owner::Owner::current_shared_context().unwrap();
let sc = ::leptos::reactive::owner::Owner::current_shared_context().unwrap();
let prev = sc.get_is_hydrating();
let value = ::leptos::reactive_graph::owner::Owner::with_no_hydration(||
let value = ::leptos::reactive::owner::Owner::with_no_hydration(||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
);
sc.set_is_hydrating(prev);
@@ -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

@@ -40,9 +40,9 @@ impl ToTokens for MemoMacroInput {
let path = &self.path;
tokens.extend(quote! {
::leptos::reactive_graph::computed::Memo::new(
::leptos::reactive::computed::Memo::new(
move |_| {
use ::leptos::reactive_graph::traits::With;
use ::leptos::reactive::traits::With;
#root.with(|st: _| st.#path.clone())
}
)

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

@@ -40,7 +40,7 @@ impl ToTokens for SliceMacroInput {
let path = &self.path;
tokens.extend(quote! {
::leptos::reactive_graph::computed::create_slice(
::leptos::reactive::computed::create_slice(
#root,
|st: &_| st.#path.clone(),
|st: &mut _, n| st.#path = n

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,19 +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 = if escape {
html_escape::encode_text(&text)
} else {
text.into()
};
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
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('<');
@@ -324,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) =
@@ -337,11 +354,13 @@ fn inert_element_to_tokens(
)) = &value.value
{
if let Lit::Str(txt) = &lit.lit {
let value = txt.value();
let value = html_escape::encode_double_quoted_attribute(&value);
if attr_name == "class" {
html.push_class(&txt.value());
html.push_class(&value);
} else {
html.push_str("=\"");
html.push_str(&txt.value());
html.push_str(&value);
html.push('"');
}
}
@@ -357,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));
}
}
}
@@ -541,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();
@@ -550,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,
@@ -718,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;
@@ -871,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)
}
@@ -921,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"
@@ -964,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" {
@@ -976,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("_");
@@ -1004,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)
@@ -1012,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(
@@ -1068,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());
@@ -1093,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);
@@ -1150,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))
@@ -1167,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))
@@ -1184,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)
}
@@ -1341,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 {
@@ -1356,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

@@ -2,7 +2,7 @@ use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
component, html,
reactive_graph::owner::use_context,
reactive::owner::use_context,
tachys::{
dom::document,
html::attribute::Attribute,

View File

@@ -2,7 +2,7 @@ use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
component, html,
reactive_graph::owner::use_context,
reactive::owner::use_context,
tachys::{
dom::document,
html::attribute::Attribute,

View File

@@ -52,7 +52,7 @@ use leptos::{
attr::NextAttribute,
component,
logging::debug_warn,
reactive_graph::owner::{provide_context, use_context},
reactive::owner::{provide_context, use_context},
tachys::{
dom::document,
html::{

View File

@@ -3,7 +3,7 @@ use leptos::{
attr::Attribute,
component,
oco::Oco,
reactive_graph::{
reactive::{
effect::RenderEffect,
owner::{use_context, Owner},
},

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

@@ -30,23 +30,21 @@ sqlx = { version = "0.8.0", features = [
], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2.0"
axum_session_auth = { version = "0.14.0", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.14.0", features = [
"sqlite-rustls",
], optional = true }
axum_session_auth = { version = "0.14.0", features = [], optional = true }
axum_session = { version = "0.14.0", features = [], optional = true }
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
bcrypt = { version = "0.15.0", optional = true }
async-trait = { version = "0.1.0", optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:axum_session_sqlx",
"dep:axum_session_auth",
"dep:axum_session",
"dep:async-trait",

View File

@@ -28,9 +28,8 @@ impl Default for User {
#[cfg(feature = "ssr")]
pub mod ssr {
pub use super::{User, UserPasshash};
pub use axum_session_auth::{
Authentication, HasPermission, SessionSqlitePool,
};
pub use axum_session_auth::{Authentication, HasPermission};
use axum_session_sqlx::SessionSqlitePool;
pub use sqlx::SqlitePool;
pub use std::collections::HashSet;
pub type AuthSession = axum_session_auth::AuthSession<

View File

@@ -8,10 +8,10 @@ use leptos_axum::ResponseOptions;
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
#[prop(optional)] errors: Option<ArcRwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => RwSignal::new(e),
Some(e) => ArcRwSignal::new(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),

View File

@@ -1,50 +0,0 @@
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, Errors, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
);
handler(req).await.into_response()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}

View File

@@ -2,8 +2,6 @@ pub mod auth;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod state;
pub mod todo;
@@ -14,5 +12,5 @@ pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(TodoApp);
leptos::mount::hydrate_body(TodoApp);
}

View File

@@ -7,14 +7,16 @@ use axum::{
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
use leptos::{get_configuration, logging::log, provide_context};
use axum_session_auth::{AuthConfig, AuthSessionLayer};
use axum_session_sqlx::SessionSqlitePool;
use leptos::{
config::get_configuration, logging::log, prelude::provide_context,
};
use leptos_axum::{
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
};
use session_auth_axum::{
auth::{ssr::AuthSession, User},
fallback::file_and_error_handler,
state::AppState,
todo::*,
};
@@ -40,19 +42,19 @@ async fn server_fn_handler(
async fn leptos_routes_handler(
auth_session: AuthSession,
State(app_state): State<AppState>,
state: State<AppState>,
req: Request<AxumBody>,
) -> Response {
let State(app_state) = state.clone();
let handler = leptos_axum::render_route_with_context(
app_state.leptos_options.clone(),
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
TodoApp,
move || shell(app_state.leptos_options.clone()),
);
handler(req).await.into_response()
handler(state, req).await.into_response()
}
#[tokio::main]
@@ -111,7 +113,7 @@ async fn main() {
get(server_fn_handler).post(server_fn_handler),
)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(file_and_error_handler)
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
.layer(
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
Some(pool.clone()),

View File

@@ -1,6 +1,6 @@
use axum::extract::FromRef;
use leptos::LeptosOptions;
use leptos_router::RouteListing;
use leptos::prelude::LeptosOptions;
use leptos_axum::AxumRouteListing;
use sqlx::SqlitePool;
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
@@ -9,5 +9,5 @@ use sqlx::SqlitePool;
pub struct AppState {
pub leptos_options: LeptosOptions,
pub pool: SqlitePool,
pub routes: Vec<RouteListing>,
pub routes: Vec<AxumRouteListing>,
}

View File

@@ -1,7 +1,7 @@
use crate::{auth::*, error_template::ErrorTemplate};
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::*;
use leptos_router::{components::*, *};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -109,13 +109,33 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
.map(|_| ())?)
}
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<link rel="stylesheet" id="leptos" href="/pkg/session_auth_axum.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
<MetaTags/>
</head>
<body>
<TodoApp/>
</body>
</html>
}
}
#[component]
pub fn TodoApp() -> impl IntoView {
let login = create_server_action::<Login>();
let logout = create_server_action::<Logout>();
let signup = create_server_action::<Signup>();
let login = ServerAction::<Login>::new();
let logout = ServerAction::<Logout>::new();
let signup = ServerAction::<Signup>::new();
let user = create_resource(
let user = Resource::new(
move || {
(
login.version().get(),
@@ -128,8 +148,6 @@ pub fn TodoApp() -> impl IntoView {
provide_meta_context();
view! {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/session_auth_axum.css"/>
<Router>
<header>
<A href="/">
@@ -149,7 +167,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<span>{format!("Login error: {}", e)}</span>
}
.into_view()
.into_any()
}
Ok(None) => {
view! {
@@ -159,7 +177,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<span>"Logged out."</span>
}
.into_view()
.into_any()
}
Ok(Some(user)) => {
view! {
@@ -169,7 +187,7 @@ pub fn TodoApp() -> impl IntoView {
{format!("Logged in as: {} ({})", user.username, user.id)}
</span>
}
.into_view()
.into_any()
}
})
}}
@@ -178,13 +196,15 @@ pub fn TodoApp() -> impl IntoView {
</header>
<hr/>
<main>
<Routes>
<FlatRoutes fallback=|| "Not found.">
// Route
<Route path="" view=Todos/>
<Route path="signup" view=move || view! { <Signup action=signup/> }/>
<Route path="login" view=move || view! { <Login action=login/> }/>
<Route
path="settings"
<Route path=path!("") view=Todos/>
<Route path=path!("signup") view=move || view! { <Signup action=signup/> }/>
<Route path=path!("login") view=move || view! { <Login action=login/> }/>
<ProtectedRoute
path=path!("settings")
condition=move || user.get().map(|r| r.ok().flatten().is_some())
redirect_path=|| "/"
view=move || {
view! {
<h1>"Settings"</h1>
@@ -193,7 +213,7 @@ pub fn TodoApp() -> impl IntoView {
}
/>
</Routes>
</FlatRoutes>
</main>
</Router>
}
@@ -201,12 +221,12 @@ pub fn TodoApp() -> impl IntoView {
#[component]
pub fn Todos() -> impl IntoView {
let add_todo = create_server_multi_action::<AddTodo>();
let delete_todo = create_server_action::<DeleteTodo>();
let add_todo = ServerMultiAction::<AddTodo>::new();
let delete_todo = ServerAction::<DeleteTodo>::new();
let submissions = add_todo.submissions();
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
let todos = Resource::new(
move || (add_todo.version().get(), delete_todo.version().get()),
move |_| get_todos(),
);
@@ -231,11 +251,11 @@ pub fn Todos() -> impl IntoView {
view! {
<pre class="error">"Server Error: " {e.to_string()}</pre>
}
.into_view()
.into_any()
}
Ok(todos) => {
if todos.is_empty() {
view! { <p>"No tasks were found."</p> }.into_view()
view! { <p>"No tasks were found."</p> }.into_any()
} else {
todos
.into_iter()
@@ -252,10 +272,11 @@ pub fn Todos() -> impl IntoView {
}
})
.collect_view()
.into_any()
}
}
})
.unwrap_or_default()
.unwrap_or(().into_any())
}
};
let pending_todos = move || {
@@ -266,7 +287,7 @@ pub fn Todos() -> impl IntoView {
.map(|submission| {
view! {
<li class="pending">
{move || submission.input.get().map(|data| data.title)}
{move || submission.input().get().map(|data| data.title)}
</li>
}
})
@@ -282,9 +303,7 @@ pub fn Todos() -> impl IntoView {
}
#[component]
pub fn Login(
action: Action<Login, Result<(), ServerFnError>>,
) -> impl IntoView {
pub fn Login(action: ServerAction<Login>) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Log In"</h1>
@@ -317,9 +336,7 @@ pub fn Login(
}
#[component]
pub fn Signup(
action: Action<Signup, Result<(), ServerFnError>>,
) -> impl IntoView {
pub fn Signup(action: ServerAction<Signup>) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Sign Up"</h1>
@@ -362,9 +379,7 @@ pub fn Signup(
}
#[component]
pub fn Logout(
action: Action<Logout, Result<(), ServerFnError>>,
) -> impl IntoView {
pub fn Logout(action: ServerAction<Logout>) -> impl IntoView {
view! {
<div id="loginbox">
<ActionForm action=action>

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

@@ -19,7 +19,7 @@ use crate::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
Write,
},
transition::AsyncTransition,
};
@@ -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,
)
}
}
@@ -600,7 +600,7 @@ impl<T: 'static> Notify for ArcAsyncDerived<T> {
}
}
impl<T: 'static> Writeable for ArcAsyncDerived<T> {
impl<T: 'static> Write for ArcAsyncDerived<T> {
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {

View File

@@ -8,7 +8,7 @@ use crate::{
signal::guards::{AsyncPlain, ReadGuard, WriteGuard},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Writeable,
UntrackableGuard, Write,
},
unwrap_signal,
};
@@ -300,7 +300,7 @@ where
}
}
impl<T, S> Writeable for AsyncDerived<T, S>
impl<T, S> Write for AsyncDerived<T, S>
where
T: 'static,
S: Storage<ArcAsyncDerived<T>>,

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

@@ -58,7 +58,7 @@ impl<Fut: Future> Future for ScopedFuture<Fut> {
pub mod suspense {
use crate::{
signal::ArcRwSignal,
traits::{Update, Writeable},
traits::{Update, Write},
};
use futures::channel::oneshot::Sender;
use or_poisoned::OrPoisoned;

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

@@ -106,7 +106,7 @@ impl Observer {
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::untrack;
/// # use reactive_graph::graph::untrack;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let (a, set_a) = signal(0);

View File

@@ -89,7 +89,6 @@ pub mod transition;
pub mod wrappers;
use computed::ScopedFuture;
pub use graph::untrack;
#[cfg(feature = "nightly")]
mod nightly;
@@ -126,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

@@ -6,7 +6,7 @@ use super::{
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Notify},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Write},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@@ -50,7 +50,7 @@ use std::{
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -253,7 +253,7 @@ impl<T> Notify for ArcRwSignal<T> {
}
}
impl<T: 'static> Writeable for ArcRwSignal<T> {
impl<T: 'static> Write for ArcRwSignal<T> {
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {

View File

@@ -2,7 +2,7 @@ use super::guards::{UntrackedWriteGuard, WriteGuard};
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Notify},
traits::{DefinedAt, UntrackableGuard, Writeable},
traits::{DefinedAt, UntrackableGuard, Write},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@@ -23,7 +23,7 @@ use std::{
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -122,7 +122,7 @@ impl<T> Notify for ArcWriteSignal<T> {
}
}
impl<T: 'static> Writeable for ArcWriteSignal<T> {
impl<T: 'static> Write for ArcWriteSignal<T> {
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {

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

@@ -9,7 +9,7 @@ use crate::{
signal::guards::{UntrackedWriteGuard, WriteGuard},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Writeable,
UntrackableGuard, Write,
},
unwrap_signal,
};
@@ -57,7 +57,7 @@ use std::{
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -349,7 +349,7 @@ where
}
}
impl<T, S> Writeable for RwSignal<T, S>
impl<T, S> Write for RwSignal<T, S>
where
T: 'static,
S: Storage<ArcRwSignal<T>>,

View File

@@ -1,9 +1,7 @@
use super::{guards::WriteGuard, ArcWriteSignal};
use crate::{
owner::{ArenaItem, Storage, SyncStorage},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Writeable,
},
traits::{DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Write},
};
use core::fmt::Debug;
use guardian::ArcRwLockWriteGuardian;
@@ -22,7 +20,7 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -128,7 +126,7 @@ where
}
}
impl<T, S> Writeable for WriteSignal<T, S>
impl<T, S> Write for WriteSignal<T, S>
where
T: 'static,
S: Storage<ArcWriteSignal<T>>,

View File

@@ -18,7 +18,7 @@
//! | [`Track`] | — | Tracks changes to this value, adding it as a source of the current reactive observer. |
//! | [`Trigger`] | — | Notifies subscribers that this value has changed. |
//! | [`ReadUntracked`] | Guard | Gives immutable access to the value of this signal. |
//! | [`Writeable`] | Guard | Gives mutable access to the value of this signal.
//! | [`Write`] | Guard | Gives mutable access to the value of this signal.
//!
//! ## Derived Traits
//!
@@ -33,7 +33,7 @@
//! ### Update
//! | Trait | Mode | Composition | Description
//! |---------------------|---------------|-----------------------------------|------------
//! | [`UpdateUntracked`] | `fn(&mut T)` | [`Writeable`] | Applies closure to the current value to update it, but doesn't notify subscribers.
//! | [`UpdateUntracked`] | `fn(&mut T)` | [`Write`] | Applies closure to the current value to update it, but doesn't notify subscribers.
//! | [`Update`] | `fn(&mut T)` | [`UpdateUntracked`] + [`Trigger`] | Applies closure to the current value to update it, and notifies subscribers.
//! | [`Set`] | `T` | [`Update`] | Sets the value to a new value, and notifies subscribers.
//!
@@ -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};
@@ -61,6 +61,7 @@ use std::{
panic::Location,
};
#[doc(hidden)]
/// Provides a sensible panic message for accessing disposed signals.
#[macro_export]
macro_rules! unwrap_signal {
@@ -167,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;
@@ -184,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
@@ -194,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()
}
}
}
@@ -213,7 +230,7 @@ pub trait UntrackableGuard: DerefMut {
/// Gives mutable access to a signal's value through a guard type. When the guard is dropped, the
/// signal's subscribers will be notified.
pub trait Writeable: Sized + DefinedAt + Notify {
pub trait Write: Sized + DefinedAt + Notify {
/// The type of the signal's value.
type Value: Sized + 'static;
@@ -306,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))
}
}
@@ -421,9 +437,9 @@ pub trait UpdateUntracked: DefinedAt {
impl<T> UpdateUntracked for T
where
T: Writeable,
T: Write,
{
type Value = <Self as Writeable>::Value;
type Value = <Self as Write>::Value;
#[track_caller]
fn try_update_untracked<U>(
@@ -478,9 +494,9 @@ pub trait Update {
impl<T> Update for T
where
T: Writeable,
T: Write,
{
type Value = <Self as Writeable>::Value;
type Value = <Self as Write>::Value;
#[track_caller]
fn try_maybe_update<U>(
@@ -638,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,14 +3,29 @@
/// Types that abstract over signals with values that can be read.
pub mod read {
use crate::{
computed::{ArcMemo, Memo},
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, With, WithUntracked},
untrack, unwrap_signal,
computed::{ArcMemo, Memo, MemoInner},
graph::untrack,
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>
@@ -23,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>
@@ -34,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()),
}
}
}
@@ -51,6 +69,9 @@ pub mod read {
Self::DerivedSignal(_) => {
f.debug_tuple("DerivedSignal").finish()
}
Self::Stored(arg0) => {
f.debug_tuple("Static").field(arg0).finish()
}
}
}
}
@@ -167,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>
@@ -174,7 +205,7 @@ pub mod read {
T: Default + Send + Sync + 'static,
{
fn default() -> Self {
Self::derive(|| Default::default())
Self::stored(Default::default())
}
}
@@ -230,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),
)
}
}
@@ -342,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>
@@ -432,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>
@@ -459,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>
@@ -466,7 +593,7 @@ pub mod read {
T: Send + Sync + Default + 'static,
{
fn default() -> Self {
Self::derive(|| Default::default())
Self::stored(Default::default())
}
}
@@ -475,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)
}
}
@@ -641,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
@@ -714,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(),
}
}
}
@@ -892,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())
@@ -967,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(),
}
}
}
@@ -1241,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

@@ -3,7 +3,7 @@ use reactive_graph::{
signal::{arc_signal, signal, ArcRwSignal, RwSignal},
traits::{
Get, GetUntracked, Read, Set, Update, UpdateUntracked, With,
WithUntracked, Writeable,
WithUntracked, Write,
},
};

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

@@ -10,7 +10,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
Write,
},
};
use std::{
@@ -171,7 +171,7 @@ where
}
}
impl<Inner, Prev> Writeable for AtIndex<Inner, Prev>
impl<Inner, Prev> Write for AtIndex<Inner, Prev>
where
Inner: StoreField<Value = Prev>,
Prev: IndexMut<usize> + 'static,
@@ -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

@@ -10,7 +10,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
Write,
},
};
use std::{
@@ -312,7 +312,7 @@ where
}
}
impl<Inner, Prev, K, T> Writeable for KeyedSubfield<Inner, Prev, K, T>
impl<Inner, Prev, K, T> Write for KeyedSubfield<Inner, Prev, K, T>
where
Self: Clone,
for<'a> &'a T: IntoIterator,
@@ -574,7 +574,7 @@ where
}
}
impl<Inner, Prev, K, T> Writeable for AtKeyed<Inner, Prev, K, T>
impl<Inner, Prev, K, T> Write for AtKeyed<Inner, Prev, K, T>
where
K: Debug + Send + Sync + PartialEq + Eq + Hash + 'static,
KeyedSubfield<Inner, Prev, K, T>: Clone,
@@ -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

@@ -7,9 +7,10 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
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))
}
}
}
@@ -254,7 +269,7 @@ where
}
}
impl<T> Writeable for ArcStore<T>
impl<T> Write for ArcStore<T>
where
T: 'static,
{
@@ -379,7 +394,7 @@ where
}
}
impl<T, S> Writeable for Store<T, S>
impl<T, S> Write for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
@@ -428,9 +443,8 @@ mod tests {
use crate::{self as reactive_stores, Patch, Store, StoreFieldIterator};
use reactive_graph::{
effect::Effect,
traits::{Read, ReadUntracked, Set, Update, Writeable},
traits::{Read, ReadUntracked, Set, Update, Write},
};
use reactive_stores_macro::{Patch, Store};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,

View File

@@ -49,9 +49,8 @@ mod tests {
use crate::{self as reactive_stores, Store};
use reactive_graph::{
effect::Effect,
traits::{Get, Read, ReadUntracked, Set, Writeable},
traits::{Get, Read, ReadUntracked, Set, Write},
};
use reactive_stores_macro::Store;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,

View File

@@ -11,7 +11,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
Write,
},
unwrap_signal,
};
@@ -251,7 +251,7 @@ where
}
}
impl<T, S> Writeable for Then<T, S>
impl<T, S> Write for Then<T, S>
where
T: 'static,
S: StoreField,

View File

@@ -10,7 +10,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Writeable,
Write,
},
};
use std::{iter, marker::PhantomData, ops::DerefMut, panic::Location};
@@ -168,7 +168,7 @@ where
}
}
impl<Inner, Prev, T> Writeable for Subfield<Inner, Prev, T>
impl<Inner, Prev, T> Write for Subfield<Inner, Prev, T>
where
T: 'static,
Inner: StoreField<Value = Prev>,

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"

Some files were not shown because too many files have changed in this diff Show More