Move to a generic GlobalLazy<T> (#2851)

* Expose a generic lazy type

* Switch to generic lazy globals

* simplify global lazy context a bit

* rename LazyGlobal to Global

* use Memo::global in more examples

* Fix soundness issue with deref_impl. It relies on the size of self, so it cannot be safe

* add a comment about safety

* Make clippy happy

* fix formatting

* Restore changes to signal impl

* Add helper methods for global signal and global memo to make getting the inner value easier
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-08-16 17:57:41 -05:00
1 parent 668d5415c8
commit b47a6cf83e
13 files changed
+281 -289

No files matched your search

+15 -82
View File
@@ -1,93 +1,26 @@
use crate::{read::Readable, Memo, ReadableRef};
use crate::{read_impls, GlobalKey};
use dioxus_core::prelude::ScopeId;
use generational_box::{BorrowResult, UnsyncStorage};
use std::ops::Deref;
use super::{Global, InitializeFromFunction};
use crate::read::Readable;
use crate::read_impls;
use crate::Memo;
use crate::Signal;
use super::get_global_context;
/// A signal that can be accessed from anywhere in the application and created in a static
pub struct GlobalMemo<T: 'static> {
selector: fn() -> T,
key: GlobalKey<'static>,
impl<T: PartialEq> InitializeFromFunction<T> for Memo<T> {
fn initialize_from_function(f: fn() -> T) -> Self {
Memo::new(f)
}
}
/// A memo that can be accessed from anywhere in the application and created in a static
pub type GlobalMemo<T> = Global<Memo<T>, T>;
impl<T: PartialEq + 'static> GlobalMemo<T> {
#[track_caller]
/// Create a new global signal
pub const fn new(selector: fn() -> T) -> GlobalMemo<T>
where
T: PartialEq,
{
let key = std::panic::Location::caller();
GlobalMemo {
selector,
key: GlobalKey::new(key),
}
}
/// Get the key for this global
pub fn key(&self) -> GlobalKey<'static> {
self.key.clone()
}
/// Get the signal that backs this global.
pub fn memo(&self) -> Memo<T> {
let key = self.key();
let context = get_global_context();
let read = context.signal.borrow();
match read.get(&key) {
Some(signal) => *signal.downcast_ref::<Memo<T>>().unwrap(),
None => {
drop(read);
// Constructors are always run in the root scope
let signal = ScopeId::ROOT.in_runtime(|| Signal::memo(self.selector));
context.signal.borrow_mut().insert(key, Box::new(signal));
signal
}
}
}
/// Get the scope the signal was created in.
pub fn origin_scope(&self) -> ScopeId {
ScopeId::ROOT
}
/// Get the generational id of the signal.
pub fn id(&self) -> generational_box::GenerationalBoxId {
self.memo().id()
}
}
impl<T: PartialEq + 'static> Readable for GlobalMemo<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.memo().try_read_unchecked()
self.resolve().id()
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
self.memo().try_peek_unchecked()
}
}
/// Allow calling a signal with memo() syntax
///
/// Currently only limited to copy types, though could probably specialize for string/arc/rc
impl<T: PartialEq + Clone + 'static> Deref for GlobalMemo<T> {
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
Readable::deref_impl(self)
/// Resolve the global memo. This will try to get the existing value from the current virtual dom, and if it doesn't exist, it will create a new one.
pub fn memo(&self) -> Memo<T> {
self.resolve()
}
}
+183 -17
View File
@@ -1,5 +1,6 @@
use dioxus_core::prelude::{provide_root_context, try_consume_context};
use std::{any::Any, cell::RefCell, collections::HashMap, panic::Location, rc::Rc};
use dioxus_core::ScopeId;
use generational_box::BorrowResult;
use std::{any::Any, cell::RefCell, collections::HashMap, ops::Deref, panic::Location, rc::Rc};
mod memo;
pub use memo::*;
@@ -7,12 +8,182 @@ pub use memo::*;
mod signal;
pub use signal::*;
use crate::Signal;
use crate::{Readable, ReadableRef, Signal, Writable, WritableRef};
/// A trait for an item that can be constructed from an initialization function
pub trait InitializeFromFunction<T> {
/// Create an instance of this type from an initialization function
fn initialize_from_function(f: fn() -> T) -> Self;
}
impl<T> InitializeFromFunction<T> for T {
fn initialize_from_function(f: fn() -> T) -> Self {
f()
}
}
/// A lazy value that is created once per application and can be accessed from anywhere in that application
pub struct Global<T, R = T> {
constructor: fn() -> R,
key: GlobalKey<'static>,
phantom: std::marker::PhantomData<fn() -> T>,
}
/// Allow calling a signal with signal() syntax
///
/// Currently only limited to copy types, though could probably specialize for string/arc/rc
impl<T: Clone + 'static, R: Clone + 'static> Deref for Global<T, R>
where
T: Readable<Target = R> + InitializeFromFunction<R>,
{
type Target = dyn Fn() -> R;
fn deref(&self) -> &Self::Target {
unsafe { Readable::deref_impl(self) }
}
}
impl<T: Clone + 'static, R: 'static> Readable for Global<T, R>
where
T: Readable<Target = R> + InitializeFromFunction<R>,
{
type Target = R;
type Storage = T::Storage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.resolve().try_read_unchecked()
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
self.resolve().try_peek_unchecked()
}
}
impl<T: Clone + 'static, R: 'static> Writable for Global<T, R>
where
T: Writable<Target = R> + InitializeFromFunction<R>,
{
type Mut<'a, Read: ?Sized + 'static> = T::Mut<'a, Read>;
fn map_mut<I: ?Sized, U: ?Sized + 'static, F: FnOnce(&mut I) -> &mut U>(
ref_: Self::Mut<'_, I>,
f: F,
) -> Self::Mut<'_, U> {
T::map_mut(ref_, f)
}
fn try_map_mut<
I: ?Sized + 'static,
U: ?Sized + 'static,
F: FnOnce(&mut I) -> Option<&mut U>,
>(
ref_: Self::Mut<'_, I>,
f: F,
) -> Option<Self::Mut<'_, U>> {
T::try_map_mut(ref_, f)
}
fn downcast_lifetime_mut<'a: 'b, 'b, Read: ?Sized + 'static>(
mut_: Self::Mut<'a, Read>,
) -> Self::Mut<'b, Read> {
T::downcast_lifetime_mut(mut_)
}
#[track_caller]
fn try_write_unchecked(
&self,
) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> {
self.resolve().try_write_unchecked()
}
}
impl<T: Clone + 'static, R: 'static> Global<T, R>
where
T: Writable<Target = R> + InitializeFromFunction<R>,
{
/// Write this value
pub fn write(&self) -> T::Mut<'static, R> {
self.resolve().try_write_unchecked().unwrap()
}
/// Run a closure with a mutable reference to the signal's value.
/// If the signal has been dropped, this will panic.
#[track_caller]
pub fn with_mut<O>(&self, f: impl FnOnce(&mut R) -> O) -> O {
self.resolve().with_mut(f)
}
}
impl<T: Clone + 'static, R> Global<T, R>
where
T: InitializeFromFunction<R>,
{
#[track_caller]
/// Create a new global value
pub const fn new(constructor: fn() -> R) -> Self {
let key = std::panic::Location::caller();
Self {
constructor,
key: GlobalKey::new(key),
phantom: std::marker::PhantomData,
}
}
/// Create this global signal with a specific key.
/// This is useful for ensuring that the signal is unique across the application and accessible from
/// outside the application too.
#[track_caller]
pub const fn with_key(constructor: fn() -> R, key: &'static str) -> Self {
Self {
constructor,
key: GlobalKey::new_from_str(key),
phantom: std::marker::PhantomData,
}
}
/// Get the key for this global
pub fn key(&self) -> GlobalKey<'static> {
self.key.clone()
}
/// Resolve the global value. This will try to get the existing value from the current virtual dom, and if it doesn't exist, it will create a new one.
// NOTE: This is not called "get" or "value" because those methods overlap with Readable and Writable
pub fn resolve(&self) -> T {
let key = self.key();
let context = get_global_context();
// Get the entry if it already exists
{
let read = context.map.borrow();
if let Some(signal) = read.get(&key) {
return signal.downcast_ref::<T>().cloned().unwrap();
}
}
// Otherwise, create it
// Constructors are always run in the root scope
let signal = ScopeId::ROOT.in_runtime(|| T::initialize_from_function(self.constructor));
context
.map
.borrow_mut()
.insert(key, Box::new(signal.clone()));
signal
}
/// Get the scope the signal was created in.
pub fn origin_scope(&self) -> ScopeId {
ScopeId::ROOT
}
}
/// The context for global signals
#[derive(Clone)]
pub struct GlobalSignalContext {
signal: Rc<RefCell<HashMap<GlobalKey<'static>, Box<dyn Any>>>>,
#[derive(Clone, Default)]
pub struct GlobalLazyContext {
map: Rc<RefCell<HashMap<GlobalKey<'static>, Box<dyn Any>>>>,
}
/// A key used to identify a signal in the global signal context
@@ -57,18 +228,18 @@ impl From<&'static Location<'static>> for GlobalKey<'static> {
}
}
impl GlobalSignalContext {
impl GlobalLazyContext {
/// Get a signal with the given string key
/// The key will be converted to a UUID with the appropriate internal namespace
pub fn get_signal_with_key<T>(&self, key: &str) -> Option<Signal<T>> {
let key = GlobalKey::new_from_str(key);
self.signal.borrow().get(&key).map(|f| {
self.map.borrow().get(&key).map(|f| {
*f.downcast_ref::<Signal<T>>().unwrap_or_else(|| {
panic!(
"Global signal with key {:?} is not of the expected type. Keys are {:?}",
key,
self.signal.borrow().keys()
self.map.borrow().keys()
)
})
})
@@ -76,15 +247,10 @@ impl GlobalSignalContext {
}
/// Get the global context for signals
pub fn get_global_context() -> GlobalSignalContext {
match try_consume_context() {
pub fn get_global_context() -> GlobalLazyContext {
match ScopeId::ROOT.has_context() {
Some(context) => context,
None => {
let context = GlobalSignalContext {
signal: Rc::new(RefCell::new(HashMap::new())),
};
provide_root_context(context)
}
None => ScopeId::ROOT.provide_context(Default::default()),
}
}
+13 -158
View File
@@ -1,171 +1,26 @@
use crate::{read::Readable, ReadableRef};
use crate::{write::Writable, GlobalKey};
use crate::{WritableRef, Write};
use dioxus_core::{prelude::ScopeId, Runtime};
use generational_box::{BorrowResult, UnsyncStorage};
use std::ops::Deref;
use super::get_global_context;
use super::{Global, InitializeFromFunction};
use crate::read::Readable;
use crate::read_impls;
use crate::Signal;
/// A signal that can be accessed from anywhere in the application and created in a static
pub struct GlobalSignal<T> {
initializer: fn() -> T,
key: GlobalKey<'static>,
created_at: &'static std::panic::Location<'static>,
impl<T> InitializeFromFunction<T> for Signal<T> {
fn initialize_from_function(f: fn() -> T) -> Self {
Signal::new(f())
}
}
/// A signal that can be accessed from anywhere in the application and created in a static
pub type GlobalSignal<T> = Global<Signal<T>, T>;
impl<T: 'static> GlobalSignal<T> {
/// Create a new global signal with the given initializer.
#[track_caller]
pub const fn new(initializer: fn() -> T) -> GlobalSignal<T> {
let key = std::panic::Location::caller();
GlobalSignal {
initializer,
key: GlobalKey::new(key),
created_at: key,
}
}
/// Get the key for this global
pub fn key(&self) -> GlobalKey<'static> {
self.key.clone()
}
/// Create this global signal with a specific key.
/// This is useful for ensuring that the signal is unique across the application and accessible from
/// outside the application too.
#[track_caller]
pub const fn with_key(initializer: fn() -> T, key: &'static str) -> GlobalSignal<T> {
GlobalSignal {
initializer,
key: GlobalKey::new_from_str(key),
created_at: std::panic::Location::caller(),
}
}
/// Get the signal that backs this .
pub fn signal(&self) -> Signal<T> {
let key = self.key();
let context = get_global_context();
let read = context.signal.borrow();
match read.get(&key) {
Some(signal) => *signal.downcast_ref::<Signal<T>>().unwrap(),
None => {
drop(read);
// Constructors are always run in the root scope
// The signal also exists in the root scope
let value = ScopeId::ROOT.in_runtime(self.initializer);
let signal = Signal::new_maybe_sync_in_scope_with_caller(
value,
ScopeId::ROOT,
self.created_at,
);
let entry = context.signal.borrow_mut().insert(key, Box::new(signal));
debug_assert!(entry.is_none(), "Global signal already exists");
signal
}
}
}
#[doc(hidden)]
pub fn maybe_with_rt<O>(&self, f: impl FnOnce(&T) -> O) -> O {
if Runtime::current().is_err() {
f(&(self.initializer)())
} else {
self.with(f)
}
}
/// Write this value
pub fn write(&self) -> Write<'static, T, UnsyncStorage> {
self.signal().try_write_unchecked().unwrap()
}
/// Get the scope the signal was created in.
pub fn origin_scope(&self) -> ScopeId {
ScopeId::ROOT
}
/// Run a closure with a mutable reference to the signal's value.
/// If the signal has been dropped, this will panic.
#[track_caller]
pub fn with_mut<O>(&self, f: impl FnOnce(&mut T) -> O) -> O {
self.signal().with_mut(f)
}
/// Get the generational id of the signal.
pub fn id(&self) -> generational_box::GenerationalBoxId {
self.signal().id()
}
}
impl<T: 'static> Readable for GlobalSignal<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
self.signal().try_read_unchecked()
self.resolve().id()
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
self.signal().try_peek_unchecked()
}
}
impl<T: 'static> Writable for GlobalSignal<T> {
type Mut<'a, R: ?Sized + 'static> = Write<'a, R, UnsyncStorage>;
fn map_mut<I: ?Sized, U: ?Sized + 'static, F: FnOnce(&mut I) -> &mut U>(
ref_: Self::Mut<'_, I>,
f: F,
) -> Self::Mut<'_, U> {
Write::map(ref_, f)
}
fn try_map_mut<
I: ?Sized + 'static,
U: ?Sized + 'static,
F: FnOnce(&mut I) -> Option<&mut U>,
>(
ref_: Self::Mut<'_, I>,
f: F,
) -> Option<Self::Mut<'_, U>> {
Write::filter_map(ref_, f)
}
fn downcast_lifetime_mut<'a: 'b, 'b, R: ?Sized + 'static>(
mut_: Self::Mut<'a, R>,
) -> Self::Mut<'b, R> {
Write::downcast_lifetime(mut_)
}
#[track_caller]
fn try_write_unchecked(
&self,
) -> Result<WritableRef<'static, Self>, generational_box::BorrowMutError> {
self.signal().try_write_unchecked()
}
}
/// Allow calling a signal with signal() syntax
///
/// Currently only limited to copy types, though could probably specialize for string/arc/rc
impl<T: Clone + 'static> Deref for GlobalSignal<T> {
type Target = dyn Fn() -> T;
fn deref(&self) -> &Self::Target {
Readable::deref_impl(self)
/// Resolve the global signal. This will try to get the existing value from the current virtual dom, and if it doesn't exist, it will create a new one.
pub fn signal(&self) -> Signal<T> {
self.resolve()
}
}