Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
557371887a fix: suppress resource loading during route generation (closes #3013) 2024-09-24 19:56:58 -04:00
158 changed files with 3389 additions and 4274 deletions

View File

@@ -94,7 +94,7 @@ jobs:
fi
done
- name: Install Deno
uses: denoland/setup-deno@v2
uses: denoland/setup-deno@v1
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-gamma3"
version = "0.7.0-beta6"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.2.0-gamma3" }
throw_error = { path = "./any_error/", version = "0.2.0-beta6" }
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-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" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta6" }
leptos = { path = "./leptos", version = "0.7.0-beta6" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta6" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta6" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta6" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta6" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta6" }
leptos_router = { path = "./router", version = "0.7.0-beta6" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta6" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta6" }
leptos_meta = { path = "./meta", version = "0.7.0-beta6" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta6" }
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-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" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta6" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta6" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta6" }
server_fn = { path = "./server_fn", version = "0.7.0-beta6" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta6" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta6" }
tachys = { path = "./tachys", version = "0.1.0-beta6" }
[profile.release]
codegen-units = 1

View File

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

View File

@@ -9,7 +9,6 @@ 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"
@@ -20,14 +19,12 @@ 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,14 +32,11 @@
use std::{future::Future, pin::Pin, sync::OnceLock};
use thiserror::Error;
/// 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>>>;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub(crate) 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)]
@@ -118,14 +115,6 @@ 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 {
@@ -204,15 +193,13 @@ impl Executor {
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
pub fn init_futures_executor() -> Result<(), ExecutorError> {
use futures::{
executor::{LocalPool, LocalSpawner, ThreadPool},
executor::{LocalPool, ThreadPool},
task::{LocalSpawnExt, SpawnExt},
};
use std::cell::RefCell;
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
thread_local! {
static LOCAL_POOL: RefCell<LocalPool> = RefCell::new(LocalPool::new());
static SPAWNER: LocalSpawner = LOCAL_POOL.with(|pool| pool.borrow().spawner());
static LOCAL_POOL: LocalPool = LocalPool::new();
}
fn get_thread_pool() -> &'static ThreadPool {
@@ -231,97 +218,28 @@ impl Executor {
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
SPAWNER.with(|spawner| {
LOCAL_POOL.with(|pool| {
let spawner = pool.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(())
}
/// 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);
#[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 {});
}
}

View File

@@ -1,55 +0,0 @@
#[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

@@ -1,38 +0,0 @@
#[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::task::tick;
use leptos::spawn::tick;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

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

View File

@@ -1,5 +1,5 @@
use counter_without_macros::counter;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::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::task::tick;
use leptos::spawn::tick;
use web_sys::HtmlElement;
#[wasm_bindgen_test]

View File

@@ -1,5 +1,5 @@
use directives::App;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::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 = [
"dont-use-islands-router",
"islands-router",
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,14 +1,19 @@
# Work in Progress
# Leptos Todo App Sqlite with Axum
This example is something I wrote on a long layover in the Orlando airport in July. (It was really hot!)
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.
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.
## Getting Started
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.
See the [Examples README](../README.md) for setup and run instructions.
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`.
## 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.

View File

@@ -1,5 +1,5 @@
use js_framework_benchmark_leptos::*;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::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::task::tick;
use leptos::spawn::tick;
use leptos::{leptos_dom::helpers::document, mount::mount_to};
use web_sys::HtmlButtonElement;

View File

@@ -5,14 +5,13 @@ use leptos::prelude::*;
use leptos_router::{
components::{
Form, Outlet, ParentRoute, ProtectedRoute, Redirect, Route, Router,
Routes, RoutingProgress, A,
Routes, 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)]
@@ -27,14 +26,9 @@ 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 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>
<Router>
<nav>
// ordinary <a> elements can be used for client-side navigation
// using <A> has two effects:
@@ -70,7 +64,7 @@ pub fn RouterExample() -> impl IntoView {
// You can define other routes in their own component.
// Routes implement the MatchNestedRoutes
#[component(transparent)]
#[component]
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("") view=ContactList>

View File

@@ -40,8 +40,6 @@ 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::*, task::spawn_local};
use leptos::{html::Input, prelude::*, spawn::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
client::{browser::BrowserClient, Client},
@@ -417,6 +417,7 @@ 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,8 +4,6 @@ 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,6 +3,7 @@ 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

View File

@@ -319,7 +319,10 @@ pub fn Todo(todo: Todo) -> impl IntoView {
node_ref=todo_input
class="toggle"
type="checkbox"
bind:checked=todo.completed
prop:checked=move || todo.completed.get()
on:input:target=move |ev| {
todo.completed.set(ev.target().checked());
}
/>
<label on:dblclick=move |_| {

58
flake.lock generated
View File

@@ -5,11 +5,29 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"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",
"type": "github"
},
"original": {
@@ -20,11 +38,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1727634051,
"narHash": "sha256-S5kVU7U82LfpEukbn/ihcyNt2+EvG7Z5unsKW9H/yFA=",
"lastModified": 1703961334,
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "06cf0e1da4208d3766d898b7fdab6513366d45b9",
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
"type": "github"
},
"original": {
@@ -36,11 +54,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
@@ -59,14 +77,15 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1727749966,
"narHash": "sha256-DUS8ehzqB1DQzfZ4bRXVSollJhu+y7cvh1DJ9mbWebE=",
"lastModified": 1704075545,
"narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "00decf1b4f9886d25030b9ee4aed7bfddccb5f66",
"rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
"type": "github"
},
"original": {
@@ -89,6 +108,21 @@
"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-gamma3"
version = "0.2.0-beta6"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -33,7 +33,7 @@ once_cell = "1"
rustdoc-args = ["--generate-link-to-definition"]
[features]
dont-use-islands-router = []
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::{computed::ScopedFuture, owner::Owner},
reactive_graph::{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 = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1381,41 +1381,39 @@ where
),
)
} else {
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!()
},
)
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!()
},
)
};
}
}

View File

@@ -16,6 +16,8 @@ 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"] }
@@ -24,8 +26,9 @@ 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 = { version = "0.4.13", features = ["util"] }
tower = "0.4.13"
tower-http = "0.5.2"
tracing = { version = "0.1.40", optional = true }
@@ -36,7 +39,7 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
dont-use-islands-router = []
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::{computed::ScopedFuture, owner::Owner},
reactive_graph::{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::util::ServiceExt;
use tower::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 = "dont-use-islands-router") {
let app = if cfg!(feature = "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 = "dont-use-islands-router") {
let app = if cfg!(feature = "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 = "dont-use-islands-router") {
let app = if cfg!(feature = "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 = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1342,7 +1342,8 @@ where
.with(|| {
// stub out a path for now
provide_context(RequestUrl::new(""));
let (mock_parts, _) = Request::new(Body::from("")).into_parts();
let (mock_parts, _) =
http::Request::new(Body::from("")).into_parts();
let (mock_meta, _) = ServerMetaContext::new();
provide_contexts("", &mock_meta, mock_parts, Default::default());
additional_context();
@@ -1401,8 +1402,8 @@ impl StaticRouteGenerator {
let add_context = additional_context.clone();
move || {
let full_path = format!("http://leptos.dev{path}");
let mock_req = Request::builder()
.method(Method::GET)
let mock_req = http::Request::builder()
.method(http::Method::GET)
.header("Accept", "text/html")
.body(Body::empty())
.unwrap();
@@ -1494,12 +1495,10 @@ impl StaticRouteGenerator {
_ = routes;
_ = app_fn;
_ = additional_context;
Self(Box::new(|_| {
panic!(
"Static routes are not currently supported on WASM32 \
server targets."
);
}))
panic!(
"Static routes are not currently supported on WASM32 server \
targets."
);
}
}
@@ -1934,7 +1933,7 @@ where
///
/// #[server]
/// pub async fn request_method() -> Result<String, ServerFnError> {
/// use axum::http::Method;
/// use 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::owner::{Owner, Sandboxed},
reactive_graph::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

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

View File

@@ -1,8 +1,10 @@
use crate::{prelude::Suspend, suspense_component::Suspense, IntoView};
use crate::Suspense;
use leptos_dom::IntoView;
use leptos_macro::{component, view};
use leptos_server::ArcOnceResource;
use reactive_graph::prelude::ReadUntracked;
use serde::{de::DeserializeOwned, Serialize};
use leptos_reactive::{
create_blocking_resource, create_local_resource, create_resource,
store_value, Serializable,
};
#[component]
/// Allows you to inline the data loading for an `async` block or
@@ -13,8 +15,11 @@ use serde::{de::DeserializeOwned, Serialize};
/// Adding `let:{variable name}` to the props makes the data available in the children
/// that variable name, when resolved.
/// ```
/// # use leptos::prelude::*;
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # if false {
/// # let runtime = create_runtime();
/// async fn fetch_monkeys(monkey: i32) -> i32 {
/// // do some expensive work
/// 3
@@ -22,23 +27,29 @@ use serde::{de::DeserializeOwned, Serialize};
///
/// 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, 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
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
/// 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.
///
@@ -47,58 +58,65 @@ pub fn Await<T, Fut, Chil, V>(
/// `let:` syntax to specify the name for the data variable.
///
/// ```rust
/// # use leptos::prelude::*;
/// # use leptos::*;
/// # 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::prelude::*;
/// # use leptos::*;
/// # 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: Chil,
children: VF,
) -> impl IntoView
where
T: Send + Sync + Serialize + DeserializeOwned + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
Chil: FnOnce(&T) -> V + Send + 'static,
V: IntoView + 'static,
Fut: std::future::Future<Output = T> + 'static,
FF: Fn() -> Fut + 'static,
V: IntoView,
VF: Fn(&T) -> V + 'static,
T: Serializable + 'static,
{
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
let ready = res.ready();
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);
view! {
<Suspense fallback=|| ()>
{Suspend::new(async move {
ready.await;
children(res.read_untracked().as_ref().unwrap())
})}
{move || res.map(|data| view.with_value(|view| view(data)))}
</Suspense>
}
}

View File

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

View File

@@ -38,7 +38,7 @@
if (islandFn) {
islandFn(el);
} else {
console.warn(`Could not find WASM function for the island ${id}.`);
console.warn(`Could not find WASM function for the island ${l}.`);
}
}
function hydrateIslands(entry, mod) {

View File

@@ -168,15 +168,14 @@ pub mod prelude {
pub use leptos_server::*;
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
actions::*, computed::*, effect::*, owner::*, signal::*, untrack,
wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::{
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
},
self,
reactive_graph::{node_ref::*, Suspend},
view::template::ViewTemplate,
};
}
pub use export_types::*;
@@ -202,12 +201,10 @@ pub mod error {
pub use throw_error::*;
}
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
/// Control-flow components like `<Show>` and `<For>`.
pub mod control_flow {
pub use crate::{animated_show::*, await_::*, for_loop::*, show::*};
pub use crate::{for_loop::*, show::*};
}
mod animated_show;
mod await_;
mod for_loop;
mod show;
@@ -233,7 +230,6 @@ 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;
@@ -241,22 +237,16 @@ 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;
#[doc(inline)]
pub use reactive_graph as reactive;
pub use reactive_graph;
/// Provide and access data along the reactive graph, sharing data without directly passing arguments.
pub mod context {
@@ -264,22 +254,17 @@ 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.
@@ -287,7 +272,7 @@ pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
pub mod task {
pub mod spawn {
pub use any_spawner::Executor;
use std::future::Future;
@@ -305,14 +290,9 @@ pub mod task {
Executor::spawn_local(fut)
}
/// Waits until the next "tick" of the current async executor.
pub async fn tick() {
Executor::tick().await
}
pub use reactive_graph::{
spawn_local_scoped, spawn_local_scoped_with_cancellation,
};
}
// these reexports are used in islands
@@ -329,3 +309,234 @@ 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, graph::untrack, owner::Owner};
use reactive_graph::{effect::Effect, owner::Owner, untrack};
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 + 'static,
Chil: IntoView,
{
let owner = Owner::current()
.expect("no current reactive Owner found")

30
leptos/src/view_fn.rs Normal file
View File

@@ -0,0 +1,30 @@
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,7 +1,5 @@
#[cfg(feature = "ssr")]
use leptos::html::HtmlElement;
#[cfg(feature = "ssr")]
#[test]
fn simple_ssr_test() {
use leptos::prelude::*;
@@ -22,7 +20,6 @@ fn simple_ssr_test() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_components() {
use leptos::prelude::*;
@@ -54,7 +51,6 @@ fn ssr_test_with_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_test_with_snake_case_components() {
use leptos::prelude::*;
@@ -85,7 +81,6 @@ fn ssr_test_with_snake_case_components() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn test_classes() {
use leptos::prelude::*;
@@ -103,7 +98,6 @@ 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::*;
@@ -125,7 +119,6 @@ fn ssr_with_styles() {
);
}
#[cfg(feature = "ssr")]
#[test]
fn ssr_option() {
use leptos::prelude::*;

View File

@@ -396,7 +396,8 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// 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-gamma3"
version = "0.7.0-beta6"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -51,27 +51,7 @@ 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",
],
[
"actix",
"axum",
],
]
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(erase_components)'] }

View File

@@ -18,7 +18,6 @@ use syn::{
};
pub struct Model {
is_transparent: bool,
island: Option<String>,
docs: Docs,
unknown_attrs: UnknownAttrs,
@@ -63,7 +62,6 @@ impl Parse for Model {
});
Ok(Self {
is_transparent: false,
island: None,
docs,
unknown_attrs,
@@ -104,7 +102,6 @@ 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,
@@ -119,22 +116,20 @@ impl ToTokens for Model {
let no_props = props.is_empty();
// check for components that end ;
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."
);
}
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);
@@ -260,7 +255,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::owner::Owner::with_hydration(move || {
::leptos::reactive_graph::owner::Owner::with_hydration(move || {
#body_name(#prop_names)
})
}
@@ -270,30 +265,14 @@ impl ToTokens for Model {
}
};
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
}
)
}
let component = quote! {
::leptos::reactive_graph::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
#body_expr
}
)
};
// add island wrapper if island
@@ -301,7 +280,7 @@ impl ToTokens for Model {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::reactive::owner::Owner::current_shared_context()
if ::leptos::reactive_graph::owner::Owner::current_shared_context()
.map(|sc| sc.get_is_hydrating())
.unwrap_or(false) {
::leptos::either::Either::Left(
@@ -337,9 +316,9 @@ impl ToTokens for Model {
quote! {
use leptos::tachys::view::any_view::IntoAny;
let children = Box::new(|| {
let sc = ::leptos::reactive::owner::Owner::current_shared_context().unwrap();
let sc = ::leptos::reactive_graph::owner::Owner::current_shared_context().unwrap();
let prev = sc.get_is_hydrating();
let value = ::leptos::reactive::owner::Owner::with_no_hydration(||
let value = ::leptos::reactive_graph::owner::Owner::with_no_hydration(||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
);
sc.set_is_hydrating(prev);
@@ -541,13 +520,6 @@ 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,24 +535,11 @@ 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 {
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)
pub fn component(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
component_macro(s, None)
}
/// Defines a component as an interactive island when you are using the
@@ -628,37 +615,17 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error2::proc_macro_error]
#[proc_macro_attribute]
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
};
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let island_src = s.to_string();
component_macro(s, is_transparent, Some(island_src))
component_macro(s, Some(island_src))
}
fn component_macro(
s: TokenStream,
is_transparent: bool,
island: Option<String>,
) -> TokenStream {
fn component_macro(s: TokenStream, 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.is_transparent(is_transparent).with_island(island).into_token_stream();
let expanded = model.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::computed::Memo::new(
::leptos::reactive_graph::computed::Memo::new(
move |_| {
use ::leptos::reactive::traits::With;
use ::leptos::reactive_graph::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) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
fn from_map(map: &::leptos_router::params::ParamsMap) -> 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::computed::create_slice(
::leptos::reactive_graph::computed::create_slice(
#root,
|st: &_| st.#path.clone(),
|st: &mut _, n| st.#path = n

View File

@@ -1,5 +1,5 @@
use super::{fragment_to_tokens, TagType};
use crate::view::{attribute_absolute, utils::filter_prefixed_attrs};
use crate::view::attribute_absolute;
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{
@@ -105,13 +105,20 @@ pub(crate) fn component_to_tokens(
return None;
}
};
let inputs = &binding.inputs;
Some(quote! { #inputs })
})
.collect::<Vec<_>>();
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
let items_to_clone = attrs
.iter()
.filter_map(|attr| {
attr.key
.to_string()
.strip_prefix("clone:")
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
})
.collect::<Vec<_>>();
// include all attribute that are either
// 1) blocks ({..attrs} or {attrs}),

View File

@@ -1,19 +1,14 @@
mod component_builder;
mod slot_helper;
mod utils;
use self::{
component_builder::component_to_tokens,
slot_helper::{get_slot, slot_to_tokens},
};
use convert_case::{
Case::{Snake, UpperCamel},
Casing,
};
use convert_case::{Case::Snake, Casing};
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use proc_macro_error2::abort;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use quote::{quote, quote_spanned, ToTokens};
use rstml::node::{
CustomNode, KVAttributeValue, KeyedAttribute, Node, NodeAttribute,
NodeBlock, NodeElement, NodeName, NodeNameFragment,
@@ -179,7 +174,7 @@ fn is_inert_element(orig_node: &Node<impl CustomNode>) -> bool {
}
enum Item<'a, T> {
Node(&'a Node<T>, bool),
Node(&'a Node<T>),
ClosingTag(String),
}
@@ -290,11 +285,10 @@ 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, escape_text)]);
let mut nodes = VecDeque::from([Item::Node(node)]);
while let Some(current) = nodes.pop_front() {
match current {
@@ -304,32 +298,19 @@ fn inert_element_to_tokens(
html.push_str(&tag);
html.push('>');
}
Item::Node(current, escape) => {
Item::Node(current) => {
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('<');
@@ -338,12 +319,9 @@ 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) =
@@ -354,13 +332,11 @@ 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(&value);
html.push_class(&txt.value());
} else {
html.push_str("=\"");
html.push_str(&value);
html.push_str(&txt.value());
html.push('"');
}
}
@@ -376,7 +352,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, escape));
nodes.push_front(Item::Node(child));
}
}
}
@@ -560,9 +536,7 @@ fn node_to_tokens(
view_marker,
disable_inert_html,
),
Node::Block(block) => {
Some(quote! { ::leptos::prelude::IntoRender::into_render(#block) })
}
Node::Block(block) => Some(quote! { #block }),
Node::Text(text) => Some(text_to_tokens(&text.value)),
Node::RawText(raw) => {
let text = raw.to_string_best();
@@ -571,11 +545,7 @@ fn node_to_tokens(
}
Node::Element(el_node) => {
if !top_level && is_inert {
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)
inert_element_to_tokens(node, global_class)
} else {
element_to_tokens(
el_node,
@@ -743,11 +713,6 @@ 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;
@@ -901,7 +866,7 @@ fn attribute_to_tokens(
NodeName::Path(path) => path.path.get_ident(),
_ => unreachable!(),
};
let value = attribute_value(node, false);
let value = attribute_value(node);
quote! {
.#node_ref(#value)
}
@@ -909,8 +874,6 @@ fn attribute_to_tokens(
directive_call_from_attribute_node(node, name)
} else if let Some(name) = name.strip_prefix("on:") {
event_to_tokens(name, node)
} else if let Some(name) = name.strip_prefix("bind:") {
two_way_binding_to_tokens(name, node)
} else if let Some(name) = name.strip_prefix("class:") {
let class = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
@@ -951,13 +914,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, true);
let value = attribute_value(node);
quote! {
.attr(#name, #value)
}
} else {
let key = attribute_name(&node.key);
let value = attribute_value(node, true);
let value = attribute_value(node);
// special case of global_class and class attribute
if &node.key.to_string() == "class"
@@ -994,11 +957,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" {
@@ -1006,7 +969,6 @@ 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("_");
@@ -1035,7 +997,6 @@ 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)
@@ -1044,7 +1005,6 @@ 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(
@@ -1097,20 +1057,6 @@ pub(crate) fn attribute_absolute(
}
}
pub(crate) fn two_way_binding_to_tokens(
name: &str,
node: &KeyedAttribute,
) -> TokenStream {
let value = attribute_value(node, false);
let ident =
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
quote! {
.bind(::leptos::attr::#ident, #value)
}
}
pub(crate) fn event_to_tokens(
name: &str,
node: &KeyedAttribute,
@@ -1126,7 +1072,7 @@ pub(crate) fn event_type_and_handler(
name: &str,
node: &KeyedAttribute,
) -> (TokenStream, TokenStream, TokenStream) {
let handler = attribute_value(node, false);
let handler = attribute_value(node);
let (event_type, is_custom, is_force_undelegated, is_targeted) =
parse_event_name(name);
@@ -1183,7 +1129,7 @@ fn class_to_tokens(
class: TokenStream,
class_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
if let Some(class_name) = class_name {
quote! {
.#class((#class_name, #value))
@@ -1200,7 +1146,7 @@ fn style_to_tokens(
style: TokenStream,
style_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
if let Some(style_name) = style_name {
quote! {
.#style((#style_name, #value))
@@ -1217,7 +1163,7 @@ fn prop_to_tokens(
prop: TokenStream,
key: &str,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
quote! {
.#prop(#key, #value)
}
@@ -1374,10 +1320,7 @@ fn attribute_name(name: &NodeName) -> TokenStream {
}
}
fn attribute_value(
attr: &KeyedAttribute,
is_attribute_proper: bool,
) -> TokenStream {
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
match attr.possible_value.to_value() {
None => quote! { true },
Some(value) => match &value.value {
@@ -1392,26 +1335,14 @@ fn attribute_value(
}
}
if matches!(expr, Expr::Lit(_)) || !is_attribute_proper {
quote! {
#expr
}
} else {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#expr)
}
quote! {
{#expr}
}
}
// any value in braces: expand as-is to give proper r-a support
KVAttributeValue::InvalidBraced(block) => {
if is_attribute_proper {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#block)
}
} else {
quote! {
#block
}
quote! {
#block
}
}
},

View File

@@ -1,7 +1,7 @@
use super::{convert_to_snake_case, ident_from_tag_name};
use crate::view::{fragment_to_tokens, utils::filter_prefixed_attrs, TagType};
use crate::view::{fragment_to_tokens, TagType};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{quote, quote_spanned};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{CustomNode, KeyedAttribute, NodeAttribute, NodeElement};
use std::collections::HashMap;
use syn::spanned::Spanned;
@@ -70,9 +70,25 @@ pub(crate) fn slot_to_tokens(
}
});
let items_to_bind = filter_prefixed_attrs(attrs.iter(), "let:");
let items_to_bind = attrs
.iter()
.filter_map(|attr| {
attr.key
.to_string()
.strip_prefix("let:")
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
})
.collect::<Vec<_>>();
let items_to_clone = filter_prefixed_attrs(attrs.iter(), "clone:");
let items_to_clone = attrs
.iter()
.filter_map(|attr| {
attr.key
.to_string()
.strip_prefix("clone:")
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
})
.collect::<Vec<_>>();
let dyn_attrs = attrs
.iter()

View File

@@ -1,19 +0,0 @@
use proc_macro2::Ident;
use quote::format_ident;
use rstml::node::KeyedAttribute;
use syn::spanned::Spanned;
pub fn filter_prefixed_attrs<'a, A>(attrs: A, prefix: &str) -> Vec<Ident>
where
A: IntoIterator<Item = &'a KeyedAttribute> + Clone,
{
attrs
.into_iter()
.filter_map(|attr| {
attr.key
.to_string()
.strip_prefix(prefix)
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
})
.collect()
}

View File

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

View File

@@ -19,7 +19,6 @@ 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,8 +8,6 @@ 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;
@@ -185,7 +183,7 @@ impl FromEncodedStr for [u8] {
mod view_implementations {
use crate::Resource;
use reactive_graph::traits::Read;
use std::future::Future;
use std::{future::Future, pin::Pin};
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
@@ -219,11 +217,16 @@ mod view_implementations {
{
type Output<SomeNewAttr: Attribute> = Box<
dyn FnMut() -> Suspend<
<T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
>
+ Send
Pin<
Box<
dyn Future<
Output = <T as AddAnyAttr>::Output<
<SomeNewAttr::CloneableOwned as Attribute>::CloneableOwned,
>,
> + Send,
>,
>,
> + 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 + 'static) -> Self
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
where
T: 'static,
Fut: Future<Output = T> + 'static,
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
let fetcher = move || {
let fut = fetcher();
@@ -60,7 +60,7 @@ impl<T> ArcLocalResource<T> {
}
};
Self {
data: ArcAsyncDerived::new_unsync(fetcher),
data: ArcAsyncDerived::new(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: 'static,
T: Send + Sync + '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> + 'static,
Fut: Future<Output = T> + Send + '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();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
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: 'static,
T: Send + Sync + '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: 'static,
T: Send + Sync + '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: 'static,
T: Send + Sync + '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: 'static,
T: Send + Sync + '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: 'static,
T: Send + Sync + 'static,
{
fn mark_dirty(&self) {
self.data.mark_dirty();
@@ -363,7 +363,7 @@ where
impl<T> Subscriber for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn add_source(&self, source: AnySource) {
self.data.add_source(source);

View File

@@ -1,708 +0,0 @@
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, SharedContext};
use hydration_context::SerializedDataId;
use reactive_graph::{
computed::{
ArcAsyncDerived, ArcMemo, AsyncDerived, AsyncDerivedFuture,
@@ -28,14 +28,10 @@ use std::{
future::{pending, IntoFuture},
ops::Deref,
panic::Location,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
sync::atomic::{AtomicBool, Ordering},
};
pub(crate) static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool =
AtomicBool::new(false);
static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool = AtomicBool::new(false);
pub struct SuppressResourceLoad;
@@ -109,49 +105,6 @@ impl<T, Ser> Deref for ArcResource<T, Ser> {
}
}
impl<T, Ser> Track for ArcResource<T, Ser>
where
T: 'static,
{
fn track(&self) {
self.data.track();
}
}
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
where
T: 'static,
{
type Value = <ArcAsyncDerived<T> as ReadUntracked>::Value;
#[track_caller]
fn try_read_untracked(&self) -> Option<Self::Value> {
#[cfg(all(feature = "hydration", debug_assertions))]
{
use reactive_graph::{
computed::suspense::SuspenseContext, owner::use_context,
};
let suspense = use_context::<SuspenseContext>();
if suspense.is_none() {
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(
"At {location}, you are reading a resource in `hydrate` \
mode outside a <Suspense/> or <Transition/>. This can \
cause hydration mismatch errors and loses out on a \
significant performance optimization. To fix this issue, \
you can either: \n1. Wrap the place where you read the \
resource in a <Suspense/> or <Transition/> component, or \
\n2. Switch to using ArcLocalResource::new(), which will \
wait to load the resource until the app is hydrated on \
the client side. (This will have worse performance in \
most cases.)",
));
}
}
self.data.try_read_untracked()
}
}
impl<T, Ser> ArcResource<T, Ser>
where
Ser: Encoder<T> + Decoder<T>,
@@ -179,7 +132,7 @@ where
.map(|sc| sc.next_id())
.unwrap_or_default();
let initial = initial_value::<T, Ser>(&id, shared_context.as_ref());
let initial = Self::initial_value(&id);
let is_ready = initial.is_some();
let refetch = ArcRwSignal::new(0);
@@ -257,53 +210,43 @@ where
pub fn refetch(&self) {
*self.refetch.write() += 1;
}
}
#[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;
#[inline(always)]
#[allow(unused)]
fn initial_value(id: &SerializedDataId) -> Option<T> {
#[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,
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:?}");
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>
@@ -621,49 +564,6 @@ where
}
}
impl<T, Ser> Track for Resource<T, Ser>
where
T: Send + Sync + 'static,
{
fn track(&self) {
self.data.track();
}
}
impl<T, Ser> ReadUntracked for Resource<T, Ser>
where
T: Send + Sync + 'static,
{
type Value = <AsyncDerived<T> as ReadUntracked>::Value;
#[track_caller]
fn try_read_untracked(&self) -> Option<Self::Value> {
#[cfg(all(feature = "hydration", debug_assertions))]
{
use reactive_graph::{
computed::suspense::SuspenseContext, owner::use_context,
};
let suspense = use_context::<SuspenseContext>();
if suspense.is_none() {
let location = std::panic::Location::caller();
reactive_graph::log_warning(format_args!(
"At {location}, you are reading a resource in `hydrate` \
mode outside a <Suspense/> or <Transition/>. This can \
cause hydration mismatch errors and loses out on a \
significant performance optimization. To fix this issue, \
you can either: \n1. Wrap the place where you read the \
resource in a <Suspense/> or <Transition/> component, or \
\n2. Switch to using LocalResource::new(), which will \
wait to load the resource until the app is hydrated on \
the client side. (This will have worse performance in \
most cases.)",
));
}
}
self.data.try_read_untracked()
}
}
impl<T> Resource<T, FromToStringCodec>
where
FromToStringCodec: Encoder<T> + Decoder<T>,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.7.0-gamma3"
version = "0.7.0-beta6"
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::owner::use_context,
reactive_graph::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::owner::use_context,
reactive_graph::owner::use_context,
tachys::{
dom::document,
html::attribute::Attribute,

View File

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

View File

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

View File

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

View File

@@ -30,21 +30,23 @@ sqlx = { version = "0.8.0", features = [
], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2.0"
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 }
axum_session_auth = { version = "0.14.0", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.14.0", features = [
"sqlite-rustls",
], optional = true }
bcrypt = { version = "0.15.0", optional = true }
async-trait = { version = "0.1.0", optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/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,8 +28,9 @@ impl Default for User {
#[cfg(feature = "ssr")]
pub mod ssr {
pub use super::{User, UserPasshash};
pub use axum_session_auth::{Authentication, HasPermission};
use axum_session_sqlx::SessionSqlitePool;
pub use axum_session_auth::{
Authentication, HasPermission, 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<ArcRwSignal<Errors>>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => ArcRwSignal::new(e),
Some(e) => RwSignal::new(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),

View File

@@ -0,0 +1,50 @@
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,6 +2,8 @@ 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;
@@ -12,5 +14,5 @@ pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(TodoApp);
leptos::mount_to_body(TodoApp);
}

View File

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

View File

@@ -1,6 +1,6 @@
use axum::extract::FromRef;
use leptos::prelude::LeptosOptions;
use leptos_axum::AxumRouteListing;
use leptos::LeptosOptions;
use leptos_router::RouteListing;
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<AxumRouteListing>,
pub routes: Vec<RouteListing>,
}

View File

@@ -1,7 +1,7 @@
use crate::{auth::*, error_template::ErrorTemplate};
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, *};
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -109,33 +109,13 @@ 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 = ServerAction::<Login>::new();
let logout = ServerAction::<Logout>::new();
let signup = ServerAction::<Signup>::new();
let login = create_server_action::<Login>();
let logout = create_server_action::<Logout>();
let signup = create_server_action::<Signup>();
let user = Resource::new(
let user = create_resource(
move || {
(
login.version().get(),
@@ -148,6 +128,8 @@ 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="/">
@@ -167,7 +149,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<span>{format!("Login error: {}", e)}</span>
}
.into_any()
.into_view()
}
Ok(None) => {
view! {
@@ -177,7 +159,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<span>"Logged out."</span>
}
.into_any()
.into_view()
}
Ok(Some(user)) => {
view! {
@@ -187,7 +169,7 @@ pub fn TodoApp() -> impl IntoView {
{format!("Logged in as: {} ({})", user.username, user.id)}
</span>
}
.into_any()
.into_view()
}
})
}}
@@ -196,15 +178,13 @@ pub fn TodoApp() -> impl IntoView {
</header>
<hr/>
<main>
<FlatRoutes fallback=|| "Not found.">
<Routes>
// Route
<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=|| "/"
<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"
view=move || {
view! {
<h1>"Settings"</h1>
@@ -213,7 +193,7 @@ pub fn TodoApp() -> impl IntoView {
}
/>
</FlatRoutes>
</Routes>
</main>
</Router>
}
@@ -221,12 +201,12 @@ pub fn TodoApp() -> impl IntoView {
#[component]
pub fn Todos() -> impl IntoView {
let add_todo = ServerMultiAction::<AddTodo>::new();
let delete_todo = ServerAction::<DeleteTodo>::new();
let add_todo = create_server_multi_action::<AddTodo>();
let delete_todo = create_server_action::<DeleteTodo>();
let submissions = add_todo.submissions();
// list of todos is loaded from the server in reaction to changes
let todos = Resource::new(
let todos = create_resource(
move || (add_todo.version().get(), delete_todo.version().get()),
move |_| get_todos(),
);
@@ -251,11 +231,11 @@ pub fn Todos() -> impl IntoView {
view! {
<pre class="error">"Server Error: " {e.to_string()}</pre>
}
.into_any()
.into_view()
}
Ok(todos) => {
if todos.is_empty() {
view! { <p>"No tasks were found."</p> }.into_any()
view! { <p>"No tasks were found."</p> }.into_view()
} else {
todos
.into_iter()
@@ -272,11 +252,10 @@ pub fn Todos() -> impl IntoView {
}
})
.collect_view()
.into_any()
}
}
})
.unwrap_or(().into_any())
.unwrap_or_default()
}
};
let pending_todos = move || {
@@ -287,7 +266,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>
}
})
@@ -303,7 +282,9 @@ pub fn Todos() -> impl IntoView {
}
#[component]
pub fn Login(action: ServerAction<Login>) -> impl IntoView {
pub fn Login(
action: Action<Login, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Log In"</h1>
@@ -336,7 +317,9 @@ pub fn Login(action: ServerAction<Login>) -> impl IntoView {
}
#[component]
pub fn Signup(action: ServerAction<Signup>) -> impl IntoView {
pub fn Signup(
action: Action<Signup, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Sign Up"</h1>
@@ -379,7 +362,9 @@ pub fn Signup(action: ServerAction<Signup>) -> impl IntoView {
}
#[component]
pub fn Logout(action: ServerAction<Logout>) -> impl IntoView {
pub fn Logout(
action: Action<Logout, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<div id="loginbox">
<ActionForm action=action>

View File

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

View File

@@ -1,12 +1,9 @@
use crate::{
computed::{ArcMemo, Memo},
diagnostics::is_suppressing_resource_load,
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
SyncStorage,
},
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{ArcRwSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update},
traits::{DefinedAt, Dispose, Get, GetUntracked, Update},
unwrap_signal,
};
use any_spawner::Executor;
@@ -27,7 +24,7 @@ use std::{future::Future, panic::Location, pin::Pin, sync::Arc};
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -96,7 +93,6 @@ 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,
@@ -112,7 +108,6 @@ 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,
@@ -140,7 +135,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = ArcAction::new(|n: &u8| {
/// let n = n.to_owned();
@@ -196,7 +191,6 @@ 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(),
@@ -236,14 +230,14 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version = self.dispatched.get_value();
let current_version =
self.version.try_get_untracked().unwrap_or_default();
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 {
@@ -255,7 +249,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = dispatched.get_value() <= current_version;
let is_latest = version.get_untracked() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -288,7 +282,8 @@ where
// Update the state before loading
self.in_flight.update(|n| *n += 1);
let current_version = self.dispatched.get_value();
let current_version =
self.version.try_get_untracked().unwrap_or_default();
self.input.try_update(|inp| *inp = Some(input));
// Spawn the task
@@ -296,7 +291,6 @@ 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! {
@@ -307,7 +301,7 @@ where
// otherwise, update the value
result = fut => {
in_flight.update(|n| *n = n.saturating_sub(1));
let is_latest = dispatched.get_value() <= current_version;
let is_latest = version.get_untracked() <= current_version;
if is_latest {
version.update(|n| *n += 1);
value.update(|n| *n = Some(result));
@@ -357,7 +351,6 @@ 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)))
}),
@@ -374,7 +367,7 @@ impl<I, O> ArcAction<I, O> {
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = ArcAction::new(|n: &u8| {
/// let n = n.to_owned();
@@ -402,7 +395,7 @@ impl<I, O> ArcAction<I, O> {
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = ArcAction::new(|n: &u8| {
/// let n = n.to_owned();
@@ -432,7 +425,7 @@ impl<I, O> ArcAction<I, O> {
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = ArcAction::new(|n: &u8| {
/// let n = n.to_owned();
@@ -463,7 +456,7 @@ impl<I, O> ArcAction<I, O> {
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = ArcAction::new(|n: &u8| {
/// let n = n.to_owned();
@@ -517,7 +510,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -568,7 +561,7 @@ where
/// function, because it is stored in [Action::input] as well.
///
/// ```rust
/// # use reactive_graph::actions::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::actions::*;
/// // if there's a single argument, just use that
/// let action1 = Action::new(|input: &String| {
/// let input = input.clone();
@@ -614,7 +607,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = Action::new(|n: &u8| {
/// let n = n.to_owned();
@@ -728,7 +721,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = Action::new(|n: &u8| {
/// let n = n.to_owned();
@@ -759,7 +752,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = Action::new(|n: &u8| {
/// let n = n.to_owned();
@@ -798,7 +791,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = ArcAction::new(|n: &u8| {
/// let n = n.to_owned();
@@ -854,7 +847,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = Action::new(|n: &u8| {
/// let n = n.to_owned();
@@ -1016,7 +1009,7 @@ impl<I, O, S> Copy for Action<I, O, S> {}
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = create_action(|n: &u8| {
/// let n = n.to_owned();

View File

@@ -24,7 +24,7 @@ use std::{fmt::Debug, future::Future, panic::Location, pin::Pin, sync::Arc};
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -105,7 +105,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// // if there's a single argument, just use that
/// let action1 = MultiAction::new(|input: &String| {
@@ -151,7 +151,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -202,7 +202,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -245,7 +245,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -286,7 +286,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -336,7 +336,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -403,7 +403,7 @@ impl<I, O> ArcMultiAction<I, O> {
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// // if there's a single argument, just use that
/// let action1 = ArcMultiAction::new(|input: &String| {
@@ -452,7 +452,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -529,7 +529,7 @@ where
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -579,7 +579,7 @@ impl<I, O> ArcMultiAction<I, O> {
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
@@ -609,7 +609,7 @@ impl<I, O> ArcMultiAction<I, O> {
/// # use reactive_graph::actions::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...

View File

@@ -15,7 +15,6 @@ use crate::{
};
pub use arc_memo::*;
pub use async_derived::*;
pub(crate) use inner::MemoInner;
pub use memo::*;
pub use selector::*;
@@ -35,7 +34,7 @@ pub use selector::*;
/// In the example below, setting an auth token will only trigger
/// the token signal, but none of the other derived signals.
/// ```
/// # use reactive_graph::prelude::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::effect::Effect;
/// # use reactive_graph::signal::RwSignal;
/// # use reactive_graph::computed::*;

View File

@@ -40,7 +40,7 @@ use std::{
///
/// ## Examples
/// ```
/// # use reactive_graph::prelude::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::signal;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
@@ -132,7 +132,10 @@ where
pub fn new_with_compare(
fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static,
changed: fn(Option<&T>, Option<&T>) -> bool,
) -> Self {
) -> Self
where
T: PartialEq,
{
Self::new_owning(move |prev: Option<T>| {
let new_value = fun(prev.as_ref());
let changed = changed(prev.as_ref(), Some(&new_value));
@@ -154,7 +157,10 @@ where
)]
pub fn new_owning(
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
) -> Self {
) -> Self
where
T: PartialEq,
{
let inner = Arc::new_cyclic(|weak| {
let subscriber = AnySubscriber(
weak.as_ptr() as usize,

View File

@@ -19,7 +19,7 @@ use crate::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
Writeable,
},
transition::AsyncTransition,
};
@@ -53,10 +53,10 @@ use std::{
/// ## Examples
/// ```rust
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
///
/// let signal1 = RwSignal::new(0);
@@ -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::new(
self.to_any_source(),
&self.loading,
&self.wakers,
)
AsyncDerivedReadyFuture {
source: self.to_any_source(),
loading: Arc::clone(&self.loading),
wakers: Arc::clone(&self.wakers),
}
}
}
@@ -600,7 +600,7 @@ impl<T: 'static> Notify for ArcAsyncDerived<T> {
}
}
impl<T: 'static> Write for ArcAsyncDerived<T> {
impl<T: 'static> Writeable 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, Write,
UntrackableGuard, Writeable,
},
unwrap_signal,
};
@@ -29,10 +29,10 @@ use std::{future::Future, ops::DerefMut, panic::Location};
/// ## Examples
/// ```rust
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// # use reactive_graph::prelude::*;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
///
/// let signal1 = RwSignal::new(0);
@@ -300,7 +300,7 @@ where
}
}
impl<T, S> Write for AsyncDerived<T, S>
impl<T, S> Writeable for AsyncDerived<T, S>
where
T: 'static,
S: Storage<ArcAsyncDerived<T>>,

View File

@@ -34,21 +34,6 @@ 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, Write},
traits::{Update, Writeable},
};
use futures::channel::oneshot::Sender;
use or_poisoned::OrPoisoned;

View File

@@ -36,7 +36,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
/// # use reactive_graph::effect::Effect;
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # tokio::task::LocalSet::new().run_until(async {
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// let (value, set_value) = signal(0);
@@ -161,7 +161,7 @@ where
/// # use reactive_graph::effect::Effect;
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// let (value, set_value) = signal(0);
///
@@ -195,7 +195,10 @@ where
pub fn new_with_compare(
fun: impl Fn(Option<&T>) -> T + Send + Sync + 'static,
changed: fn(Option<&T>, Option<&T>) -> bool,
) -> Self {
) -> Self
where
T: PartialEq,
{
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
@@ -219,7 +222,10 @@ where
)]
pub fn new_owning(
fun: impl Fn(Option<T>) -> (T, bool) + Send + Sync + 'static,
) -> Self {
) -> Self
where
T: PartialEq,
{
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),

View File

@@ -19,13 +19,13 @@ use std::{
///
/// ```
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::effect::Effect;
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::owner::StoredValue;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let a = RwSignal::new(0);
/// let is_selected = Selector::new(move || a.get());

View File

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

View File

@@ -37,13 +37,13 @@ use std::{
///
/// ```
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::effect::Effect;
/// # use reactive_graph::owner::ArenaItem;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// let a = RwSignal::new(0);
/// let b = RwSignal::new(0);
///
@@ -185,7 +185,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
///
@@ -217,7 +217,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
/// let (cb_num, set_cb_num) = signal(0);
@@ -256,7 +256,7 @@ impl Effect<LocalStorage> {
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # tokio::task::LocalSet::new().run_until(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// #
/// let (num, set_num) = signal(0);
///

View File

@@ -104,11 +104,11 @@ impl Observer {
///
/// ```rust
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::graph::untrack;
/// # use reactive_graph::untrack;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # any_spawner::Executor::init_tokio();
/// let (a, set_a) = signal(0);
/// let (b, set_b) = signal(0);
/// let c = Memo::new(move |_| {

View File

@@ -14,7 +14,6 @@
//!
//! ```rust
//! # any_spawner::Executor::init_futures_executor();
//! # let owner = reactive_graph::owner::Owner::new(); owner.set();
//! use reactive_graph::{
//! computed::ArcMemo,
//! effect::Effect,
@@ -88,7 +87,7 @@ pub mod traits;
pub mod transition;
pub mod wrappers;
use computed::ScopedFuture;
pub use graph::untrack;
#[cfg(feature = "nightly")]
mod nightly;
@@ -125,42 +124,9 @@ 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 fn spawn(task: impl Future<Output = ()> + Send + 'static) {
pub(crate) fn spawn(task: impl Future<Output = ()> + Send + 'static) {
#[cfg(feature = "sandboxed-arenas")]
let task = owner::Sandboxed::new(task);
any_spawner::Executor::spawn(task);
}
/// Calls [`Executor::spawn_local`], but ensures that the task runs under the current reactive [`Owner`]
/// and [`Observed`]. Does not cancel the task if the owner is cleaned up.
pub fn spawn_local_scoped(task: impl Future<Output = ()> + 'static) {
let task = ScopedFuture::new(task);
#[cfg(feature = "sandboxed-arenas")]
let task = owner::Sandboxed::new(task);
any_spawner::Executor::spawn_local(task);
}
/// Calls [`Executor::spawn_local`], but ensures that the task runs under the current reactive [`Owner`]
/// and [`Observed`]. Cancels the task if the owner is cleaned up.
pub fn spawn_local_scoped_with_cancellation(
task: impl Future<Output = ()> + 'static,
) {
use crate::owner::on_cleanup;
use futures::future::{AbortHandle, Abortable};
let (abort_handle, abort_registration) = AbortHandle::new_pair();
on_cleanup(move || abort_handle.abort());
let task = Abortable::new(task, abort_registration);
let task = ScopedFuture::new(task);
#[cfg(feature = "sandboxed-arenas")]
let task = owner::Sandboxed::new(task);
any_spawner::Executor::spawn_local(async move {
_ = task.await;
});
}

View File

@@ -12,18 +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")]
use arena::ArenaMap;
use arena::NodeId;
pub use arena_item::*;
pub use context::*;
@@ -124,12 +120,6 @@ impl Owner {
contexts: Default::default(),
cleanups: Default::default(),
children: Default::default(),
#[cfg(feature = "sandboxed-arenas")]
arena: parent
.as_ref()
.and_then(|parent| parent.upgrade())
.map(|parent| parent.read().or_poisoned().arena.clone())
.unwrap_or_default(),
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -150,10 +140,11 @@ impl Owner {
/// Only one `SharedContext` needs to be created per request, and will be automatically shared
/// by any other `Owner`s created under this one.
#[cfg(feature = "hydration")]
#[track_caller]
pub fn new_root(
shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
) -> Self {
Arena::enter_new();
let this = Self {
inner: Arc::new(RwLock::new(OwnerInner {
parent: None,
@@ -161,21 +152,17 @@ impl Owner {
contexts: Default::default(),
cleanups: Default::default(),
children: Default::default(),
#[cfg(feature = "sandboxed-arenas")]
arena: Default::default(),
})),
#[cfg(feature = "hydration")]
shared_context,
};
this.set();
OWNER.with_borrow_mut(|owner| *owner = Some(this.clone()));
this
}
/// Creates a new `Owner` that is the child of the current `Owner`, if any.
pub fn child(&self) -> Self {
let parent = Some(Arc::downgrade(&self.inner));
#[cfg(feature = "sandboxed-arenas")]
let arena = self.inner.read().or_poisoned().arena.clone();
let child = Self {
inner: Arc::new(RwLock::new(OwnerInner {
parent,
@@ -183,8 +170,6 @@ impl Owner {
contexts: Default::default(),
cleanups: Default::default(),
children: Default::default(),
#[cfg(feature = "sandboxed-arenas")]
arena,
})),
#[cfg(feature = "hydration")]
shared_context: self.shared_context.clone(),
@@ -200,8 +185,6 @@ impl Owner {
/// Sets this as the current `Owner`.
pub fn set(&self) {
OWNER.with_borrow_mut(|owner| *owner = Some(self.clone()));
#[cfg(feature = "sandboxed-arenas")]
Arena::set(&self.inner.read().or_poisoned().arena);
}
/// Runs the given function with this as the current `Owner`.
@@ -211,8 +194,6 @@ impl Owner {
mem::replace(&mut *o.borrow_mut(), Some(self.clone()))
})
};
#[cfg(feature = "sandboxed-arenas")]
Arena::set(&self.inner.read().or_poisoned().arena);
let val = fun();
OWNER.with(|o| {
*o.borrow_mut() = prev;
@@ -353,8 +334,6 @@ pub(crate) struct OwnerInner {
pub contexts: FxHashMap<TypeId, Box<dyn Any + Send + Sync>>,
pub cleanups: Vec<Box<dyn FnOnce() + Send + Sync>>,
pub children: Vec<Weak<RwLock<OwnerInner>>>,
#[cfg(feature = "sandboxed-arenas")]
arena: Arc<RwLock<ArenaMap>>,
}
impl Debug for OwnerInner {
@@ -382,19 +361,11 @@ impl Drop for OwnerInner {
let nodes = mem::take(&mut self.nodes);
if !nodes.is_empty() {
#[cfg(not(feature = "sandboxed-arenas"))]
Arena::with_mut(|arena| {
for node in nodes {
_ = arena.remove(node);
}
});
#[cfg(feature = "sandboxed-arenas")]
{
let mut arena = self.arena.write().or_poisoned();
for node in nodes {
_ = arena.remove(node);
}
}
}
}
}
@@ -423,20 +394,11 @@ impl Cleanup for RwLock<OwnerInner> {
}
if !nodes.is_empty() {
#[cfg(not(feature = "sandboxed-arenas"))]
Arena::with_mut(|arena| {
for node in nodes {
_ = arena.remove(node);
}
});
#[cfg(feature = "sandboxed-arenas")]
{
let arena = self.read().or_poisoned().arena.clone();
let mut arena = arena.write().or_poisoned();
for node in nodes {
_ = arena.remove(node);
}
}
}
}
}

View File

@@ -1,126 +0,0 @@
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,42 +1,38 @@
use or_poisoned::OrPoisoned;
use slotmap::{new_key_type, SlotMap};
#[cfg(feature = "sandboxed-arenas")]
use std::cell::RefCell;
#[cfg(not(feature = "sandboxed-arenas"))]
use std::sync::OnceLock;
use std::{any::Any, hash::Hash, sync::RwLock};
#[cfg(feature = "sandboxed-arenas")]
use std::sync::Weak;
use std::{
any::Any,
hash::Hash,
sync::{Arc, RwLock},
};
use std::{cell::RefCell, sync::Arc};
new_key_type! {
/// Unique identifier for an item stored in the arena.
pub struct NodeId;
}
pub struct Arena;
pub(crate) struct Arena;
pub type ArenaMap = SlotMap<NodeId, Box<dyn Any + Send + Sync>>;
type ArenaMap = SlotMap<NodeId, Box<dyn Any + Send + Sync>>;
#[cfg(not(feature = "sandboxed-arenas"))]
static MAP: OnceLock<RwLock<ArenaMap>> = OnceLock::new();
#[cfg(feature = "sandboxed-arenas")]
thread_local! {
pub(crate) static MAP: RefCell<Option<Weak<RwLock<ArenaMap>>>> = RefCell::new(Some(Default::default()));
pub(crate) static MAP: RefCell<Option<Arc<RwLock<ArenaMap>>>> = RefCell::new(Some(Default::default()));
}
impl Arena {
#[cfg(feature = "hydration")]
#[inline(always)]
#[allow(unused)]
pub fn set(arena: &Arc<RwLock<ArenaMap>>) {
pub fn enter_new() {
#[cfg(feature = "sandboxed-arenas")]
{
let new_arena = Arc::downgrade(arena);
use std::sync::Arc;
MAP.with_borrow_mut(|arena| {
*arena = Some(new_arena);
*arena = Some(Arc::new(RwLock::new(
SlotMap::with_capacity_and_key(32),
)))
})
}
}
@@ -52,7 +48,6 @@ impl Arena {
MAP.with_borrow(|arena| {
fun(&arena
.as_ref()
.and_then(Weak::upgrade)
.unwrap_or_else(|| {
panic!(
"at {}, the `sandboxed-arenas` feature is active, \
@@ -78,7 +73,6 @@ impl Arena {
MAP.with_borrow(|arena| {
fun(&mut arena
.as_ref()
.and_then(Weak::upgrade)
.unwrap_or_else(|| {
panic!(
"at {}, the `sandboxed-arenas` feature is active, \
@@ -100,11 +94,20 @@ pub mod sandboxed {
use pin_project_lite::pin_project;
use std::{
future::Future,
mem,
pin::Pin,
sync::{Arc, RwLock, Weak},
sync::{Arc, RwLock},
task::{Context, Poll},
};
impl Arena {
fn set(new_arena: Arc<RwLock<ArenaMap>>) -> UnsetArenaOnDrop {
MAP.with_borrow_mut(|arena| {
UnsetArenaOnDrop(mem::replace(arena, Some(new_arena)))
})
}
}
pin_project! {
/// A [`Future`] that restores its associated arena as the current arena whenever it is
/// polled.
@@ -114,7 +117,7 @@ pub mod sandboxed {
/// stored values. Wrapping a `Future` in `Sandboxed` ensures that it will always use the
/// same arena that it was created under.
pub struct Sandboxed<T> {
arena: Option<Arc<RwLock<ArenaMap>>>,
arena: Arc<RwLock<ArenaMap>>,
#[pin]
inner: T,
}
@@ -124,7 +127,12 @@ pub mod sandboxed {
/// Wraps the given [`Future`], ensuring that any [`ArenaItem`] created while it is being
/// polled will be associated with the same arena that was active when this was called.
pub fn new(inner: T) -> Self {
let arena = MAP.with_borrow(|n| n.as_ref().and_then(Weak::upgrade));
let arena = MAP.with_borrow(|current| {
Arc::clone(current.as_ref().expect(
"the `sandboxed-arenas` feature is active, but no Arena \
is active",
))
});
Self { arena, inner }
}
}
@@ -139,11 +147,11 @@ pub mod sandboxed {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output> {
if let Some(arena) = self.arena.as_ref() {
Arena::set(arena);
}
let unset = Arena::set(Arc::clone(&self.arena));
let this = self.project();
this.inner.poll(cx)
let res = this.inner.poll(cx);
drop(unset);
res
}
}
@@ -157,11 +165,22 @@ pub mod sandboxed {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
if let Some(arena) = self.arena.as_ref() {
Arena::set(arena);
}
let unset = Arena::set(Arc::clone(&self.arena));
let this = self.project();
this.inner.poll_next(cx)
let res = this.inner.poll_next(cx);
drop(unset);
res
}
}
#[derive(Debug)]
struct UnsetArenaOnDrop(Option<Arc<RwLock<ArenaMap>>>);
impl Drop for UnsetArenaOnDrop {
fn drop(&mut self) {
if let Some(inner) = self.0.take() {
MAP.with_borrow_mut(|current_map| *current_map = Some(inner));
}
}
}
}

View File

@@ -108,7 +108,6 @@ impl Owner {
/// ```rust
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::owner::*;
/// # let owner = Owner::new(); owner.set();
/// # use reactive_graph::effect::Effect;
/// # futures::executor::block_on(async move {
/// # any_spawner::Executor::init_futures_executor();
@@ -141,7 +140,6 @@ impl Owner {
/// ```rust
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::owner::*;
/// # let owner = Owner::new(); owner.set();
/// # use reactive_graph::effect::Effect;
/// # futures::executor::block_on(async move {
/// # any_spawner::Executor::init_futures_executor();
@@ -189,7 +187,6 @@ pub fn provide_context<T: Send + Sync + 'static>(value: T) {
/// ```rust
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::owner::*;
/// # let owner = Owner::new(); owner.set();
/// # use reactive_graph::effect::Effect;
/// # futures::executor::block_on(async move {
/// # any_spawner::Executor::init_futures_executor();
@@ -235,7 +232,6 @@ pub fn use_context<T: Clone + 'static>() -> Option<T> {
/// ```rust
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::owner::*;
/// # let owner = Owner::new(); owner.set();
/// # use reactive_graph::effect::Effect;
/// # futures::executor::block_on(async move {
/// # any_spawner::Executor::init_futures_executor();
@@ -290,7 +286,6 @@ pub fn expect_context<T: Clone + 'static>() -> T {
/// ```rust
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::owner::*;
/// # let owner = Owner::new(); owner.set();
/// # use reactive_graph::effect::Effect;
/// # futures::executor::block_on(async move {
/// # any_spawner::Executor::init_futures_executor();

View File

@@ -1,16 +1,13 @@
use super::{
arc_stored_value::ArcStoredValue, ArenaItem, LocalStorage, Storage,
SyncStorage,
};
use super::{ArenaItem, LocalStorage, Storage, SyncStorage};
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, Dispose, IsDisposed, ReadValue, WriteValue},
traits::{DefinedAt, Dispose, IsDisposed},
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.
@@ -20,8 +17,9 @@ 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<ArcStoredValue<T>, S>,
value: ArenaItem<Arc<RwLock<T>>, S>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -34,18 +32,6 @@ 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
@@ -76,13 +62,13 @@ impl<T, S> DefinedAt for StoredValue<T, S> {
impl<T, S> StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
S: Storage<Arc<RwLock<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(ArcStoredValue::new(value)),
value: ArenaItem::new_with_storage(Arc::new(RwLock::new(value))),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
@@ -92,7 +78,7 @@ where
impl<T, S> Default for StoredValue<T, S>
where
T: Default + 'static,
S: Storage<ArcStoredValue<T>>,
S: Storage<Arc<RwLock<T>>>,
{
#[track_caller] // Default trait is not annotated with #[track_caller]
fn default() -> Self {
@@ -122,31 +108,230 @@ where
}
}
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>>> {
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;
/// # 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> {
self.value
.try_get_value()
.and_then(|inner| inner.try_read_value())
.map(|inner| fun(&*inner.read().or_poisoned()))
}
}
impl<T, S> WriteValue for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = T;
/// 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;
///
/// // 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))
}
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
/// 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()
.and_then(|inner| inner.try_write_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;
///
/// #[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;
/// # 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 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);
}
}
@@ -156,39 +341,90 @@ 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;
/// # 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;
///
/// // 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: Clone + Send + Sync + Serialize,
T: Send + Sync + Serialize,
St: Storage<SignalTypes<T, St>> + Storage<T>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>

View File

@@ -36,7 +36,7 @@ pub use write::*;
///
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// let (count, set_count) = arc_signal(0);
///
/// // ✅ calling the getter clones and returns the value
@@ -82,7 +82,7 @@ pub fn arc_signal<T>(value: T) -> (ArcReadSignal<T>, ArcWriteSignal<T>) {
/// as long as a reference to it is alive, see [`arc_signal`].
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// let (count, set_count) = signal(0);
///
/// // ✅ calling the getter clones and returns the value
@@ -142,7 +142,7 @@ pub fn signal_local<T: 'static>(
/// as long as a reference to it is alive, see [`arc_signal`].
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// let (count, set_count) = create_signal(0);
///
/// // ✅ calling the getter clones and returns the value

View File

@@ -6,7 +6,7 @@ use super::{
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Notify},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Write},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable},
};
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::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) 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
@@ -63,7 +63,7 @@ use std::{
///
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// let count = ArcRwSignal::new(0);
///
/// // ✅ calling the getter clones and returns the value
@@ -253,7 +253,7 @@ impl<T> Notify for ArcRwSignal<T> {
}
}
impl<T: 'static> Write for ArcRwSignal<T> {
impl<T: 'static> Writeable 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, Write},
traits::{DefinedAt, UntrackableGuard, Writeable},
};
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::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) 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> Write for ArcWriteSignal<T> {
impl<T: 'static> Writeable for ArcWriteSignal<T> {
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {

View File

@@ -623,46 +623,3 @@ 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

@@ -49,7 +49,7 @@ use std::{
///
/// ## Examples
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*;
/// let (count, set_count) = signal(0);
///
/// // calling .get() clones and returns the value

View File

@@ -9,7 +9,7 @@ use crate::{
signal::guards::{UntrackedWriteGuard, WriteGuard},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Write,
UntrackableGuard, Writeable,
},
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::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) 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
@@ -69,7 +69,7 @@ use std::{
///
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::signal::*;
/// let count = ArcRwSignal::new(0);
///
/// // ✅ calling the getter clones and returns the value
@@ -349,7 +349,7 @@ where
}
}
impl<T, S> Write for RwSignal<T, S>
impl<T, S> Writeable for RwSignal<T, S>
where
T: 'static,
S: Storage<ArcRwSignal<T>>,

View File

@@ -1,7 +1,9 @@
use super::{guards::WriteGuard, ArcWriteSignal};
use crate::{
owner::{ArenaItem, Storage, SyncStorage},
traits::{DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Write},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Writeable,
},
};
use core::fmt::Debug;
use guardian::ArcRwLockWriteGuardian;
@@ -20,7 +22,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::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) 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
@@ -30,7 +32,7 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
///
/// ## Examples
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*;
/// let (count, set_count) = signal(0);
///
/// // ✅ calling the setter sets the value
@@ -126,7 +128,7 @@ where
}
}
impl<T, S> Write for WriteSignal<T, S>
impl<T, S> Writeable 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. |
//! | [`Write`] | Guard | Gives mutable access to the value of this signal.
//! | [`Writeable`] | 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)` | [`Write`] | Applies closure to the current value to update it, but doesn't notify subscribers.
//! | [`UpdateUntracked`] | `fn(&mut T)` | [`Writeable`] | 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, guards::UntrackedWriteGuard, ArcReadSignal},
signal::{arc_signal, ArcReadSignal},
};
use any_spawner::Executor;
use futures::{Stream, StreamExt};
@@ -61,7 +61,6 @@ use std::{
panic::Location,
};
#[doc(hidden)]
/// Provides a sensible panic message for accessing disposed signals.
#[macro_export]
macro_rules! unwrap_signal {
@@ -168,20 +167,11 @@ 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: DefinedAt {
pub trait Read {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
@@ -194,9 +184,7 @@ pub trait Read: DefinedAt {
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
fn read(&self) -> Self::Value;
}
impl<T> Read for T
@@ -206,18 +194,13 @@ where
type Value = T::Value;
fn try_read(&self) -> Option<Self::Value> {
// 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()
}
self.track();
self.try_read_untracked()
}
fn read(&self) -> Self::Value {
self.track();
self.read_untracked()
}
}
@@ -230,7 +213,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 Write: Sized + DefinedAt + Notify {
pub trait Writeable: Sized + DefinedAt + Notify {
/// The type of the signal's value.
type Value: Sized + 'static;
@@ -323,13 +306,14 @@ pub trait With: DefinedAt {
impl<T> With for T
where
T: Read,
T: WithUntracked + Track,
{
type Value = <<T as Read>::Value as Deref>::Target;
type Value = <T as WithUntracked>::Value;
#[track_caller]
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
self.try_read().map(|val| fun(&val))
self.track();
self.try_with_untracked(fun)
}
}
@@ -437,9 +421,9 @@ pub trait UpdateUntracked: DefinedAt {
impl<T> UpdateUntracked for T
where
T: Write,
T: Writeable,
{
type Value = <Self as Write>::Value;
type Value = <Self as Writeable>::Value;
#[track_caller]
fn try_update_untracked<U>(
@@ -494,9 +478,9 @@ pub trait Update {
impl<T> Update for T
where
T: Write,
T: Writeable,
{
type Value = <Self as Write>::Value;
type Value = <Self as Writeable>::Value;
#[track_caller]
fn try_maybe_update<U>(
@@ -654,189 +638,3 @@ 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)
}
}
}

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