Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
73db030535 feat: add reactive lenses into signals 2023-08-24 17:50:34 -04:00
19 changed files with 233 additions and 192 deletions

View File

@@ -15,12 +15,13 @@ cfg_if! {
}
}
#[server]
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server]
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(
delta: i32,
msg: String,
@@ -32,7 +33,7 @@ pub async fn adjust_server_count(
Ok(new)
}
#[server]
#[server(ClearServerCount, "/api")]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
@@ -146,8 +147,6 @@ pub fn Counter() -> impl IntoView {
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter() -> impl IntoView {
// these struct names are auto-generated by #[server]
// they are just the PascalCased versions of the function names
let adjust = create_server_action::<AdjustServerCount>();
let clear = create_server_action::<ClearServerCount>();

View File

@@ -107,8 +107,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let pool = pool()?;

View File

@@ -173,7 +173,7 @@ pub struct PostMetadata {
title: String,
}
#[server]
#[server(ListPostMetadata, "/api")]
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS
@@ -185,7 +185,7 @@ pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
.collect())
}
#[server]
#[server(GetPost, "/api")]
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS.iter().find(|post| post.id == id).cloned())

View File

@@ -173,7 +173,7 @@ pub struct PostMetadata {
title: String,
}
#[server]
#[server(ListPostMetadata, "/api")]
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS
@@ -185,7 +185,7 @@ pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
.collect())
}
#[server]
#[server(GetPost, "/api")]
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(POSTS.iter().find(|post| post.id == id).cloned())

View File

@@ -4,14 +4,14 @@ use leptos_router::*;
const WAIT_ONE_SECOND: u64 = 1;
const WAIT_TWO_SECONDS: u64 = 2;
#[server]
#[server(FirstWaitFn "/api")]
async fn first_wait_fn(seconds: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;
Ok(())
}
#[server]
#[server(SecondWaitFn "/api")]
async fn second_wait_fn(seconds: u64) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(seconds)).await;

View File

@@ -62,8 +62,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View File

@@ -79,8 +79,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View File

@@ -79,8 +79,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
}
// The struct name and path prefix arguments are optional.
#[server]
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;

View File

@@ -59,10 +59,10 @@ pub fn add_event_listener<E>(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = Box::new(move |e| {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit(prev);
leptos_reactive::SpecialNonReactiveZone::exit();
});
}
}
@@ -88,10 +88,10 @@ pub(crate) fn add_event_listener_undelegated<E>(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = Box::new(move |e| {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit(prev);
leptos_reactive::SpecialNonReactiveZone::exit();
});
}
}

View File

@@ -218,10 +218,10 @@ pub fn set_timeout_with_handle(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit(prev);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -273,10 +273,10 @@ pub fn debounce<T: 'static>(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |value| {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(value);
leptos_reactive::SpecialNonReactiveZone::exit(prev);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -351,10 +351,10 @@ pub fn set_interval_with_handle(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit(prev);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -392,10 +392,10 @@ pub fn window_event_listener_untyped(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit(prev);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}

View File

@@ -8,6 +8,7 @@ use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use quote::ToTokens;
use rstml::{node::KeyedAttribute, parse};
use server_fn_macro::server_macro_impl;
use syn::parse_macro_input;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -33,7 +34,6 @@ mod params;
mod view;
use view::{client_template::render_template, render_view};
mod component;
mod server;
mod slot;
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
@@ -760,23 +760,17 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
/// are enabled), it will instead make a network request to the server.
///
/// You can specify one, two, three, or four arguments to the server function. All of these arguments are optional.
/// 1. A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`). Defaults to a PascalCased version of the function name.
/// 2. A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/api"`.
/// 3. The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
/// You can specify one, two, three, or four arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
///
/// ```rust,ignore
/// // will generate a server function at `/api-prefix/hello`
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
/// pub async fn my_server_fn_type() /* ... */
///
/// // will generate a server function with struct `HelloWorld` and path
/// // `/api/hello2349232342342` (hash based on location in source)
/// #[server]
/// pub async fn hello_world() /* ... */
/// ```
///
/// The server function itself can take any number of arguments, each of which should be serializable
@@ -865,7 +859,16 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
#[proc_macro_attribute]
#[proc_macro_error]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
server::server_impl(args, s)
match server_macro_impl(
args.into(),
s.into(),
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
None,
Some(syn::parse_quote!(::leptos::server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
/// Derives a trait that parses a map of string keys and values into a typed

View File

@@ -1,109 +0,0 @@
use convert_case::{Case, Converter};
use proc_macro::TokenStream;
use proc_macro2::Literal;
use quote::{ToTokens, __private::TokenStream as TokenStream2};
use syn::{
parse::{Parse, ParseStream},
Ident, ItemFn, Token,
};
pub fn server_impl(
args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let function: syn::ItemFn =
match syn::parse(s).map_err(|e| e.to_compile_error()) {
Ok(f) => f,
Err(e) => return e.into(),
};
let ItemFn {
attrs,
vis,
sig,
block,
} = function;
// TODO apply middleware: https://github.com/leptos-rs/leptos/issues/1461
let mapped_body = quote::quote! {
#(#attrs)*
#vis #sig {
#block
}
};
let mut args: ServerFnArgs = match syn::parse(args) {
Ok(args) => args,
Err(e) => return e.to_compile_error().into(),
};
// default to PascalCase version of function name if no struct name given
if args.struct_name.is_none() {
let upper_cammel_case_name = Converter::new()
.from_case(Case::Snake)
.to_case(Case::UpperCamel)
.convert(sig.ident.to_string());
args.struct_name =
Some(Ident::new(&upper_cammel_case_name, sig.ident.span()));
}
// default to "/api" if no prefix given
if args.prefix.is_none() {
args.prefix = Some(Literal::string("/api"));
}
match server_fn_macro::server_macro_impl(
quote::quote!(#args),
mapped_body,
syn::parse_quote!(::leptos::leptos_server::ServerFnTraitObj),
None,
Some(syn::parse_quote!(::leptos::server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
struct ServerFnArgs {
struct_name: Option<Ident>,
_comma: Option<Token![,]>,
prefix: Option<Literal>,
_comma2: Option<Token![,]>,
encoding: Option<Literal>,
_comma3: Option<Token![,]>,
fn_path: Option<Literal>,
}
impl ToTokens for ServerFnArgs {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let struct_name =
self.struct_name.as_ref().map(|s| quote::quote! { #s, });
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
tokens.extend(quote::quote! {
#struct_name
#prefix
#encoding
#fn_path
})
}
}
impl Parse for ServerFnArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let struct_name = input.parse()?;
let _comma = input.parse()?;
let prefix = input.parse()?;
let _comma2 = input.parse()?;
let encoding = input.parse()?;
let _comma3 = input.parse()?;
let fn_path = input.parse()?;
Ok(Self {
struct_name,
_comma,
prefix,
_comma2,
encoding,
_comma3,
fn_path,
})
}
}

View File

@@ -43,18 +43,18 @@ impl SpecialNonReactiveZone {
false
}
#[cfg(debug_assertions)]
pub fn enter() -> bool {
IS_SPECIAL_ZONE.with(|val| {
let prev = val.get();
val.set(true);
prev
})
#[inline(always)]
pub fn enter() {
#[cfg(debug_assertions)]
{
IS_SPECIAL_ZONE.with(|val| val.set(true))
}
}
#[cfg(debug_assertions)]
pub fn exit(prev: bool) {
if !prev {
#[inline(always)]
pub fn exit() {
#[cfg(debug_assertions)]
{
IS_SPECIAL_ZONE.with(|val| val.set(false))
}
}

158
leptos_reactive/src/lens.rs Normal file
View File

@@ -0,0 +1,158 @@
// Paths are fn pointers. They can be safely cast to usize but not back.
use crate::{
create_trigger, runtime::FxIndexMap, store_value, Signal, StoredValue,
Trigger,
};
use std::{any::Any, fmt::Debug};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
struct PathId(usize);
pub struct StoreInner<T>
where
T: 'static,
{
value: StoredValue<T>,
lenses: FxIndexMap<PathId, Trigger>,
}
impl<T: Debug> Debug for StoreInner<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StoreInner")
.field("value", &self.value)
.finish()
}
}
impl<T> StoreInner<T>
where
T: 'static,
{
pub fn new(value: T) -> Self {
Self {
value: store_value(value),
lenses: Default::default(),
}
}
pub fn try_update<U, V>(
&mut self,
lens: fn(&mut T) -> &mut U,
setter: impl FnOnce(&mut U) -> V + 'static,
) -> Option<V> {
// get or create trigger
let id = PathId(lens as usize);
let trigger = *self.lenses.entry(id).or_default();
// run update function
let result = self.value.try_update_value(|value| {
let zone = lens(value);
setter(zone)
})?;
// notify trigger
if trigger.try_notify() {
Some(result)
} else {
None
}
}
pub fn try_read<U: 'static, V>(
&mut self,
lens: fn(&mut T) -> &mut U,
getter: impl Fn(&U) -> V + 'static,
) -> Signal<Option<V>> {
// get or create trigger
let id = PathId(lens as usize);
let trigger = *self.lenses.entry(id).or_default();
let value = self.value;
// run update function
Signal::derive(move || {
trigger.track();
value.try_update_value(|value| {
let zone = lens(value);
getter(&*zone)
})
})
}
}
#[cfg(test)]
mod tests {
use super::StoreInner;
use crate::{
create_effect, create_runtime, SignalGet, SignalGetUntracked,
SignalWith, SignalWithUntracked,
};
use std::{cell::Cell, rc::Rc};
#[derive(Default)]
struct SomeComplexType {
a: NonCloneableUsize,
b: NonCloneableString,
}
#[derive(Default, Debug, PartialEq, Eq)]
struct NonCloneableUsize(usize);
#[derive(Default, Debug, PartialEq, Eq)]
struct NonCloneableString(String);
#[test]
pub fn create_lens() {
let rt = create_runtime();
// create the store
let mut store = StoreInner::new(SomeComplexType::default());
// create two signal lenses
fn lens_a(store: &mut SomeComplexType) -> &mut NonCloneableUsize {
&mut store.a
}
fn lens_b(store: &mut SomeComplexType) -> &mut NonCloneableString {
&mut store.b
}
let read_a = store.try_read(lens_a, |a| a.0);
read_a.with_untracked(|val| assert_eq!(val, &Some(0)));
assert_eq!(read_a.get_untracked(), Some(0));
let read_b = store.try_read(lens_b, |b| b.0.len());
assert_eq!(read_b.get_untracked(), Some(0));
// track how many times each variable notifies
let reads_on_a = Rc::new(Cell::new(0));
let reads_on_b = Rc::new(Cell::new(0));
create_effect({
let reads_on_a = Rc::clone(&reads_on_a);
move |_| {
read_a.track();
reads_on_a.set(reads_on_a.get() + 1);
}
});
create_effect({
let reads_on_b = Rc::clone(&reads_on_b);
move |_| {
read_b.track();
reads_on_b.set(reads_on_b.get() + 1);
}
});
assert_eq!(reads_on_a.get(), 1);
assert_eq!(reads_on_b.get(), 1);
// update each one once
store.try_update(lens_a, |a| *a = NonCloneableUsize(42));
assert_eq!(read_a.get_untracked(), Some(42));
store.try_update(lens_b, |b| b.0.push_str("hello, world!"));
assert_eq!(read_b.get_untracked(), Some(13));
// each effect has only run once
// none of the values has been cloned (they can't)
assert_eq!(reads_on_a.get(), 2);
assert_eq!(reads_on_b.get(), 2);
rt.dispose();
}
}

View File

@@ -83,6 +83,7 @@ mod context;
mod diagnostics;
mod effect;
mod hydration;
mod lens;
mod memo;
mod node;
mod resource;

View File

@@ -4,7 +4,7 @@ use crate::{
signal_prelude::format_signal_warning, spawn::spawn_local, use_context,
GlobalSuspenseContext, Memo, ReadSignal, ScopeProperty, SignalDispose,
SignalGet, SignalGetUntracked, SignalSet, SignalUpdate, SignalWith,
SpecialNonReactiveZone, SuspenseContext, WriteSignal,
SuspenseContext, WriteSignal,
};
use std::{
any::Any,
@@ -523,13 +523,7 @@ where
pub fn refetch(&self) {
_ = with_runtime(|runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
#[cfg(debug_assertions)]
let prev = SpecialNonReactiveZone::enter();
resource.refetch();
#[cfg(debug_assertions)]
{
SpecialNonReactiveZone::exit(prev);
}
resource.refetch()
})
});
}

View File

@@ -11,7 +11,7 @@ use crate::{
use cfg_if::cfg_if;
use core::hash::BuildHasherDefault;
use futures::stream::FuturesUnordered;
use indexmap::IndexSet;
use indexmap::{IndexMap, IndexSet};
use rustc_hash::{FxHashMap, FxHasher};
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
@@ -46,7 +46,8 @@ tokio::task_local! {
pub(crate) static TASK_RUNTIME: Option<RuntimeId>;
}
type FxIndexSet<T> = IndexSet<T, BuildHasherDefault<FxHasher>>;
pub(crate) type FxIndexSet<T> = IndexSet<T, BuildHasherDefault<FxHasher>>;
pub(crate) type FxIndexMap<T, U> = IndexMap<T, U, BuildHasherDefault<FxHasher>>;
// The data structure that owns all the signals, memos, effects,
// and other data included in the reactive system.
@@ -776,12 +777,9 @@ impl RuntimeId {
with_runtime(|runtime| {
let untracked_result;
#[cfg(debug_assertions)]
let prev = if !diagnostics {
SpecialNonReactiveZone::enter()
} else {
false
};
if !diagnostics {
SpecialNonReactiveZone::enter();
}
let prev_observer =
SetObserverOnDrop(self, runtime.observer.take());
@@ -791,9 +789,8 @@ impl RuntimeId {
runtime.observer.set(prev_observer.1);
std::mem::forget(prev_observer); // avoid Drop
#[cfg(debug_assertions)]
if !diagnostics {
SpecialNonReactiveZone::exit(prev);
SpecialNonReactiveZone::exit();
}
untracked_result
@@ -1239,13 +1236,9 @@ impl Drop for SetBatchingOnDrop {
pub fn on_cleanup(cleanup_fn: impl FnOnce() + 'static) {
#[cfg(debug_assertions)]
let cleanup_fn = move || {
#[cfg(debug_assertions)]
let prev = crate::SpecialNonReactiveZone::enter();
crate::SpecialNonReactiveZone::enter();
cleanup_fn();
#[cfg(debug_assertions)]
{
crate::SpecialNonReactiveZone::exit(prev);
}
crate::SpecialNonReactiveZone::exit();
};
push_cleanup(Box::new(cleanup_fn))
}

View File

@@ -17,6 +17,12 @@ pub struct Trigger {
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl Default for Trigger {
fn default() -> Self {
create_trigger()
}
}
impl Trigger {
/// Notifies any reactive code where this trigger is tracked to rerun.
///
@@ -29,7 +35,7 @@ impl Trigger {
/// Attempts to notify any reactive code where this trigger is tracked to rerun.
///
/// Returns `None` if the runtime has been disposed.
/// Returns `false` if the runtime has been disposed.
pub fn try_notify(&self) -> bool {
with_runtime(|runtime| {
runtime.mark_dirty(self.id);

View File

@@ -281,7 +281,7 @@ pub fn server_macro_impl(
}
impl #struct_name {
const URL: &'static str = if #fn_path.is_empty() {
const URL: &str = if #fn_path.is_empty() {
#server_fn_path::const_format::concatcp!(
#fn_name_as_str,
#server_fn_path::xxhash_rust::const_xxh64::xxh64(
@@ -292,7 +292,7 @@ pub fn server_macro_impl(
} else {
#fn_path
};
const PREFIX: &'static str = #prefix;
const PREFIX: &str = #prefix;
const ENCODING: #server_fn_path::Encoding = #encoding;
}