Skip to content

Commit

Permalink
feat: add temporary container listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
GregoryConrad committed Jul 8, 2023
1 parent d7d191d commit 53c2041
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 9 deletions.
124 changes: 116 additions & 8 deletions rearch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,11 @@ pub use side_effect_registrar::*;
mod txn;
pub use txn::*;

// TODO listener function instead of exposed garbage collection.
// container.listen(|get| do_something(get(some_capsule)))
// returns a ListenerKeepAlive that removes listener once dropped
// internally implemented as an "impure" capsule that is dropped when keep alive drops
// what about the listener's dependencies? should they be trimmed if possible?
// maybe go off container's aggressiveness setting
// TODO side effect macro to bust the `move |register| {}` boilerplate
// TODO aggressive garbage collection mode
// (delete all created super pure capsules that aren't needed at end of a requested build)
// TODO aggressive garbage collection mode, which:
// - Deletes all created super pure leaf subgraphs at end of every requested build
// - There might only be one, including built node itself; think some more about this
// - See what we can trim of a listener's dependencies when its handle is dropped

/// Capsules are blueprints for creating some immutable data
/// and do not actually contain any data themselves.
Expand Down Expand Up @@ -186,6 +182,74 @@ impl Container {
pub fn read<CL: CapsuleList>(&self, capsules: CL) -> CL::Data {
capsules.read(self)
}

/// Provides a mechanism to *temporarily* listen to changes in some capsule(s).
/// The provided listener is called once at the time of the listener's registration,
/// and then once again everytime a dependency changes.
///
/// Returns a `ListenerHandle`, which doesn't do anything other than implement Drop,
/// and its Drop implementation will remove `listener` from the Container.
///
/// Thus, if you want the handle to live for as long as the Container itself,
/// it is instead recommended to create an impure capsule (just call `registrar.register(());`)
/// that acts as your listener. When you normally would call `Container::listen()`,
/// instead call `container.read(my_impure_listener)` to initialize it.
///
/// # Panics
/// Panics if you attempt to register the same listener twice,
/// before the first `ListenerHandle` is dropped.
#[must_use]
pub fn listen<Listener>(&self, listener: Listener) -> ListenerHandle
where
Listener: Fn(CapsuleReader) + Send + 'static,
{
// We make a temporary *impure* capsule for the listener so that
// it doesn't get disposed by the super pure gc
let tmp_capsule = move |reader: CapsuleReader, registrar: SideEffectRegistrar| {
registrar.register(());
listener(reader);
};
let id = tmp_capsule.type_id();

// Put the temporary capsule into the container to listen to updates
self.with_write_txn(move |txn| {
assert_eq!(
txn.try_read(&tmp_capsule),
None,
"You cannot pass the same listener into Container::listen() {}",
"until the original returned ListenerHandle is dropped!"
);
txn.read_or_init(tmp_capsule);
});

ListenerHandle {
id,
store: Arc::downgrade(&self.0),
}
}
}

/// Represents a handle onto a particular listener, as created with `Container::listen()`.
///
/// This struct doesn't do anything other than implement Drop,
/// and its Drop implementation will remove the listener from the Container.
///
/// Thus, if you want the handle to live for as long as the Container itself,
/// it is instead recommended to create an impure capsule (just call `registrar.register(());`)
/// that acts as your listener. When you normally would call `Container::listen()`,
/// instead call `container.read(my_impure_listener)` to initialize it.
pub struct ListenerHandle {
id: TypeId,
store: Weak<ContainerStore>,
}
impl Drop for ListenerHandle {
fn drop(&mut self) {
if let Some(store) = self.store.upgrade() {
// Note: The node is guaranteed to be in the graph here since it is a listener.
let rebuilder = CapsuleRebuilder(Weak::clone(&self.store));
store.with_write_txn(rebuilder, |txn| txn.dispose_single_node(self.id));
}
}
}

/// A list of capsules.
Expand Down Expand Up @@ -483,6 +547,50 @@ mod tests {
assert_eq!(2, s2);
}

#[test]
fn listener_gets_updates() {
use std::sync::{Arc, Mutex};

fn stateful(
_: CapsuleReader,
registrar: SideEffectRegistrar,
) -> (u8, impl Fn(u8) + Clone + Send + Sync) {
let (state, set_state) = registrar.register(side_effects::state(0));
(*state, set_state)
}

let states = Arc::new(Mutex::new(Vec::new()));

let listener = {
let states = states.clone();
move |mut reader: CapsuleReader| {
let mut states = states.lock().unwrap();
states.push(reader.read(stateful).0);
}
};

let container = Container::new();

container.read(stateful).1(1);
let handle = container.listen(listener.clone());
container.read(stateful).1(2);
container.read(stateful).1(3);

drop(handle);
container.read(stateful).1(4);

container.read(stateful).1(5);
let handle = container.listen(listener);
container.read(stateful).1(6);
container.read(stateful).1(7);

drop(handle);
container.read(stateful).1(8);

let states = states.lock().unwrap();
assert_eq!(*states, vec![1, 2, 3, 5, 6, 7]);
}

// We use a more sophisticated graph here for a more thorough test of all functionality
//
// -> A -> B -> C -> D
Expand Down
2 changes: 1 addition & 1 deletion rearch/src/txn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl ContainerWriteTxn<'_> {

/// Forcefully disposes only the requested node, cleaning up the node's direct dependencies.
/// Panics if the node is not in the graph.
fn dispose_single_node(&mut self, id: TypeId) {
pub(crate) fn dispose_single_node(&mut self, id: TypeId) {
self.data.remove(&id);
self.nodes
.remove(&id)
Expand Down

0 comments on commit 53c2041

Please sign in to comment.