Remove unused reactive system

This commit is contained in:
Greg Johnston 2022-10-07 06:41:34 -04:00
parent 450f521ad0
commit 24d6965893
15 changed files with 0 additions and 1674 deletions

View file

@ -1,18 +0,0 @@
[package]
name = "leptos_reactive_new"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
append-only-vec = "0.1"
log = "0.4"
slotmap = "1"
serde = { version = "1", features = ["derive"] }
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
[features]
default = []
browser = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures"]

View file

@ -1,234 +0,0 @@
use std::{
cell::{Cell, RefCell},
collections::HashSet,
fmt::Debug,
rc::{Rc, Weak},
};
use crate::{ObservableLink, Scope, SignalState, System};
pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
where
T: Debug + Clone + 'static,
{
let c = Rc::new(Computation::new(cx.system, f, None));
Rc::clone(&c).run();
cx.push_computation(c);
}
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
where
T: Debug + Clone + 'static,
{
let c = Rc::new(Computation::new(cx.system, f, None));
Rc::clone(&c).run();
cx.push_computation(c);
}
pub struct Computation<T>
where
T: Clone + 'static,
{
system: &'static System,
// function to execute when dependencies change
f: Box<RefCell<dyn FnMut(Option<T>) -> T>>,
// previous value of the effect
value: RefCell<Option<T>>,
// internal signal holding value of the function
signal: Option<Rc<SignalState<Option<T>>>>,
// how many of our dependencies have changed since the last time we ran the function?
waiting: Cell<u32>,
// did something actually change in one of our dependencies?
fresh: Cell<bool>,
// all the signals this computation depends on
signals: RefCell<HashSet<ObservableLink>>,
// custom cleanup functions to call
cleanups: RefCell<Vec<Box<dyn FnOnce()>>>,
}
impl<T> Computation<T>
where
T: Clone,
{
pub(crate) fn new(
system: &'static System,
f: impl FnMut(Option<T>) -> T + 'static,
signal: Option<Rc<SignalState<Option<T>>>>,
) -> Self {
Self {
system,
f: Box::new(RefCell::new(f)),
value: Default::default(),
signal,
waiting: Cell::new(0),
fresh: Cell::new(false),
signals: Default::default(),
cleanups: Default::default(),
}
}
}
impl<T> Observer for Computation<T>
where
T: Clone + 'static,
{
fn run(self: Rc<Self>) {
// clean up dependencies and cleanups
(Rc::clone(&self)).cleanup();
// run the computation
self.system.wrap(
{
let this = self.clone();
move || {
let curr = { this.value.borrow_mut().take() };
let v = { (this.f.borrow_mut())(curr) };
*this.value.borrow_mut() = Some(v);
}
},
Some(ObserverLink(Rc::downgrade(&self) as Weak<dyn Observer>)),
true,
)
}
fn update(self: Rc<Self>) {
// reset waiting, in case this is a force-refresh
self.waiting.set(0);
// run the effect
Rc::clone(&self).run();
// set the signal, if there is one
if let Some(signal) = &self.signal {
Rc::clone(signal).update(move |n| *n = self.value.borrow().clone());
}
}
fn add_signal(self: Rc<Self>, signal: ObservableLink) {
self.signals.borrow_mut().insert(signal);
}
fn is_waiting(&self) -> bool {
self.waiting.get() > 0
}
fn stale(self: Rc<Self>, increment: WaitingCount, fresh: bool) {
let waiting = self.waiting.get();
// If waiting is already 0 but change is -1, the computation has been force-refreshed
if waiting == 0 && increment == WaitingCount::Decrement {
return;
}
// mark computations that depend on the internal signal stale
if waiting == 0 && increment == WaitingCount::Increment && let Some(signal) = &self.signal {
// we don't mark it fresh, because we don't know for a fact that something has changed yet
signal.stale(WaitingCount::Increment, false);
}
match increment {
WaitingCount::Decrement => {
if waiting > 0 {
self.waiting.set(waiting - 1);
}
}
WaitingCount::Unchanged => {}
WaitingCount::Increment => {
self.waiting.set(waiting + 1);
}
}
self.fresh.set(self.fresh.get() || fresh);
// are we still waiting?
let waiting = self.waiting.get();
if waiting == 0 {
if self.fresh.get() {
Rc::clone(&self).update();
}
// mark any computations that depend on us as not stale
if let Some(signal) = &self.signal {
signal.stale(WaitingCount::Decrement, false);
}
}
}
}
impl<T> Computation<T>
where
T: Clone + 'static,
{
fn cleanup(self: Rc<Self>) {
for source in self.signals.borrow().iter() {
source.unsubscribe(ObserverLink(Rc::downgrade(&self) as Weak<dyn Observer>));
}
for cleanup in self.cleanups.take() {
cleanup();
}
}
}
pub trait Observer {
fn run(self: Rc<Self>);
fn add_signal(self: Rc<Self>, signal: ObservableLink);
fn is_waiting(&self) -> bool;
fn update(self: Rc<Self>);
fn stale(self: Rc<Self>, increment: WaitingCount, fresh: bool);
}
#[derive(Clone)]
pub struct ObserverLink(pub(crate) Weak<dyn Observer>);
impl std::fmt::Debug for ObserverLink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ObserverLink").finish()
}
}
impl ObserverLink {
pub(crate) fn is_waiting(&self) -> bool {
if let Some(c) = self.0.upgrade() {
c.is_waiting()
} else {
false
}
}
pub(crate) fn update(&self) {
if let Some(c) = self.0.upgrade() {
c.update()
}
}
pub(crate) fn stale(&self, increment: WaitingCount, fresh: bool) {
if let Some(c) = self.0.upgrade() {
c.stale(increment, fresh);
}
}
}
impl std::hash::Hash for ObserverLink {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(&self.0, state);
}
}
impl PartialEq for ObserverLink {
fn eq(&self, other: &Self) -> bool {
Weak::ptr_eq(&self.0, &other.0)
}
}
impl Eq for ObserverLink {}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum WaitingCount {
Decrement,
Unchanged,
Increment,
}

View file

@ -1,33 +0,0 @@
use std::any::{Any, TypeId};
use crate::Scope;
pub fn provide_context<T>(cx: Scope, value: T)
where
T: Clone + 'static,
{
let id = value.type_id();
cx.system.scope(cx.id, |scope_state| {
scope_state
.contexts
.borrow_mut()
.insert(id, Box::new(value));
})
}
pub fn use_context<T>(cx: Scope) -> Option<T>
where
T: Clone + 'static,
{
let id = TypeId::of::<T>();
cx.system.scope(cx.id, |scope_state| {
let contexts = scope_state.contexts.borrow();
let local_value = contexts.get(&id).and_then(|val| val.downcast_ref::<T>());
match local_value {
Some(val) => Some(val.clone()),
None => scope_state
.parent
.and_then(|parent| use_context::<T>(parent)),
}
})
}

View file

@ -1,61 +0,0 @@
#![feature(fn_traits)]
#![feature(let_chains)]
#![feature(unboxed_closures)]
#![feature(test)]
// The implementation of this reactive system is largely a Rust port of [Flimsy](https://github.com/fabiospampinato/flimsy/blob/master/src/flimsy.annotated.ts),
// which is itself a simplified and annotated version of SolidJS reactivity.
mod computation;
mod context;
mod memo;
mod resource;
mod scope;
mod signal;
mod spawn;
mod suspense;
mod system;
mod transition;
pub use computation::*;
pub use context::*;
pub use memo::*;
pub use resource::*;
pub use scope::*;
pub use signal::*;
pub use spawn::*;
pub use suspense::*;
pub use system::*;
pub use transition::*;
extern crate test;
#[cfg(test)]
mod tests {
use test::Bencher;
use std::{cell::Cell, rc::Rc};
use crate::{create_effect, create_scope, create_signal};
#[bench]
fn create_and_update_1000_signals(b: &mut Bencher) {
b.iter(|| {
create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
assert_eq!(sigs.len(), 1000);
/* create_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
for sig in &sigs {
acc.set(acc.get() + (sig.0)())
}
}
});
assert_eq!(acc.get(), 499500) */
})
.dispose()
});
}
}

View file

@ -1,79 +0,0 @@
use std::{fmt::Debug, marker::PhantomData, rc::Rc};
use crate::{Computation, Observer, ReadSignal, Scope, SignalState};
pub fn create_memo<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
where
T: Debug + Clone + 'static,
{
// create the computation
let sig = Rc::new(SignalState::new(cx.system, None));
let c = Rc::new(Computation::new(cx.system, f, Some(Rc::clone(&sig))));
Rc::clone(&c).update();
cx.push_computation(c);
// generate ReadSignal handle for the memo
let id = cx.push_signal(sig);
Memo {
inner: ReadSignal {
system: cx.system,
scope: cx.id,
id,
ty: PhantomData,
},
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Memo<T>
where
T: Clone + 'static,
{
inner: ReadSignal<Option<T>>,
}
impl<T> Copy for Memo<T> where T: Clone + 'static {}
impl<T> Memo<T>
where
T: Clone + 'static,
{
pub fn get(self) -> T {
self.with(|val| val.clone())
}
pub fn with<U>(self, f: impl Fn(&T) -> U) -> U {
// unwrap because the effect runs while the memo is being created
// so there will always be a value here
self.inner.with(|n| f(n.as_ref().unwrap()))
}
}
impl<T> FnOnce<()> for Memo<T>
where
T: Debug + Clone,
{
type Output = T;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T> FnMut<()> for Memo<T>
where
T: Debug + Clone,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T> Fn<()> for Memo<T>
where
T: Debug + Clone,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}

View file

@ -1,263 +0,0 @@
use std::{
cell::{Cell, RefCell},
collections::HashSet,
fmt::Debug,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
};
use serde::{Deserialize, Serialize};
use crate::{
create_effect, create_memo, create_signal, queue_microtask, spawn::spawn_local, use_context,
Memo, ReadSignal, Scope, ScopeId, SuspenseContext, System, WriteSignal, ResourceId,
};
pub fn create_resource<S, T, Fu>(
cx: Scope,
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fu + 'static,
) -> Resource<S, T>
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
Fu: Future<Output = T> + 'static,
{
create_resource_with_initial_value(cx, source, fetcher, None)
}
pub fn create_resource_with_initial_value<S, T, Fu>(
cx: Scope,
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fu + 'static,
initial_value: Option<T>,
) -> Resource<S, T>
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
Fu: Future<Output = T> + 'static,
{
let resolved = initial_value.is_some();
let (value, set_value) = create_signal(cx, initial_value);
let (loading, set_loading) = create_signal(cx, false);
let (track, trigger) = create_signal(cx, 0);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
// TODO hydration/streaming logic
let r = Rc::new(ResourceState {
scope: cx,
value,
set_value,
loading,
set_loading,
track,
trigger,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
suspense_contexts: Default::default(),
});
// initial load fires immediately
create_effect(cx, {
let r = Rc::clone(&r);
move |_| r.load(false)
});
let id = cx.push_resource(r);
Resource {
system: cx.system,
scope: cx.id,
id,
source_ty: PhantomData,
out_ty: PhantomData,
}
}
impl<S, T> Resource<S, T>
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
pub fn read(&self) -> Option<T> {
self.system
.resource((self.scope, self.id), |resource: &ResourceState<S, T>| {
resource.read()
})
}
pub fn loading(&self) -> bool {
self.system
.resource((self.scope, self.id), |resource: &ResourceState<S, T>| {
resource.loading.get()
})
}
pub fn refetch(&self) {
self.system
.resource((self.scope, self.id), |resource: &ResourceState<S, T>| {
resource.refetch()
})
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Resource<S, T>
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
system: &'static System,
pub(crate) scope: ScopeId,
pub(crate) id: ResourceId,
pub(crate) source_ty: PhantomData<S>,
pub(crate) out_ty: PhantomData<T>,
}
impl<S, T> Clone for Resource<S, T>
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
fn clone(&self) -> Self {
Self {
system: self.system,
scope: self.scope,
id: self.id,
source_ty: PhantomData,
out_ty: PhantomData,
}
}
}
impl<S, T> Copy for Resource<S, T>
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
}
#[derive(Clone)]
pub struct ResourceState<S, T>
where
S: Clone + 'static,
T: Clone + Debug + 'static,
{
scope: Scope,
value: ReadSignal<Option<T>>,
set_value: WriteSignal<Option<T>>,
pub loading: ReadSignal<bool>,
set_loading: WriteSignal<bool>,
track: ReadSignal<usize>,
trigger: WriteSignal<usize>,
source: Memo<S>,
fetcher: Rc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
resolved: Rc<Cell<bool>>,
scheduled: Rc<Cell<bool>>,
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
}
impl<S, T> ResourceState<S, T>
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
pub fn read(&self) -> Option<T> {
let suspense_cx = use_context::<SuspenseContext>(self.scope);
let v = self.value.get();
let suspense_contexts = self.suspense_contexts.clone();
let has_value = v.is_some();
create_effect(self.scope, move |_| {
if let Some(s) = &suspense_cx {
let mut contexts = suspense_contexts.borrow_mut();
if !contexts.contains(s) {
contexts.insert(*s);
// on subsequent reads, increment will be triggered in load()
// because the context has been tracked here
// on the first read, resource is already loading without having incremented
if !has_value {
s.increment();
}
}
}
});
v
}
pub fn refetch(&self) {
self.load(true);
}
fn load(&self, refetching: bool) {
// doesn't refetch if already refetching
if refetching && self.scheduled.get() {
return;
}
self.scheduled.set(false);
let loaded_under_transition = self.scope.system.running_transition().is_some();
let fut = (self.fetcher)(self.source.get());
// `scheduled` is true for the rest of this code only
self.scheduled.set(true);
queue_microtask({
let scheduled = Rc::clone(&self.scheduled);
move || {
scheduled.set(false);
}
});
self.set_loading.update(|n| *n = true);
self.trigger.update(|n| *n += 1);
// increment counter everywhere it's read
let suspense_contexts = self.suspense_contexts.clone();
let running_transition = self.scope.system.running_transition();
for suspense_context in suspense_contexts.borrow().iter() {
suspense_context.increment();
if let Some(transition) = &running_transition {
transition
.resources
.borrow_mut()
.insert(suspense_context.pending_resources);
}
}
// run the Future
spawn_local({
let resolved = self.resolved.clone();
let scope = self.scope;
let set_value = self.set_value;
let set_loading = self.set_loading;
async move {
let res = fut.await;
resolved.set(true);
// TODO hydration
if let Some(transition) = scope.system.transition() {
// TODO transition
}
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
for suspense_context in suspense_contexts.borrow().iter() {
suspense_context.decrement();
}
}
})
}
}

View file

@ -1,130 +0,0 @@
use crate::{Computation, ResourceState, SignalState, System};
use append_only_vec::AppendOnlyVec;
use serde::{Deserialize, Serialize};
use std::{
any::{Any, TypeId},
cell::RefCell,
collections::HashMap,
fmt::Debug,
rc::Rc,
};
#[must_use = "Scope will leak memory if the disposer function is never called"]
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
let runtime = Box::leak(Box::new(System::new()));
runtime.create_scope(f, None)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Scope {
pub(crate) system: &'static System,
pub(crate) id: ScopeId,
}
impl Scope {
pub fn child_scope(self, f: impl FnOnce(Scope)) -> ScopeDisposer {
self.system.create_scope(f, Some(self))
}
pub fn transition_pending(&self) -> bool {
// TODO transition self.system.transition().is_some()
false
}
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
self.system.untrack(f)
}
}
// Internals
impl Scope {
pub(crate) fn push_signal<T>(&self, state: Rc<SignalState<T>>) -> SignalId
where
T: Debug + 'static,
{
self.system.scope(self.id, |scope| {
scope.arena.push(state);
SignalId(scope.arena.len() - 1)
})
}
pub(crate) fn push_computation<T>(&self, state: Rc<Computation<T>>) -> ComputationId
where
T: Clone + Debug + 'static,
{
self.system.scope(self.id, |scope| {
scope.arena.push(state);
ComputationId(scope.arena.len() - 1)
})
}
pub(crate) fn push_resource<S, T>(&self, state: Rc<ResourceState<S, T>>) -> ResourceId
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
self.system.scope(self.id, |scope| {
scope.arena.push(state);
ResourceId(scope.arena.len() - 1)
})
}
pub fn dispose(self) {
// first, drop child scopes
self.system.scope(self.id, |scope| {
for id in scope.children.borrow().iter() {
self.system.remove_scope(id)
}
})
// removing from the runtime will drop this Scope, and all its Signals/Effects/Memos
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) struct SignalId(pub(crate) usize);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) struct ComputationId(pub(crate) usize);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) struct ResourceId(pub(crate) usize);
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);
impl ScopeDisposer {
pub fn dispose(self) {
(self.0)()
}
}
impl Debug for ScopeDisposer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ScopeDisposer").finish()
}
}
slotmap::new_key_type! { pub(crate) struct ScopeId; }
pub(crate) struct ScopeState {
pub(crate) parent: Option<Scope>,
pub(crate) contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
pub(crate) children: RefCell<Vec<ScopeId>>,
pub(crate) arena: AppendOnlyVec<Rc<dyn Any>>,
}
impl Debug for ScopeState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScopeState").finish()
}
}
impl ScopeState {
pub(crate) fn new(parent: Option<Scope>) -> Self {
Self {
parent,
contexts: Default::default(),
children: Default::default(),
arena: AppendOnlyVec::new(),
}
}
}

View file

@ -1,279 +0,0 @@
use std::{
cell::RefCell,
collections::HashSet,
fmt::Debug,
marker::PhantomData,
rc::{Rc, Weak},
};
use crate::{ObserverLink, Scope, ScopeId, SignalId, System, WaitingCount};
pub fn create_signal<T>(cx: Scope, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Clone + Debug,
{
let state = Rc::new(SignalState::new(cx.system, value));
let id = cx.push_signal(state);
let read = ReadSignal {
system: cx.system,
scope: cx.id,
id,
ty: PhantomData,
};
let write = WriteSignal {
system: cx.system,
scope: cx.id,
id,
ty: PhantomData,
};
(read, write)
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ReadSignal<T>
where
T: 'static,
{
pub(crate) system: &'static System,
pub(crate) scope: ScopeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
impl<T> ReadSignal<T> {
pub fn get(self) -> T
where
T: Clone,
{
self.with(|val| val.clone())
}
pub fn with<U>(self, f: impl Fn(&T) -> U) -> U {
self.system
.signal((self.scope, self.id), |state| state.with(f))
}
}
impl<T> Clone for ReadSignal<T> {
fn clone(&self) -> Self {
Self {
system: self.system,
scope: self.scope,
id: self.id,
ty: PhantomData,
}
}
}
impl<T> Copy for ReadSignal<T> {}
impl<T> FnOnce<()> for ReadSignal<T>
where
T: Debug + Clone,
{
type Output = T;
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T> FnMut<()> for ReadSignal<T>
where
T: Debug + Clone,
{
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T> Fn<()> for ReadSignal<T>
where
T: Debug + Clone,
{
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct WriteSignal<T>
where
T: Clone + 'static,
{
system: &'static System,
pub(crate) scope: ScopeId,
pub(crate) id: SignalId,
pub(crate) ty: PhantomData<T>,
}
impl<T> WriteSignal<T>
where
T: Clone,
{
pub fn update(self, f: impl FnOnce(&mut T)) {
self.system
.signal((self.scope, self.id), move |state| state.update(f))
}
}
impl<T> Clone for WriteSignal<T>
where
T: Clone,
{
fn clone(&self) -> Self {
Self {
system: self.system,
scope: self.scope,
id: self.id,
ty: PhantomData,
}
}
}
impl<T> Copy for WriteSignal<T> where T: Clone {}
impl<T, F> FnOnce<(F,)> for WriteSignal<T>
where
F: Fn(&mut T) + 'static,
T: Clone + 'static,
{
type Output = ();
extern "rust-call" fn call_once(self, args: (F,)) -> Self::Output {
self.update(args.0)
}
}
impl<T, F> FnMut<(F,)> for WriteSignal<T>
where
F: Fn(&mut T) + 'static,
T: Clone + 'static,
{
extern "rust-call" fn call_mut(&mut self, args: (F,)) -> Self::Output {
self.update(args.0)
}
}
impl<T, F> Fn<(F,)> for WriteSignal<T>
where
F: Fn(&mut T) + 'static,
T: Clone + 'static,
{
extern "rust-call" fn call(&self, args: (F,)) -> Self::Output {
self.update(args.0)
}
}
pub(crate) struct SignalState<T>
where
T: 'static,
{
system: &'static System,
parent: RefCell<Option<ObserverLink>>,
value: RefCell<T>,
t_value: RefCell<Option<T>>,
subscribers: RefCell<HashSet<ObserverLink>>,
}
impl<T> SignalState<T>
where
T: 'static,
{
pub fn new(system: &'static System, value: T) -> Self {
Self {
system,
parent: Default::default(),
value: RefCell::new(value),
t_value: Default::default(),
subscribers: Default::default(),
}
}
pub fn with<U>(self: Rc<Self>, f: impl Fn(&T) -> U) -> U {
let observer = self.system.observer();
if let Some(observer) = observer.and_then(|o| o.0.upgrade()) {
// register the signal as a dependency, if we are tracking and the parent is a Observer
// (rather than, for example, a root, which does not track)
if self.system.tracking() {
self.add_observer(ObserverLink(Rc::downgrade(&observer)));
observer.add_signal(Rc::clone(&self).as_observable());
}
}
// if there's a stale parent, it needs to be refreshed
// this may cause other upstream Observers to refresh
if let Some(parent) = self.parent.borrow().clone() && parent.is_waiting() {
parent.update();
}
f(&self.value.borrow())
}
pub fn update(self: Rc<Self>, f: impl FnOnce(&mut T)) {
if self.system.batching() {
let this = Rc::clone(&self);
// TODO transition
self.system
.add_to_batch(move || (f)(&mut *this.value.borrow_mut()));
} else {
// TODO transition
(f)(&mut *self.value.borrow_mut());
// notify observers that there's a new value
self.stale(WaitingCount::Unchanged, true);
}
}
pub fn add_observer(&self, observer: ObserverLink) {
self.subscribers.borrow_mut().insert(observer);
}
pub fn as_observable(self: Rc<Self>) -> ObservableLink {
ObservableLink(Rc::downgrade(&self) as Weak<dyn Observable>)
}
pub fn stale(&self, delta: WaitingCount, fresh: bool) {
let subs = { self.subscribers.borrow().clone() };
for observer in subs {
observer.stale(delta, fresh);
}
}
}
pub(crate) trait Observable {
fn unsubscribe(&self, observer: ObserverLink);
}
impl<T> Observable for SignalState<T> {
fn unsubscribe(&self, observer: ObserverLink) {
self.subscribers.borrow_mut().remove(&observer);
}
}
pub struct ObservableLink(pub(crate) Weak<dyn Observable>);
impl ObservableLink {
pub(crate) fn unsubscribe(&self, observer: ObserverLink) {
if let Some(observable) = self.0.upgrade() {
observable.unsubscribe(observer);
}
}
}
impl std::hash::Hash for ObservableLink {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(&self.0, state);
}
}
impl PartialEq for ObservableLink {
fn eq(&self, other: &Self) -> bool {
Weak::ptr_eq(&self.0, &other.0)
}
}
impl Eq for ObservableLink {}

View file

@ -1,38 +0,0 @@
use std::future::Future;
// run immediately on server
#[cfg(feature = "ssr")]
pub fn queue_microtask(task: impl FnOnce()) {
task();
}
// run immediately on server
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub fn queue_microtask(task: impl FnOnce() + 'static) {
microtask(wasm_bindgen::closure::Closure::once_into_js(task));
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
#[wasm_bindgen::prelude::wasm_bindgen(
inline_js = "export function microtask(f) { queueMicrotask(f); }"
)]
extern "C" {
fn microtask(task: wasm_bindgen::JsValue);
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub fn spawn_local<F>(fut: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(fut)
}
#[cfg(feature = "ssr")]
pub fn spawn_local<F>(_fut: F)
where
F: Future<Output = ()> + 'static,
{
// noop for now; useful for ignoring any async tasks on the server side
// could be replaced with a Tokio dependency
}

View file

@ -1,53 +0,0 @@
use crate::{create_signal, spawn::queue_microtask, ReadSignal, Scope, WriteSignal};
#[derive(Copy, Clone, Debug)]
pub struct SuspenseContext {
pub pending_resources: ReadSignal<usize>,
set_pending_resources: WriteSignal<usize>,
}
impl std::hash::Hash for SuspenseContext {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.pending_resources.scope.hash(state);
self.pending_resources.id.hash(state);
}
}
impl PartialEq for SuspenseContext {
fn eq(&self, other: &Self) -> bool {
self.pending_resources.scope == other.pending_resources.scope
&& self.pending_resources.id == other.pending_resources.id
}
}
impl Eq for SuspenseContext {}
impl SuspenseContext {
pub fn new(cx: Scope) -> Self {
let (pending_resources, set_pending_resources) = create_signal(cx, 0);
Self {
pending_resources,
set_pending_resources,
}
}
pub fn increment(&self) {
let setter = self.set_pending_resources;
queue_microtask(move || setter.update(|n| *n += 1));
}
pub fn decrement(&self) {
let setter = self.set_pending_resources;
queue_microtask(move || {
setter.update(|n| {
if *n > 0 {
*n -= 1
}
})
});
}
pub fn ready(&self) -> bool {
self.pending_resources.get() == 0
}
}

View file

@ -1,191 +0,0 @@
use std::{
cell::{Cell, RefCell},
fmt::Debug,
rc::Rc,
};
use slotmap::SlotMap;
use crate::{
ObserverLink, ResourceId, ResourceState, Scope, ScopeDisposer, ScopeId, ScopeState, SignalId,
SignalState, TransitionState,
};
pub struct System {
observer: RefCell<Option<ObserverLink>>,
tracking: Cell<bool>,
batch: RefCell<Vec<Box<dyn FnOnce()>>>,
pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>,
}
impl System {
pub fn new() -> Self {
Self {
observer: Default::default(),
tracking: Default::default(),
batch: Default::default(),
scopes: Default::default(),
}
}
pub fn create_scope(
&'static self,
f: impl FnOnce(Scope),
parent: Option<Scope>,
) -> ScopeDisposer {
let id = {
self.scopes
.borrow_mut()
.insert(Rc::new(ScopeState::new(parent)))
};
let scope = Scope { system: self, id };
f(scope);
ScopeDisposer(Box::new(move || scope.dispose()))
}
pub(crate) fn wrap<T>(
&self,
f: impl FnOnce() -> T,
observer: Option<ObserverLink>,
tracking: bool,
) -> T {
let prev_observer = self.observer.replace(observer);
let prev_tracking = self.tracking.get();
self.tracking.set(tracking);
let value = f();
*self.observer.borrow_mut() = prev_observer;
self.tracking.set(prev_tracking);
value
}
pub(crate) fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
self.wrap(f, self.observer(), false)
}
pub(crate) fn tracking(&self) -> bool {
self.tracking.get()
}
pub(crate) fn observer(&self) -> Option<ObserverLink> {
self.observer.borrow().clone()
}
pub(crate) fn batching(&self) -> bool {
!self.batch.borrow().is_empty()
}
pub(crate) fn add_to_batch(&self, deferred_fn: impl FnOnce()) {
let deferred: Box<dyn FnOnce()> = Box::new(deferred_fn);
// TODO safety
let deferred: Box<dyn FnOnce() + 'static> = unsafe { std::mem::transmute(deferred) };
self.batch.borrow_mut().push(deferred);
}
pub(crate) fn scope<T>(&self, id: ScopeId, f: impl FnOnce(&ScopeState) -> T) -> T {
let scope = { self.scopes.borrow().get(id).cloned() };
if let Some(scope) = scope {
(f)(&scope)
} else {
log::error!(
"couldn't locate {id:?} in scopes {:#?}",
self.scopes.borrow()
);
panic!("couldn't locate {id:?}");
}
}
pub(crate) fn remove_scope(&self, scope: &ScopeId) {
self.scopes.borrow_mut().remove(*scope);
}
pub(crate) fn signal<T, U>(
&self,
id: (ScopeId, SignalId),
f: impl FnOnce(Rc<SignalState<T>>) -> U,
) -> U
where
T: 'static,
{
self.scope(id.0, |scope| {
if let Ok(n) = scope.arena[id.1 .0].clone().downcast::<SignalState<T>>() {
(f)(n)
} else {
panic!(
"couldn't convert {id:?} to SignalState<{}>",
std::any::type_name::<T>()
);
}
})
}
pub(crate) fn resource<S, T, U>(
&self,
id: (ScopeId, ResourceId),
f: impl FnOnce(&ResourceState<S, T>) -> U,
) -> U
where
S: Debug + Clone + 'static,
T: Debug + Clone + 'static,
{
self.scope(id.0, |scope| {
if let Ok(n) = scope.arena[id.1 .0]
.clone()
.downcast::<ResourceState<S, T>>()
{
(f)(&n)
} else {
panic!(
"couldn't convert {id:?} to SignalState<{}>",
std::any::type_name::<T>()
);
}
})
}
pub(crate) fn running_transition(&self) -> Option<TransitionState> {
None
// TODO transition
}
pub(crate) fn transition(&self) -> Option<TransitionState> {
None
// TODO transition
}
}
impl Default for System {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for System {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("System")
.field("observer", &self.observer)
.field("tracking", &self.tracking)
.field("batch", &self.batch.borrow().len())
.field("scopes", &self.scopes)
.finish()
}
}
impl PartialEq for System {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self, other)
}
}
impl Eq for System {}
impl std::hash::Hash for System {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(&self, state);
}
}

View file

@ -1,92 +0,0 @@
use std::{
cell::{Cell, RefCell},
collections::HashSet,
rc::Rc,
};
use crate::{
create_effect, create_signal, spawn::queue_microtask, system::System, ObserverLink, ReadSignal,
Scope, ScopeId, SignalId, WriteSignal,
};
pub fn use_transition(cx: Scope) -> Transition {
let (pending, set_pending) = create_signal(cx, false);
Transition {
system: cx.system,
scope: cx,
pending,
set_pending,
}
}
#[derive(Copy, Clone)]
pub struct Transition {
system: &'static System,
scope: Scope,
pending: ReadSignal<bool>,
set_pending: WriteSignal<bool>,
}
impl Transition {
pub fn start(&self, f: impl FnOnce()) {
/* if self.system.running_transition().is_some() {
f();
} else {
{
self.set_pending.update(|n| *n = true);
*self.system.transition.borrow_mut() = Some(Rc::new(TransitionState {
running: Cell::new(true),
resources: Default::default(),
signals: Default::default(),
effects: Default::default(),
}));
}
f();
if let Some(running_transition) = self.system.running_transition() {
running_transition.running.set(false);
let system = self.system;
let scope = self.scope;
let resources = running_transition.resources.clone();
let signals = running_transition.signals.clone();
let effects = running_transition.effects.clone();
let set_pending = self.set_pending;
// place this at end of task queue so it doesn't start at 0
queue_microtask(move || {
create_effect(scope, move |_| {
let pending = resources.borrow().iter().map(|p| p.get()).sum::<usize>();
if pending == 0 {
for signal in signals.borrow().iter() {
system.any_signal(*signal, |signal| {
signal.end_transition(system);
});
}
for effect in effects.borrow().iter() {
system.any_effect(*effect, |any_effect| {
any_effect.run(*effect);
});
}
set_pending.update(|n| *n = false);
}
});
});
}
} */
todo!()
}
pub fn pending(&self) -> bool {
self.pending.get()
}
}
#[derive(Debug)]
pub(crate) struct TransitionState {
pub running: Cell<bool>,
pub resources: Rc<RefCell<HashSet<ReadSignal<usize>>>>,
pub signals: Rc<RefCell<HashSet<(ScopeId, SignalId)>>>,
pub computation: Rc<RefCell<Vec<ObserverLink>>>,
}

View file

@ -1,89 +0,0 @@
use leptos_reactive::{create_effect, create_memo, create_scope, create_signal};
#[test]
fn effect_runs() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
create_effect(cx, {
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");
})
.dispose()
}
#[test]
fn effect_tracks_memo() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
let (a, set_a) = create_signal(cx, -1);
let b = create_memo(cx, move |_| format!("Value is {}", a()));
// simulate an arbitrary side effect
let c = Rc::new(RefCell::new(String::new()));
create_effect(cx, {
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");
})
.dispose()
}
#[test]
fn untrack_mutes_effect() {
use std::cell::RefCell;
use std::rc::Rc;
create_scope(|cx| {
let (a, set_a) = create_signal(cx, -1);
// simulate an arbitrary side effect
let b = Rc::new(RefCell::new(String::new()));
create_effect(cx, {
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");
})
.dispose()
}

View file

@ -1,87 +0,0 @@
use leptos_reactive::{create_memo, create_scope, create_signal};
#[test]
fn basic_memo() {
create_scope(|cx| {
let a = create_memo(cx, |_| 5);
assert_eq!(a(), 5);
})
.dispose()
}
#[test]
fn memo_with_computed_value() {
create_scope(|cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = create_memo(cx, 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);
})
.dispose()
}
#[test]
fn nested_memos() {
create_scope(|cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = create_memo(cx, move |_| a() + b());
let d = create_memo(cx, move |_| c() * 2);
let e = create_memo(cx, 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);
})
.dispose()
}
#[test]
fn memo_runs_only_when_inputs_change() {
use std::{cell::Cell, rc::Rc};
create_scope(|cx| {
let call_count = Rc::new(Cell::new(0));
let (a, set_a) = create_signal(cx, 0);
let (b, _) = create_signal(cx, 0);
let (c, _) = create_signal(cx, 0);
// pretend that this is some kind of expensive Observer and we need to access its its value often
// we could do this with a derived signal, but that would re-run the Observer
// memos should only run when their inputs actually change: this is the only point
let c = create_memo(cx, {
let call_count = call_count.clone();
move |_| {
call_count.set(call_count.get() + 1);
a() + b() + c()
}
});
assert_eq!(call_count.get(), 1);
// here we access the value a bunch of times
assert_eq!(c(), 0);
assert_eq!(c(), 0);
assert_eq!(c(), 0);
assert_eq!(c(), 0);
assert_eq!(c(), 0);
// we've still only called the memo calculation once
assert_eq!(call_count.get(), 1);
// and we only call it again when an input changes
set_a(|n| *n = 1);
assert_eq!(c(), 1);
assert_eq!(call_count.get(), 2);
})
.dispose()
}

View file

@ -1,27 +0,0 @@
use leptos_reactive::{create_scope, create_signal};
#[test]
fn basic_signal() {
create_scope(|cx| {
let (a, set_a) = create_signal(cx, 0);
assert_eq!(a(), 0);
set_a(|a| *a = 5);
assert_eq!(a(), 5);
})
.dispose()
}
#[test]
fn derived_signals() {
create_scope(|cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 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);
})
.dispose()
}