Use my old reactive system again

This commit is contained in:
Greg Johnston 2022-08-08 14:09:16 -04:00
parent 47cb10ccb0
commit c2c4a4afe6
21 changed files with 308 additions and 53 deletions

View File

@ -16,7 +16,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
});
let add_counter = move |_| {
let id = *next_counter_id.get_untracked();
let id = next_counter_id();
let (read, write) = cx.create_signal(0);
set_counters(|counters| counters.push((id, (read.clone(), write.clone()))));
set_next_counter_id(|id| *id += 1);

View File

@ -24,7 +24,7 @@ pub fn map_keyed<'a, T, U, K>(
where
T: PartialEq + Clone + 'a,
K: Eq + Hash,
U: Clone,
U: PartialEq + Clone,
{
// Previous state used for diffing.
let mut items = Vec::new();

View File

@ -98,16 +98,6 @@ impl<'a> IntoChild<'a> for Vec<web_sys::Element> {
}
}
impl<'a, T> IntoChild<'a> for ReadSignal<T>
where
T: Clone + IntoChild<'a>,
{
fn into_child(self, cx: Scope<'a>) -> Child<'a> {
let modified_fn = cx.create_ref(move || self.get().clone().into_child(cx));
Child::Fn(modified_fn)
}
}
impl<'a, T, U> IntoChild<'a> for T
where
T: Fn() -> U + 'a,

View File

@ -73,7 +73,7 @@ fn class_expression(el: &web_sys::Element, class_name: &str, value: bool) {
}
pub fn insert<'a>(
cx: Scope,
cx: Scope<'a>,
parent: web_sys::Node,
value: Child<'a>,
before: Option<web_sys::Node>,

View File

@ -34,17 +34,17 @@ mod tests {
let (a, set_a) = cx.create_signal(0);
let (b, set_b) = cx.create_signal(0);
let c = cx.create_memo(|| *(&a)() + *(&b)());
let c = cx.create_memo(|| a() + b());
assert_eq!(*c.get_untracked(), 0);
assert_eq!(c(), 0);
set_a(|n| *n = 2);
assert_eq!(*c.get_untracked(), 2);
assert_eq!(c(), 2);
set_b(|n| *n = 2);
assert_eq!(*c.get_untracked(), 4);
assert_eq!(c(), 4);
});
}
@ -57,8 +57,8 @@ mod tests {
let (show_last_name, set_show_last_name) = cx.create_signal(true);
let out = cx.create_memo(move || {
if *(&show_last_name)() {
format!("{} {}", *(&first_name)(), *(&last_name)())
if show_last_name() {
format!("{} {}", first_name(), last_name())
} else {
(*(&first_name)()).to_string()
}

View File

@ -13,6 +13,12 @@ use std::{
pub type Scope<'a> = BoundedScope<'a, 'a>;
#[must_use = "Creating a Scope without calling its disposer will leak memory."]
pub fn with_root_scope<'a>(f: impl FnOnce(Scope)) -> ScopeDisposer<'a> {
let root = Box::leak(Box::new(RootContext::new()));
create_scope(root, |cx| f(cx))
}
#[must_use = "Creating a Scope without calling its disposer will leak memory."]
pub fn create_scope<'disposer>(
root_context: &'static RootContext,

View File

@ -71,7 +71,7 @@ impl<T: 'static> ReadSignal<T> {
self.value()
}
pub fn get_untracked(&self) -> ReadSignalRef<T> {
pub(crate) fn get_untracked(&self) -> ReadSignalRef<T> {
self.value()
}
@ -138,28 +138,38 @@ where
}
}
impl<'a, T> FnOnce<()> for &'a ReadSignal<T> {
type Output = ReadSignalRef<'a, T>;
impl<T> FnOnce<()> for ReadSignal<T>
where
T: Clone,
{
type Output = T;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
self.get().clone()
}
}
impl<'a, T> FnMut<()> for &'a ReadSignal<T> {
impl<T> FnMut<()> for ReadSignal<T>
where
T: Clone,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
self.get().clone()
}
}
impl<'a, T> Fn<()> for &'a ReadSignal<T> {
impl<T> Fn<()> for ReadSignal<T>
where
T: Clone,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
self.get().clone()
}
}
pub struct SignalState<T> {
pub(crate) value: RefCell<T>,
pub(crate) t_value: RefCell<Option<T>>,
pub(crate) subscriptions: RefCell<HashSet<Rc<EffectInner>>>,
}

View File

@ -8,14 +8,12 @@ use futures::{
use crate::{BoundedScope, Effect, EffectDependency, ReadSignal, SuspenseContext};
impl<'a, 'b> BoundedScope<'a, 'b> {
pub fn use_transition<F>(self) -> (ReadSignal<bool>, impl Fn())
where
F: Fn(),
{
if let Some(transition) = self.inner.root_context.transition {
/* pub fn use_transition(self) -> (ReadSignal<bool>, impl Fn()) {
todo!()
/* if let Some(transition) = self.inner.root_context.transition {
transition
}
}
} */
} */
/* pub fn start_transition(self, f: impl Fn()) {
// If a transition is already running, run this function
@ -77,9 +75,10 @@ impl TransitionState {
}
pub fn complete(&self) -> bool {
match &self.pending_resources {
todo!()
/* match &self.pending_resources {
Some(_) => !self.pending(),
None => false,
}
} */
}
}

View File

@ -0,0 +1,89 @@
use leptos_reactive::{with_root_scope, Scope};
#[test]
fn effect_runs() {
use std::cell::RefCell;
use std::rc::Rc;
let d = with_root_scope(|cx| {
let (a, set_a) = cx.create_signal(-1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
cx.create_effect({
let b = b.clone();
move || {
let formatted = format!("Value is {}", a());
*b.borrow_mut() = formatted;
}
});
assert_eq!(b.borrow().as_str(), "Value is -1");
set_a(|a| *a = 1);
assert_eq!(b.borrow().as_str(), "Value is 1");
});
unsafe { d.dispose() }
}
#[test]
fn effect_tracks_memo() {
use std::cell::RefCell;
use std::rc::Rc;
let d = with_root_scope(|cx| {
let (a, set_a) = cx.create_signal(-1);
let b = cx.create_memo(move || format!("Value is {}", a()));
// simulate an arbitrary side effect
let c = Rc::new(RefCell::new(String::new()));
cx.create_effect({
let c = c.clone();
move || {
*c.borrow_mut() = b();
}
});
assert_eq!(b().as_str(), "Value is -1");
assert_eq!(c.borrow().as_str(), "Value is -1");
set_a(|a| *a = 1);
assert_eq!(b().as_str(), "Value is 1");
assert_eq!(c.borrow().as_str(), "Value is 1");
});
unsafe { d.dispose() }
}
#[test]
fn untrack_mutes_effect() {
use std::cell::RefCell;
use std::rc::Rc;
let d = with_root_scope(|cx| {
let (a, set_a) = cx.create_signal(-1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
cx.create_effect({
let b = b.clone();
move || {
let formatted = format!("Value is {}", cx.untrack(a));
*b.borrow_mut() = formatted;
}
});
assert_eq!(a(), -1);
assert_eq!(b.borrow().as_str(), "Value is -1");
set_a(|a| *a = 1);
assert_eq!(a(), 1);
assert_eq!(b.borrow().as_str(), "Value is -1");
});
unsafe { d.dispose() }
}

View File

@ -0,0 +1,72 @@
use leptos_reactive::{with_root_scope, Scope};
#[test]
fn basic_signal() {
let d = with_root_scope(|cx| {
let (a, set_a) = cx.create_signal(0);
assert_eq!(a(), 0);
set_a(|a| *a = 5);
assert_eq!(a(), 5);
});
unsafe { d.dispose() }
}
#[test]
fn derived_signals() {
let d = with_root_scope(|cx| {
let (a, set_a) = cx.create_signal(0);
let (b, set_b) = cx.create_signal(0);
let c = move || a() + b();
assert_eq!(c(), 0);
set_a(|a| *a = 5);
assert_eq!(c(), 5);
set_b(|b| *b = 1);
assert_eq!(c(), 6);
});
unsafe { d.dispose() }
}
#[test]
fn basic_memo() {
let d = with_root_scope(|cx| {
let a = cx.create_memo(|| 5);
assert_eq!(a(), 5);
});
unsafe { d.dispose() }
}
#[test]
fn memo_with_computed_value() {
let d = with_root_scope(|cx| {
let (a, set_a) = cx.create_signal(0);
let (b, set_b) = cx.create_signal(0);
let c = cx.create_memo(move || a() + b());
assert_eq!(c(), 0);
set_a(|a| *a = 5);
assert_eq!(c(), 5);
set_b(|b| *b = 1);
assert_eq!(c(), 6);
});
unsafe { d.dispose() }
}
#[test]
fn nested_memos() {
let d = with_root_scope(|cx| {
let (a, set_a) = cx.create_signal(0);
let (b, set_b) = cx.create_signal(0);
let c = cx.create_memo(move || a() + b());
let d = cx.create_memo(move || c() * 2);
let e = cx.create_memo(move || d() + 1);
assert_eq!(d(), 0);
set_a(|a| *a = 5);
assert_eq!(c(), 5);
assert_eq!(d(), 10);
assert_eq!(e(), 11);
set_b(|b| *b = 1);
assert_eq!(c(), 6);
assert_eq!(d(), 12);
assert_eq!(e(), 13);
});
unsafe { d.dispose() }
}

View File

@ -1,7 +1,7 @@
use leptos_core as leptos;
use leptos_dom::IntoChild;
use leptos_leptos_reactive::Scope;
use leptos_macro::{component, Props};
use leptos_reactive::Scope;
use serde::{de::DeserializeOwned, Serialize};
pub struct RouterProps<C, D>

View File

@ -3,7 +3,7 @@ use std::{
rc::{Rc, Weak},
};
use crate::{Observer, RootContext, Scope, State, SuspenseContextType, Update, WeakSignalState};
use crate::{Observer, RootContext, Scope, State, SuspenseContext, Update, WeakSignalState};
pub(crate) type Source = WeakSignalState;
@ -30,7 +30,7 @@ pub(crate) trait AnyComputation {
fn pure(&self) -> bool;
fn suspense(&self) -> Option<&SuspenseContextType>;
fn suspense(&self) -> Option<&SuspenseContext>;
fn updated_at(&self) -> Option<Update>;
@ -104,7 +104,7 @@ pub struct ComputationInner<T> {
updated_at: Cell<Update>,
pure: Cell<bool>,
user: Cell<bool>,
suspense: RefCell<Option<SuspenseContextType>>,
suspense: RefCell<Option<SuspenseContext>>,
}
impl<T> AnyComputation for Computation<T> {
@ -152,7 +152,7 @@ impl<T> AnyComputation for Computation<T> {
*self.inner.t_state.borrow_mut() = state;
}
fn suspense(&self) -> Option<&SuspenseContextType> {
fn suspense(&self) -> Option<&SuspenseContext> {
todo!() //self.inner.suspense.borrow().as_ref()
}

View File

@ -3,13 +3,13 @@ use std::any::{Any, TypeId};
use crate::{BoundedScope, ScopeInner};
impl<'a, 'b> BoundedScope<'a, 'b> {
pub fn provide_context<T: 'static>(&'a self, value: T) {
pub fn provide_context<T: 'static>(self, value: T) {
let id = value.type_id();
let value = self.inner.arena.alloc(value);
self.inner.context.borrow_mut().insert(id, &*value);
}
pub fn use_context<T: 'static>(&'a self) -> Option<&T> {
pub fn use_context<T: 'static>(self) -> Option<&'a T> {
self.inner.use_context()
}
}

View File

@ -6,6 +6,16 @@ impl<'a, 'b> BoundedScope<'a, 'b> {
pub fn create_effect<T>(self, effect_fn: impl FnMut(Option<&T>) -> T + 'a)
where
T: 'static,
{
self.create_effect_with_init(effect_fn, None)
}
pub fn create_effect_with_init<T>(
self,
effect_fn: impl FnMut(Option<&T>) -> T + 'a,
init: Option<T>,
) where
T: 'static,
{
let f: Box<dyn FnMut(Option<&T>) -> T + 'a> = Box::new(effect_fn);
// SAFETY: Memo will be cleaned up when the Scope lifetime 'a is over,
@ -13,7 +23,7 @@ impl<'a, 'b> BoundedScope<'a, 'b> {
// This is necessary to allow &'a Signal<_> etc. to be moved into F
let f: Box<dyn FnMut(Option<&T>) -> T + 'static> = unsafe { std::mem::transmute(f) };
let c = Computation::new(self, f, None, false);
let c = Computation::new(self, f, init, false);
// TODO suspense piece here
c.set_user(true);
let root = self.root_context();

View File

@ -63,6 +63,41 @@ impl<'a, 'b> BoundedScope<'a, 'b> {
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
self.inner.root_context.untrack(f)
}
pub fn child_scope<F>(self, f: F) -> ScopeDisposer<'a>
where
F: for<'child_lifetime> FnOnce(BoundedScope<'child_lifetime, 'a>),
{
let mut child = ScopeInner::new(self.inner.root_context);
// SAFETY: The only fields that are accessed on self from child is `context` which does not
// have any lifetime annotations.
child.parent = Some(unsafe { &*(self.inner as *const _) });
let boxed = Box::new(child);
let ptr = Box::into_raw(boxed);
let key = self
.inner
.children
.borrow_mut()
// SAFETY: None of the fields of ptr are accessed through child_scopes therefore we can
// safely transmute the lifetime.
.insert(unsafe { std::mem::transmute(ptr) });
// SAFETY: the address of the cx lives as long as 'a because:
// - It is allocated on the heap and therefore has a stable address.
// - self.child_cx is append only. That means that the Box<cx> will not be dropped until Self is
// dropped.
f(unsafe { Scope::new(&*ptr) });
// ^^^ -> `ptr` is still accessible here after call to f.
ScopeDisposer::new(move || unsafe {
let cx = self.inner.children.borrow_mut().remove(key).unwrap();
// SAFETY: Safe because ptr created using Box::into_raw and closure cannot live longer
// than 'a.
let cx = Box::from_raw(cx);
// SAFETY: Outside of call to f.
cx.dispose();
})
}
}
pub(crate) struct ScopeInner<'a> {

View File

@ -5,7 +5,7 @@ use std::{
use crate::{
Accessor, AnyComputation, AnySignalState, Observer, Pending, ReadSignalRef, RootContext,
Source, State, SuspenseContextType, Update, WeakSignalState,
Source, State, SuspenseContext, Update, WeakSignalState,
};
pub struct Memo<T>
@ -236,7 +236,7 @@ where
self.t_state.set(state);
}
fn suspense(&self) -> Option<&SuspenseContextType> {
fn suspense(&self) -> Option<&SuspenseContext> {
None
}

View File

@ -20,7 +20,7 @@ impl<'a, 'b> BoundedScope<'a, 'b> {
/// `([ReadSignal](crate::ReadSignal), [WriteSignal](crate::WriteSignal)`
/// pair. This is the basic building block of reactivity.
/// ```
/// # use reactive::{create_scope, RootContext, Scope};
/// # use leptos_reactive::{create_scope, RootContext, Scope};
/// # let root = Box::leak(Box::new(RootContext::new()));
/// # let _ = create_scope(root, |cx| {
/// let (a, set_a) = cx.create_signal(0);
@ -63,7 +63,7 @@ impl<'a, 'b> BoundedScope<'a, 'b> {
/// takes the previous value of the memo as its argument (as `Option<T>`, as it is `None`
/// the first time the memo is read.)
/// ```
/// # use reactive::{create_scope, RootContext, Scope};
/// # use leptos_reactive::{create_scope, RootContext, Scope};
/// # let root = Box::leak(Box::new(RootContext::new()));
/// # let _ = create_scope(root, |cx| {
/// let (a, set_a) = cx.create_signal(0);

View File

@ -324,3 +324,19 @@ where
WeakSignalState(self.state.clone())
}
}
impl<T> PartialEq for ReadSignal<T> {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.state, &other.state)
}
}
impl<T> Eq for ReadSignal<T> {}
impl<T> PartialEq for WriteSignal<T> {
fn eq(&self, other: &Self) -> bool {
Weak::ptr_eq(&self.state, &other.state)
}
}
impl<T> Eq for WriteSignal<T> {}

View File

@ -1 +1,29 @@
pub struct SuspenseContextType {}
use crate::{ReadSignal, Scope, WriteSignal};
#[derive(Clone)]
pub struct SuspenseContext {
pending_resources: ReadSignal<usize>,
set_pending_resources: WriteSignal<usize>,
}
impl SuspenseContext {
pub fn new(cx: Scope) -> Self {
let (pending_resources, set_pending_resources) = cx.create_signal_owned(0);
Self {
pending_resources,
set_pending_resources,
}
}
pub fn increment(&self) {
self.set_pending_resources.update(|n| *n += 1);
}
pub fn decrement(&self) {
self.set_pending_resources.update(|n| *n -= 1);
}
pub fn ready(&self) -> bool {
*self.pending_resources.get() == 0
}
}

View File

@ -1,7 +1,7 @@
use reactive::Scope;
use leptos_reactive::Scope;
fn with_testing_scope(f: impl FnOnce(Scope)) {
use reactive::{create_scope, RootContext};
use leptos_reactive::{create_scope, RootContext};
let root = Box::leak(Box::new(RootContext::new()));
let _ = create_scope(root, |cx| f(cx));
}

View File

@ -1,7 +1,7 @@
use reactive::Scope;
use leptos_reactive::Scope;
fn with_testing_scope(f: impl FnOnce(Scope)) {
use reactive::{create_scope, RootContext};
use leptos_reactive::{create_scope, RootContext};
let root = Box::leak(Box::new(RootContext::new()));
let _ = create_scope(root, |cx| f(cx));
}