1use std::cell::RefCell;
9use std::collections::VecDeque;
10use std::hash::Hash;
11use std::rc::Rc;
12
13use bolero::generator::bolero_generator::driver::object::Borrowed;
14use bolero::{ValueGenerator, produce};
15use dfir_rs::rustc_hash::{FxHashMap, FxHashSet};
16use dfir_rs::util::unsync::mpsc::Sender;
17
18use super::{
19 HookLocationMeta, ManualDebug, RuntimeHook, ScriptDecision, ScriptableHook,
20 ScriptableTickInputHook, TickInputHook, TruncatedVecDebug, abort, describe_keyed_pending,
21 keyed_buffer_len, log_release,
22};
23use crate::live_collections::stream::{NoOrder, Ordering, TotalOrder};
24
25pub struct StreamHook<T, Order: Ordering> {
26 pub input: Rc<RefCell<VecDeque<T>>>,
27 pub to_release: Option<Vec<T>>,
28 pub output: Sender<T>,
29 pub batch_location: HookLocationMeta,
30 pub format_item_debug: fn(&T) -> Option<String>,
31 pub _order: std::marker::PhantomData<Order>,
32}
33
34impl<T> RuntimeHook for StreamHook<T, TotalOrder> {
35 fn has_pending_input(&self) -> bool {
36 !self.input.borrow().is_empty()
37 }
38
39 fn only_one_possible_decision(&self) -> bool {
40 self.input.borrow().is_empty()
42 }
43
44 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
45 if let Some(to_release) = self.to_release.take() {
46 if let Some(log_writer) = log_writer {
47 let HookLocationMeta {
48 location: batch_location,
49 line,
50 caret_indent,
51 } = self.batch_location;
52 let note_str = if to_release.is_empty() {
53 "^ releasing no items".to_owned()
54 } else {
55 format!(
56 "^ releasing items: {:?}",
57 TruncatedVecDebug(
58 RefCell::new(Some(to_release.iter())),
59 8,
60 self.format_item_debug
61 )
62 )
63 };
64
65 log_release(
66 log_writer,
67 batch_location,
68 line,
69 caret_indent,
70 ¬e_str,
71 colored::Color::Green,
72 );
73 }
74
75 for item in to_release {
76 self.output.try_send(item).unwrap();
77 }
78 } else {
79 panic!("No decision to release");
80 }
81 }
82
83 fn location_meta(&self) -> HookLocationMeta {
84 self.batch_location
85 }
86}
87
88impl<T> TickInputHook for StreamHook<T, TotalOrder> {
89 fn can_trigger_tick(&self) -> bool {
90 !self.input.borrow().is_empty()
91 }
92
93 fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>, force_trigger: bool) -> bool {
94 let mut current_input = self.input.borrow_mut();
95 let count = ((if force_trigger { 1 } else { 0 })..=current_input.len())
96 .generate(driver)
97 .unwrap();
98
99 self.to_release = Some(current_input.drain(0..count).collect());
100 count > 0
101 }
102}
103
104impl<T> RuntimeHook for StreamHook<T, NoOrder> {
105 fn has_pending_input(&self) -> bool {
106 !self.input.borrow().is_empty()
107 }
108
109 fn only_one_possible_decision(&self) -> bool {
110 self.input.borrow().is_empty()
112 }
113
114 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
115 if let Some(to_release) = self.to_release.take() {
116 if let Some(log_writer) = log_writer {
117 let HookLocationMeta {
118 location: batch_location,
119 line,
120 caret_indent,
121 } = self.batch_location;
122 let note_str = if to_release.is_empty() {
123 "^ releasing no items".to_owned()
124 } else {
125 format!(
126 "^ releasing unordered items: {:?}",
127 TruncatedVecDebug(
128 RefCell::new(Some(to_release.iter())),
129 8,
130 self.format_item_debug
131 )
132 )
133 };
134
135 log_release(
136 log_writer,
137 batch_location,
138 line,
139 caret_indent,
140 ¬e_str,
141 colored::Color::Green,
142 );
143 }
144
145 for item in to_release {
146 self.output.try_send(item).unwrap();
147 }
148 } else {
149 panic!("No decision to release");
150 }
151 }
152
153 fn location_meta(&self) -> HookLocationMeta {
154 self.batch_location
155 }
156}
157
158impl<T> TickInputHook for StreamHook<T, NoOrder> {
159 fn can_trigger_tick(&self) -> bool {
160 !self.input.borrow().is_empty()
161 }
162
163 fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>, force_trigger: bool) -> bool {
164 let mut current_input = self.input.borrow_mut();
165 let mut out = vec![];
166 let mut min_index = 0;
167 while !current_input.is_empty() {
168 let must_release = force_trigger && out.is_empty();
169 if !must_release && produce().generate(driver).unwrap() {
170 break;
171 }
172
173 let idx = (min_index..current_input.len()).generate(driver).unwrap();
174 let item = current_input.remove(idx).unwrap();
175 out.push(item);
176
177 min_index = idx;
178 if min_index == current_input.len() {
183 break;
184 }
185 }
186
187 let triggered = !out.is_empty();
188 self.to_release = Some(out);
189 triggered
190 }
191}
192
193#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
195pub enum BatchDecision<T> {
196 Prefix(usize),
198 Values(Vec<T>),
200 All,
202}
203
204impl<T> ScriptDecision for BatchDecision<T>
205where
206 T: serde::Serialize + serde::de::DeserializeOwned,
207{
208 fn describe(&self) -> String {
209 match self {
210 BatchDecision::Prefix(n) => format!("release({})", n),
211 BatchDecision::Values(values) => {
212 format!("release_values({} value(s))", values.len())
213 }
214 BatchDecision::All => "release_all()".to_owned(),
215 }
216 }
217}
218
219#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
221pub enum UnorderedBatchDecision<T> {
222 Values(Vec<T>),
224 All,
226}
227
228impl<T> ScriptDecision for UnorderedBatchDecision<T>
229where
230 T: serde::Serialize + serde::de::DeserializeOwned,
231{
232 fn describe(&self) -> String {
233 match self {
234 UnorderedBatchDecision::Values(values) => {
235 format!("release_values({} value(s))", values.len())
236 }
237 UnorderedBatchDecision::All => "release_all()".to_owned(),
238 }
239 }
240}
241
242#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
245pub struct BatchStatus {
246 pub buffered: usize,
248}
249
250impl<T> ScriptableHook for StreamHook<T, TotalOrder>
251where
252 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
253{
254 type Decision = BatchDecision<T>;
255 type Status = BatchStatus;
256
257 fn is_honorable(&self, decision: &BatchDecision<T>) -> Result<bool, String> {
258 let input = self.input.borrow();
259 match decision {
260 BatchDecision::Prefix(n) => Ok(input.len() >= *n),
261 BatchDecision::Values(values) => {
262 if let Some(idx) = input
263 .iter()
264 .zip(values)
265 .position(|(buffered, expected)| buffered != expected)
266 {
267 Err(format!(
268 "release_values: buffered item at prefix position {} did not match the expected value",
269 idx
270 ))
271 } else {
272 Ok(input.len() >= values.len())
273 }
274 }
275 BatchDecision::All => Ok(true),
276 }
277 }
278
279 fn apply(&mut self, decision: BatchDecision<T>) {
280 let mut input = self.input.borrow_mut();
281 let out: Vec<T> = match decision {
282 BatchDecision::Prefix(n) => input.drain(0..n).collect(),
283 BatchDecision::Values(values) => input.drain(0..values.len()).collect(),
284 BatchDecision::All => input.drain(..).collect(),
285 };
286
287 self.to_release = Some(out);
288 }
289
290 fn implicit(&mut self) {
291 self.to_release = Some(vec![]);
292 }
293
294 fn status(&self) -> BatchStatus {
295 BatchStatus {
296 buffered: self.input.borrow().len(),
297 }
298 }
299
300 fn describe_pending(&self) -> Option<String> {
301 let input = self.input.borrow();
302 (!input.is_empty()).then(|| {
303 format!(
304 "{} buffered item(s): {:?}",
305 input.len(),
306 TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
307 )
308 })
309 }
310}
311
312impl<T> ScriptableTickInputHook for StreamHook<T, TotalOrder>
313where
314 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
315{
316 fn decision_triggers_tick(&self, decision: &BatchDecision<T>) -> bool {
317 match decision {
318 BatchDecision::Prefix(n) => *n > 0,
319 BatchDecision::Values(values) => !values.is_empty(),
320 BatchDecision::All => !self.input.borrow().is_empty(),
321 }
322 }
323}
324
325impl<T> ScriptableHook for StreamHook<T, NoOrder>
326where
327 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
328{
329 type Decision = UnorderedBatchDecision<T>;
330 type Status = BatchStatus;
331
332 fn is_honorable(&self, decision: &UnorderedBatchDecision<T>) -> Result<bool, String> {
333 Ok(match decision {
334 UnorderedBatchDecision::Values(values) => {
335 let mut unmatched: Vec<&T> = values.iter().collect();
336 for buffered in self.input.borrow().iter() {
337 if let Some(idx) = unmatched
338 .iter()
339 .position(|requested| *requested == buffered)
340 {
341 unmatched.swap_remove(idx);
342 }
343 }
344 unmatched.is_empty()
345 }
346 UnorderedBatchDecision::All => true,
347 })
348 }
349
350 fn apply(&mut self, decision: UnorderedBatchDecision<T>) {
351 let mut input = self.input.borrow_mut();
352 let out: Vec<T> = match decision {
353 UnorderedBatchDecision::Values(mut values) => {
354 let (selected, remaining): (Vec<_>, Vec<_>) =
355 input.drain(..).partition(|buffered| {
356 values
357 .iter()
358 .position(|requested| requested == buffered)
359 .is_some_and(|idx| {
360 values.swap_remove(idx);
361 true
362 })
363 });
364 assert!(
365 values.is_empty(),
366 "scripted unordered batch decision was not honorable"
367 );
368 *input = remaining.into();
369 selected
370 }
371 UnorderedBatchDecision::All => input.drain(..).collect(),
372 };
373
374 self.to_release = Some(out);
375 }
376
377 fn implicit(&mut self) {
378 self.to_release = Some(vec![]);
379 }
380
381 fn status(&self) -> BatchStatus {
382 BatchStatus {
383 buffered: self.input.borrow().len(),
384 }
385 }
386
387 fn describe_pending(&self) -> Option<String> {
388 let input = self.input.borrow();
389 (!input.is_empty()).then(|| {
390 format!(
391 "{} buffered item(s): {:?}",
392 input.len(),
393 TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
394 )
395 })
396 }
397}
398
399impl<T> ScriptableTickInputHook for StreamHook<T, NoOrder>
400where
401 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
402{
403 fn decision_triggers_tick(&self, decision: &UnorderedBatchDecision<T>) -> bool {
404 match decision {
405 UnorderedBatchDecision::Values(values) => !values.is_empty(),
406 UnorderedBatchDecision::All => !self.input.borrow().is_empty(),
407 }
408 }
409}
410
411pub struct KeyedStreamHook<K: Hash + Eq + Clone, V, Order: Ordering> {
412 pub input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>, pub to_release: Option<Vec<(K, V)>>,
414 pub output: Sender<(K, V)>,
415 pub batch_location: HookLocationMeta,
416 pub format_item_debug: fn(&(K, V)) -> Option<String>,
417 pub _order: std::marker::PhantomData<Order>,
418}
419
420impl<K: Hash + Eq + Clone, V> RuntimeHook for KeyedStreamHook<K, V, TotalOrder> {
421 fn has_pending_input(&self) -> bool {
422 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
423 !self.input.borrow().values().all(|q| q.is_empty())
424 }
425
426 fn only_one_possible_decision(&self) -> bool {
427 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
429 self.input.borrow().values().all(|q| q.is_empty())
430 }
431
432 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
433 if let Some(to_release) = self.to_release.take() {
434 if let Some(log_writer) = log_writer {
435 let HookLocationMeta {
436 location: batch_location,
437 line,
438 caret_indent,
439 } = self.batch_location;
440 let note_str = if to_release.is_empty() {
441 "^ releasing no items".to_owned()
442 } else {
443 format!(
444 "^ releasing items: {:?}",
445 TruncatedVecDebug(
446 RefCell::new(Some(to_release.iter())),
447 8,
448 self.format_item_debug
449 )
450 )
451 };
452
453 log_release(
454 log_writer,
455 batch_location,
456 line,
457 caret_indent,
458 ¬e_str,
459 colored::Color::Green,
460 );
461 }
462
463 for item in to_release {
464 self.output.try_send(item).unwrap();
465 }
466 } else {
467 panic!("No decision to release");
468 }
469 }
470
471 fn location_meta(&self) -> HookLocationMeta {
472 self.batch_location
473 }
474}
475
476impl<K: Hash + Eq + Clone, V> TickInputHook for KeyedStreamHook<K, V, TotalOrder> {
477 fn can_trigger_tick(&self) -> bool {
478 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
479 !self.input.borrow().values().all(|q| q.is_empty())
480 }
481
482 fn autonomous_decision<'a>(
483 &mut self,
484 driver: &mut Borrowed<'a>,
485 mut force_trigger: bool,
486 ) -> bool {
487 let mut current_input = self.input.borrow_mut();
488 self.to_release = Some(vec![]);
489 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
490 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
491
492 let mut remaining_nonempty_keys = nonempty_key_count;
493 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
494 for (key, queue) in current_input.iter_mut() {
495 if queue.is_empty() {
496 continue;
497 }
498
499 remaining_nonempty_keys -= 1;
500
501 let count = ((if force_trigger && remaining_nonempty_keys == 0 {
502 1
503 } else {
504 0
505 })..=queue.len())
506 .generate(driver)
507 .unwrap();
508
509 let items: Vec<(K, V)> = queue.drain(0..count).map(|v| (key.clone(), v)).collect();
510 self.to_release.as_mut().unwrap().extend(items);
511
512 if count > 0 {
513 force_trigger = false;
514 }
515 }
516
517 !self.to_release.as_ref().unwrap().is_empty()
518 }
519}
520
521impl<K: Hash + Eq + Clone, V> RuntimeHook for KeyedStreamHook<K, V, NoOrder> {
522 fn has_pending_input(&self) -> bool {
523 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
524 !self.input.borrow().values().all(|q| q.is_empty())
525 }
526
527 fn only_one_possible_decision(&self) -> bool {
528 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
530 self.input.borrow().values().all(|q| q.is_empty())
531 }
532
533 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
534 if let Some(to_release) = self.to_release.take() {
535 if let Some(log_writer) = log_writer {
536 let HookLocationMeta {
537 location: batch_location,
538 line,
539 caret_indent,
540 } = self.batch_location;
541 let note_str = if to_release.is_empty() {
542 "^ releasing no items".to_owned()
543 } else {
544 format!(
545 "^ releasing unordered items: {:?}",
546 TruncatedVecDebug(
547 RefCell::new(Some(to_release.iter())),
548 8,
549 self.format_item_debug
550 )
551 )
552 };
553
554 log_release(
555 log_writer,
556 batch_location,
557 line,
558 caret_indent,
559 ¬e_str,
560 colored::Color::Green,
561 );
562 }
563
564 for item in to_release {
565 self.output.try_send(item).unwrap();
566 }
567 } else {
568 panic!("No decision to release");
569 }
570 }
571
572 fn location_meta(&self) -> HookLocationMeta {
573 self.batch_location
574 }
575}
576
577impl<K: Hash + Eq + Clone, V> TickInputHook for KeyedStreamHook<K, V, NoOrder> {
578 fn can_trigger_tick(&self) -> bool {
579 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
580 !self.input.borrow().values().all(|q| q.is_empty())
581 }
582
583 fn autonomous_decision<'a>(
584 &mut self,
585 driver: &mut Borrowed<'a>,
586 mut force_trigger: bool,
587 ) -> bool {
588 let mut current_input = self.input.borrow_mut();
589 self.to_release = Some(vec![]);
590 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
591 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
592
593 let mut remaining_nonempty_keys = nonempty_key_count;
594 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
595 for (key, queue) in current_input.iter_mut() {
596 if queue.is_empty() {
597 continue;
598 }
599
600 remaining_nonempty_keys -= 1;
601
602 let mut min_index = 0;
603 while !queue.is_empty() {
604 let must_release = force_trigger && remaining_nonempty_keys == 0;
605 if !must_release && produce().generate(driver).unwrap() {
606 break;
607 }
608
609 let idx = (min_index..queue.len()).generate(driver).unwrap();
610 let item = queue.remove(idx).unwrap();
611 self.to_release.as_mut().unwrap().push((key.clone(), item));
612 force_trigger = false;
613
614 min_index = idx;
615 if min_index == queue.len() {
620 break;
621 }
622 }
623 }
624
625 !self.to_release.as_ref().unwrap().is_empty()
626 }
627}
628
629#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
637pub enum KeyedBatchDecision<K, V> {
638 Prefixes(Vec<(K, usize)>),
641 Values(Vec<(K, V)>),
643 All,
645}
646
647impl<K, V> ScriptDecision for KeyedBatchDecision<K, V>
648where
649 K: serde::Serialize + serde::de::DeserializeOwned,
650 V: serde::Serialize + serde::de::DeserializeOwned,
651{
652 fn describe(&self) -> String {
653 match self {
654 KeyedBatchDecision::Prefixes(counts) => {
655 format!("release({} per-key count(s))", counts.len())
656 }
657 KeyedBatchDecision::Values(values) => {
658 format!("release_values({} entr(ies))", values.len())
659 }
660 KeyedBatchDecision::All => "release_all()".to_owned(),
661 }
662 }
663}
664
665#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
668pub enum UnorderedKeyedBatchDecision<K, V> {
669 Values(Vec<(K, V)>),
671 All,
673}
674
675impl<K, V> ScriptDecision for UnorderedKeyedBatchDecision<K, V>
676where
677 K: serde::Serialize + serde::de::DeserializeOwned,
678 V: serde::Serialize + serde::de::DeserializeOwned,
679{
680 fn describe(&self) -> String {
681 match self {
682 UnorderedKeyedBatchDecision::Values(values) => {
683 format!("release_values({} entr(ies))", values.len())
684 }
685 UnorderedKeyedBatchDecision::All => "release_all()".to_owned(),
686 }
687 }
688}
689
690impl<K, V> ScriptableHook for KeyedStreamHook<K, V, TotalOrder>
691where
692 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
693 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
694{
695 type Decision = KeyedBatchDecision<K, V>;
696 type Status = BatchStatus;
697
698 fn is_honorable(&self, decision: &KeyedBatchDecision<K, V>) -> Result<bool, String> {
699 let input = self.input.borrow();
700 match decision {
701 KeyedBatchDecision::Prefixes(counts) => {
702 for (key, count) in counts {
705 if input.get(key).is_none_or(|queue| queue.len() < *count) {
706 return Ok(false);
707 }
708 }
709 Ok(true)
710 }
711 KeyedBatchDecision::Values(values) => {
712 let mut consumed: FxHashMap<&K, usize> = FxHashMap::default();
715 for (position, (key, expected)) in values.iter().enumerate() {
716 let offset = consumed.entry(key).or_insert(0);
717 match input.get(key).and_then(|queue| queue.get(*offset)) {
718 Some(buffered) if buffered == expected => {}
719 Some(_) => {
720 return Err(format!(
721 "release_values: buffered item at per-key prefix position {} did not match the expected value (scripted entry {})",
722 *offset, position
723 ));
724 }
725 None => return Ok(false),
726 }
727 *offset += 1;
728 }
729 Ok(true)
730 }
731 KeyedBatchDecision::All => Ok(true),
732 }
733 }
734
735 fn apply(&mut self, decision: KeyedBatchDecision<K, V>) {
736 let mut input = self.input.borrow_mut();
737 let out: Vec<(K, V)> = match decision {
738 KeyedBatchDecision::Prefixes(counts) => {
739 let mut out = vec![];
740 for (key, count) in counts {
741 let queue = input.get_mut(&key).unwrap();
742 out.extend(queue.drain(0..count).map(|v| (key.clone(), v)));
743 }
744 out
745 }
746 KeyedBatchDecision::Values(values) => values
747 .into_iter()
748 .map(|(key, _expected)| {
749 let item = input.get_mut(&key).unwrap().pop_front().unwrap();
753 (key, item)
754 })
755 .collect(),
756 KeyedBatchDecision::All => {
757 let mut out = vec![];
758 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
759 for (key, queue) in input.iter_mut() {
760 out.extend(queue.drain(..).map(|v| (key.clone(), v)));
761 }
762 out
763 }
764 };
765
766 self.to_release = Some(out);
767 }
768
769 fn implicit(&mut self) {
770 self.to_release = Some(vec![]);
771 }
772
773 fn status(&self) -> BatchStatus {
774 BatchStatus {
775 buffered: keyed_buffer_len(&self.input.borrow()),
776 }
777 }
778
779 fn describe_pending(&self) -> Option<String> {
780 describe_keyed_pending(&self.input.borrow())
781 }
782}
783
784impl<K, V> ScriptableTickInputHook for KeyedStreamHook<K, V, TotalOrder>
785where
786 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
787 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
788{
789 fn decision_triggers_tick(&self, decision: &KeyedBatchDecision<K, V>) -> bool {
790 match decision {
791 KeyedBatchDecision::Prefixes(counts) => counts.iter().any(|(_, count)| *count > 0),
792 KeyedBatchDecision::Values(values) => !values.is_empty(),
793 KeyedBatchDecision::All => keyed_buffer_len(&self.input.borrow()) > 0,
794 }
795 }
796}
797
798impl<K, V> ScriptableHook for KeyedStreamHook<K, V, NoOrder>
799where
800 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
801 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
802{
803 type Decision = UnorderedKeyedBatchDecision<K, V>;
804 type Status = BatchStatus;
805
806 fn is_honorable(&self, decision: &UnorderedKeyedBatchDecision<K, V>) -> Result<bool, String> {
807 Ok(match decision {
808 UnorderedKeyedBatchDecision::Values(values) => {
809 let input = self.input.borrow();
812 let mut unmatched: Vec<(&K, &V)> = values.iter().map(|(k, v)| (k, v)).collect();
813 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
814 for (key, queue) in input.iter() {
815 for buffered in queue {
816 if let Some(idx) = unmatched
817 .iter()
818 .position(|(k, v)| *k == key && *v == buffered)
819 {
820 unmatched.swap_remove(idx);
821 }
822 }
823 }
824 unmatched.is_empty()
825 }
826 UnorderedKeyedBatchDecision::All => true,
827 })
828 }
829
830 fn apply(&mut self, decision: UnorderedKeyedBatchDecision<K, V>) {
831 let mut input = self.input.borrow_mut();
832 let out: Vec<(K, V)> = match decision {
833 UnorderedKeyedBatchDecision::Values(values) => values
834 .into_iter()
835 .map(|(key, expected)| {
836 let queue = input.get_mut(&key).unwrap();
837 let idx = queue.iter().position(|item| *item == expected).unwrap();
838 let item = queue.remove(idx).unwrap();
839 (key, item)
840 })
841 .collect(),
842 UnorderedKeyedBatchDecision::All => {
843 let mut out = vec![];
844 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
845 for (key, queue) in input.iter_mut() {
846 out.extend(queue.drain(..).map(|v| (key.clone(), v)));
847 }
848 out
849 }
850 };
851
852 self.to_release = Some(out);
853 }
854
855 fn implicit(&mut self) {
856 self.to_release = Some(vec![]);
857 }
858
859 fn status(&self) -> BatchStatus {
860 BatchStatus {
861 buffered: keyed_buffer_len(&self.input.borrow()),
862 }
863 }
864
865 fn describe_pending(&self) -> Option<String> {
866 describe_keyed_pending(&self.input.borrow())
867 }
868}
869
870impl<K, V> ScriptableTickInputHook for KeyedStreamHook<K, V, NoOrder>
871where
872 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
873 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
874{
875 fn decision_triggers_tick(&self, decision: &UnorderedKeyedBatchDecision<K, V>) -> bool {
876 match decision {
877 UnorderedKeyedBatchDecision::Values(values) => !values.is_empty(),
878 UnorderedKeyedBatchDecision::All => keyed_buffer_len(&self.input.borrow()) > 0,
879 }
880 }
881}
882
883pub struct SingletonHook<T> {
884 input: Rc<RefCell<VecDeque<T>>>,
885 to_release: Option<(T, bool)>, last_released: Option<T>,
887 skipped_states: Vec<T>,
888 output: Sender<T>,
889 batch_location: HookLocationMeta,
890 format_item_debug: fn(&T) -> Option<String>,
891}
892
893impl<T: Clone> SingletonHook<T> {
894 pub fn new(
895 input: Rc<RefCell<VecDeque<T>>>,
896 output: Sender<T>,
897 batch_location: HookLocationMeta,
898 format_item_debug: fn(&T) -> Option<String>,
899 ) -> Self {
900 Self {
901 input,
902 to_release: None,
903 last_released: None,
904 skipped_states: vec![],
905 output,
906 batch_location,
907 format_item_debug,
908 }
909 }
910}
911
912impl<T: Clone> RuntimeHook for SingletonHook<T> {
913 fn has_pending_input(&self) -> bool {
914 !self.input.borrow().is_empty()
915 }
916
917 fn only_one_possible_decision(&self) -> bool {
918 let input = self.input.borrow();
923 input.is_empty() || (input.len() == 1 && self.last_released.is_none())
924 }
925
926 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
927 if let Some((to_release, is_new)) = self.to_release.take() {
928 self.last_released = Some(to_release.clone());
929
930 if let Some(log_writer) = log_writer {
931 let HookLocationMeta {
932 location: batch_location,
933 line,
934 caret_indent,
935 } = self.batch_location;
936 let note_str = if self.skipped_states.is_empty() {
937 if is_new {
938 format!(
939 "^ releasing snapshot: {:?}",
940 ManualDebug(&to_release, self.format_item_debug)
941 )
942 } else {
943 format!(
944 "^ releasing unchanged snapshot: {:?}",
945 ManualDebug(&to_release, self.format_item_debug)
946 )
947 }
948 } else {
949 format!(
950 "^ releasing snapshot: {:?} (skipping earlier states: {:?})",
951 ManualDebug(&to_release, self.format_item_debug),
952 self.skipped_states
953 .iter()
954 .map(|s| ManualDebug(s, self.format_item_debug))
955 .collect::<Vec<_>>()
956 )
957 };
958
959 log_release(
960 log_writer,
961 batch_location,
962 line,
963 caret_indent,
964 ¬e_str,
965 colored::Color::Green,
966 );
967 }
968
969 self.output.try_send(to_release).unwrap();
970 } else {
971 panic!("No decision to release");
972 }
973 }
974
975 fn location_meta(&self) -> HookLocationMeta {
976 self.batch_location
977 }
978}
979
980impl<T: Clone> TickInputHook for SingletonHook<T> {
981 fn can_trigger_tick(&self) -> bool {
982 !self.input.borrow().is_empty()
984 }
985
986 fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>, force_trigger: bool) -> bool {
987 let mut current_input = self.input.borrow_mut();
988 if current_input.is_empty() {
989 if force_trigger {
990 panic!("Cannot make a triggering decision when there is no input");
991 }
992
993 if let Some(last) = &self.last_released {
994 self.to_release = Some((last.clone(), false));
996 false
997 } else {
998 panic!("No input and no last released item to re-release");
999 }
1000 } else if !force_trigger
1001 && let Some(last) = &self.last_released
1002 && produce().generate(driver).unwrap()
1003 {
1004 self.to_release = Some((last.clone(), false));
1006 false
1007 } else {
1008 let idx_to_release = (0..current_input.len()).generate(driver).unwrap();
1010 self.skipped_states = current_input.drain(0..idx_to_release).collect(); let item = current_input.pop_front().unwrap();
1012 self.to_release = Some((item, true));
1013 true
1014 }
1015 }
1016}
1017
1018pub struct OptionalInitNoneHook<T> {
1028 input: Rc<RefCell<VecDeque<T>>>,
1029 to_release: Option<(Option<T>, bool)>, last_released: Option<T>, skipped_states: Vec<T>,
1032 output: Sender<T>,
1033 batch_location: HookLocationMeta,
1034 format_item_debug: fn(&T) -> Option<String>,
1035}
1036
1037impl<T: Clone> OptionalInitNoneHook<T> {
1038 pub fn new(
1039 input: Rc<RefCell<VecDeque<T>>>,
1040 output: Sender<T>,
1041 batch_location: HookLocationMeta,
1042 format_item_debug: fn(&T) -> Option<String>,
1043 ) -> Self {
1044 Self {
1045 input,
1046 to_release: None,
1047 last_released: None,
1048 skipped_states: vec![],
1049 output,
1050 batch_location,
1051 format_item_debug,
1052 }
1053 }
1054}
1055
1056impl<T: Clone> RuntimeHook for OptionalInitNoneHook<T> {
1057 fn has_pending_input(&self) -> bool {
1058 !self.input.borrow().is_empty()
1059 }
1060
1061 fn only_one_possible_decision(&self) -> bool {
1062 self.input.borrow().is_empty()
1066 }
1067
1068 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1069 let Some((to_release, is_new)) = self.to_release.take() else {
1070 panic!("No decision to release");
1071 };
1072
1073 if let Some(value) = &to_release {
1074 self.last_released = Some(value.clone());
1075 }
1076
1077 if let Some(log_writer) = log_writer {
1078 let HookLocationMeta {
1079 location: batch_location,
1080 line,
1081 caret_indent,
1082 } = self.batch_location;
1083 let note_str = match (&to_release, is_new) {
1084 (None, _) => "^ releasing null snapshot".to_owned(),
1085 (Some(value), true) => {
1086 if self.skipped_states.is_empty() {
1087 format!(
1088 "^ releasing snapshot: {:?}",
1089 ManualDebug(value, self.format_item_debug)
1090 )
1091 } else {
1092 format!(
1093 "^ releasing snapshot: {:?} (skipping earlier states: {:?})",
1094 ManualDebug(value, self.format_item_debug),
1095 self.skipped_states
1096 .iter()
1097 .map(|s| ManualDebug(s, self.format_item_debug))
1098 .collect::<Vec<_>>()
1099 )
1100 }
1101 }
1102 (Some(value), false) => format!(
1103 "^ releasing unchanged snapshot: {:?}",
1104 ManualDebug(value, self.format_item_debug)
1105 ),
1106 };
1107
1108 log_release(
1109 log_writer,
1110 batch_location,
1111 line,
1112 caret_indent,
1113 ¬e_str,
1114 colored::Color::Green,
1115 );
1116 }
1117
1118 if let Some(value) = to_release {
1119 self.output.try_send(value).unwrap();
1120 }
1121 }
1122
1123 fn location_meta(&self) -> HookLocationMeta {
1124 self.batch_location
1125 }
1126}
1127
1128impl<T: Clone> TickInputHook for OptionalInitNoneHook<T> {
1129 fn can_trigger_tick(&self) -> bool {
1130 !self.input.borrow().is_empty()
1134 }
1135
1136 fn autonomous_decision<'a>(
1137 &mut self,
1138 driver: &mut Borrowed<'a>,
1139 force_nontrivial: bool,
1140 ) -> bool {
1141 let mut current_input = self.input.borrow_mut();
1142 if current_input.is_empty() {
1143 if force_nontrivial {
1145 panic!("Cannot make nontrivial decision when there is no input");
1146 }
1147
1148 if let Some(last) = &self.last_released {
1149 self.to_release = Some((Some(last.clone()), false));
1151 } else {
1152 self.to_release = Some((None, false));
1154 }
1155 false
1156 } else if !force_nontrivial && produce().generate(driver).unwrap() {
1157 if let Some(last) = &self.last_released {
1159 self.to_release = Some((Some(last.clone()), false));
1161 } else {
1162 self.to_release = Some((None, false));
1165 }
1166 false
1167 } else {
1168 let idx_to_release = (0..current_input.len()).generate(driver).unwrap();
1170 self.skipped_states = current_input.drain(0..idx_to_release).collect(); let item = current_input.pop_front().unwrap();
1172 self.to_release = Some((Some(item), true));
1173 true
1174 }
1175 }
1176}
1177
1178#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1181pub enum SnapshotDecision<T> {
1182 Reveal(T),
1185 RevealNext,
1187 RevealLatest,
1189 Keep,
1191}
1192
1193impl<T> ScriptDecision for SnapshotDecision<T>
1194where
1195 T: serde::Serialize + serde::de::DeserializeOwned,
1196{
1197 fn describe(&self) -> String {
1198 match self {
1199 SnapshotDecision::Reveal(_) => "reveal(..)".to_owned(),
1200 SnapshotDecision::RevealNext => "reveal_next()".to_owned(),
1201 SnapshotDecision::RevealLatest => "reveal_latest()".to_owned(),
1202 SnapshotDecision::Keep => "keep()".to_owned(),
1203 }
1204 }
1205}
1206
1207#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1210pub struct SnapshotStatus {
1211 pub newer_versions: usize,
1213}
1214
1215impl<T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned> ScriptableHook
1216 for SingletonHook<T>
1217{
1218 type Decision = SnapshotDecision<T>;
1219 type Status = SnapshotStatus;
1220
1221 fn is_honorable(&self, decision: &SnapshotDecision<T>) -> Result<bool, String> {
1222 let input = self.input.borrow();
1223 Ok(match decision {
1224 SnapshotDecision::Reveal(target) => input.iter().any(|version| version == target),
1225 SnapshotDecision::RevealNext => !input.is_empty(),
1226 SnapshotDecision::RevealLatest => !input.is_empty() || self.last_released.is_some(),
1227 SnapshotDecision::Keep => self.last_released.is_some(),
1228 })
1229 }
1230
1231 fn apply(&mut self, decision: SnapshotDecision<T>) {
1232 match decision {
1233 SnapshotDecision::Reveal(target) => {
1234 let mut input = self.input.borrow_mut();
1235 let idx = input.iter().position(|version| *version == target).unwrap();
1236 self.skipped_states = input.drain(0..idx).collect();
1237 let item = input.pop_front().unwrap();
1238 self.to_release = Some((item, true));
1239 }
1240 SnapshotDecision::RevealNext => {
1241 let mut input = self.input.borrow_mut();
1242 self.skipped_states = vec![];
1243 let item = input.pop_front().unwrap();
1244 self.to_release = Some((item, true));
1245 }
1246 SnapshotDecision::RevealLatest => {
1247 let mut input = self.input.borrow_mut();
1248 if input.is_empty() {
1249 self.skipped_states = vec![];
1250 self.to_release = Some((self.last_released.clone().unwrap(), false));
1251 } else {
1252 let skip_count = input.len() - 1;
1253 self.skipped_states = input.drain(0..skip_count).collect();
1254 let item = input.pop_front().unwrap();
1255 self.to_release = Some((item, true));
1256 }
1257 }
1258 SnapshotDecision::Keep => {
1259 self.skipped_states = vec![];
1260 self.to_release = Some((self.last_released.clone().unwrap(), false));
1261 }
1262 }
1263 }
1264
1265 fn implicit(&mut self) {
1266 if let Some(last) = &self.last_released {
1267 self.skipped_states = vec![];
1268 self.to_release = Some((last.clone(), false));
1269 } else {
1270 abort!("scripted snapshot hook asked for implicit behavior with no revealed value");
1273 }
1274 }
1275
1276 fn status(&self) -> SnapshotStatus {
1277 SnapshotStatus {
1278 newer_versions: self.input.borrow().len(),
1279 }
1280 }
1281
1282 fn describe_pending(&self) -> Option<String> {
1283 let input = self.input.borrow();
1284 (!input.is_empty()).then(|| {
1285 format!(
1286 "{} buffered version(s): {:?}",
1287 input.len(),
1288 TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
1289 )
1290 })
1291 }
1292}
1293
1294impl<T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned> ScriptableTickInputHook
1295 for SingletonHook<T>
1296{
1297 fn decision_triggers_tick(&self, decision: &SnapshotDecision<T>) -> bool {
1298 match decision {
1299 SnapshotDecision::Reveal(_) | SnapshotDecision::RevealNext => true,
1300 SnapshotDecision::RevealLatest => !self.input.borrow().is_empty(),
1301 SnapshotDecision::Keep => false,
1302 }
1303 }
1304}
1305pub struct PassthroughSingletonHook<T> {
1310 input: Rc<RefCell<VecDeque<T>>>,
1311 to_release: Option<T>,
1312 output: Sender<T>,
1313 batch_location: HookLocationMeta,
1314 format_item_debug: fn(&T) -> Option<String>,
1315}
1316
1317impl<T> PassthroughSingletonHook<T> {
1318 pub fn new(
1319 input: Rc<RefCell<VecDeque<T>>>,
1320 output: Sender<T>,
1321 batch_location: HookLocationMeta,
1322 format_item_debug: fn(&T) -> Option<String>,
1323 ) -> Self {
1324 Self {
1325 input,
1326 to_release: None,
1327 output,
1328 batch_location,
1329 format_item_debug,
1330 }
1331 }
1332}
1333
1334impl<T> RuntimeHook for PassthroughSingletonHook<T> {
1335 fn has_pending_input(&self) -> bool {
1336 !self.input.borrow().is_empty()
1337 }
1338
1339 fn only_one_possible_decision(&self) -> bool {
1340 true
1345 }
1346
1347 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1348 if let Some(to_release) = self.to_release.take() {
1349 if let Some(log_writer) = log_writer {
1350 let HookLocationMeta {
1351 location: batch_location,
1352 line,
1353 caret_indent,
1354 } = self.batch_location;
1355 let note_str = format!(
1356 "^ releasing snapshot: {:?}",
1357 ManualDebug(&to_release, self.format_item_debug)
1358 );
1359
1360 log_release(
1361 log_writer,
1362 batch_location,
1363 line,
1364 caret_indent,
1365 ¬e_str,
1366 colored::Color::Green,
1367 );
1368 }
1369
1370 self.output.try_send(to_release).unwrap();
1371 } else {
1372 panic!("No decision to release");
1373 }
1374 }
1375
1376 fn location_meta(&self) -> HookLocationMeta {
1377 self.batch_location
1378 }
1379}
1380
1381impl<T> TickInputHook for PassthroughSingletonHook<T> {
1382 fn can_trigger_tick(&self) -> bool {
1383 !self.input.borrow().is_empty()
1384 }
1385
1386 fn autonomous_decision<'a>(
1387 &mut self,
1388 _driver: &mut Borrowed<'a>,
1389 _force_trigger: bool,
1390 ) -> bool {
1391 let mut current_input = self.input.borrow_mut();
1392 if let Some(item) = current_input.pop_back() {
1394 current_input.clear();
1395 self.to_release = Some(item);
1396 true
1397 } else {
1398 false
1399 }
1400 }
1401}
1402
1403pub struct KeyedSingletonHook<K: Hash + Eq + Clone, V: Clone> {
1404 input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>, to_release: Option<Vec<(K, V, bool)>>, last_released: FxHashMap<K, V>,
1407 skipped_states: FxHashMap<K, Vec<V>>,
1408 output: Sender<(K, V)>,
1409 batch_location: HookLocationMeta,
1410 format_key_debug: fn(&K) -> Option<String>,
1411 format_value_debug: fn(&V) -> Option<String>,
1412}
1413
1414impl<K: Hash + Eq + Clone, V: Clone> KeyedSingletonHook<K, V> {
1415 pub fn new(
1416 input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>,
1417 output: Sender<(K, V)>,
1418 batch_location: HookLocationMeta,
1419 format_key_debug: fn(&K) -> Option<String>,
1420 format_value_debug: fn(&V) -> Option<String>,
1421 ) -> Self {
1422 Self {
1423 input,
1424 to_release: None,
1425 last_released: FxHashMap::default(),
1426 skipped_states: FxHashMap::default(),
1427 output,
1428 batch_location,
1429 format_key_debug,
1430 format_value_debug,
1431 }
1432 }
1433}
1434
1435impl<K: Hash + Eq + Clone, V: Clone> RuntimeHook for KeyedSingletonHook<K, V> {
1436 fn has_pending_input(&self) -> bool {
1437 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1438 !self.input.borrow().values().all(|q| q.is_empty())
1439 }
1440
1441 fn only_one_possible_decision(&self) -> bool {
1442 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1445 self.input.borrow().values().all(|q| q.is_empty())
1446 }
1447
1448 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1449 if let Some(to_release) = self.to_release.take() {
1450 if let Some(log_writer) = log_writer {
1451 let HookLocationMeta {
1452 location: batch_location,
1453 line,
1454 caret_indent,
1455 } = self.batch_location;
1456 let note_str = if to_release.is_empty() {
1457 "^ releasing no items".to_owned()
1458 } else {
1459 let mut mapping_text = String::new();
1460 for (key, value, is_new) in &to_release {
1461 let entry_text = if *is_new {
1462 format!(
1463 "{:?}: {:?}",
1464 ManualDebug(key, self.format_key_debug),
1465 ManualDebug(value, self.format_value_debug)
1466 )
1467 } else {
1468 format!(
1469 "{:?}: {:?} (unchanged)",
1470 ManualDebug(key, self.format_key_debug),
1471 ManualDebug(value, self.format_value_debug)
1472 )
1473 };
1474 if !mapping_text.is_empty() {
1475 mapping_text.push_str(", ");
1476 }
1477 mapping_text.push_str(&entry_text);
1478 }
1479 format!("^ releasing items: {{ {} }}", mapping_text)
1480 };
1481
1482 log_release(
1483 log_writer,
1484 batch_location,
1485 line,
1486 caret_indent,
1487 ¬e_str,
1488 colored::Color::Green,
1489 );
1490 }
1491
1492 for (key, value, _) in to_release {
1493 self.output.try_send((key, value)).unwrap();
1494 }
1495 } else {
1496 panic!("No decision to release");
1497 }
1498 }
1499
1500 fn location_meta(&self) -> HookLocationMeta {
1501 self.batch_location
1502 }
1503}
1504
1505impl<K: Hash + Eq + Clone, V: Clone> TickInputHook for KeyedSingletonHook<K, V> {
1506 fn can_trigger_tick(&self) -> bool {
1507 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1508 !self.input.borrow().values().all(|q| q.is_empty())
1509 }
1510
1511 fn autonomous_decision<'a>(
1512 &mut self,
1513 driver: &mut Borrowed<'a>,
1514 mut force_trigger: bool,
1515 ) -> bool {
1516 let mut current_input = self.input.borrow_mut();
1517 self.to_release = Some(vec![]);
1518 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1519 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
1520
1521 let mut remaining_nonempty_keys = nonempty_key_count;
1522 let mut any_triggered = false;
1523 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1524 for (key, queue) in current_input.iter_mut() {
1525 if queue.is_empty() {
1526 self.to_release.as_mut().unwrap().push((
1527 key.clone(),
1528 self.last_released.get(key).unwrap().clone(),
1529 false,
1530 ));
1531
1532 continue;
1533 }
1534
1535 remaining_nonempty_keys -= 1;
1536
1537 let must_reveal = force_trigger && remaining_nonempty_keys == 0;
1538
1539 if !must_reveal
1540 && self.last_released.contains_key(key)
1541 && produce().generate(driver).unwrap()
1542 {
1543 let last = self.last_released.get(key).unwrap().clone();
1545 self.to_release
1546 .as_mut()
1547 .unwrap()
1548 .push((key.clone(), last, false));
1549 } else {
1550 let allow_null_release = !must_reveal && !self.last_released.contains_key(key);
1551 if allow_null_release && produce().generate(driver).unwrap() {
1552 continue;
1554 } else {
1555 let idx_to_release = (0..queue.len()).generate(driver).unwrap();
1557 let skipped: Vec<V> = queue.drain(0..idx_to_release).collect();
1558 let item = queue.pop_front().unwrap();
1559 self.skipped_states.insert(key.clone(), skipped);
1560 self.to_release
1561 .as_mut()
1562 .unwrap()
1563 .push((key.clone(), item.clone(), true));
1564 self.last_released.insert(key.clone(), item);
1565
1566 any_triggered |= true;
1567 force_trigger = false;
1568 }
1569 }
1570 }
1571
1572 any_triggered
1573 }
1574}
1575
1576#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1583pub enum KeyedSnapshotDecision<K, V> {
1584 Reveal(Vec<(K, V)>),
1590 RevealLatest,
1593 Keep,
1595}
1596
1597impl<K, V> ScriptDecision for KeyedSnapshotDecision<K, V>
1598where
1599 K: serde::Serialize + serde::de::DeserializeOwned,
1600 V: serde::Serialize + serde::de::DeserializeOwned,
1601{
1602 fn describe(&self) -> String {
1603 match self {
1604 KeyedSnapshotDecision::Reveal(_) => "reveal(..)".to_owned(),
1605 KeyedSnapshotDecision::RevealLatest => "reveal_latest()".to_owned(),
1606 KeyedSnapshotDecision::Keep => "keep()".to_owned(),
1607 }
1608 }
1609}
1610
1611#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1614pub struct KeyedSnapshotStatus {
1615 pub newer_versions: usize,
1618 pub keys_with_newer_versions: usize,
1620}
1621
1622impl<K, V> ScriptableHook for KeyedSingletonHook<K, V>
1623where
1624 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
1625 V: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned,
1626{
1627 type Decision = KeyedSnapshotDecision<K, V>;
1628 type Status = KeyedSnapshotStatus;
1629
1630 fn is_honorable(&self, decision: &KeyedSnapshotDecision<K, V>) -> Result<bool, String> {
1631 let input = self.input.borrow();
1632 Ok(match decision {
1633 KeyedSnapshotDecision::Reveal(entries) => entries.iter().all(|(key, target)| {
1634 input
1635 .get(key)
1636 .is_some_and(|queue| queue.iter().any(|version| version == target))
1637 }),
1638 KeyedSnapshotDecision::RevealLatest => {
1639 keyed_buffer_len(&input) > 0 || !self.last_released.is_empty()
1640 }
1641 KeyedSnapshotDecision::Keep => true,
1642 })
1643 }
1644
1645 fn apply(&mut self, decision: KeyedSnapshotDecision<K, V>) {
1646 let mut input = self.input.borrow_mut();
1647 match decision {
1648 KeyedSnapshotDecision::Reveal(entries) => {
1649 let mut to_release = vec![];
1650 let mut named: FxHashSet<K> = FxHashSet::default();
1651 for (key, target) in entries {
1652 let queue = input.get_mut(&key).unwrap();
1653 let idx = queue.iter().position(|version| *version == target).unwrap();
1654 let skipped: Vec<V> = queue.drain(0..idx).collect();
1655 let item = queue.pop_front().unwrap();
1656 self.skipped_states.insert(key.clone(), skipped);
1657 self.last_released.insert(key.clone(), item.clone());
1658 to_release.push((key.clone(), item, true));
1659 named.insert(key);
1660 }
1661 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1663 for (key, last) in self.last_released.iter() {
1664 if !named.contains(key) {
1665 to_release.push((key.clone(), last.clone(), false));
1666 }
1667 }
1668 self.to_release = Some(to_release);
1669 }
1670 KeyedSnapshotDecision::RevealLatest => {
1671 let mut to_release = vec![];
1672 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1673 for (key, queue) in input.iter_mut() {
1674 if queue.is_empty() {
1675 if let Some(last) = self.last_released.get(key) {
1676 to_release.push((key.clone(), last.clone(), false));
1677 }
1678 } else {
1679 let skip_count = queue.len() - 1;
1680 let skipped: Vec<V> = queue.drain(0..skip_count).collect();
1681 let item = queue.pop_front().unwrap();
1682 self.skipped_states.insert(key.clone(), skipped);
1683 self.last_released.insert(key.clone(), item.clone());
1684 to_release.push((key.clone(), item, true));
1685 }
1686 }
1687 self.to_release = Some(to_release);
1688 }
1689 KeyedSnapshotDecision::Keep => {
1690 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1691 let to_release = self
1692 .last_released
1693 .iter()
1694 .map(|(key, last)| (key.clone(), last.clone(), false))
1695 .collect();
1696 self.to_release = Some(to_release);
1697 }
1698 }
1699 }
1700
1701 fn implicit(&mut self) {
1702 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1704 let to_release = self
1705 .last_released
1706 .iter()
1707 .map(|(key, last)| (key.clone(), last.clone(), false))
1708 .collect();
1709 self.to_release = Some(to_release);
1710 }
1711
1712 fn status(&self) -> KeyedSnapshotStatus {
1713 let input = self.input.borrow();
1714 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1715 let keys_with_newer_versions = input.values().filter(|q| !q.is_empty()).count();
1716 KeyedSnapshotStatus {
1717 newer_versions: keyed_buffer_len(&input),
1718 keys_with_newer_versions,
1719 }
1720 }
1721
1722 fn describe_pending(&self) -> Option<String> {
1723 let input = self.input.borrow();
1724 let total = keyed_buffer_len(&input);
1725 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1726 let keys = input.values().filter(|q| !q.is_empty()).count();
1727 (total > 0).then(|| format!("{} buffered version(s) across {} key(s)", total, keys))
1728 }
1729}
1730
1731impl<K, V> ScriptableTickInputHook for KeyedSingletonHook<K, V>
1732where
1733 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
1734 V: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned,
1735{
1736 fn decision_triggers_tick(&self, decision: &KeyedSnapshotDecision<K, V>) -> bool {
1737 match decision {
1738 KeyedSnapshotDecision::Reveal(entries) => !entries.is_empty(),
1739 KeyedSnapshotDecision::RevealLatest => keyed_buffer_len(&self.input.borrow()) > 0,
1740 KeyedSnapshotDecision::Keep => false,
1741 }
1742 }
1743}