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