Feat: rename recoil to atoms

This commit is contained in:
Jonathan Kelley committed 2021-06-16 11:19:37 -04:00
1 parent 47e896038e
commit 36ea39ae30
29 files changed
+398 -249

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"rust-analyzer.inlayHints.enable": false
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "atoms"
version = "0.0.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.40"
dioxus-core = { path="../core" }
thiserror = "1.0.24"
log = "0.4.14"
im-rc = "15.0.0"
futures = "0.3.15"
[dev-dependencies]
uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] }
dioxus-web = { path="../web" }
wasm-bindgen-futures = "*"
wasm-logger = "0.2.0"
console_error_panic_hook = "0.1.6"
+137
View File
@@ -0,0 +1,137 @@
# Atom.rs - Official global state management solution for Dioxus Apps
Atom.rs provides a global state management API for Dioxus apps built on the concept of "atomic state." Instead of grouping state together into a single bundle ALA Redux, Atom provides individual building blocks of state called Atoms. These atoms can be set/get anywhere in the app and combined to craft complex state. Atom should be easier to learn and more efficient than Redux. Atom.rs is modeled after the Atom.JS project.
Atom.rs is officially supported by the Dioxus team. By doing so, we are "planting our flag in the sand" for atomic state management instead of bundled (Redux-style) state management. Atomic state management fits well with the internals of Dioxus and idiomatic Rust, meaning Atom.rs state management will be faster, more efficient, and less sensitive to data races than Redux-style apps.
Internally, Dioxus uses batching to speed up linear-style operations. Atom.rs integrates with this batching optimization, making app-wide state changes extremely fast. This way, Atom.rs can be pushed significantly harder than Redux without the need to enable/disable debug flags to prevent performance slowdowns.
# Guide
## Atoms
A simple atom of state is defined globally as a const:
```rust
const Light: Atom<&str> = |_| "Green";
```
This atom of state is initialized with a value of `"Green"`. The atom that is returned does not actually contain any values. Instead, the atom's key - which is automatically generated in this instance - is used in the context of a Atom App.
This is then later used in components like so:
```rust
fn App(ctx: Context<()>) -> VNode {
// The Atom root must be initialized at the top of the application before any use_Atom hooks
Atoms::init_Atom_root(&ctx, |_| {});
let color = Atoms::use_read(ctx, Light);
ctx.render(rsx!{
h1 {"Color of light: {color}"}
})
}
```
Atoms are considered "Writable" objects since any consumer may also set the Atom's value with their own:
```rust
fn App(ctx: Context<()>) -> VNode {
let color = Atoms::use_read(ctx, Light);
let set_color = Atoms::use_write(ctx, Light);
rsx!{in ctx,
div {
h1{"Color: {color}"}
button {onclick: move |_| set_color("red"), "red"}
button {onclick: move |_| set_color("yellow"), "yellow"}
button {onclick: move |_| set_color("green"), "green"}
}
}
}
```
"Reading" a value with use_read subscribes that component to any changes to that value while "Writing" a value does not. It's a good idea to use `write-only` whenever it makes sense to prevent unnecessary evaluations. Both `read` and `write` may be combined together to provide a `use_state` style hook.
```rust
fn App(ctx: Context<()>) -> VNode {
let (color, set_color) = Atoms::use_read_write(ctx, Light);
rsx!{in ctx,
div {
h1{"Color: {color}"}
button {onclick: move |_| set_color("red"), "red"}
}
}
}
```
## Selectors
Selectors are a concept popular in the JS world as a way of narrowing down a selection of state outside the VDOM lifecycle. Selectors have two functions: 1) summarize/narrow down some complex state and 2) memoize calculations.
Selectors are only `readable` - they cannot be set. This differs from AtomJS where selectors _are_ `writable`. Selectors, as you might've guessed, "select" some data from atoms and other selectors.
Selectors provide a `SelectorApi` which essentially exposes a read-only `AtomApi`. They have the `get` method which allows any readable valued to be obtained for the purpose of generating a new value. A `Selector` may only return `'static` data, however `SelectorBorrowed` may return borrowed data.
returning static data:
```rust
const Light: Atom<&'static str> = |_| "Green";
const NumChars: Selector<usize> = |api| api.get(&Light).len();
```
Selectors will update their selection and notify subscribed components whenever their dependencies also update. The `get` calls in a selector will subscribe that selector to whatever `Readable` is being `get`-ted. Unlike hooks, you may use `get` in conditionals; an internal algorithm decides when a selector needs to be updated based on what it `get`-ted last time it was ran.
Selectors may also returned borrowed data:
```rust
const Light: Atom<&'static str> = |_| "Green";
const ThreeChars: SelectorBorrowed<str> = |api| api.get(&Light).chars().take(3).unwrap();
```
Unfortunately, Rust tries to coerce `T` (the type in the Selector generics) to the `'static` lifetime because we defined it as a static. `SelectorBorrowed` defines a type for a function that returns `&T` and provides the right lifetime for `T`. If you don't like having to use a dedicated `Borrowed` type, regular functions work too:
```rust
fn Light(api: &mut AtomBuilder) -> &'static str {
"Green"
}
fn ThreeChars(api: &mut SelectorApi) -> &'static str {
api.get(&Light).chars().take(3).unwrap()
}
```
Returning borrowed data is generally frowned upon, but may be useful when used wisely.
- If a selected value equals its previous selection (via PartialEq), the old value must be kept around to avoid evaluating subscribed components.
- It's unlikely that a change in a dependency's data will not change the selector's output.
In general, borrowed selectors introduce a slight memory overhead as they need to retain previous state to safely memoize downstream subscribers. The amount of state kept around scales with the amount of `gets` in a selector - though as the number of dependencies increase, the less likely the selector actually stays memoized. Atom tries to optimize this behavior the best it can to balance component evaluations with memory overhead.
## Families
You might notice that collections will not be performant with just sets/gets. We don't want to clone our entire HashMap every time we want to insert or remove an entry! That's where `Families` come in. Families are memoized collections (HashMap and OrdMap) that wrap the immutable datastructures library `im-rc`. Families are defined by a function that takes the FamilyApi and returns a function that provides a default value given a key. In this example, we insert a value into the collection when initialized, and then return a function that takes a key and returns a default value.
```rust
const CloudRatings: AtomFamily<&str, i32> = |api| {
api.insert("Oracle", -1);
|key| match key {
"AWS" => 1,
"Azure" => 2,
"GCP" => 3,
_ => 0
}
}
```
Whenever you `select` on a `Family`, the ID of the entry is tracked. Other subscribers will only be updated if they too select the same family with the same key and that value is updated. Otherwise, these subscribers will never re-render on an "insert", "remove", or "update" of the collection. You could easily implement this yourself with Atoms, immutable datastructures, and selectors, but our implementation is more efficient and first-class.
```rust
fn App(ctx: Context<()>) -> VNode {
let (rating, set_rating) = Atoms::use_read_write(ctx, CloudRatings.select("AWS"));
rsx!{in ctx,
div {
h1{ "AWS rating: {rating}" }
button { onclick: move |_| set_rating((rating + 1) % 5), "incr" }
}
}
}
```
+45
View File
@@ -0,0 +1,45 @@
//! RecoilAPI Pattern
//! ----------------
//! This example demonstrates how the use_recoil_callback hook can be used to build view controllers.
//! These view controllers are cheap to make and can be easily shared across the app to provide global
//! state logic to many components.
//!
//! This pattern is meant to replace the typical use_dispatch pattern used in Redux apps.
use dioxus_core::prelude::*;
use recoil::*;
const TITLE: Atom<String> = |_| format!("Heading");
const SUBTITLE: Atom<String> = |_| format!("Subheading");
struct TitleController(RecoilApi);
impl TitleController {
fn new(api: RecoilApi) -> Self {
Self(api)
}
fn uppercase(&self) {
self.0.modify(&TITLE, |f| *f = f.to_uppercase());
self.0.modify(&SUBTITLE, |f| *f = f.to_uppercase());
}
fn lowercase(&self) {
self.0.modify(&TITLE, |f| *f = f.to_lowercase());
self.0.modify(&SUBTITLE, |f| *f = f.to_lowercase());
}
}
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(|ctx| {
let title = use_read(&ctx, &TITLE);
let subtitle = use_read(&ctx, &SUBTITLE);
let controller = TitleController::new(use_recoil_api(&ctx));
rsx! { in ctx,
div {
"{title}"
"{subtitle}"
button { onclick: move |_| controller.uppercase(), "Uppercase" }
button { onclick: move |_| controller.lowercase(), "Lowercase" }
}
}
}))
}
+36
View File
@@ -0,0 +1,36 @@
//! Example: RecoilCallback
//! ------------------------
//! This example shows how use_recoil_callback can be used to abstract over sets/gets.
//! This hook provides a way to capture the RecoilApi object. In this case, we capture
//! it in a closure an abstract the set/get functionality behind the update_title function.
//!
//! It should be straightforward to build a complex app with recoil_callback.
use dioxus_core::prelude::*;
use recoil::*;
const TITLE: Atom<&str> = |_| "red";
fn update_title(api: &RecoilApi) {
match *api.get(&TITLE) {
"green" => api.set(&TITLE, "yellow"),
"yellow" => api.set(&TITLE, "red"),
"red" => api.set(&TITLE, "green"),
_ => {}
}
}
static App: FC<()> = |ctx| {
let title = use_read(&ctx, &TITLE);
let next_light = use_recoil_api(&ctx, |api| move |_| update_title(&api));
rsx! { in ctx,
div {
"{title}"
button { onclick: {next_light}, "Next light" }
}
}
};
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
}
+50
View File
@@ -0,0 +1,50 @@
//! Example: ECS Architecture for very list selectors
//! --------------------------------------------------
//! Sometimes, you need *peak* performance. Cloning and Rc might simply be too much overhead for your app.
//! If you're building CPU intense apps like graphics editors, simulations, or advanced visualizations,
//! slicing up your state beyond atoms might be desirable.
//!
//! Instead of storing groups of entities in a collection of structs, the ECS Architecture instead stores
//! an array for each field in of a struct. This tends to improve performance for batch operations on
//! individual fields at the cost of complexity. Fortunately, this ECS model is built right into Recoil,
//! making it easier than ever to enable sharded datastructures in your app.
//!
//! Instead of defining a struct for our primary datastructure, we'll instead use a type tuple, and then later
//! index that tuple to get the value we care about. Unfortunately, we lose name information wrt to each
//! type in the type tuple. This can be solved with an associated module, the derive EcsMacro, or just
//! by good documentation.
//!
//! This approach is best suited for applications where individual entries in families are very large
//! and updates to neighbors are costly in terms of Clone or field comparisons for memoization.
use dioxus::prelude::*;
use dioxus_core as dioxus;
use recoil::*;
type TodoModel = (
bool, // checked
String, // name
String, // contents
);
const TODOS: EcsModel<u32, TodoModel> = |builder| {};
// const SELECT_TITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(0).select(k);
// const SELECT_SUBTITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(1).select(k);
static App: FC<()> = |ctx| {
use_init_recoil_root(ctx, |_| {});
// let title = use_recoil_value(ctx, &C_SELECTOR);
let title = "";
rsx! { in ctx,
div {
"{title}"
// button { onclick: {next_light}, "Next light" }
}
}
};
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
}
+54
View File
@@ -0,0 +1,54 @@
use std::{collections::HashMap, rc::Rc};
use dioxus_core::prelude::*;
use recoil::*;
use uuid::Uuid;
const TODOS: AtomHashMap<Uuid, Rc<Todo>> = |map| {};
#[derive(PartialEq)]
struct Todo {
checked: bool,
title: String,
content: String,
}
static App: FC<()> = |ctx| {
use_init_recoil_root(ctx, move |cfg| {});
let todos = use_read_family(ctx, &TODOS);
// rsx! { in ctx,
// div {
// h1 {"Basic Todolist with AtomFamilies in Recoil.rs"}
// ul { { todos.keys().map(|id| rsx! { Child { id: *id } }) } }
// }
// }
ctx.render(html! {
<a href="#" class="">
<img class="inline-block h-10 w-10 rounded-full object-cover ring-2 ring-white" src="/images/person/4.jpg" alt="Jade"/>
</a>
})
};
#[derive(Props, PartialEq)]
struct ChildProps {
id: Uuid,
}
static Child: FC<ChildProps> = |ctx| {
let (todo, set_todo) = use_read_write(ctx, &TODOS.select(&ctx.id));
rsx! { in ctx,
li {
h1 {"{todo.title}"}
input { type: "checkbox", name: "scales", checked: "{todo.checked}" }
label { "{todo.content}", for: "scales" }
p {"{todo.content}"}
}
}
};
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
}
+47
View File
@@ -0,0 +1,47 @@
use dioxus_core::prelude::*;
use im_rc::HashMap as ImMap;
use recoil::*;
use uuid::Uuid;
const TODOS: Atom<ImMap<Uuid, Todo>> = |_| ImMap::new();
#[derive(PartialEq)]
struct Todo {
checked: bool,
title: String,
contents: String,
}
static App: FC<()> = |ctx| {
use_init_recoil_root(ctx, |_| {});
let todos = use_read(&ctx, &TODOS);
rsx! { in ctx,
div {
"Basic Todolist with AtomFamilies in Recoil.rs"
}
}
};
#[derive(Props, PartialEq)]
struct ChildProps {
id: Uuid,
}
static Child: FC<ChildProps> = |ctx| {
let todo = use_read(ctx, &TODOS).get(&ctx.id).unwrap();
// let (todo, set_todo) = use_read_write(ctx, &TODOS);
rsx! { in ctx,
div {
h1 {"{todo.title}"}
input { type: "checkbox", name: "scales", checked: "{todo.checked}" }
label { "{todo.contents}", for: "scales" }
p {"{todo.contents}"}
}
}
};
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
}
+29
View File
@@ -0,0 +1,29 @@
use dioxus_core::prelude::*;
use recoil::*;
const COUNT: Atom<i32> = |_| 0;
static App: FC<()> = |ctx| {
use_init_recoil_root(ctx, |_| {});
let (count, set_count) = use_read_write(&ctx, &COUNT);
rsx! { in ctx,
div {
"Count: {count}"
br {}
button { onclick: move |_| set_count(count + 1), "<Incr" }
">___<"
button { onclick: move |_| set_count(count - 1), "Decr>" }
}
}
};
fn main() {
// Setup logging
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
log::debug!("asd");
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App));
}
+35
View File
@@ -0,0 +1,35 @@
use std::collections::HashMap;
use dioxus_core::prelude::*;
use recoil::*;
const A_ITEMS: AtomHashMap<i32, i32> = |_| HashMap::new();
const B_ITEMS: AtomHashMap<i32, i32> = |_| HashMap::new();
const C_SELECTOR: SelectorFamily<i32, i32> = |api, key| {
let a = api.get(&A_ITEMS.select(&key));
let b = api.get(&B_ITEMS.select(&key));
a + b
};
const D_SELECTOR: SelectorFamilyBorrowed<i32, i32> = |api, key| -> &i32 {
let a = api.get(&A_ITEMS.select(&key));
a
};
static App: FC<()> = |ctx| {
use_init_recoil_root(ctx, |_| {});
let title = use_read(ctx, &C_SELECTOR);
rsx! { in ctx,
div {
"{title}"
// button { onclick: {next_light}, "Next light" }
}
}
};
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
}
+46
View File
@@ -0,0 +1,46 @@
use dioxus_core::prelude::*;
use recoil::*;
const A: Atom<i32> = |_| 0;
const B: Atom<i32> = |_| 0;
const C: Selector<i32> = |api| api.get(&A) + api.get(&B);
static App: FC<()> = |ctx| {
use_init_recoil_root(ctx, |_| {});
rsx! { in ctx,
div {
Banner {}
BtnA {}
BtnB {}
}
}
};
static Banner: FC<()> = |ctx| {
let count = use_read(&ctx, &C);
ctx.render(rsx! { h1 { "Count: {count}" } })
};
static BtnA: FC<()> = |ctx| {
let (a, set) = use_read_write(&ctx, &A);
rsx! { in ctx,
div { "a"
button { "+", onclick: move |_| set(a + 1) }
button { "-", onclick: move |_| set(a - 1) }
}
}
};
static BtnB: FC<()> = |ctx| {
let (b, set) = use_read_write(&ctx, &B);
rsx! { in ctx,
div { "b"
button { "+", onclick: move |_| set(b + 1) }
button { "-", onclick: move |_| set(b - 1) }
}
}
};
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
}
@@ -0,0 +1,50 @@
// const Posts: AtomFamily = |family| {
// family.on_get(|key| {
// //
// })
// };
fn main() {
wasm_bindgen_futures::spawn_local(dioxus_web::WebsysRenderer::start(App))
}
use std::future::Future;
use dioxus_core::prelude::*;
static App: FC<()> = |ctx| {
//
let title = use_async_atom();
let title_card = suspend(&ctx, title, move |val| {
//
rsx!(in ctx, div {
h3 { "{val}" }
})
});
// let fut = (use_async_atom(), use_async_atom());
// let title_card2 = ctx.suspend(fut, move |(text, text2)| {
// ctx.render(rsx!( h3 { "{text}" } ))
// });
ctx.render(rsx! {
div {
{title_card}
// {title_card2}
}
})
};
async fn use_async_atom() -> String {
todo!()
}
fn suspend<'a, O>(
c: &impl Scoped<'a>,
f: impl Future<Output = O>,
g: impl FnOnce(O) -> VNode<'a> + 'a,
) -> VNode<'a> {
todo!()
}
+1
View File
@@ -0,0 +1 @@
println!("asd")
+112
View File
@@ -0,0 +1,112 @@
# Architecture
## ECS
It's often ideal to represent list-y state as an SoA (struct of arrays) instead of an AoS (array of structs). In 99% of apps, normal clone-y performance is fine. If you need more performance on top of cloning on update, IM.rs will provide fast immutable data structures.
But, if you need **extreme performance** consider the ECS (SoA) model. With ECS model, we can modify fields of an entry without invalidating selectors on neighboring fields.
This approach is for that 0.1% of apps that need peak performance. Our philosophy is that these tools should be available when needed, but you shouldn't need to reach for them often. An example use case might be a graphics editor or simulation engine where thousands of entities with many fields are rendered to the screen in realtime.
Recoil will even help you:
```rust
type TodoModel = (
String, // title
String // subtitle
);
const TODOS: RecoilEcs<u32, TodoModel> = |builder| {
builder.push("SomeTitle".to_string(), "SomeSubtitle".to_string());
};
const SELECT_TITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(0).select(k);
const SELECT_SUBTITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(1).select(k);
```
Or with a custom derive macro to take care of some boilerplate and maintain readability. This macro simply generates the type tuple from the model fields and then some associated constants for indexing them.
```rust
#[derive(EcsModel)]
struct TodoModel {
title: String,
subtitle: String
}
// derives these impl (you don't need to write this yourself, but can if you want):
mod TodoModel {
type Layout = (String, String);
const title: u8 = 1;
const subtitle: u8 = 2;
}
const TODOS: RecoilEcs<u32, TodoModel::Layout> = |builder| {};
const SELECT_TITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(TodoModel::title).select(k);
const SELECT_SUBTITLE: SelectorBorrowed<u32, &str> = |s, k| TODOS.field(TodoModel::subtitle).select(k);
```
## Optimization
## Selectors and references.
Because new values are inserted without touching the original, we can keep old values around. As such, it makes sense to allow borrowed data since we can continue to reference old data if the data itself hasn't changed.
However, this does lead to a greater memory overhead, so occasionally we'll want to sacrifice an a component render in order to evict old values. This will be done automatically for us due to the nature of Rc.
Also, we want selectors to have internal dependencies.
## Async Atoms
- make it so "waiting" data can be provided
- someone updates via a future IE `atom.set(|| async {})`
- RecoilJS handles this with a "loadable" that has different states (ready, waiting, err just like Rust's poll method on future)
- I think the old value is provided until the new value is ready(?)
- Integrates with suspense to directly await the contents
## Async atoms
```rust
let (atom, set_atom, modify_atom) = (ATOM.use_read(ctx), ATOM.use_write(ctx), ATOM.use_modify(ctx));
const Title: AsyncAtom<String> = |builder| {
builder.on_set(|api, new_val| async {
})
builder.on_get(|api| async {
})
}
```
## Async selectors
```rust
struct ContentCard {
title: String,
content: String,
}
const ROOT_URL: Atom<&str> = |_| "localhost:8080";
// Since we don't plan on changing any content during the lifetime of the app, a selector works fine
const ContentCards: SelectorFamily<Uuid, ContentCard> = |api, key| api.on_get_async(async {
// Whenever the root_url changes, this atom will be re-evaluated
let root_url = api.get(&ROOT_URL);
let data: ContentCard = fetch(format!("{}/{}", root_url, key).await?.json().await?;
data
})
static ContentCard: FC<()> = |ctx| {
let body = async match use_recoil_value()(ctx.id).await {
Ok(content) => rsx!{in ctx, p {"{content}"} }
Err(e) => rsx!{in ctx, p {"Failed to load"}}
};
rsx!{
div {
h1{ "Non-async data here" }}
Suspense {
content: {body}
fallback: { rsx!{ div {"Loading..."} } }
}
}
}
};
```
+573
View File
@@ -0,0 +1,573 @@
use std::{
cell::{Ref, RefCell},
collections::HashMap,
hash::Hash,
marker::PhantomData,
rc::Rc,
};
pub use atomfamily::*;
pub use atoms::*;
pub use ecs::*;
use error::*;
pub use hooks::*;
pub use hooks::*;
pub use root::*;
pub use selector::*;
pub use selectorfamily::*;
pub use traits::*;
pub use utils::*;
mod tracingimmap;
mod traits {
use dioxus_core::prelude::Context;
use super::*;
pub trait MapKey: PartialEq + Hash + 'static {}
impl<T: PartialEq + Hash + 'static> MapKey for T {}
pub trait AtomValue: PartialEq + 'static {}
impl<T: PartialEq + 'static> AtomValue for T {}
// Atoms, selectors, and their family variants are readable
pub trait Readable<T: AtomValue>: Sized + Copy {
fn use_read<'a, P: 'static>(self, ctx: Context<'a, P>) -> &'a T {
hooks::use_read(&ctx, self)
}
// This returns a future of the value
// If the atom is currently pending, that future will resolve to pending
// If the atom is currently ready, the future will immediately resolve
// if the atom switches from ready to pending, the component will re-run, returning a pending future
fn use_read_async<'a, P>(self, ctx: Context<'a, P>) -> &'a T {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> T;
// We use the Raw Ptr to the atom
// TODO: Make sure atoms with the same definitions don't get merged together. I don't think they do, but double check
fn static_id(self) -> u32;
}
// Only atoms and atom families are writable
// Selectors and selector families are not
pub trait Writable<T: AtomValue>: Readable<T> + Sized {
fn use_read_write<'a, P>(self, ctx: Context<'a, P>) -> (&'a T, &'a Rc<dyn Fn(T)>) {
todo!()
}
fn use_write<'a, P>(self, ctx: Context<'a, P>) -> &'a Rc<dyn Fn(T)> {
todo!()
}
}
}
mod atoms {
use super::*;
// Currently doesn't do anything, but will eventually add effects, id, serialize/deserialize keys, etc
// Doesn't currently allow async values, but the goal is to eventually enable them
pub struct AtomBuilder {}
pub type Atom<T> = fn(&mut AtomBuilder) -> T;
// impl<T: AtomValue> Readable<T> for Atom<T> {}
impl<T: AtomValue> Readable<T> for &'static Atom<T> {
fn static_id(self) -> u32 {
self as *const _ as u32
}
fn initialize(self, api: &RecoilRoot) -> T {
let mut builder = AtomBuilder {};
let p = self(&mut builder);
p
}
}
impl<T: AtomValue> Writable<T> for &'static Atom<T> {}
mod compilests {
use super::*;
use dioxus_core::prelude::Context;
fn _test(ctx: Context<()>) {
const EXAMPLE_ATOM: Atom<i32> = |_| 10;
// ensure that atoms are both read and write
let _ = use_read(&ctx, &EXAMPLE_ATOM);
let _ = use_read_write(&ctx, &EXAMPLE_ATOM);
let _ = use_write(&ctx, &EXAMPLE_ATOM);
}
}
}
mod atomfamily {
use super::*;
pub trait FamilyCollection<K, V> {}
impl<K, V> FamilyCollection<K, V> for HashMap<K, V> {}
use im_rc::HashMap as ImHashMap;
/// AtomHashMaps provide an efficient way of maintaing collections of atoms.
///
/// Under the hood, AtomHashMaps uses [IM](https://www.rust-lang.org)'s immutable HashMap implementation to lazily
/// clone data as it is modified.
///
///
///
///
///
///
pub type AtomHashMap<K, V> = fn(&mut ImHashMap<K, V>);
pub trait AtomFamilySelector<K: MapKey, V: AtomValue + Clone> {
fn select(&'static self, k: &K) -> AtomMapSelection<K, V> {
todo!()
}
}
impl<K: MapKey, V: AtomValue + Clone> AtomFamilySelector<K, V> for AtomHashMap<K, V> {
fn select(&'static self, k: &K) -> AtomMapSelection<K, V> {
todo!()
}
}
pub struct AtomMapSelection<'a, K: MapKey, V: AtomValue> {
root: &'static AtomHashMap<K, V>,
key: &'a K,
}
impl<'a, K: MapKey, V: AtomValue> Readable<V> for &AtomMapSelection<'a, K, V> {
fn static_id(self) -> u32 {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> V {
todo!()
// let mut builder = AtomBuilder {};
// let p = self(&mut builder);
// p
}
}
impl<'a, K: MapKey, T: AtomValue> Writable<T> for &AtomMapSelection<'a, K, T> {}
mod compiletests {
use dioxus_core::prelude::Context;
use super::*;
const Titles: AtomHashMap<u32, &str> = |map| {};
fn test(ctx: Context<()>) {
let title = Titles.select(&10).use_read(ctx);
let t2 = use_read(&ctx, &Titles.select(&10));
}
}
}
mod selector {
use super::*;
pub struct SelectorBuilder {}
impl SelectorBuilder {
pub fn get<T: AtomValue>(&self, t: impl Readable<T>) -> &T {
todo!()
}
}
pub type Selector<T> = fn(&mut SelectorBuilder) -> T;
impl<T: AtomValue> Readable<T> for &'static Selector<T> {
fn static_id(self) -> u32 {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> T {
todo!()
}
}
pub struct SelectorFamilyBuilder {}
impl SelectorFamilyBuilder {
pub fn get<T: AtomValue>(&self, t: impl Readable<T>) -> &T {
todo!()
}
}
}
mod selectorfamily {
use super::*;
// pub trait SelectionSelector<K, V> {
// fn select(&self, k: &K) -> CollectionSelection<K, V> {
// todo!()
// }
// }
// impl<K, V, F> SelectionSelector<K, V> for AtomFamily<K, V, F> {}
/// Create a new value as a result of a combination of previous values
/// If you need to return borrowed data, check out [`SelectorFamilyBorrowed`]
pub type SelectorFamily<Key, Value> = fn(&mut SelectorFamilyBuilder, Key) -> Value;
impl<K, V: AtomValue> Readable<V> for &'static SelectorFamily<K, V> {
fn static_id(self) -> u32 {
todo!()
}
fn initialize(self, api: &RecoilRoot) -> V {
todo!()
}
}
/// Borrowed selector families are – surprisingly - discouraged.
/// This is because it's not possible safely memoize these values without keeping old versions around.
///
/// However, it does come in handy to borrow the contents of an item without re-rendering child components.
pub type SelectorFamilyBorrowed<Key, Value> =
for<'a> fn(&'a mut SelectorFamilyBuilder, Key) -> &'a Value;
// impl<'a, K, V: 'a> SelectionSelector<K, V> for fn(&'a mut SelectorFamilyBuilder, K) -> V {}
}
mod root {
use std::{
any::{Any, TypeId},
collections::{HashSet, VecDeque},
iter::FromIterator,
sync::atomic::{AtomicU32, AtomicUsize},
};
use super::*;
// use generational_arena::Index as ConsumerId;
type AtomId = u32;
type ConsumerId = u32;
pub type RecoilContext = RefCell<RecoilRoot>;
// Sometimes memoization means we don't need to re-render components that holds "correct values"
// IE we consider re-render more expensive than keeping the old value around.
// We *could* unsafely overwrite this slot, but that's just **asking** for UB (holding a &mut while & is held in components)
//
// Instead, we choose to let the hook itself hold onto the Rc<T> by not forcing a render when T is the same.
// Whenever the component needs to be re-rendered for other reasons, the "get" method will automatically update the Rc<T> to the most recent one.
pub struct RecoilRoot {
nodes: RefCell<HashMap<AtomId, Slot>>,
consumer_map: HashMap<ConsumerId, AtomId>,
}
struct Slot {
type_id: TypeId,
source: AtomId,
value: Rc<dyn Any>,
consumers: HashMap<ConsumerId, Rc<dyn Fn()>>,
dependents: HashSet<AtomId>,
}
static NEXT_ID: AtomicU32 = AtomicU32::new(0);
fn next_consumer_id() -> u32 {
NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
impl RecoilRoot {
pub(crate) fn new() -> Self {
Self {
nodes: Default::default(),
consumer_map: Default::default(),
}
}
pub fn subscribe<T: AtomValue>(
&mut self,
readable: impl Readable<T>,
receiver_fn: Rc<dyn Fn()>,
) -> ConsumerId {
let consumer_id = next_consumer_id();
let atom_id = readable.static_id();
log::debug!("Subscribing consumer to atom {} {}", consumer_id, atom_id);
let mut nodes = self.nodes.borrow_mut();
let slot = nodes.get_mut(&atom_id).unwrap();
slot.consumers.insert(consumer_id, receiver_fn);
self.consumer_map.insert(consumer_id, atom_id);
consumer_id
}
pub fn unsubscribe(&mut self, consumer_id: ConsumerId) {
let atom_id = self.consumer_map.get(&consumer_id).unwrap();
let mut nodes = self.nodes.borrow_mut();
let slot = nodes.get_mut(&atom_id).unwrap();
slot.consumers.remove(&consumer_id);
}
/// Directly get the *slot*
/// All Atoms are held in slots (an Rc)
///
///
pub fn try_get_raw<T: AtomValue>(&self, readable: impl Readable<T>) -> Result<Rc<T>> {
let atom_id = readable.static_id();
let mut nodes = self.nodes.borrow_mut();
if !nodes.contains_key(&atom_id) {
let value = Slot {
type_id: TypeId::of::<T>(),
source: atom_id,
value: Rc::new(readable.initialize(self)),
consumers: Default::default(),
dependents: Default::default(),
};
nodes.insert(atom_id, value);
}
let out = nodes
.get(&atom_id)
.unwrap()
.value
.clone()
.downcast::<T>()
.unwrap();
Ok(out)
}
pub fn try_set<T: AtomValue>(
&mut self,
writable: impl Writable<T>,
new_val: T,
) -> crate::error::Result<()> {
let atom_id = writable.static_id();
self.set_by_id(atom_id, new_val);
Ok(())
}
// A slightly dangerous method to manually overwrite any slot given an AtomId
pub(crate) fn set_by_id<T: AtomValue>(&mut self, atom_id: AtomId, new_val: T) {
let mut nodes = self.nodes.borrow_mut();
let consumers = match nodes.get_mut(&atom_id) {
Some(slot) => {
slot.value = Rc::new(new_val);
&slot.consumers
}
None => {
let value = Slot {
type_id: TypeId::of::<T>(),
source: atom_id,
value: Rc::new(new_val),
// value: Rc::new(writable.initialize(self)),
consumers: Default::default(),
dependents: Default::default(),
};
nodes.insert(atom_id, value);
&nodes.get(&atom_id).unwrap().consumers
}
};
for (id, consumer_fn) in consumers {
log::debug!("triggering selector {}", id);
consumer_fn();
}
}
}
}
mod hooks {
use super::*;
use dioxus_core::{hooks::use_ref, prelude::Context, virtual_dom::Scoped};
pub fn use_init_recoil_root<P>(ctx: Context<P>, cfg: impl Fn(())) {
ctx.use_create_context(move || RefCell::new(RecoilRoot::new()))
}
/// Gain access to the recoil API directly - set, get, modify, everything
/// This is the foundational hook in which read/write/modify are built on
///
/// This does not subscribe the component to *any* updates
///
/// You can use this method to create controllers that perform much more complex actions than set/get
/// However, be aware that "getting" values through this hook will not subscribe the component to any updates.
pub fn use_recoil_api<'a>(ctx: &impl Scoped<'a>) -> &'a Rc<RecoilContext> {
ctx.use_context::<RecoilContext>()
}
pub fn use_write<'a, T: AtomValue>(
ctx: &impl Scoped<'a>,
// todo: this shouldn't need to be static
writable: impl Writable<T>,
) -> &'a Rc<dyn Fn(T)> {
let api = use_recoil_api(ctx);
ctx.use_hook(
move || {
let api = api.clone();
let raw_id = writable.static_id();
Rc::new(move |new_val| {
//
log::debug!("setting new value ");
let mut api = api.as_ref().borrow_mut();
// api.try_set(writable, new_val).expect("failed to set");
api.set_by_id(raw_id, new_val);
}) as Rc<dyn Fn(T)>
},
move |hook| &*hook,
|hook| {},
)
}
/// Read the atom and get the Rc directly to the Atom's slot
/// This is useful if you need the memoized Atom value. However, Rc<T> is not as easy to
/// work with as
pub fn use_read_raw<'a, T: AtomValue>(
ctx: &impl Scoped<'a>,
readable: impl Readable<T>,
) -> &'a Rc<T> {
struct ReadHook<T> {
value: Rc<T>,
consumer_id: u32,
}
let api = use_recoil_api(ctx);
ctx.use_hook(
move || {
let mut api = api.as_ref().borrow_mut();
let update = ctx.schedule_update();
let val = api.try_get_raw(readable).unwrap();
let id = api.subscribe(readable, update);
ReadHook {
value: val,
consumer_id: id,
}
},
move |hook| {
let api = api.as_ref().borrow();
let val = api.try_get_raw(readable).unwrap();
hook.value = val;
&hook.value
},
move |hook| {
let mut api = api.as_ref().borrow_mut();
api.unsubscribe(hook.consumer_id);
},
)
}
///
pub fn use_read<'a, T: AtomValue>(ctx: &impl Scoped<'a>, readable: impl Readable<T>) -> &'a T {
use_read_raw(ctx, readable).as_ref()
}
/// # Use an atom in both read and write
///
/// This method is only available for atoms and family selections (not selectors).
///
/// This is equivalent to calling both `use_read` and `use_write`, but saves you the hassle and repitition
///
/// ## Example
///
/// ```
/// const Title: Atom<&str> = |_| "hello";
/// let (title, set_title) = use_read_write(ctx, &Title);
///
/// // equivalent to:
/// let (title, set_title) = (use_read(ctx, &Title), use_write(ctx, &Title));
/// ```
pub fn use_read_write<'a, T: AtomValue + 'static>(
ctx: &impl Scoped<'a>,
writable: impl Writable<T>,
) -> (&'a T, &'a Rc<dyn Fn(T)>) {
(use_read(ctx, writable), use_write(ctx, writable))
}
/// # Modify an atom without using `use_read`.
///
/// Occasionally, a component might want to write to an atom without subscribing to its changes. `use_write` does not
/// provide this functionality, so `use_modify` exists to gain access to the current atom value while setting it.
///
/// ## Notes
///
/// Do note that this hook can only be used with Atoms where T: Clone since we actually clone the current atom to make
/// it mutable.
///
/// Also note that you need to stack-borrow the closure since the modify closure expects an &dyn Fn. If we made it
/// a static type, it wouldn't be possible to use the `modify` closure more than once (two closures always are different)
///
/// ## Example
///
/// ```ignore
/// let modify_atom = use_modify(ctx, Atom);
///
/// modify_atom(&|a| *a += 1)
/// ```
pub fn use_modify<'a, T: AtomValue + 'static + Clone, P>(
ctx: Context<'a, P>,
writable: impl Writable<T>,
) -> impl Fn(&dyn Fn()) {
|_| {}
}
/// Use a family collection directly
/// !! Any changes to the family will cause this subscriber to update
/// Try not to put this at the very top-level of your app.
pub fn use_read_family<'a, K, V, P>(
ctx: Context<'a, P>,
t: &AtomHashMap<K, V>,
) -> &'a im_rc::HashMap<K, V> {
todo!()
}
}
mod ecs {
use super::*;
pub struct Blah<K, V> {
_p: PhantomData<(K, V)>,
}
pub type EcsModel<K, Ty> = fn(Blah<K, Ty>);
}
mod utils {
use super::*;
use dioxus_core::prelude::*;
pub struct RecoilApi {}
/// This tiny util wraps your main component with the initializer for the recoil root.
/// This is useful for small programs and the examples in this crate
pub fn RecoilApp<T: 'static>(
root: impl for<'a> Fn(Context<'a, T>) -> VNode,
) -> impl for<'a> Fn(Context<'a, T>) -> VNode {
move |ctx| {
use_init_recoil_root(ctx, |_| {});
root(ctx)
}
}
}
mod compiletests {}
pub mod error {
use thiserror::Error as ThisError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(ThisError, Debug)]
pub enum Error {
#[error("Fatal Internal Error: {0}")]
FatalInternal(&'static str),
// #[error("Context is missing")]
// MissingSharedContext,
// #[error("No event to progress")]
// NoEvent,
// #[error("Wrong Properties Type")]
// WrongProps,
// #[error("Base scope has not been mounted yet")]
// NotMounted,
// #[error("I/O Error: {0}")]
// BorrowMut(#[from] std::),
// #[error("eee Error: {0}")]
// IO(#[from] core::result::),
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
#[error(transparent)]
Other(#[from] anyhow::Error), // source and Display delegate to anyhow::Error
}
}
+29
View File
@@ -0,0 +1,29 @@
//! Tracing immap
//! Traces modifications since last generation
//! To reconstruct the history, you will need *all* the generations between the start and end points
use im_rc::HashMap as ImMap;
pub struct TracedHashMap<K, V> {
inner: ImMap<K, V>,
generation: u32,
mods_since_last_gen: Vec<K>,
}
impl<K: Clone, V: Clone> TracedHashMap<K, V> {
fn next_generation(&self) -> Self {
Self {
generation: self.generation + 1,
inner: self.inner.clone(),
mods_since_last_gen: vec![],
}
}
}
#[test]
fn compare_dos() {
let map1 = im_rc::hashmap! {3 => 2, 2 => 3};
let map2 = im_rc::hashmap! {2 => 3, 3 => 2};
assert_eq!(map1, map2);
}