mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 11:40:37 +00:00
Refactored Dymamics to be weak + drop bug fixes
After adding weak_clone, I realized that all map functions should use weak references anyways. In the process of implementing this, I ran tests a lot and found other edge cases where I hadn't properly reasoned about Drop behavior. While some of the state cleanup is a bit overkill at the moment, this is in anticipation of wanting a WeakDynamicReader-like type.
This commit is contained in:
parent
e4dfd9320d
commit
a9a41a1582
5 changed files with 90 additions and 56 deletions
|
|
@ -31,12 +31,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- A potential edge case where a `DynamicReader` would not return after being
|
- A potential edge case where a `DynamicReader` would not return after being
|
||||||
disconnected has been removed.
|
disconnected has been removed.
|
||||||
- [#120][120]: Dividing a `ZeroToOne` now properly checks for `NaN` and `0.`.
|
- [#120][120]: Dividing a `ZeroToOne` now properly checks for `NaN` and `0.`.
|
||||||
|
- Removed a possible deadlock when using `DynamicReader::block_until_updated`.
|
||||||
|
- Removed an edge case ensuring `Waker`s are signaled for `DynamicReader`s that
|
||||||
|
are waiting for value when the last `Dynamic` is dropped.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `WidgetCacheKey` now includes the `KludgineId` of the context it was created
|
- `WidgetCacheKey` now includes the `KludgineId` of the context it was created
|
||||||
from. This ensures if a `WidgetInstance` moves or is shared between windows,
|
from. This ensures if a `WidgetInstance` moves or is shared between windows,
|
||||||
the cache is invalidated.
|
the cache is invalidated.
|
||||||
|
- All `Dynamic` mapping functions now utilize weak references, and clean up as
|
||||||
|
necessary if a value is not able to be upgraded.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
@ -57,10 +62,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- `Window::titled` allows setting a window's title, and can be provided a
|
- `Window::titled` allows setting a window's title, and can be provided a
|
||||||
string-type or a `Dynamic<String>` to allow updating the title while the
|
string-type or a `Dynamic<String>` to allow updating the title while the
|
||||||
window is open.
|
window is open.
|
||||||
- `Dynamic::weak_clone` returns a new dynamic that is updated from the original
|
|
||||||
dynamic, but is careful to not add any strong references to the source
|
|
||||||
`Dynamic`. This allows breaking dynamic graphs into independent "sections"
|
|
||||||
that can be deallocated independently of other graphs it is connected with.
|
|
||||||
- `DynamicReader::on_disconnect` allows attaching a callback that is invoked
|
- `DynamicReader::on_disconnect` allows attaching a callback that is invoked
|
||||||
once the final source `Dynamic` is dropped.
|
once the final source `Dynamic` is dropped.
|
||||||
- `Dynamic::instances()` returns the number of clones the dynamic has in
|
- `Dynamic::instances()` returns the number of clones the dynamic has in
|
||||||
|
|
|
||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -2208,9 +2208,9 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "similar"
|
name = "similar"
|
||||||
version = "2.3.0"
|
version = "2.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597"
|
checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
|
members = ["cushy-macros"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "cushy"
|
name = "cushy"
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ impl DebugContext {
|
||||||
section.values.lock().push(Box::new(RegisteredValue {
|
section.values.lock().push(Box::new(RegisteredValue {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
value: reader.clone(),
|
value: reader.clone(),
|
||||||
widget: make_observer(value.weak_clone()).make_widget(),
|
widget: make_observer(value.clone()).make_widget(),
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
|
|
|
||||||
130
src/value.rs
130
src/value.rs
|
|
@ -261,28 +261,6 @@ impl<T> Dynamic<T> {
|
||||||
self.map_each(|value| U::from(value))
|
self.map_each(|value| U::from(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new dynamic that contains a weak clone of `self`'s contents.
|
|
||||||
///
|
|
||||||
/// The returned `Dynamic` does not use any strong references, ensuring the
|
|
||||||
/// returned dynamic does not extend the lifetime of `self`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn weak_clone(&self) -> Self
|
|
||||||
where
|
|
||||||
T: Clone + Send + 'static,
|
|
||||||
{
|
|
||||||
let weak_source = self.downgrade();
|
|
||||||
let weak_out = Dynamic::new(self.get());
|
|
||||||
weak_out.set_source(self.0.for_each({
|
|
||||||
let weak_out = weak_out.clone();
|
|
||||||
move || {
|
|
||||||
if let Some(source) = weak_source.upgrade() {
|
|
||||||
*weak_out.lock() = source.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
weak_out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attaches `for_each` to this value so that it is invoked each time the
|
/// Attaches `for_each` to this value so that it is invoked each time the
|
||||||
/// value's contents are updated.
|
/// value's contents are updated.
|
||||||
pub fn for_each<F>(&self, mut for_each: F) -> CallbackHandle
|
pub fn for_each<F>(&self, mut for_each: F) -> CallbackHandle
|
||||||
|
|
@ -290,9 +268,11 @@ impl<T> Dynamic<T> {
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
F: for<'a> FnMut(&'a T) + Send + 'static,
|
F: for<'a> FnMut(&'a T) + Send + 'static,
|
||||||
{
|
{
|
||||||
let this = self.clone();
|
let this = self.downgrade();
|
||||||
self.0.for_each(move || {
|
self.0.for_each(move || {
|
||||||
|
let this = this.upgrade().ok_or(CallbackDisconnected)?;
|
||||||
this.map_ref(&mut for_each);
|
this.map_ref(&mut for_each);
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,9 +283,11 @@ impl<T> Dynamic<T> {
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
||||||
{
|
{
|
||||||
let this = self.clone();
|
let this = self.downgrade();
|
||||||
self.0.for_each(move || {
|
self.0.for_each(move || {
|
||||||
|
let this = this.upgrade().ok_or(CallbackDisconnected)?;
|
||||||
this.map_generational(&mut for_each);
|
this.map_generational(&mut for_each);
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,9 +298,11 @@ impl<T> Dynamic<T> {
|
||||||
T: Clone + Send + 'static,
|
T: Clone + Send + 'static,
|
||||||
F: FnMut(T) + Send + 'static,
|
F: FnMut(T) + Send + 'static,
|
||||||
{
|
{
|
||||||
let this = self.clone();
|
let this = self.downgrade();
|
||||||
self.0.for_each(move || {
|
self.0.for_each(move || {
|
||||||
|
let this = this.upgrade().ok_or(CallbackDisconnected)?;
|
||||||
for_each(this.get());
|
for_each(this.get());
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,8 +326,11 @@ impl<T> Dynamic<T> {
|
||||||
F: for<'a> FnMut(&'a T) -> R + Send + 'static,
|
F: for<'a> FnMut(&'a T) -> R + Send + 'static,
|
||||||
R: PartialEq + Send + 'static,
|
R: PartialEq + Send + 'static,
|
||||||
{
|
{
|
||||||
let this = self.clone();
|
let this = self.downgrade();
|
||||||
self.0.map_each(move || this.map_ref(&mut map))
|
self.0.map_each(move || {
|
||||||
|
let this = this.upgrade().ok_or(CallbackDisconnected)?;
|
||||||
|
Ok(this.map_ref(&mut map))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new dynamic value that contains the result of invoking `map`
|
/// Creates a new dynamic value that contains the result of invoking `map`
|
||||||
|
|
@ -354,8 +341,11 @@ impl<T> Dynamic<T> {
|
||||||
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
||||||
R: PartialEq + Send + 'static,
|
R: PartialEq + Send + 'static,
|
||||||
{
|
{
|
||||||
let this = self.clone();
|
let this = self.downgrade();
|
||||||
self.0.map_each(move || this.map_generational(&mut map))
|
self.0.map_each(move || {
|
||||||
|
let this = this.upgrade().ok_or(CallbackDisconnected)?;
|
||||||
|
Ok(this.map_generational(&mut map))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new dynamic value that contains the result of invoking `map`
|
/// Creates a new dynamic value that contains the result of invoking `map`
|
||||||
|
|
@ -366,8 +356,11 @@ impl<T> Dynamic<T> {
|
||||||
F: FnMut(T) -> R + Send + 'static,
|
F: FnMut(T) -> R + Send + 'static,
|
||||||
R: PartialEq + Send + 'static,
|
R: PartialEq + Send + 'static,
|
||||||
{
|
{
|
||||||
let this = self.clone();
|
let this = self.downgrade();
|
||||||
self.0.map_each(move || map(this.get()))
|
self.0.map_each(move || {
|
||||||
|
let this = this.upgrade().ok_or(CallbackDisconnected)?;
|
||||||
|
Ok(map(this.get()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A helper function that invokes `with_clone` with a clone of self. This
|
/// A helper function that invokes `with_clone` with a clone of self. This
|
||||||
|
|
@ -811,12 +804,9 @@ impl<T> Drop for Dynamic<T> {
|
||||||
// `Dynamic`.
|
// `Dynamic`.
|
||||||
if let Ok(mut state) = self.state() {
|
if let Ok(mut state) = self.state() {
|
||||||
if Arc::strong_count(&self.0) == state.readers + 1 {
|
if Arc::strong_count(&self.0) == state.readers + 1 {
|
||||||
let on_disconnect = state.on_disconnect.take();
|
let cleanup = state.cleanup();
|
||||||
drop(state);
|
drop(state);
|
||||||
|
drop(cleanup);
|
||||||
for on_disconnect in on_disconnect.into_iter().flatten() {
|
|
||||||
on_disconnect.invoke(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.0.sync.notify_all();
|
self.0.sync.notify_all();
|
||||||
}
|
}
|
||||||
|
|
@ -962,7 +952,7 @@ impl<T> DynamicData<T> {
|
||||||
|
|
||||||
pub fn for_each<F>(&self, map: F) -> CallbackHandle
|
pub fn for_each<F>(&self, map: F) -> CallbackHandle
|
||||||
where
|
where
|
||||||
F: for<'a> FnMut() + Send + 'static,
|
F: for<'a> FnMut() -> Result<(), CallbackDisconnected> + Send + 'static,
|
||||||
{
|
{
|
||||||
let state = self.state().expect("deadlocked");
|
let state = self.state().expect("deadlocked");
|
||||||
let mut data = state.callbacks.callbacks.lock().ignore_poison();
|
let mut data = state.callbacks.callbacks.lock().ignore_poison();
|
||||||
|
|
@ -974,15 +964,16 @@ impl<T> DynamicData<T> {
|
||||||
|
|
||||||
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
|
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||||
where
|
where
|
||||||
F: for<'a> FnMut() -> R + Send + 'static,
|
F: for<'a> FnMut() -> Result<R, CallbackDisconnected> + Send + 'static,
|
||||||
R: PartialEq + Send + 'static,
|
R: PartialEq + Send + 'static,
|
||||||
{
|
{
|
||||||
let initial_value = map();
|
let initial_value = map().expect("initial map returned Disconnected");
|
||||||
let mapped_value = Dynamic::new(initial_value);
|
let mapped_value = Dynamic::new(initial_value);
|
||||||
let returned = mapped_value.clone();
|
let returned = mapped_value.clone();
|
||||||
|
|
||||||
let callback = self.for_each(move || {
|
let callback = self.for_each(move || {
|
||||||
mapped_value.set(map());
|
mapped_value.set(map()?);
|
||||||
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
returned.set_source(callback);
|
returned.set_source(callback);
|
||||||
|
|
@ -991,6 +982,10 @@ impl<T> DynamicData<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A callback function is no longer connected to its source.
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub struct CallbackDisconnected;
|
||||||
|
|
||||||
struct DebugDynamicData<'a, T>(&'a Arc<DynamicData<T>>);
|
struct DebugDynamicData<'a, T>(&'a Arc<DynamicData<T>>);
|
||||||
|
|
||||||
impl<T> Debug for DebugDynamicData<'_, T>
|
impl<T> Debug for DebugDynamicData<'_, T>
|
||||||
|
|
@ -1215,6 +1210,39 @@ impl<T> State<T> {
|
||||||
.field("generation", &self.wrapped.generation.0)
|
.field("generation", &self.wrapped.generation.0)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn cleanup(&mut self) -> StateCleanup {
|
||||||
|
StateCleanup {
|
||||||
|
on_disconnect: self.on_disconnect.take(),
|
||||||
|
wakers: std::mem::take(&mut self.wakers),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Drop for State<T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Ensure any disconnections that didn't fire due to deadlocking still
|
||||||
|
// are invoked.
|
||||||
|
drop(self.cleanup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StateCleanup {
|
||||||
|
on_disconnect: Option<Vec<OnceCallback>>,
|
||||||
|
wakers: Vec<Waker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for StateCleanup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for on_disconnect in self.on_disconnect.take().into_iter().flatten() {
|
||||||
|
on_disconnect.invoke(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for waker in self.wakers.drain(..) {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Debug for State<T>
|
impl<T> Debug for State<T>
|
||||||
|
|
@ -1275,9 +1303,11 @@ impl Drop for ChangeCallbacks {
|
||||||
// thread wrote, we can skip the callbacks.
|
// thread wrote, we can skip the callbacks.
|
||||||
if state.invoked_at < self.changed_at {
|
if state.invoked_at < self.changed_at {
|
||||||
state.invoked_at = Instant::now();
|
state.invoked_at = Instant::now();
|
||||||
for callback in &mut state.callbacks {
|
// Invoke all callbacks, removing those that report an
|
||||||
callback.changed();
|
// error.
|
||||||
}
|
state
|
||||||
|
.callbacks
|
||||||
|
.drain_filter(|callback| callback.changed().is_err());
|
||||||
}
|
}
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
|
|
@ -1309,15 +1339,15 @@ impl Drop for ChangeCallbacks {
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ValueCallback: Send {
|
trait ValueCallback: Send {
|
||||||
fn changed(&mut self);
|
fn changed(&mut self) -> Result<(), CallbackDisconnected>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F> ValueCallback for F
|
impl<F> ValueCallback for F
|
||||||
where
|
where
|
||||||
F: for<'a> FnMut() + Send + 'static,
|
F: for<'a> FnMut() -> Result<(), CallbackDisconnected> + Send + 'static,
|
||||||
{
|
{
|
||||||
fn changed(&mut self) {
|
fn changed(&mut self) -> Result<(), CallbackDisconnected> {
|
||||||
self();
|
self()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2144,7 +2174,7 @@ macro_rules! impl_tuple_for_each {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($self:ident $for_each:ident $handles:ident [] [$type:ident $field:tt $var:ident]) => {
|
($self:ident $for_each:ident $handles:ident [] [$type:ident $field:tt $var:ident]) => {
|
||||||
$handles += $self.$field.for_each(move |field: &$type| $for_each((field,)));
|
$handles += $self.$field.for_each(move |field| $for_each((field,)));
|
||||||
};
|
};
|
||||||
($self:ident $for_each:ident $handles:ident [] [$($type:ident $field:tt $var:ident),+]) => {
|
($self:ident $for_each:ident $handles:ident [] [$($type:ident $field:tt $var:ident),+]) => {
|
||||||
let $for_each = Arc::new(Mutex::new($for_each));
|
let $for_each = Arc::new(Mutex::new($for_each));
|
||||||
|
|
@ -2221,7 +2251,9 @@ impl_all_tuples!(impl_tuple_for_each);
|
||||||
/// function.
|
/// function.
|
||||||
pub trait MapEach<T, U> {
|
pub trait MapEach<T, U> {
|
||||||
/// The borrowed representation of `T` passed into the mapping function.
|
/// The borrowed representation of `T` passed into the mapping function.
|
||||||
type Ref<'a>;
|
type Ref<'a>
|
||||||
|
where
|
||||||
|
T: 'a;
|
||||||
|
|
||||||
/// Apply `map_each` to each value in `self`, storing the result in the
|
/// Apply `map_each` to each value in `self`, storing the result in the
|
||||||
/// returned dynamic.
|
/// returned dynamic.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue