fret_runtime/
action_payload.rs1use std::any::Any;
2use std::collections::HashMap;
3
4use fret_core::AppWindowId;
5
6use crate::{ActionId, TickId};
7
8#[derive(Debug)]
9struct PendingActionPayloadV1 {
10 tick_id: TickId,
11 window: AppWindowId,
12 action: ActionId,
13 payload: Box<dyn Any + Send + Sync>,
14}
15
16#[derive(Default)]
25pub struct WindowPendingActionPayloadService {
26 per_window: HashMap<AppWindowId, Vec<PendingActionPayloadV1>>,
27}
28
29impl WindowPendingActionPayloadService {
30 const MAX_PENDING_PER_WINDOW: usize = 32;
31 const PENDING_TTL_TICKS: u64 = 64;
32
33 pub fn record(
34 &mut self,
35 window: AppWindowId,
36 tick_id: TickId,
37 action: ActionId,
38 payload: Box<dyn Any + Send + Sync>,
39 ) {
40 let pending = PendingActionPayloadV1 {
41 tick_id,
42 window,
43 action,
44 payload,
45 };
46 let entries = self.per_window.entry(window).or_default();
47 entries.push(pending);
48 if entries.len() > Self::MAX_PENDING_PER_WINDOW {
49 let extra = entries.len().saturating_sub(Self::MAX_PENDING_PER_WINDOW);
50 entries.drain(0..extra);
51 }
52 }
53
54 pub fn consume(
55 &mut self,
56 window: AppWindowId,
57 tick_id: TickId,
58 action: &ActionId,
59 ) -> Option<Box<dyn Any + Send + Sync>> {
60 let entries = self.per_window.get_mut(&window)?;
61
62 let min_tick = TickId(tick_id.0.saturating_sub(Self::PENDING_TTL_TICKS));
69 entries.retain(|e| e.tick_id.0 >= min_tick.0 && e.tick_id.0 <= tick_id.0);
70
71 let pos = entries
72 .iter()
73 .rposition(|e| &e.action == action && e.window == window)?;
74 Some(entries.remove(pos).payload)
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn pending_payload_expires_across_ticks() {
84 let mut svc = WindowPendingActionPayloadService::default();
85 let window = AppWindowId::default();
86 let action = ActionId::from("test.payload_action");
87
88 svc.record(
89 window,
90 TickId(10),
91 action.clone(),
92 Box::new(123u32) as Box<dyn Any + Send + Sync>,
93 );
94
95 assert!(svc.consume(window, TickId(10), &action).is_some());
96
97 svc.record(
98 window,
99 TickId(10),
100 action.clone(),
101 Box::new(456u32) as Box<dyn Any + Send + Sync>,
102 );
103
104 assert!(svc.consume(window, TickId(75), &action).is_none());
106 }
107
108 #[test]
109 fn pending_payload_consumes_most_recent() {
110 let mut svc = WindowPendingActionPayloadService::default();
111 let window = AppWindowId::default();
112 let action = ActionId::from("test.payload_action");
113
114 svc.record(
115 window,
116 TickId(10),
117 action.clone(),
118 Box::new(1u32) as Box<dyn Any + Send + Sync>,
119 );
120 svc.record(
121 window,
122 TickId(11),
123 action.clone(),
124 Box::new(2u32) as Box<dyn Any + Send + Sync>,
125 );
126
127 let payload = svc
128 .consume(window, TickId(11), &action)
129 .expect("payload must exist");
130 let payload = payload.downcast::<u32>().expect("type must match");
131 assert_eq!(*payload, 2);
132 }
133}