app_world/
lib.rs

1//! app-world provides a framework agnostic approach to managing frontend application state.
2//!
3//! # The Data Model
4//!
5//! An `AppWorld` is a type that you define that holds your application state as well as other
6//! resources that you've deemed useful to have around during your application's runtime.
7//!
8//! Here's an example of what an AppWorld for a basic e-commerce app frontend might look like:
9//!
10//! ```rust
11//! # use std::collections::HashMap;
12//! struct MyAppWorld {
13//!     state: MyAppState,
14//!     resources: MyAppResources
15//! }
16//!
17//! struct MyAppState {
18//!     user: User,
19//!     products: HashMap<Uuid, Product>
20//! }
21//!
22//! struct MyAppResources {
23//!     file_store: Box<dyn MyFileStoreTrait>,
24//!     api_client: ApiClient
25//! }
26//!
27//! # trait MyFileStoreTrait {}
28//! # type ApiClient = ();
29//! # type Product = ();
30//! # type User = ();
31//! # type Uuid = ();
32//! ```
33//!
34//! The `MyAppWorld` struct would be defined in your crate, but it wouldn't be used directly when
35//! you were passing data around to your views.
36//!
37//! Instead, you wrap it in an `app_world::AppWorldWrapper<W>`
38//!
39//! ```rust
40//! type MyAppWorldWrapper = app_world::AppWorldWrapper<MyAppWorld>;
41//!
42//! # type MyAppWorld = ();
43//! ```
44//!
45//! # AppWorldWrapper<W: AppWorld>
46//!
47//! The `AppWorldWrapper` prevents direct mutable access to your application state, so you cannot
48//! mutate fields wherever you please.
49//!
50//! Instead, the [`AppWorld`] trait defines a [`AppWorld.msg()`] method that can be used to update
51//! your application state.
52//!
53//! You can pass your `AppWorldWrapper<W>` to different threads by calling
54//! [`AppWorldWrapper.clone()`]. Under the hood an [`Arc`] is used to share your data across
55//! threads.
56//!
57//! # Example Usage
58//!
59//! TODO
60//!
61//! # When to Use app-world
62//!
63//! app-world shines in applications that do not have extreme real time rendering requirements,
64//! such as almost all browser, desktop and mobile applications.
65//! In games and real-time simulations, you're better off using something like an entity component
66//! system to manage your application state.
67//!
68//! This is because app-world is designed such that your application state can only be written to
69//! from one thread at a time. This is totally fine for almost all browser, desktop and mobile
70//! applications, but could be an issue for games and simulations.
71//!
72//! If you're writing a game or simulation you're likely better off reaching for an
73//! entity-component-system library. Otherwise, you should be in good hands here.
74//! which could be an issue for a high-performing game or simulation.
75
76#![deny(missing_docs)]
77
78use std::cell::RefCell;
79use std::ops::Deref;
80use std::sync::{Arc, RwLock, RwLockReadGuard};
81use std::thread::LocalKey;
82
83/// Holds application state and resources.
84/// See the [crate level documentation](crate) for more details.
85///
86/// # Cloning
87///
88/// Cloning an `AppWorldWrapper` is a very cheap operation.
89///
90/// All clones hold pointers to the same inner state.
91pub struct AppWorldWrapper<W: AppWorld> {
92    world: Arc<RwLock<W>>,
93}
94
95/// Defines how messages that indicate that something has happened get sent to the World.
96pub trait AppWorld: Sized {
97    /// Indicates that something has happened.
98    ///
99    /// ```
100    /// # use std::time::SystemTime;
101    /// #[allow(unused)]
102    /// enum MyMessageType {
103    ///     IncreaseClickCounter,
104    ///     SetLastPausedAt(SystemTime)
105    /// }
106    /// ```
107    type Message;
108
109    /// Send a message to the state object.
110    /// This will usually lead to a state update
111    fn msg(&mut self, message: Self::Message);
112}
113
114impl<W: AppWorld + 'static> AppWorldWrapper<W> {
115    /// Create a new AppWorldWrapper.
116    pub fn new(world: W) -> Self {
117        let world = Arc::new(RwLock::new(world));
118        Self { world }
119    }
120
121    /// Acquire write access to the AppWorld then send a message.
122    pub fn msg(&self, msg: W::Message) {
123        self.world.write().unwrap().msg(msg)
124    }
125}
126
127impl<W: AppWorld + 'static> AppWorldWrapper<W> {
128    thread_local!(
129        static HAS_READ: RefCell<bool> = RefCell::new(false);
130    );
131
132    /// Acquire read access to AppWorld.
133    ///
134    /// # Panics
135    /// Panics if the current thread is already holding a read guard.
136    ///
137    /// This panic prevents the following scenario from deadlocking:
138    ///
139    /// 1. Thread A acquires a read guard
140    /// 2. Thread B calls `AppWorld::msg`, which attempts to acquire a write lock
141    /// 3. Thread A attempts to acquire a second read guard while the first is still active
142    pub fn read(&self) -> WorldReadGuard<'_, W> {
143        Self::HAS_READ.with(|has_read| {
144            let mut has_read = has_read.borrow_mut();
145
146            if *has_read {
147                panic!("Thread already holds read guard")
148            }
149
150            *has_read = true
151        });
152        WorldReadGuard {
153            guard: self.world.read().unwrap(),
154            read_tracker: &Self::HAS_READ,
155        }
156    }
157
158    /// Acquire write access to AppWorld.
159    ///
160    /// Under normal circumstances you should only ever write to the world through the `.msg()`
161    /// method.
162    ///
163    /// This .write() method is useful when writing tests where you want to quickly set up some
164    /// initial state.
165    #[cfg(feature = "test-utils")]
166    pub fn write(&self) -> std::sync::RwLockWriteGuard<'_, W> {
167        self.world.write().unwrap()
168    }
169}
170
171impl<W: AppWorld> Clone for AppWorldWrapper<W> {
172    fn clone(&self) -> Self {
173        AppWorldWrapper {
174            world: self.world.clone(),
175        }
176    }
177}
178
179/// Holds a read guard on a World.
180pub struct WorldReadGuard<'a, W> {
181    guard: RwLockReadGuard<'a, W>,
182    read_tracker: &'static LocalKey<RefCell<bool>>,
183}
184impl<'a, W> Deref for WorldReadGuard<'a, W> {
185    type Target = RwLockReadGuard<'a, W>;
186
187    fn deref(&self) -> &Self::Target {
188        &self.guard
189    }
190}
191impl<'a, W> Drop for WorldReadGuard<'a, W> {
192    fn drop(&mut self) {
193        self.read_tracker.with(|has_reads| {
194            *has_reads.borrow_mut() = false;
195        })
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use std::thread;
203    use std::time::Duration;
204
205    /// Verify that we prevent deadlocks when a thread tries to acquire a read guard on the world
206    /// twice.
207    ///
208    /// ---
209    ///
210    /// Given Thread A and B, a deadlock can occur if:
211    ///
212    /// 1. Thread A acquires read guard
213    /// 2. Thread B begins waiting for a write guard
214    /// 3. Thread A begins waiting for a second read guard
215    ///
216    /// On some platforms the guard acquisition order would be Thread A, Thread A, Thread B.
217    /// That is to say that attempts to acquire write guards do not block attempts to acquire read
218    /// guards.
219    ///
220    /// On other platforms, attempts to write may take precedence over attempts to read.
221    ///
222    /// On those platforms, Thread A will deadlock on the second read, and Thread B will deadlock
223    /// on the write.
224    ///
225    /// On macOS Ventura the sequence described above will cause a deadlock.
226    ///
227    /// This test uses two threads and `std::time::sleep` to simulate the sequence above and
228    /// confirm that we panic if a thread tries to hold two active read guards at once.
229    #[test]
230    #[should_panic = "Second read attempt panicked"]
231    fn deadlock_prevention_same_thread_double_read_another_thread_write() {
232        let world = AppWorldWrapper::new(TestWorld { was_mutated: false });
233        let world_clone1 = world.clone();
234        let world_clone2 = world.clone();
235
236        let handle = thread::spawn(move || {
237            let guard_1 = world.read();
238            assert_eq!(guard_1.was_mutated, false);
239
240            let handle = thread::spawn(move || {
241                world_clone1.msg(());
242            });
243
244            thread::sleep(Duration::from_millis(50));
245            let guard_3 = world.read();
246            assert_eq!(guard_3.was_mutated, true);
247
248            handle.join().unwrap();
249        });
250
251        let join = handle.join();
252
253        assert_eq!(world_clone2.read().was_mutated, true);
254        join.expect("Second read attempt panicked");
255    }
256
257    /// Verify that the same thread can acquire a second read guard after the first has been
258    /// dropped.
259    #[test]
260    fn two_non_colliding_reads() {
261        let world = AppWorldWrapper::new(TestWorld::default());
262
263        {
264            let _guard = world.read();
265        }
266
267        let _guard = world.read();
268    }
269
270    #[derive(Default)]
271    struct TestWorld {
272        was_mutated: bool,
273    }
274    impl AppWorld for TestWorld {
275        type Message = ();
276        fn msg(&mut self, _message: Self::Message) {
277            self.was_mutated = true;
278        }
279    }
280}