1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::Arc;
3use std::thread::{self, JoinHandle};
4use std::time::Duration;
5
6use crossbeam_channel::RecvTimeoutError;
7use parking_lot::Mutex;
8use tokio::runtime::Runtime as TokioRuntime;
9
10use crate::bus::{Bus, BusSender};
11use crate::cmd::Cmd;
12use crate::effect::EffectId;
13use crate::env::Environment;
14use crate::program::{Program, ReducerProgram};
15use crate::reducer::Reducer;
16use crate::safe_reducer::safe_reduce_update;
17use crate::sub::Sub;
18
19use super::config::RuntimeConfig;
20use super::error::{build_tokio, RuntimeError};
21use super::interpreter::{interpret_effects_async, InterpreterState};
22use super::store::{StoreBackend, StoreWork, StoreWorkUnwindGuard};
23use super::subscription;
24
25pub struct Runtime<S, M> {
27 pub state: Arc<Mutex<S>>,
28 pub bus: Bus<M>,
29 pub env: Environment,
30 backend: StoreBackend<S, M>,
31 shutdown: Arc<AtomicBool>,
32 _thread: Option<JoinHandle<()>>,
33 _tokio: Arc<TokioRuntime>,
34 _subscriptions: Arc<subscription::SubscriptionHandles>,
35}
36
37impl<S, M> Runtime<S, M>
38where
39 S: Send + Sync + 'static,
40 M: Send + 'static,
41{
42 pub fn from_program(
43 program: Program<S, M>,
44 env: Environment,
45 config: RuntimeConfig,
46 ) -> Result<Self, RuntimeError> {
47 Self::bootstrap(
48 program.init,
49 program.update,
50 program.subscriptions,
51 env,
52 config,
53 )
54 }
55
56 pub fn from_reducer_program<R>(
57 program: ReducerProgram<R>,
58 env: Environment,
59 config: RuntimeConfig,
60 ) -> Result<Self, RuntimeError>
61 where
62 R: Reducer<State = S, Action = M> + Send + Sync + 'static,
63 {
64 let reducer = Arc::new(program.reducer);
65 let init = program.init;
66 let subscriptions = program.subscriptions;
67 Self::bootstrap(
68 init,
69 move |state, msg| reducer.reduce(state, msg),
70 subscriptions,
71 env,
72 config,
73 )
74 }
75
76 fn bootstrap(
77 init: fn() -> (S, Cmd<M>),
78 update: impl Fn(&mut S, M) -> Cmd<M> + Send + Sync + 'static,
79 subscriptions: fn(&S) -> Sub<M>,
80 env: Environment,
81 config: RuntimeConfig,
82 ) -> Result<Self, RuntimeError> {
83 let RuntimeConfig {
84 bus_capacity,
85 worker_threads,
86 thread_name,
87 } = config;
88 let (initial_state, init_cmd) = init();
89 let state = Arc::new(Mutex::new(initial_state));
90 let bus = Bus::new(bus_capacity);
91 let shutdown = Arc::new(AtomicBool::new(false));
92 let tokio = build_tokio(worker_threads, thread_name)?;
93 let interpreter = InterpreterState::new();
94 let sub_handles = Arc::new(subscription::SubscriptionHandles::new());
95 let backend = StoreBackend::new(state.clone(), bus.sender(), interpreter);
96 let tx = bus.sender();
97 let init_handles = tokio.block_on(interpret_effects_async(
98 init_cmd.into_effects(),
99 tx.clone(),
100 env.clone(),
101 tokio.handle().clone(),
102 backend.clone(),
103 ));
104 tokio.handle().spawn(async move {
105 for join in init_handles {
106 let _ = join.await;
107 }
108 });
109
110 let receiver = bus.receiver().clone();
111 let subs_fn = subscriptions;
112 let state_for_thread = state.clone();
113 let env_for_thread = env.clone();
114 let shutdown_for_thread = shutdown.clone();
115 let tokio_for_thread = tokio.clone();
116 let backend_for_thread = backend.clone();
117 let tx_for_thread = tx;
118 let subs_registry = sub_handles.clone();
119
120 let thread = thread::Builder::new()
121 .name(thread_name.to_string())
122 .spawn(move || {
123 let rt = tokio_for_thread;
124 let sync_subs = |state: &S| {
125 let sub = subs_fn(state);
126 subscription::sync_subscriptions(
127 &sub,
128 &subs_registry,
129 rt.handle().clone(),
130 tx_for_thread.clone(),
131 backend_for_thread.clone(),
132 shutdown_for_thread.clone(),
133 );
134 };
135
136 sync_subs(&state_for_thread.lock());
137
138 while !shutdown_for_thread.load(Ordering::Relaxed) {
139 match receiver.recv_timeout(Duration::from_millis(50)) {
140 Ok(msg) => {
141 let mut unwind = StoreWorkUnwindGuard::new(&backend_for_thread);
142 let cmd = {
143 let mut guard = state_for_thread.lock();
144 match safe_reduce_update(&mut *guard, |s, a| update(s, a), msg) {
145 Ok(cmd) => cmd,
146 Err(_) => {
147 drop(guard);
148 backend_for_thread.notify_state();
149 continue;
150 }
151 }
152 };
153 unwind.disarm();
154 backend_for_thread.notify_state();
155 let handles = rt.block_on(interpret_effects_async(
156 cmd.into_effects(),
157 tx_for_thread.clone(),
158 env_for_thread.clone(),
159 rt.handle().clone(),
160 backend_for_thread.clone(),
161 ));
162 if handles.is_empty() {
163 backend_for_thread.end_store_work();
164 } else {
165 let backend_wait = backend_for_thread.clone();
166 rt.handle().spawn(async move {
167 for join in handles {
168 let _ = join.await;
169 }
170 backend_wait.end_store_work();
171 });
172 }
173
174 sync_subs(&state_for_thread.lock());
175 }
176 Err(RecvTimeoutError::Timeout) => continue,
177 Err(RecvTimeoutError::Disconnected) => break,
178 }
179 }
180 })
181 .map_err(RuntimeError::ThreadSpawn)?;
182
183 Ok(Self {
184 state,
185 bus,
186 env,
187 backend,
188 shutdown,
189 _thread: Some(thread),
190 _tokio: tokio,
191 _subscriptions: sub_handles,
192 })
193 }
194
195 pub fn store(&self) -> crate::Store<S, M>
196 where
197 S: Clone + Send + Sync + 'static,
198 {
199 self.backend.store()
200 }
201
202 pub fn dispatch(&self, msg: M) {
203 let _ = self.bus.sender().send_blocking(msg);
204 }
205
206 pub fn sender(&self) -> BusSender<M> {
207 self.bus.sender()
208 }
209
210 pub fn cancel(&self, id: EffectId) {
211 if let Some(handle) = self.backend.hub.interpreter.cancel_tokens.lock().remove(&id) {
212 handle.abort();
213 }
214 }
215
216 pub fn shutdown(mut self) {
217 super::lifecycle::join_reducer_thread(&self.shutdown, &self._subscriptions, &mut self._thread);
218 }
219}
220
221impl<S, M> Drop for Runtime<S, M> {
222 fn drop(&mut self) {
223 super::lifecycle::join_reducer_thread(&self.shutdown, &self._subscriptions, &mut self._thread);
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::effect::Effect;
231 use crate::panic_on_state_clone;
232
233 panic_on_state_clone! {
234 #[derive(Default)]
235 struct Counter {
236 n: i32,
237 }
238 }
239
240 fn init() -> (Counter, Cmd<i32>) {
241 (Counter::default(), Cmd::none())
242 }
243
244 fn update(s: &mut Counter, msg: i32) -> Cmd<i32> {
245 s.n += msg;
246 Cmd::none()
247 }
248
249 fn subs(_: &Counter) -> Sub<i32> {
250 Sub::none()
251 }
252
253 fn boot(program: Program<Counter, i32>) -> Runtime<Counter, i32> {
254 match Runtime::from_program(program, Environment::new(), RuntimeConfig::new(16)) {
255 Ok(runtime) => runtime,
256 Err(err) => panic!("test runtime bootstrap failed: {err}"),
257 }
258 }
259
260 #[test]
261 fn runtime_tick_subscription_dispatches() {
262 fn subs(_: &Counter) -> Sub<i32> {
263 Sub::tick(7, Duration::from_millis(30), || 1)
264 }
265
266 let program = Program::new(init, update, subs);
267 let runtime = boot(program);
268 std::thread::sleep(Duration::from_millis(200));
269 assert!(runtime.state.lock().n >= 1);
270 runtime.shutdown();
271 }
272
273 #[test]
274 fn runtime_applies_dispatched_messages() {
275 let program = Program::new(init, update, subs);
276 let runtime = boot(program);
277 runtime.dispatch(3);
278 runtime.dispatch(4);
279 std::thread::sleep(Duration::from_millis(200));
280 assert_eq!(runtime.state.lock().n, 7);
281 runtime.shutdown();
282 }
283
284 #[test]
285 fn runtime_runs_task_effects() {
286 fn update_with_effect(s: &mut Counter, msg: i32) -> Cmd<i32> {
287 if msg == 0 {
288 Cmd::single(Effect::task(1, || Box::pin(async { Ok(10) })))
289 } else {
290 s.n += msg;
291 Cmd::none()
292 }
293 }
294 let program = Program::new(init, update_with_effect, subs);
295 let runtime = boot(program);
296 runtime.dispatch(0);
297 std::thread::sleep(Duration::from_millis(200));
298 assert_eq!(runtime.state.lock().n, 10);
299 runtime.shutdown();
300 }
301
302 #[test]
303 fn runtime_safe_reduce_update_unwinds_store_work() {
304 fn panicking_update(s: &mut Counter, msg: i32) -> Cmd<i32> {
305 s.n = 99;
306 if msg < 0 {
307 panic!("reduce panic");
308 }
309 s.n = msg;
310 Cmd::none()
311 }
312
313 let program = Program::new(init, panicking_update, subs);
314 let runtime = boot(program);
315 let store = runtime.store();
316 let task = store.send(-1);
317 assert!(task.finish().is_ok());
318 assert_eq!(runtime.state.lock().n, 99);
319
320 runtime.dispatch(4);
321 std::thread::sleep(Duration::from_millis(200));
322 assert_eq!(runtime.state.lock().n, 4);
323 runtime.shutdown();
324 }
325}