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 = (usize::from(force_trigger)..=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 must_reveal = force_trigger && remaining_nonempty_keys == 0;
502 let count = (usize::from(must_reveal)..=queue.len())
503 .generate(driver)
504 .unwrap();
505
506 let items: Vec<(K, V)> = queue.drain(0..count).map(|v| (key.clone(), v)).collect();
507 self.to_release.as_mut().unwrap().extend(items);
508
509 if count > 0 {
510 force_trigger = false;
511 }
512 }
513
514 !self.to_release.as_ref().unwrap().is_empty()
515 }
516}
517
518impl<K: Hash + Eq + Clone, V> RuntimeHook for KeyedStreamHook<K, V, NoOrder> {
519 fn has_pending_input(&self) -> bool {
520 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
521 !self.input.borrow().values().all(|q| q.is_empty())
522 }
523
524 fn only_one_possible_decision(&self) -> bool {
525 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
527 self.input.borrow().values().all(|q| q.is_empty())
528 }
529
530 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
531 if let Some(to_release) = self.to_release.take() {
532 if let Some(log_writer) = log_writer {
533 let HookLocationMeta {
534 location: batch_location,
535 line,
536 caret_indent,
537 } = self.batch_location;
538 let note_str = if to_release.is_empty() {
539 "^ releasing no items".to_owned()
540 } else {
541 format!(
542 "^ releasing unordered items: {:?}",
543 TruncatedVecDebug(
544 RefCell::new(Some(to_release.iter())),
545 8,
546 self.format_item_debug
547 )
548 )
549 };
550
551 log_release(
552 log_writer,
553 batch_location,
554 line,
555 caret_indent,
556 ¬e_str,
557 colored::Color::Green,
558 );
559 }
560
561 for item in to_release {
562 self.output.try_send(item).unwrap();
563 }
564 } else {
565 panic!("No decision to release");
566 }
567 }
568
569 fn location_meta(&self) -> HookLocationMeta {
570 self.batch_location
571 }
572}
573
574impl<K: Hash + Eq + Clone, V> TickInputHook for KeyedStreamHook<K, V, NoOrder> {
575 fn can_trigger_tick(&self) -> bool {
576 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
577 !self.input.borrow().values().all(|q| q.is_empty())
578 }
579
580 fn autonomous_decision<'a>(
581 &mut self,
582 driver: &mut Borrowed<'a>,
583 mut force_trigger: bool,
584 ) -> bool {
585 let mut current_input = self.input.borrow_mut();
586 self.to_release = Some(vec![]);
587 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
588 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
589
590 let mut remaining_nonempty_keys = nonempty_key_count;
591 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
592 for (key, queue) in current_input.iter_mut() {
593 if queue.is_empty() {
594 continue;
595 }
596
597 remaining_nonempty_keys -= 1;
598
599 let mut min_index = 0;
600 while !queue.is_empty() {
601 let must_release = force_trigger && remaining_nonempty_keys == 0;
602 if !must_release && produce().generate(driver).unwrap() {
603 break;
604 }
605
606 let idx = (min_index..queue.len()).generate(driver).unwrap();
607 let item = queue.remove(idx).unwrap();
608 self.to_release.as_mut().unwrap().push((key.clone(), item));
609 force_trigger = false;
610
611 min_index = idx;
612 if min_index == queue.len() {
617 break;
618 }
619 }
620 }
621
622 !self.to_release.as_ref().unwrap().is_empty()
623 }
624}
625
626#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
634pub enum KeyedBatchDecision<K, V> {
635 Prefixes(Vec<(K, usize)>),
638 Values(Vec<(K, V)>),
640 All,
642}
643
644impl<K, V> ScriptDecision for KeyedBatchDecision<K, V>
645where
646 K: serde::Serialize + serde::de::DeserializeOwned,
647 V: serde::Serialize + serde::de::DeserializeOwned,
648{
649 fn describe(&self) -> String {
650 match self {
651 KeyedBatchDecision::Prefixes(counts) => {
652 format!("release({} per-key count(s))", counts.len())
653 }
654 KeyedBatchDecision::Values(values) => {
655 format!("release_values({} entr(ies))", values.len())
656 }
657 KeyedBatchDecision::All => "release_all()".to_owned(),
658 }
659 }
660}
661
662#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
665pub enum UnorderedKeyedBatchDecision<K, V> {
666 Values(Vec<(K, V)>),
668 All,
670}
671
672impl<K, V> ScriptDecision for UnorderedKeyedBatchDecision<K, V>
673where
674 K: serde::Serialize + serde::de::DeserializeOwned,
675 V: serde::Serialize + serde::de::DeserializeOwned,
676{
677 fn describe(&self) -> String {
678 match self {
679 UnorderedKeyedBatchDecision::Values(values) => {
680 format!("release_values({} entr(ies))", values.len())
681 }
682 UnorderedKeyedBatchDecision::All => "release_all()".to_owned(),
683 }
684 }
685}
686
687impl<K, V> ScriptableHook for KeyedStreamHook<K, V, TotalOrder>
688where
689 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
690 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
691{
692 type Decision = KeyedBatchDecision<K, V>;
693 type Status = BatchStatus;
694
695 fn is_honorable(&self, decision: &KeyedBatchDecision<K, V>) -> Result<bool, String> {
696 let input = self.input.borrow();
697 match decision {
698 KeyedBatchDecision::Prefixes(counts) => {
699 for (key, count) in counts {
702 if input.get(key).is_none_or(|queue| queue.len() < *count) {
703 return Ok(false);
704 }
705 }
706 Ok(true)
707 }
708 KeyedBatchDecision::Values(values) => {
709 let mut consumed: FxHashMap<&K, usize> = FxHashMap::default();
712 for (position, (key, expected)) in values.iter().enumerate() {
713 let offset = consumed.entry(key).or_insert(0);
714 match input.get(key).and_then(|queue| queue.get(*offset)) {
715 Some(buffered) if buffered == expected => {}
716 Some(_) => {
717 return Err(format!(
718 "release_values: buffered item at per-key prefix position {} did not match the expected value (scripted entry {})",
719 *offset, position
720 ));
721 }
722 None => return Ok(false),
723 }
724 *offset += 1;
725 }
726 Ok(true)
727 }
728 KeyedBatchDecision::All => Ok(true),
729 }
730 }
731
732 fn apply(&mut self, decision: KeyedBatchDecision<K, V>) {
733 let mut input = self.input.borrow_mut();
734 let out: Vec<(K, V)> = match decision {
735 KeyedBatchDecision::Prefixes(counts) => {
736 let mut out = vec![];
737 for (key, count) in counts {
738 let queue = input.get_mut(&key).unwrap();
739 out.extend(queue.drain(0..count).map(|v| (key.clone(), v)));
740 }
741 out
742 }
743 KeyedBatchDecision::Values(values) => values
744 .into_iter()
745 .map(|(key, _expected)| {
746 let item = input.get_mut(&key).unwrap().pop_front().unwrap();
750 (key, item)
751 })
752 .collect(),
753 KeyedBatchDecision::All => {
754 let mut out = vec![];
755 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
756 for (key, queue) in input.iter_mut() {
757 out.extend(queue.drain(..).map(|v| (key.clone(), v)));
758 }
759 out
760 }
761 };
762
763 self.to_release = Some(out);
764 }
765
766 fn implicit(&mut self) {
767 self.to_release = Some(vec![]);
768 }
769
770 fn status(&self) -> BatchStatus {
771 BatchStatus {
772 buffered: keyed_buffer_len(&self.input.borrow()),
773 }
774 }
775
776 fn describe_pending(&self) -> Option<String> {
777 describe_keyed_pending(&self.input.borrow())
778 }
779}
780
781impl<K, V> ScriptableTickInputHook for KeyedStreamHook<K, V, TotalOrder>
782where
783 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
784 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
785{
786 fn decision_triggers_tick(&self, decision: &KeyedBatchDecision<K, V>) -> bool {
787 match decision {
788 KeyedBatchDecision::Prefixes(counts) => counts.iter().any(|(_, count)| *count > 0),
789 KeyedBatchDecision::Values(values) => !values.is_empty(),
790 KeyedBatchDecision::All => keyed_buffer_len(&self.input.borrow()) > 0,
791 }
792 }
793}
794
795impl<K, V> ScriptableHook for KeyedStreamHook<K, V, NoOrder>
796where
797 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
798 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
799{
800 type Decision = UnorderedKeyedBatchDecision<K, V>;
801 type Status = BatchStatus;
802
803 fn is_honorable(&self, decision: &UnorderedKeyedBatchDecision<K, V>) -> Result<bool, String> {
804 Ok(match decision {
805 UnorderedKeyedBatchDecision::Values(values) => {
806 let input = self.input.borrow();
809 let mut unmatched: Vec<(&K, &V)> = values.iter().map(|(k, v)| (k, v)).collect();
810 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
811 for (key, queue) in input.iter() {
812 for buffered in queue {
813 if let Some(idx) = unmatched
814 .iter()
815 .position(|(k, v)| *k == key && *v == buffered)
816 {
817 unmatched.swap_remove(idx);
818 }
819 }
820 }
821 unmatched.is_empty()
822 }
823 UnorderedKeyedBatchDecision::All => true,
824 })
825 }
826
827 fn apply(&mut self, decision: UnorderedKeyedBatchDecision<K, V>) {
828 let mut input = self.input.borrow_mut();
829 let out: Vec<(K, V)> = match decision {
830 UnorderedKeyedBatchDecision::Values(values) => values
831 .into_iter()
832 .map(|(key, expected)| {
833 let queue = input.get_mut(&key).unwrap();
834 let idx = queue.iter().position(|item| *item == expected).unwrap();
835 let item = queue.remove(idx).unwrap();
836 (key, item)
837 })
838 .collect(),
839 UnorderedKeyedBatchDecision::All => {
840 let mut out = vec![];
841 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
842 for (key, queue) in input.iter_mut() {
843 out.extend(queue.drain(..).map(|v| (key.clone(), v)));
844 }
845 out
846 }
847 };
848
849 self.to_release = Some(out);
850 }
851
852 fn implicit(&mut self) {
853 self.to_release = Some(vec![]);
854 }
855
856 fn status(&self) -> BatchStatus {
857 BatchStatus {
858 buffered: keyed_buffer_len(&self.input.borrow()),
859 }
860 }
861
862 fn describe_pending(&self) -> Option<String> {
863 describe_keyed_pending(&self.input.borrow())
864 }
865}
866
867impl<K, V> ScriptableTickInputHook for KeyedStreamHook<K, V, NoOrder>
868where
869 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
870 V: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
871{
872 fn decision_triggers_tick(&self, decision: &UnorderedKeyedBatchDecision<K, V>) -> bool {
873 match decision {
874 UnorderedKeyedBatchDecision::Values(values) => !values.is_empty(),
875 UnorderedKeyedBatchDecision::All => keyed_buffer_len(&self.input.borrow()) > 0,
876 }
877 }
878}
879
880pub struct SingletonHook<T> {
881 input: Rc<RefCell<VecDeque<T>>>,
882 to_release: Option<(T, bool)>, last_released: Option<T>,
884 skipped_states: Vec<T>,
885 output: Sender<T>,
886 batch_location: HookLocationMeta,
887 format_item_debug: fn(&T) -> Option<String>,
888}
889
890impl<T: Clone> SingletonHook<T> {
891 pub fn new(
892 input: Rc<RefCell<VecDeque<T>>>,
893 output: Sender<T>,
894 batch_location: HookLocationMeta,
895 format_item_debug: fn(&T) -> Option<String>,
896 ) -> Self {
897 Self {
898 input,
899 to_release: None,
900 last_released: None,
901 skipped_states: vec![],
902 output,
903 batch_location,
904 format_item_debug,
905 }
906 }
907}
908
909impl<T: Clone> RuntimeHook for SingletonHook<T> {
910 fn has_pending_input(&self) -> bool {
911 !self.input.borrow().is_empty()
912 }
913
914 fn only_one_possible_decision(&self) -> bool {
915 let input = self.input.borrow();
920 input.is_empty() || (input.len() == 1 && self.last_released.is_none())
921 }
922
923 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
924 if let Some((to_release, is_new)) = self.to_release.take() {
925 self.last_released = Some(to_release.clone());
926
927 if let Some(log_writer) = log_writer {
928 let HookLocationMeta {
929 location: batch_location,
930 line,
931 caret_indent,
932 } = self.batch_location;
933 let note_str = if self.skipped_states.is_empty() {
934 if is_new {
935 format!(
936 "^ releasing snapshot: {:?}",
937 ManualDebug(&to_release, self.format_item_debug)
938 )
939 } else {
940 format!(
941 "^ releasing unchanged snapshot: {:?}",
942 ManualDebug(&to_release, self.format_item_debug)
943 )
944 }
945 } else {
946 format!(
947 "^ releasing snapshot: {:?} (skipping earlier states: {:?})",
948 ManualDebug(&to_release, self.format_item_debug),
949 self.skipped_states
950 .iter()
951 .map(|s| ManualDebug(s, self.format_item_debug))
952 .collect::<Vec<_>>()
953 )
954 };
955
956 log_release(
957 log_writer,
958 batch_location,
959 line,
960 caret_indent,
961 ¬e_str,
962 colored::Color::Green,
963 );
964 }
965
966 self.output.try_send(to_release).unwrap();
967 } else {
968 panic!("No decision to release");
969 }
970 }
971
972 fn location_meta(&self) -> HookLocationMeta {
973 self.batch_location
974 }
975}
976
977impl<T: Clone> TickInputHook for SingletonHook<T> {
978 fn can_trigger_tick(&self) -> bool {
979 !self.input.borrow().is_empty()
981 }
982
983 fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>, force_trigger: bool) -> bool {
984 let mut current_input = self.input.borrow_mut();
985 if current_input.is_empty() {
986 assert!(
987 !force_trigger,
988 "Cannot make a triggering decision when there is no input"
989 );
990
991 if let Some(last) = &self.last_released {
992 self.to_release = Some((last.clone(), false));
994 false
995 } else {
996 panic!("No input and no last released item to re-release");
997 }
998 } else if !force_trigger
999 && let Some(last) = &self.last_released
1000 && produce().generate(driver).unwrap()
1001 {
1002 self.to_release = Some((last.clone(), false));
1004 false
1005 } else {
1006 let idx_to_release = (0..current_input.len()).generate(driver).unwrap();
1008 self.skipped_states = current_input.drain(0..idx_to_release).collect(); let item = current_input.pop_front().unwrap();
1010 self.to_release = Some((item, true));
1011 true
1012 }
1013 }
1014}
1015
1016pub struct OptionalInitNoneHook<T> {
1026 input: Rc<RefCell<VecDeque<T>>>,
1027 to_release: Option<(Option<T>, bool)>, last_released: Option<T>, skipped_states: Vec<T>,
1030 output: Sender<T>,
1031 batch_location: HookLocationMeta,
1032 format_item_debug: fn(&T) -> Option<String>,
1033}
1034
1035impl<T: Clone> OptionalInitNoneHook<T> {
1036 pub fn new(
1037 input: Rc<RefCell<VecDeque<T>>>,
1038 output: Sender<T>,
1039 batch_location: HookLocationMeta,
1040 format_item_debug: fn(&T) -> Option<String>,
1041 ) -> Self {
1042 Self {
1043 input,
1044 to_release: None,
1045 last_released: None,
1046 skipped_states: vec![],
1047 output,
1048 batch_location,
1049 format_item_debug,
1050 }
1051 }
1052}
1053
1054impl<T: Clone> RuntimeHook for OptionalInitNoneHook<T> {
1055 fn has_pending_input(&self) -> bool {
1056 !self.input.borrow().is_empty()
1057 }
1058
1059 fn only_one_possible_decision(&self) -> bool {
1060 self.input.borrow().is_empty()
1064 }
1065
1066 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1067 let Some((to_release, is_new)) = self.to_release.take() else {
1068 panic!("No decision to release");
1069 };
1070
1071 if let Some(value) = &to_release {
1072 self.last_released = Some(value.clone());
1073 }
1074
1075 if let Some(log_writer) = log_writer {
1076 let HookLocationMeta {
1077 location: batch_location,
1078 line,
1079 caret_indent,
1080 } = self.batch_location;
1081 let note_str = match (&to_release, is_new) {
1082 (None, _) => "^ releasing null snapshot".to_owned(),
1083 (Some(value), true) => {
1084 if self.skipped_states.is_empty() {
1085 format!(
1086 "^ releasing snapshot: {:?}",
1087 ManualDebug(value, self.format_item_debug)
1088 )
1089 } else {
1090 format!(
1091 "^ releasing snapshot: {:?} (skipping earlier states: {:?})",
1092 ManualDebug(value, self.format_item_debug),
1093 self.skipped_states
1094 .iter()
1095 .map(|s| ManualDebug(s, self.format_item_debug))
1096 .collect::<Vec<_>>()
1097 )
1098 }
1099 }
1100 (Some(value), false) => format!(
1101 "^ releasing unchanged snapshot: {:?}",
1102 ManualDebug(value, self.format_item_debug)
1103 ),
1104 };
1105
1106 log_release(
1107 log_writer,
1108 batch_location,
1109 line,
1110 caret_indent,
1111 ¬e_str,
1112 colored::Color::Green,
1113 );
1114 }
1115
1116 if let Some(value) = to_release {
1117 self.output.try_send(value).unwrap();
1118 }
1119 }
1120
1121 fn location_meta(&self) -> HookLocationMeta {
1122 self.batch_location
1123 }
1124}
1125
1126impl<T: Clone> TickInputHook for OptionalInitNoneHook<T> {
1127 fn can_trigger_tick(&self) -> bool {
1128 !self.input.borrow().is_empty()
1132 }
1133
1134 fn autonomous_decision<'a>(
1135 &mut self,
1136 driver: &mut Borrowed<'a>,
1137 force_nontrivial: bool,
1138 ) -> bool {
1139 let mut current_input = self.input.borrow_mut();
1140 if current_input.is_empty() {
1141 assert!(
1143 !force_nontrivial,
1144 "Cannot make nontrivial decision when there is no input"
1145 );
1146
1147 if let Some(last) = &self.last_released {
1148 self.to_release = Some((Some(last.clone()), false));
1150 } else {
1151 self.to_release = Some((None, false));
1153 }
1154 false
1155 } else if !force_nontrivial && produce().generate(driver).unwrap() {
1156 if let Some(last) = &self.last_released {
1158 self.to_release = Some((Some(last.clone()), false));
1160 } else {
1161 self.to_release = Some((None, false));
1164 }
1165 false
1166 } else {
1167 let idx_to_release = (0..current_input.len()).generate(driver).unwrap();
1169 self.skipped_states = current_input.drain(0..idx_to_release).collect(); let item = current_input.pop_front().unwrap();
1171 self.to_release = Some((Some(item), true));
1172 true
1173 }
1174 }
1175}
1176
1177#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1180pub enum SnapshotDecision<T> {
1181 Reveal(T),
1184 RevealNext,
1186 RevealLatest,
1188 Keep,
1190}
1191
1192impl<T> ScriptDecision for SnapshotDecision<T>
1193where
1194 T: serde::Serialize + serde::de::DeserializeOwned,
1195{
1196 fn describe(&self) -> String {
1197 match self {
1198 SnapshotDecision::Reveal(_) => "reveal(..)".to_owned(),
1199 SnapshotDecision::RevealNext => "reveal_next()".to_owned(),
1200 SnapshotDecision::RevealLatest => "reveal_latest()".to_owned(),
1201 SnapshotDecision::Keep => "keep()".to_owned(),
1202 }
1203 }
1204}
1205
1206#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1209pub struct SnapshotStatus {
1210 pub newer_versions: usize,
1212}
1213
1214impl<T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned> ScriptableHook
1215 for SingletonHook<T>
1216{
1217 type Decision = SnapshotDecision<T>;
1218 type Status = SnapshotStatus;
1219
1220 fn is_honorable(&self, decision: &SnapshotDecision<T>) -> Result<bool, String> {
1221 let input = self.input.borrow();
1222 Ok(match decision {
1223 SnapshotDecision::Reveal(target) => input.iter().any(|version| version == target),
1224 SnapshotDecision::RevealNext => !input.is_empty(),
1225 SnapshotDecision::RevealLatest => !input.is_empty() || self.last_released.is_some(),
1226 SnapshotDecision::Keep => self.last_released.is_some(),
1227 })
1228 }
1229
1230 fn apply(&mut self, decision: SnapshotDecision<T>) {
1231 match decision {
1232 SnapshotDecision::Reveal(target) => {
1233 let mut input = self.input.borrow_mut();
1234 let idx = input.iter().position(|version| *version == target).unwrap();
1235 self.skipped_states = input.drain(0..idx).collect();
1236 let item = input.pop_front().unwrap();
1237 self.to_release = Some((item, true));
1238 }
1239 SnapshotDecision::RevealNext => {
1240 let mut input = self.input.borrow_mut();
1241 self.skipped_states = vec![];
1242 let item = input.pop_front().unwrap();
1243 self.to_release = Some((item, true));
1244 }
1245 SnapshotDecision::RevealLatest => {
1246 let mut input = self.input.borrow_mut();
1247 if input.is_empty() {
1248 self.skipped_states = vec![];
1249 self.to_release = Some((self.last_released.clone().unwrap(), false));
1250 } else {
1251 let skip_count = input.len() - 1;
1252 self.skipped_states = input.drain(0..skip_count).collect();
1253 let item = input.pop_front().unwrap();
1254 self.to_release = Some((item, true));
1255 }
1256 }
1257 SnapshotDecision::Keep => {
1258 self.skipped_states = vec![];
1259 self.to_release = Some((self.last_released.clone().unwrap(), false));
1260 }
1261 }
1262 }
1263
1264 fn implicit(&mut self) {
1265 if let Some(last) = &self.last_released {
1266 self.skipped_states = vec![];
1267 self.to_release = Some((last.clone(), false));
1268 } else {
1269 abort!("scripted snapshot hook asked for implicit behavior with no revealed value");
1272 }
1273 }
1274
1275 fn status(&self) -> SnapshotStatus {
1276 SnapshotStatus {
1277 newer_versions: self.input.borrow().len(),
1278 }
1279 }
1280
1281 fn describe_pending(&self) -> Option<String> {
1282 let input = self.input.borrow();
1283 (!input.is_empty()).then(|| {
1284 format!(
1285 "{} buffered version(s): {:?}",
1286 input.len(),
1287 TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
1288 )
1289 })
1290 }
1291}
1292
1293impl<T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned> ScriptableTickInputHook
1294 for SingletonHook<T>
1295{
1296 fn decision_triggers_tick(&self, decision: &SnapshotDecision<T>) -> bool {
1297 match decision {
1298 SnapshotDecision::Reveal(_) | SnapshotDecision::RevealNext => true,
1299 SnapshotDecision::RevealLatest => !self.input.borrow().is_empty(),
1300 SnapshotDecision::Keep => false,
1301 }
1302 }
1303}
1304pub struct PassthroughSingletonHook<T> {
1309 input: Rc<RefCell<VecDeque<T>>>,
1310 to_release: Option<T>,
1311 output: Sender<T>,
1312 batch_location: HookLocationMeta,
1313 format_item_debug: fn(&T) -> Option<String>,
1314}
1315
1316impl<T> PassthroughSingletonHook<T> {
1317 pub fn new(
1318 input: Rc<RefCell<VecDeque<T>>>,
1319 output: Sender<T>,
1320 batch_location: HookLocationMeta,
1321 format_item_debug: fn(&T) -> Option<String>,
1322 ) -> Self {
1323 Self {
1324 input,
1325 to_release: None,
1326 output,
1327 batch_location,
1328 format_item_debug,
1329 }
1330 }
1331}
1332
1333impl<T> RuntimeHook for PassthroughSingletonHook<T> {
1334 fn has_pending_input(&self) -> bool {
1335 !self.input.borrow().is_empty()
1336 }
1337
1338 fn only_one_possible_decision(&self) -> bool {
1339 true
1344 }
1345
1346 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1347 if let Some(to_release) = self.to_release.take() {
1348 if let Some(log_writer) = log_writer {
1349 let HookLocationMeta {
1350 location: batch_location,
1351 line,
1352 caret_indent,
1353 } = self.batch_location;
1354 let note_str = format!(
1355 "^ releasing snapshot: {:?}",
1356 ManualDebug(&to_release, self.format_item_debug)
1357 );
1358
1359 log_release(
1360 log_writer,
1361 batch_location,
1362 line,
1363 caret_indent,
1364 ¬e_str,
1365 colored::Color::Green,
1366 );
1367 }
1368
1369 self.output.try_send(to_release).unwrap();
1370 } else {
1371 panic!("No decision to release");
1372 }
1373 }
1374
1375 fn location_meta(&self) -> HookLocationMeta {
1376 self.batch_location
1377 }
1378}
1379
1380impl<T> TickInputHook for PassthroughSingletonHook<T> {
1381 fn can_trigger_tick(&self) -> bool {
1382 !self.input.borrow().is_empty()
1383 }
1384
1385 fn autonomous_decision<'a>(
1386 &mut self,
1387 _driver: &mut Borrowed<'a>,
1388 _force_trigger: bool,
1389 ) -> bool {
1390 let mut current_input = self.input.borrow_mut();
1391 if let Some(item) = current_input.pop_back() {
1393 current_input.clear();
1394 self.to_release = Some(item);
1395 true
1396 } else {
1397 false
1398 }
1399 }
1400}
1401
1402pub struct KeyedSingletonHook<K: Hash + Eq + Clone, V: Clone> {
1403 input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>, to_release: Option<Vec<(K, V, bool)>>, last_released: FxHashMap<K, V>,
1406 skipped_states: FxHashMap<K, Vec<V>>,
1407 output: Sender<(K, V)>,
1408 batch_location: HookLocationMeta,
1409 format_key_debug: fn(&K) -> Option<String>,
1410 format_value_debug: fn(&V) -> Option<String>,
1411}
1412
1413impl<K: Hash + Eq + Clone, V: Clone> KeyedSingletonHook<K, V> {
1414 pub fn new(
1415 input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>,
1416 output: Sender<(K, V)>,
1417 batch_location: HookLocationMeta,
1418 format_key_debug: fn(&K) -> Option<String>,
1419 format_value_debug: fn(&V) -> Option<String>,
1420 ) -> Self {
1421 Self {
1422 input,
1423 to_release: None,
1424 last_released: FxHashMap::default(),
1425 skipped_states: FxHashMap::default(),
1426 output,
1427 batch_location,
1428 format_key_debug,
1429 format_value_debug,
1430 }
1431 }
1432}
1433
1434impl<K: Hash + Eq + Clone, V: Clone> RuntimeHook for KeyedSingletonHook<K, V> {
1435 fn has_pending_input(&self) -> bool {
1436 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1437 !self.input.borrow().values().all(|q| q.is_empty())
1438 }
1439
1440 fn only_one_possible_decision(&self) -> bool {
1441 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1444 self.input.borrow().values().all(|q| q.is_empty())
1445 }
1446
1447 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1448 if let Some(to_release) = self.to_release.take() {
1449 if let Some(log_writer) = log_writer {
1450 let HookLocationMeta {
1451 location: batch_location,
1452 line,
1453 caret_indent,
1454 } = self.batch_location;
1455 let note_str = if to_release.is_empty() {
1456 "^ releasing no items".to_owned()
1457 } else {
1458 let mut mapping_text = String::new();
1459 for (key, value, is_new) in &to_release {
1460 let entry_text = if *is_new {
1461 format!(
1462 "{:?}: {:?}",
1463 ManualDebug(key, self.format_key_debug),
1464 ManualDebug(value, self.format_value_debug)
1465 )
1466 } else {
1467 format!(
1468 "{:?}: {:?} (unchanged)",
1469 ManualDebug(key, self.format_key_debug),
1470 ManualDebug(value, self.format_value_debug)
1471 )
1472 };
1473 if !mapping_text.is_empty() {
1474 mapping_text.push_str(", ");
1475 }
1476 mapping_text.push_str(&entry_text);
1477 }
1478 format!("^ releasing items: {{ {} }}", mapping_text)
1479 };
1480
1481 log_release(
1482 log_writer,
1483 batch_location,
1484 line,
1485 caret_indent,
1486 ¬e_str,
1487 colored::Color::Green,
1488 );
1489 }
1490
1491 for (key, value, _) in to_release {
1492 self.output.try_send((key, value)).unwrap();
1493 }
1494 } else {
1495 panic!("No decision to release");
1496 }
1497 }
1498
1499 fn location_meta(&self) -> HookLocationMeta {
1500 self.batch_location
1501 }
1502}
1503
1504impl<K: Hash + Eq + Clone, V: Clone> TickInputHook for KeyedSingletonHook<K, V> {
1505 fn can_trigger_tick(&self) -> bool {
1506 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1507 !self.input.borrow().values().all(|q| q.is_empty())
1508 }
1509
1510 fn autonomous_decision<'a>(
1511 &mut self,
1512 driver: &mut Borrowed<'a>,
1513 mut force_trigger: bool,
1514 ) -> bool {
1515 let mut current_input = self.input.borrow_mut();
1516 self.to_release = Some(vec![]);
1517 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1518 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
1519
1520 let mut remaining_nonempty_keys = nonempty_key_count;
1521 let mut any_triggered = false;
1522 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1523 for (key, queue) in current_input.iter_mut() {
1524 if queue.is_empty() {
1525 self.to_release.as_mut().unwrap().push((
1526 key.clone(),
1527 self.last_released.get(key).unwrap().clone(),
1528 false,
1529 ));
1530
1531 continue;
1532 }
1533
1534 remaining_nonempty_keys -= 1;
1535
1536 let must_reveal = force_trigger && remaining_nonempty_keys == 0;
1537
1538 if !must_reveal
1539 && self.last_released.contains_key(key)
1540 && produce().generate(driver).unwrap()
1541 {
1542 let last = self.last_released.get(key).unwrap().clone();
1544 self.to_release
1545 .as_mut()
1546 .unwrap()
1547 .push((key.clone(), last, false));
1548 } else {
1549 let allow_null_release = !must_reveal && !self.last_released.contains_key(key);
1550 if allow_null_release && produce().generate(driver).unwrap() {
1551 continue;
1553 }
1554
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 any_triggered
1572 }
1573}
1574
1575#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1582pub enum KeyedSnapshotDecision<K, V> {
1583 Reveal(Vec<(K, V)>),
1589 RevealLatest,
1592 Keep,
1594}
1595
1596impl<K, V> ScriptDecision for KeyedSnapshotDecision<K, V>
1597where
1598 K: serde::Serialize + serde::de::DeserializeOwned,
1599 V: serde::Serialize + serde::de::DeserializeOwned,
1600{
1601 fn describe(&self) -> String {
1602 match self {
1603 KeyedSnapshotDecision::Reveal(_) => "reveal(..)".to_owned(),
1604 KeyedSnapshotDecision::RevealLatest => "reveal_latest()".to_owned(),
1605 KeyedSnapshotDecision::Keep => "keep()".to_owned(),
1606 }
1607 }
1608}
1609
1610#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
1613pub struct KeyedSnapshotStatus {
1614 pub newer_versions: usize,
1617 pub keys_with_newer_versions: usize,
1619}
1620
1621impl<K, V> ScriptableHook for KeyedSingletonHook<K, V>
1622where
1623 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
1624 V: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned,
1625{
1626 type Decision = KeyedSnapshotDecision<K, V>;
1627 type Status = KeyedSnapshotStatus;
1628
1629 fn is_honorable(&self, decision: &KeyedSnapshotDecision<K, V>) -> Result<bool, String> {
1630 let input = self.input.borrow();
1631 Ok(match decision {
1632 KeyedSnapshotDecision::Reveal(entries) => entries.iter().all(|(key, target)| {
1633 input
1634 .get(key)
1635 .is_some_and(|queue| queue.iter().any(|version| version == target))
1636 }),
1637 KeyedSnapshotDecision::RevealLatest => {
1638 keyed_buffer_len(&input) > 0 || !self.last_released.is_empty()
1639 }
1640 KeyedSnapshotDecision::Keep => true,
1641 })
1642 }
1643
1644 fn apply(&mut self, decision: KeyedSnapshotDecision<K, V>) {
1645 let mut input = self.input.borrow_mut();
1646 match decision {
1647 KeyedSnapshotDecision::Reveal(entries) => {
1648 let mut to_release = vec![];
1649 let mut named: FxHashSet<K> = FxHashSet::default();
1650 for (key, target) in entries {
1651 let queue = input.get_mut(&key).unwrap();
1652 let idx = queue.iter().position(|version| *version == target).unwrap();
1653 let skipped: Vec<V> = queue.drain(0..idx).collect();
1654 let item = queue.pop_front().unwrap();
1655 self.skipped_states.insert(key.clone(), skipped);
1656 self.last_released.insert(key.clone(), item.clone());
1657 to_release.push((key.clone(), item, true));
1658 named.insert(key);
1659 }
1660 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1662 for (key, last) in self.last_released.iter() {
1663 if !named.contains(key) {
1664 to_release.push((key.clone(), last.clone(), false));
1665 }
1666 }
1667 self.to_release = Some(to_release);
1668 }
1669 KeyedSnapshotDecision::RevealLatest => {
1670 let mut to_release = vec![];
1671 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1672 for (key, queue) in input.iter_mut() {
1673 if queue.is_empty() {
1674 if let Some(last) = self.last_released.get(key) {
1675 to_release.push((key.clone(), last.clone(), false));
1676 }
1677 } else {
1678 let skip_count = queue.len() - 1;
1679 let skipped: Vec<V> = queue.drain(0..skip_count).collect();
1680 let item = queue.pop_front().unwrap();
1681 self.skipped_states.insert(key.clone(), skipped);
1682 self.last_released.insert(key.clone(), item.clone());
1683 to_release.push((key.clone(), item, true));
1684 }
1685 }
1686 self.to_release = Some(to_release);
1687 }
1688 KeyedSnapshotDecision::Keep => {
1689 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1690 let to_release = self
1691 .last_released
1692 .iter()
1693 .map(|(key, last)| (key.clone(), last.clone(), false))
1694 .collect();
1695 self.to_release = Some(to_release);
1696 }
1697 }
1698 }
1699
1700 fn implicit(&mut self) {
1701 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1703 let to_release = self
1704 .last_released
1705 .iter()
1706 .map(|(key, last)| (key.clone(), last.clone(), false))
1707 .collect();
1708 self.to_release = Some(to_release);
1709 }
1710
1711 fn status(&self) -> KeyedSnapshotStatus {
1712 let input = self.input.borrow();
1713 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1714 let keys_with_newer_versions = input.values().filter(|q| !q.is_empty()).count();
1715 KeyedSnapshotStatus {
1716 newer_versions: keyed_buffer_len(&input),
1717 keys_with_newer_versions,
1718 }
1719 }
1720
1721 fn describe_pending(&self) -> Option<String> {
1722 let input = self.input.borrow();
1723 let total = keyed_buffer_len(&input);
1724 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1725 let keys = input.values().filter(|q| !q.is_empty()).count();
1726 (total > 0).then(|| format!("{} buffered version(s) across {} key(s)", total, keys))
1727 }
1728}
1729
1730impl<K, V> ScriptableTickInputHook for KeyedSingletonHook<K, V>
1731where
1732 K: Hash + Eq + Clone + serde::Serialize + serde::de::DeserializeOwned,
1733 V: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned,
1734{
1735 fn decision_triggers_tick(&self, decision: &KeyedSnapshotDecision<K, V>) -> bool {
1736 match decision {
1737 KeyedSnapshotDecision::Reveal(entries) => !entries.is_empty(),
1738 KeyedSnapshotDecision::RevealLatest => keyed_buffer_len(&self.input.borrow()) > 0,
1739 KeyedSnapshotDecision::Keep => false,
1740 }
1741 }
1742}