1use std::collections::VecDeque;
2use std::hash::Hash;
3use std::marker::PhantomData;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::Arc;
6use std::time::Duration;
7
8use crossbeam_channel::{Receiver, Sender};
9use key_paths_core::{FieldDiff, hash_value};
10use parking_lot::Mutex;
11
12use crate::bus::BusSender;
13use crate::effect::EffectId;
14use crate::optics::Casepath;
15use crate::runtime::binding::StateBinding;
16use key_paths_core::RefKpTrait;
17use super::interpreter::InterpreterState;
18
19pub(crate) struct StoreWorkUnwindGuard<'a, S, M, B: StoreWork<S, M> + ?Sized>
24where
25 S: Send + 'static,
26 M: Send + 'static,
27{
28 backend: &'a B,
29 active: bool,
30 _marker: PhantomData<(S, M)>,
31}
32
33impl<'a, S, M, B> StoreWorkUnwindGuard<'a, S, M, B>
34where
35 S: Send + 'static,
36 M: Send + 'static,
37 B: StoreWork<S, M> + ?Sized,
38{
39 pub(crate) fn new(backend: &'a B) -> Self {
40 Self {
41 backend,
42 active: true,
43 _marker: PhantomData,
44 }
45 }
46
47 pub(crate) fn disarm(&mut self) {
48 self.active = false;
49 }
50}
51
52impl<S, M, B> Drop for StoreWorkUnwindGuard<'_, S, M, B>
53where
54 S: Send + 'static,
55 M: Send + 'static,
56 B: StoreWork<S, M> + ?Sized,
57{
58 fn drop(&mut self) {
59 if self.active {
60 self.backend.end_store_work();
61 }
62 }
63}
64
65pub struct Store<S, M> {
70 pub(crate) backend: StoreBackend<S, M>,
71}
72
73impl<S, M> Clone for Store<S, M>
74where
75 S: Send + 'static,
76 M: Send + 'static,
77{
78 fn clone(&self) -> Self {
79 Self {
80 backend: self.backend.clone(),
81 }
82 }
83}
84
85pub(crate) struct StoreHub<S, M> {
86 pub sender: BusSender<M>,
87 pub state_listeners: Arc<Mutex<Vec<Sender<()>>>>,
88 pub effect_done: Arc<Mutex<VecDeque<Sender<()>>>>,
89 pub in_flight: Arc<AtomicUsize>,
90 pub interpreter: Arc<InterpreterState<M>>,
91 _marker: PhantomData<S>,
92}
93
94pub(crate) trait StoreWork<S, M>
96where
97 S: Send + 'static,
98 M: Send + 'static,
99{
100 fn hub(&self) -> &Arc<StoreHub<S, M>>;
101
102 fn begin_store_work(&self) {
103 self.hub().in_flight.fetch_add(1, Ordering::SeqCst);
104 }
105
106 fn end_store_work(&self) {
107 let prev = self.hub().in_flight.fetch_sub(1, Ordering::SeqCst);
108 if prev == 1 {
109 self.hub().notify_state();
110 self.hub().signal_effect_done();
111 }
112 }
113
114 fn notify_state(&self) {
115 self.hub().notify_state();
116 }
117}
118
119impl<S, M> StoreHub<S, M>
120where
121 S: Send + 'static,
122 M: Send + 'static,
123{
124 pub fn new(sender: BusSender<M>, interpreter: Arc<InterpreterState<M>>) -> Arc<Self> {
125 Arc::new(Self {
126 sender,
127 state_listeners: Arc::new(Mutex::new(Vec::new())),
128 effect_done: Arc::new(Mutex::new(VecDeque::new())),
129 in_flight: Arc::new(AtomicUsize::new(0)),
130 interpreter,
131 _marker: PhantomData,
132 })
133 }
134
135 pub fn notify_state(&self) {
136 self.state_listeners
137 .lock()
138 .retain(|tx| tx.send(()).is_ok());
139 }
140
141 pub fn pop_effect_done(&self) -> Option<Sender<()>> {
142 self.effect_done.lock().pop_front()
143 }
144
145 pub fn signal_effect_done(&self) {
146 if let Some(tx) = self.pop_effect_done() {
147 let _ = tx.send(());
148 }
149 }
150}
151
152pub(crate) struct StoreBackend<S, M> {
153 pub state: Arc<Mutex<S>>,
154 pub hub: Arc<StoreHub<S, M>>,
155}
156
157impl<S, M> Clone for StoreBackend<S, M>
158where
159 S: Send + 'static,
160 M: Send + 'static,
161{
162 fn clone(&self) -> Self {
163 Self {
164 state: self.state.clone(),
165 hub: self.hub.clone(),
166 }
167 }
168}
169
170impl<S, M> StoreWork<S, M> for StoreBackend<S, M>
171where
172 S: Send + 'static,
173 M: Send + 'static,
174{
175 fn hub(&self) -> &Arc<StoreHub<S, M>> {
176 &self.hub
177 }
178}
179
180impl<S, M> StoreBackend<S, M>
181where
182 S: Send + 'static,
183 M: Send + 'static,
184{
185 pub fn new(
186 state: Arc<Mutex<S>>,
187 sender: BusSender<M>,
188 interpreter: Arc<InterpreterState<M>>,
189 ) -> Self {
190 Self {
191 state,
192 hub: StoreHub::new(sender, interpreter),
193 }
194 }
195
196 pub fn store(&self) -> Store<S, M> {
197 Store {
198 backend: self.clone(),
199 }
200 }
201}
202
203impl<S, M> Store<S, M>
204where
205 S: Send + Sync + Clone + 'static,
206 M: Send + 'static,
207{
208 pub fn send(&self, action: M) -> StoreTask {
210 let (tx, rx) = crossbeam_channel::bounded(1);
211 self.backend.hub.effect_done.lock().push_back(tx);
212 self.backend.begin_store_work();
213 let _ = self.backend.hub.sender.send_blocking(action);
214 StoreTask::from_rx(rx)
215 }
216
217 pub fn dispatch(&self, action: M) {
219 let _ = self.send(action);
220 }
221
222 pub fn state(&self) -> S {
223 self.backend.state.lock().clone()
224 }
225
226 pub fn binding(&self) -> StateBinding<S> {
228 StateBinding::new(Arc::clone(&self.backend.state))
229 }
230
231 pub fn cancel(&self, id: EffectId) {
232 if let Some(handle) = self.backend.hub.interpreter.cancel_tokens.lock().remove(&id) {
233 handle.abort();
234 }
235 }
236
237 pub fn subscribe_state(&self) -> StateSubscriber<S>
239 where
240 S: Clone + PartialEq,
241 {
242 let (tx, rx) = crossbeam_channel::unbounded();
243 self.backend.hub.state_listeners.lock().push(tx);
244 StateSubscriber {
245 rx,
246 state: self.backend.state.clone(),
247 last: Some(Arc::new(self.backend.state.lock().clone())),
248 }
249 }
250
251 pub fn subscribe_changes(&self) -> ChangeSubscriber<S>
256 where
257 S: FieldDiff + Hash,
258 {
259 let (tx, rx) = crossbeam_channel::unbounded();
260 self.backend.hub.state_listeners.lock().push(tx);
261 let (last_hash, last_fields) = {
262 let guard = self.backend.state.lock();
263 let root = hash_value(&*guard);
264 let mut fields = Vec::new();
265 guard.field_hashes(&mut fields);
266 (Some(root), fields)
267 };
268 ChangeSubscriber {
269 rx,
270 state: self.backend.state.clone(),
271 last_hash,
272 last_fields,
273 _marker: PhantomData,
274 }
275 }
276
277 pub fn scope<CS, CM, AK, SK>(
279 &self,
280 state_kp: SK,
281 action_kp: AK,
282 ) -> ScopedStore<S, M, CS, CM, AK, SK>
283 where
284 CS: Clone + PartialEq + Send + Sync + 'static,
285 CM: Clone + Send + 'static,
286 S: 'static,
287 M: 'static,
288 AK: Casepath<M, CM> + Clone + Send + Sync + 'static,
289 SK: RefKpTrait<S, CS> + Clone,
290 {
291 ScopedStore {
292 store: self.clone(),
293 state_kp,
294 action_kp,
295 _marker: PhantomData,
296 }
297 }
298}
299
300pub struct StoreTask {
302 rx: Receiver<()>,
303}
304
305impl StoreTask {
306 pub(crate) fn from_rx(rx: Receiver<()>) -> Self {
307 Self { rx }
308 }
309
310 pub fn finish(self) -> Result<(), StoreTaskError> {
311 self.finish_with_timeout(Duration::from_secs(5))
312 }
313
314 pub fn finish_with_timeout(self, timeout: Duration) -> Result<(), StoreTaskError> {
315 self.rx
316 .recv_timeout(timeout)
317 .map_err(|_| StoreTaskError::Timeout)
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub enum StoreTaskError {
323 Timeout,
324}
325
326impl std::fmt::Display for StoreTaskError {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 match self {
329 Self::Timeout => write!(f, "timed out waiting for store effects to finish"),
330 }
331 }
332}
333
334impl std::error::Error for StoreTaskError {}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct ChangeSet<P> {
339 pub paths: Vec<P>,
340}
341
342pub struct ChangeSubscriber<S>
344where
345 S: FieldDiff + Hash,
346{
347 rx: Receiver<()>,
348 state: Arc<Mutex<S>>,
349 last_hash: Option<u64>,
350 last_fields: Vec<(S::Path, u64)>,
351 _marker: PhantomData<S>,
352}
353
354impl<S> ChangeSubscriber<S>
355where
356 S: FieldDiff + Hash,
357{
358 fn snapshot_fields(&self) -> (u64, Vec<(S::Path, u64)>) {
359 let guard = self.state.lock();
360 let root = hash_value(&*guard);
361 let mut fields = Vec::new();
362 guard.field_hashes(&mut fields);
363 (root, fields)
364 }
365
366 fn diff_fields(&self, fields: &[(S::Path, u64)]) -> Vec<S::Path> {
367 fields
368 .iter()
369 .filter(|(path, hash)| {
370 self.last_fields
371 .iter()
372 .find(|(prev_path, _)| prev_path == path)
373 .is_none_or(|(_, prev_hash)| prev_hash != hash)
374 })
375 .map(|(path, _)| *path)
376 .collect()
377 }
378
379 pub fn binding(&self) -> StateBinding<S> {
381 StateBinding::new(Arc::clone(&self.state))
382 }
383
384 #[allow(clippy::should_implement_trait)]
385 pub fn next(&mut self) -> Option<ChangeSet<S::Path>> {
386 loop {
387 match self.rx.try_recv() {
388 Ok(()) => {
389 while self.rx.try_recv().is_ok() {}
390 let (root_hash, fields) = self.snapshot_fields();
391 if Some(root_hash) == self.last_hash {
392 continue;
393 }
394 let paths = self.diff_fields(&fields);
395 self.last_hash = Some(root_hash);
396 self.last_fields = fields;
397 if paths.is_empty() {
398 continue;
399 }
400 return Some(ChangeSet { paths });
401 }
402 Err(crossbeam_channel::TryRecvError::Empty) => return None,
403 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
404 }
405 }
406 }
407
408 pub fn wait_next(&mut self, timeout: Duration) -> Option<ChangeSet<S::Path>> {
409 let deadline = std::time::Instant::now() + timeout;
410 while std::time::Instant::now() < deadline {
411 if let Some(change) = self.next() {
412 return Some(change);
413 }
414 std::thread::sleep(Duration::from_millis(5));
415 }
416 None
417 }
418}
419
420pub struct StateSubscriber<S> {
422 rx: Receiver<()>,
423 state: Arc<Mutex<S>>,
424 last: Option<Arc<S>>,
425}
426
427impl<S: PartialEq + Clone> StateSubscriber<S> {
428 #[allow(clippy::should_implement_trait)]
429 pub fn next(&mut self) -> Option<Arc<S>> {
430 loop {
431 match self.rx.try_recv() {
432 Ok(()) => {
433 while self.rx.try_recv().is_ok() {}
434 let snapshot = Arc::new(self.state.lock().clone());
435 if self
436 .last
437 .as_ref()
438 .is_some_and(|prev| prev.as_ref() == snapshot.as_ref())
439 {
440 continue;
441 }
442 self.last = Some(snapshot.clone());
443 return Some(snapshot);
444 }
445 Err(crossbeam_channel::TryRecvError::Empty) => return None,
446 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
447 }
448 }
449 }
450
451 pub fn wait_next(&mut self, timeout: Duration) -> Option<Arc<S>> {
452 let deadline = std::time::Instant::now() + timeout;
453 while std::time::Instant::now() < deadline {
454 if let Some(s) = self.next() {
455 return Some(s);
456 }
457 std::thread::sleep(Duration::from_millis(5));
458 }
459 None
460 }
461
462 pub fn latest(&self) -> Option<Arc<S>> {
463 self.last.clone()
464 }
465}
466
467pub struct ScopedStore<S: 'static, M: 'static, CS: 'static, CM: 'static, AK: 'static, SK>
469where
470 SK: RefKpTrait<S, CS> + Clone,
471{
472 store: Store<S, M>,
473 state_kp: SK,
474 action_kp: AK,
475 _marker: PhantomData<(M, CM, S, CS)>,
476}
477
478impl<S: 'static, M: 'static, CS: 'static, CM: 'static, AK: 'static, SK> Clone
479 for ScopedStore<S, M, CS, CM, AK, SK>
480where
481 S: Send,
482 M: Send,
483 AK: Clone,
484 SK: RefKpTrait<S, CS> + Clone,
485{
486 fn clone(&self) -> Self {
487 Self {
488 store: self.store.clone(),
489 state_kp: self.state_kp.clone(),
490 action_kp: self.action_kp.clone(),
491 _marker: PhantomData,
492 }
493 }
494}
495
496impl<S: 'static, M: 'static, CS: 'static, CM: 'static, AK: 'static, SK>
497 ScopedStore<S, M, CS, CM, AK, SK>
498where
499 S: Send + Sync + Clone,
500 M: Send,
501 CS: Clone + PartialEq + Send + Sync,
502 CM: Clone + Send,
503 AK: Casepath<M, CM> + Clone + Send + Sync + 'static,
504 SK: RefKpTrait<S, CS> + Clone,
505{
506 pub fn send(&self, action: CM) -> StoreTask {
507 self.store.send(self.action_kp.wrap(action))
508 }
509
510 pub fn dispatch(&self, action: CM) {
511 self.store.dispatch(self.action_kp.wrap(action));
512 }
513
514 pub fn child_state(&self) -> Option<CS> {
515 self.state_kp.focus(&self.store.state()).cloned()
516 }
517
518 pub fn binding(&self) -> crate::runtime::binding::ProjectedBinding<S, CS, SK> {
520 self.store.binding().project(self.state_kp.clone())
521 }
522
523 pub fn subscribe_state(&self) -> ScopedStateSubscriber<S, CS, SK>
524 where
525 S: Clone + PartialEq,
526 SK: Clone,
527 {
528 ScopedStateSubscriber {
529 inner: self.store.subscribe_state(),
530 state_kp: self.state_kp.clone(),
531 _marker: PhantomData,
532 }
533 }
534
535 pub fn subscribe_changes(&self) -> ScopedChangeSubscriber<S, CS, SK>
537 where
538 CS: FieldDiff + Hash,
539 {
540 let (tx, rx) = crossbeam_channel::unbounded();
541 self.store.backend.hub.state_listeners.lock().push(tx);
542 let (last_hash, last_fields) = {
543 let guard = self.store.backend.state.lock();
544 if let Some(child) = self.state_kp.focus(&*guard) {
545 let root = hash_value(child);
546 let mut fields = Vec::new();
547 child.field_hashes(&mut fields);
548 (Some(root), fields)
549 } else {
550 (None, Vec::new())
551 }
552 };
553 ScopedChangeSubscriber {
554 rx,
555 state: self.store.backend.state.clone(),
556 state_kp: self.state_kp.clone(),
557 last_hash,
558 last_fields,
559 _marker: PhantomData,
560 }
561 }
562}
563
564pub struct ScopedStateSubscriber<S: 'static, CS: 'static, SK>
565where
566 SK: RefKpTrait<S, CS> + Clone,
567{
568 inner: StateSubscriber<S>,
569 state_kp: SK,
570 _marker: PhantomData<(S, CS)>,
571}
572
573impl<S, CS, SK> ScopedStateSubscriber<S, CS, SK>
574where
575 S: PartialEq + Clone,
576 CS: Clone + PartialEq,
577 SK: RefKpTrait<S, CS> + Clone,
578{
579 #[allow(clippy::should_implement_trait)]
580 pub fn next(&mut self) -> Option<CS> {
581 loop {
582 let parent = self.inner.next()?;
583 let Some(child) = self.state_kp.focus(parent.as_ref()).cloned() else {
584 continue;
585 };
586 return Some(child);
587 }
588 }
589}
590
591pub struct ScopedChangeSubscriber<S: 'static, CS: 'static, SK>
593where
594 CS: FieldDiff,
595 SK: RefKpTrait<S, CS> + Clone,
596{
597 rx: Receiver<()>,
598 state: Arc<Mutex<S>>,
599 state_kp: SK,
600 last_hash: Option<u64>,
601 last_fields: Vec<(CS::Path, u64)>,
602 _marker: PhantomData<(S, CS)>,
603}
604
605impl<S, CS, SK> ScopedChangeSubscriber<S, CS, SK>
606where
607 S: Send + Sync,
608 CS: FieldDiff + Hash + Send + Sync,
609 SK: RefKpTrait<S, CS> + Clone,
610{
611 fn snapshot_fields(&self) -> Option<(u64, Vec<(CS::Path, u64)>)> {
612 let guard = self.state.lock();
613 let child = self.state_kp.focus(&*guard)?;
614 let root = hash_value(child);
615 let mut fields = Vec::new();
616 child.field_hashes(&mut fields);
617 Some((root, fields))
618 }
619
620 fn diff_fields(&self, fields: &[(CS::Path, u64)]) -> Vec<CS::Path> {
621 fields
622 .iter()
623 .filter(|(path, hash)| {
624 self.last_fields
625 .iter()
626 .find(|(prev_path, _)| prev_path == path)
627 .is_none_or(|(_, prev_hash)| prev_hash != hash)
628 })
629 .map(|(path, _)| *path)
630 .collect()
631 }
632
633 pub fn binding(&self) -> crate::runtime::binding::ProjectedBinding<S, CS, SK> {
634 crate::runtime::binding::StateBinding::new(Arc::clone(&self.state))
635 .project(self.state_kp.clone())
636 }
637
638 #[allow(clippy::should_implement_trait)]
639 pub fn next(&mut self) -> Option<ChangeSet<CS::Path>> {
640 loop {
641 match self.rx.try_recv() {
642 Ok(()) => {
643 while self.rx.try_recv().is_ok() {}
644 let Some((root_hash, fields)) = self.snapshot_fields() else {
645 continue;
646 };
647 if Some(root_hash) == self.last_hash {
648 continue;
649 }
650 let paths = self.diff_fields(&fields);
651 self.last_hash = Some(root_hash);
652 self.last_fields = fields;
653 if paths.is_empty() {
654 continue;
655 }
656 return Some(ChangeSet { paths });
657 }
658 Err(crossbeam_channel::TryRecvError::Empty) => return None,
659 Err(crossbeam_channel::TryRecvError::Disconnected) => return None,
660 }
661 }
662 }
663
664 pub fn wait_next(&mut self, timeout: Duration) -> Option<ChangeSet<CS::Path>> {
665 let deadline = std::time::Instant::now() + timeout;
666 while std::time::Instant::now() < deadline {
667 if let Some(change) = self.next() {
668 return Some(change);
669 }
670 std::thread::sleep(Duration::from_millis(5));
671 }
672 None
673 }
674}