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}