1use std::collections::VecDeque;
2use std::marker::PhantomData;
3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::Arc;
5use std::time::Duration;
6
7use crossbeam_channel::{Receiver, Sender};
8use parking_lot::Mutex;
9
10use crate::bus::BusSender;
11use crate::effect::EffectId;
12use crate::optics::Casepath;
13use key_paths_core::RefKpTrait;
14use crate::runtime::InterpreterState;
15
16pub(crate) struct StoreWorkUnwindGuard<'a, S, M>
21where
22 S: Send + 'static,
23 M: Send + 'static,
24{
25 backend: &'a StoreBackend<S, M>,
26 active: bool,
27}
28
29impl<'a, S, M> StoreWorkUnwindGuard<'a, S, M>
30where
31 S: Send + 'static,
32 M: Send + 'static,
33{
34 pub(crate) fn new(backend: &'a StoreBackend<S, M>) -> Self {
35 Self {
36 backend,
37 active: true,
38 }
39 }
40
41 pub(crate) fn disarm(&mut self) {
42 self.active = false;
43 }
44}
45
46impl<S, M> Drop for StoreWorkUnwindGuard<'_, S, M>
47where
48 S: Send + 'static,
49 M: Send + 'static,
50{
51 fn drop(&mut self) {
52 if self.active {
53 self.backend.end_store_work();
54 }
55 }
56}
57
58pub struct Store<S, M> {
63 pub(crate) backend: StoreBackend<S, M>,
64}
65
66impl<S, M> Clone for Store<S, M>
67where
68 S: Send + 'static,
69 M: Send + 'static,
70{
71 fn clone(&self) -> Self {
72 Self {
73 backend: self.backend.clone(),
74 }
75 }
76}
77
78pub(crate) struct StoreBackend<S, M> {
79 pub state: Arc<Mutex<S>>,
80 pub sender: BusSender<M>,
81 state_listeners: Arc<Mutex<Vec<Sender<()>>>>,
82 effect_done: Arc<Mutex<VecDeque<Sender<()>>>>,
83 in_flight: Arc<AtomicUsize>,
84 pub interpreter: Arc<InterpreterState<M>>,
85}
86
87impl<S, M> Clone for StoreBackend<S, M>
88where
89 S: Send + 'static,
90 M: Send + 'static,
91{
92 fn clone(&self) -> Self {
93 Self {
94 state: self.state.clone(),
95 sender: self.sender.clone(),
96 state_listeners: self.state_listeners.clone(),
97 effect_done: self.effect_done.clone(),
98 in_flight: self.in_flight.clone(),
99 interpreter: self.interpreter.clone(),
100 }
101 }
102}
103
104impl<S, M> StoreBackend<S, M>
105where
106 S: Send + 'static,
107 M: Send + 'static,
108{
109 pub fn new(
110 state: Arc<Mutex<S>>,
111 sender: BusSender<M>,
112 interpreter: Arc<InterpreterState<M>>,
113 ) -> Self {
114 Self {
115 state,
116 sender,
117 state_listeners: Arc::new(Mutex::new(Vec::new())),
118 effect_done: Arc::new(Mutex::new(VecDeque::new())),
119 in_flight: Arc::new(AtomicUsize::new(0)),
120 interpreter,
121 }
122 }
123
124 pub fn begin_store_work(&self) {
125 self.in_flight.fetch_add(1, Ordering::SeqCst);
126 }
127
128 pub fn end_store_work(&self) {
129 let prev = self.in_flight.fetch_sub(1, Ordering::SeqCst);
130 if prev == 1 {
131 self.notify_state();
132 self.signal_effect_done();
133 }
134 }
135
136 pub fn store(&self) -> Store<S, M> {
137 Store {
138 backend: self.clone(),
139 }
140 }
141
142 pub fn notify_state(&self) {
143 self.state_listeners
144 .lock()
145 .retain(|tx| tx.send(()).is_ok());
146 }
147
148 pub fn pop_effect_done(&self) -> Option<Sender<()>> {
149 self.effect_done.lock().pop_front()
150 }
151
152 pub fn signal_effect_done(&self) {
153 if let Some(tx) = self.pop_effect_done() {
154 let _ = tx.send(());
155 }
156 }
157}
158
159impl<S, M> Store<S, M>
160where
161 S: Send + Sync + Clone + 'static,
162 M: Send + 'static,
163{
164 pub fn send(&self, action: M) -> StoreTask {
166 let (tx, rx) = crossbeam_channel::bounded(1);
167 self.backend.effect_done.lock().push_back(tx);
168 self.backend.begin_store_work();
169 let _ = self.backend.sender.send_blocking(action);
170 StoreTask { rx }
171 }
172
173 pub fn dispatch(&self, action: M) {
175 let _ = self.send(action);
176 }
177
178 pub fn state(&self) -> S {
179 self.backend.state.lock().clone()
180 }
181
182 pub fn cancel(&self, id: EffectId) {
183 if let Some(handle) = self.backend.interpreter.cancel_tokens.lock().remove(&id) {
184 handle.abort();
185 }
186 }
187
188 pub fn subscribe_state(&self) -> StateSubscriber<S>
190 where
191 S: Clone + PartialEq,
192 {
193 let (tx, rx) = crossbeam_channel::unbounded();
194 self.backend.state_listeners.lock().push(tx);
195 StateSubscriber {
196 rx,
197 state: self.backend.state.clone(),
198 last: Some(Arc::new(self.backend.state.lock().clone())),
199 }
200 }
201
202 pub fn scope<CS, CM, AK, SK>(
204 &self,
205 state_kp: SK,
206 action_kp: AK,
207 ) -> ScopedStore<S, M, CS, CM, AK, SK>
208 where
209 CS: Clone + PartialEq + Send + Sync + 'static,
210 CM: Clone + Send + 'static,
211 S: 'static,
212 M: 'static,
213 AK: Casepath<M, CM> + Clone + Send + Sync + 'static,
214 SK: RefKpTrait<S, CS> + Clone,
215 {
216 ScopedStore {
217 store: self.clone(),
218 state_kp,
219 action_kp,
220 _marker: PhantomData,
221 }
222 }
223}
224
225pub struct StoreTask {
227 rx: Receiver<()>,
228}
229
230impl StoreTask {
231 pub fn finish(self) -> Result<(), StoreTaskError> {
232 self.finish_with_timeout(Duration::from_secs(5))
233 }
234
235 pub fn finish_with_timeout(self, timeout: Duration) -> Result<(), StoreTaskError> {
236 self.rx
237 .recv_timeout(timeout)
238 .map_err(|_| StoreTaskError::Timeout)
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum StoreTaskError {
244 Timeout,
245}
246
247impl std::fmt::Display for StoreTaskError {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 match self {
250 Self::Timeout => write!(f, "timed out waiting for store effects to finish"),
251 }
252 }
253}
254
255impl std::error::Error for StoreTaskError {}
256
257pub struct StateSubscriber<S> {
259 rx: Receiver<()>,
260 state: Arc<Mutex<S>>,
261 last: Option<Arc<S>>,
262}
263
264impl<S: PartialEq + Clone> StateSubscriber<S> {
265 #[allow(clippy::should_implement_trait)]
266 pub fn next(&mut self) -> Option<Arc<S>> {
267 loop {
268 match self.rx.try_recv() {
269 Ok(()) => {
270 while self.rx.try_recv().is_ok() {}
271 let snapshot = Arc::new(self.state.lock().clone());
272 if self
273 .last
274 .as_ref()
275 .is_some_and(|prev| prev.as_ref() == snapshot.as_ref())
276 {
277 continue;
278 }
279 self.last = Some(snapshot.clone());
280 return Some(snapshot);
281 }
282 Err(crossbeam_channel::TryRecvError::Empty) => return None,
283 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
284 }
285 }
286 }
287
288 pub fn wait_next(&mut self, timeout: Duration) -> Option<Arc<S>> {
289 let deadline = std::time::Instant::now() + timeout;
290 while std::time::Instant::now() < deadline {
291 if let Some(s) = self.next() {
292 return Some(s);
293 }
294 std::thread::sleep(Duration::from_millis(5));
295 }
296 None
297 }
298
299 pub fn latest(&self) -> Option<Arc<S>> {
300 self.last.clone()
301 }
302}
303
304pub struct ScopedStore<S: 'static, M: 'static, CS: 'static, CM: 'static, AK: 'static, SK>
306where
307 SK: RefKpTrait<S, CS> + Clone,
308{
309 store: Store<S, M>,
310 state_kp: SK,
311 action_kp: AK,
312 _marker: PhantomData<(M, CM, S, CS)>,
313}
314
315impl<S: 'static, M: 'static, CS: 'static, CM: 'static, AK: 'static, SK> Clone
316 for ScopedStore<S, M, CS, CM, AK, SK>
317where
318 S: Send,
319 M: Send,
320 AK: Clone,
321 SK: RefKpTrait<S, CS> + Clone,
322{
323 fn clone(&self) -> Self {
324 Self {
325 store: self.store.clone(),
326 state_kp: self.state_kp.clone(),
327 action_kp: self.action_kp.clone(),
328 _marker: PhantomData,
329 }
330 }
331}
332
333impl<S: 'static, M: 'static, CS: 'static, CM: 'static, AK: 'static, SK>
334 ScopedStore<S, M, CS, CM, AK, SK>
335where
336 S: Send + Sync + Clone,
337 M: Send,
338 CS: Clone + PartialEq + Send + Sync,
339 CM: Clone + Send,
340 AK: Casepath<M, CM> + Clone + Send + Sync + 'static,
341 SK: RefKpTrait<S, CS> + Clone,
342{
343 pub fn send(&self, action: CM) -> StoreTask {
344 self.store.send(self.action_kp.wrap(action))
345 }
346
347 pub fn dispatch(&self, action: CM) {
348 self.store.dispatch(self.action_kp.wrap(action));
349 }
350
351 pub fn child_state(&self) -> Option<CS> {
352 self.state_kp.focus(&self.store.state()).cloned()
353 }
354
355 pub fn subscribe_state(&self) -> ScopedStateSubscriber<S, CS, SK>
356 where
357 S: Clone + PartialEq,
358 SK: Clone,
359 {
360 ScopedStateSubscriber {
361 inner: self.store.subscribe_state(),
362 state_kp: self.state_kp.clone(),
363 _marker: PhantomData,
364 }
365 }
366}
367
368pub struct ScopedStateSubscriber<S: 'static, CS: 'static, SK>
369where
370 SK: RefKpTrait<S, CS> + Clone,
371{
372 inner: StateSubscriber<S>,
373 state_kp: SK,
374 _marker: PhantomData<(S, CS)>,
375}
376
377impl<S, CS, SK> ScopedStateSubscriber<S, CS, SK>
378where
379 S: PartialEq + Clone,
380 CS: Clone + PartialEq,
381 SK: RefKpTrait<S, CS> + Clone,
382{
383 #[allow(clippy::should_implement_trait)]
384 pub fn next(&mut self) -> Option<CS> {
385 loop {
386 let parent = self.inner.next()?;
387 let Some(child) = self.state_kp.focus(parent.as_ref()).cloned() else {
388 continue;
389 };
390 return Some(child);
391 }
392 }
393}