1use std::{
2 collections::{BTreeMap, BTreeSet, VecDeque},
3 error::Error,
4 fmt,
5};
6
7use crate::{
8 ChangePolicyV0, ReactiveNodeIdV0, ReactiveNodeKindV0, ReactiveStateV0, ReactiveUnavailableV0,
9 ReactiveValueV0,
10 graph::{NodeBlueprintV0, NodeOperationV0, ReactiveGraphIdV0},
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct EffectReceiptV0 {
16 pub channel: String,
17 pub wave: u64,
18 pub state: ReactiveStateV0,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum StabilizeStatusV0 {
24 #[non_exhaustive]
25 Settled {
26 wave: u64,
27 recomputed_node_count: usize,
28 },
29 #[non_exhaustive]
30 Pending {
31 wave: u64,
32 recomputed_node_count: usize,
33 },
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub enum ReactiveEngineErrorV0 {
39 #[non_exhaustive]
40 InvalidNode {
41 node_index: usize,
42 },
43 #[non_exhaustive]
44 ForeignNodeId {
45 node_index: usize,
46 expected_graph: u64,
47 actual_graph: u64,
48 },
49 #[non_exhaustive]
50 NodeDoesNotAcceptDeposits {
51 node_index: usize,
52 },
53 ObserverMutationDuringWave,
54 ZeroStepBudget,
55}
56
57impl fmt::Display for ReactiveEngineErrorV0 {
58 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::InvalidNode { node_index } => {
61 write!(formatter, "reactive node {node_index} does not exist")
62 }
63 Self::ForeignNodeId {
64 node_index,
65 expected_graph,
66 actual_graph,
67 } => write!(
68 formatter,
69 "reactive node {node_index} belongs to reactive graph {actual_graph}, not {expected_graph}"
70 ),
71 Self::NodeDoesNotAcceptDeposits { node_index } => {
72 write!(
73 formatter,
74 "reactive node {node_index} does not accept deposits"
75 )
76 }
77 Self::ObserverMutationDuringWave => {
78 write!(
79 formatter,
80 "observers cannot be changed during stabilization"
81 )
82 }
83 Self::ZeroStepBudget => write!(formatter, "stabilization step budget must be non-zero"),
84 }
85 }
86}
87
88impl Error for ReactiveEngineErrorV0 {}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum DeltaFoldParityErrorV0 {
93 #[non_exhaustive]
94 InvalidNode { node_index: usize },
95 #[non_exhaustive]
96 ForeignNodeId {
97 node_index: usize,
98 expected_graph: u64,
99 actual_graph: u64,
100 },
101 #[non_exhaustive]
102 NotDeltaFold { node: ReactiveNodeIdV0 },
103 #[non_exhaustive]
104 Diverged {
105 node: ReactiveNodeIdV0,
106 incremental_digest: [u8; 32],
107 rebuilt_digest: [u8; 32],
108 },
109}
110
111impl fmt::Display for DeltaFoldParityErrorV0 {
112 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 Self::InvalidNode { node_index } => {
115 write!(formatter, "reactive node {node_index} does not exist")
116 }
117 Self::ForeignNodeId {
118 node_index,
119 expected_graph,
120 actual_graph,
121 } => write!(
122 formatter,
123 "reactive node {node_index} belongs to reactive graph {actual_graph}, not {expected_graph}"
124 ),
125 Self::NotDeltaFold { node } => {
126 write!(
127 formatter,
128 "reactive node {} is not a delta fold",
129 node.index()
130 )
131 }
132 Self::Diverged { node, .. } => write!(
133 formatter,
134 "delta-fold node {} diverged from a full rebuild",
135 node.index()
136 ),
137 }
138 }
139}
140
141impl Error for DeltaFoldParityErrorV0 {}
142
143struct RuntimeNodeV0 {
144 operation: NodeOperationV0,
145 dependencies: Vec<ReactiveNodeIdV0>,
146 parents: Vec<ReactiveNodeIdV0>,
147 height: u32,
148 state: ReactiveStateV0,
149 change_policy: ChangePolicyV0,
150 observer_count: usize,
151 needed_by: usize,
152 stale: bool,
153 scheduled_wave: Option<u64>,
154 last_recomputed_wave: Option<u64>,
155 recompute_count: usize,
156 delta_entries: BTreeMap<String, ReactiveStateV0>,
157 delta_dirty_dependencies: BTreeSet<ReactiveNodeIdV0>,
158 delta_update_count: usize,
159}
160
161pub struct ReactiveEngineV0 {
162 graph_id: ReactiveGraphIdV0,
163 nodes: Vec<RuntimeNodeV0>,
164 deferred_deposits: BTreeMap<ReactiveNodeIdV0, ReactiveStateV0>,
165 activation_nodes: BTreeSet<ReactiveNodeIdV0>,
166 queues: BTreeMap<u32, VecDeque<ReactiveNodeIdV0>>,
167 effect_receipts: Vec<EffectReceiptV0>,
168 current_wave: u64,
169 current_wave_recomputes: usize,
170 stabilizing: bool,
171}
172
173impl ReactiveEngineV0 {
174 pub(crate) fn from_blueprints(
175 graph_id: ReactiveGraphIdV0,
176 blueprints: Vec<NodeBlueprintV0>,
177 ) -> Self {
178 let mut nodes: Vec<_> = blueprints
179 .into_iter()
180 .map(|blueprint| {
181 let stale = !matches!(
182 blueprint.operation,
183 NodeOperationV0::Input | NodeOperationV0::AsyncResult
184 );
185 RuntimeNodeV0 {
186 operation: blueprint.operation,
187 dependencies: blueprint.dependencies,
188 parents: Vec::new(),
189 height: blueprint.height,
190 state: blueprint.initial_state,
191 change_policy: blueprint.change_policy,
192 observer_count: 0,
193 needed_by: 0,
194 stale,
195 scheduled_wave: None,
196 last_recomputed_wave: None,
197 recompute_count: 0,
198 delta_entries: BTreeMap::new(),
199 delta_dirty_dependencies: BTreeSet::new(),
200 delta_update_count: 0,
201 }
202 })
203 .collect();
204
205 for node_index in 0..nodes.len() {
206 let parent = ReactiveNodeIdV0 {
207 index: node_index,
208 graph: graph_id,
209 };
210 for dependency in nodes[node_index].dependencies.clone() {
211 nodes[dependency.index()].parents.push(parent);
212 }
213 }
214
215 Self {
216 graph_id,
217 nodes,
218 deferred_deposits: BTreeMap::new(),
219 activation_nodes: BTreeSet::new(),
220 queues: BTreeMap::new(),
221 effect_receipts: Vec::new(),
222 current_wave: 0,
223 current_wave_recomputes: 0,
224 stabilizing: false,
225 }
226 }
227
228 pub fn node_count(&self) -> usize {
229 self.nodes.len()
230 }
231
232 pub fn node_kind(
233 &self,
234 node: ReactiveNodeIdV0,
235 ) -> Result<ReactiveNodeKindV0, ReactiveEngineErrorV0> {
236 Ok(self.node(node)?.operation.kind())
237 }
238
239 pub fn state(&self, node: ReactiveNodeIdV0) -> Result<&ReactiveStateV0, ReactiveEngineErrorV0> {
240 Ok(&self.node(node)?.state)
241 }
242
243 pub fn is_necessary(&self, node: ReactiveNodeIdV0) -> Result<bool, ReactiveEngineErrorV0> {
244 Ok(self.node(node)?.needed_by > 0)
245 }
246
247 pub fn is_stale(&self, node: ReactiveNodeIdV0) -> Result<bool, ReactiveEngineErrorV0> {
248 Ok(self.node(node)?.stale)
249 }
250
251 pub fn node_recompute_count(
252 &self,
253 node: ReactiveNodeIdV0,
254 ) -> Result<usize, ReactiveEngineErrorV0> {
255 Ok(self.node(node)?.recompute_count)
256 }
257
258 pub fn delta_update_count(
259 &self,
260 node: ReactiveNodeIdV0,
261 ) -> Result<usize, ReactiveEngineErrorV0> {
262 Ok(self.node(node)?.delta_update_count)
263 }
264
265 pub fn current_wave(&self) -> u64 {
266 self.current_wave
267 }
268
269 pub fn is_stabilizing(&self) -> bool {
270 self.stabilizing
271 }
272
273 pub fn has_pending_work(&self) -> bool {
274 self.stabilizing || !self.deferred_deposits.is_empty() || !self.activation_nodes.is_empty()
275 }
276
277 pub fn deposit(
281 &mut self,
282 node: ReactiveNodeIdV0,
283 state: ReactiveStateV0,
284 ) -> Result<(), ReactiveEngineErrorV0> {
285 match self.node(node)?.operation {
286 NodeOperationV0::Input | NodeOperationV0::AsyncResult => {
287 self.deferred_deposits.insert(node, state);
288 Ok(())
289 }
290 _ => Err(ReactiveEngineErrorV0::NodeDoesNotAcceptDeposits {
291 node_index: node.index(),
292 }),
293 }
294 }
295
296 pub fn observe(&mut self, node: ReactiveNodeIdV0) -> Result<(), ReactiveEngineErrorV0> {
297 if self.stabilizing {
298 return Err(ReactiveEngineErrorV0::ObserverMutationDuringWave);
299 }
300 self.node(node)?;
301 self.nodes[node.index()].observer_count += 1;
302 if self.nodes[node.index()].observer_count == 1 {
303 self.increase_necessity(node);
304 }
305 Ok(())
306 }
307
308 pub fn unobserve(&mut self, node: ReactiveNodeIdV0) -> Result<(), ReactiveEngineErrorV0> {
309 if self.stabilizing {
310 return Err(ReactiveEngineErrorV0::ObserverMutationDuringWave);
311 }
312 self.node(node)?;
313 if self.nodes[node.index()].observer_count == 0 {
314 return Ok(());
315 }
316 self.nodes[node.index()].observer_count -= 1;
317 if self.nodes[node.index()].observer_count == 0 {
318 self.decrease_necessity(node);
319 }
320 Ok(())
321 }
322
323 pub fn stabilize_step(
324 &mut self,
325 maximum_recomputes: usize,
326 ) -> Result<StabilizeStatusV0, ReactiveEngineErrorV0> {
327 if maximum_recomputes == 0 {
328 return Err(ReactiveEngineErrorV0::ZeroStepBudget);
329 }
330 if !self.stabilizing {
331 self.begin_wave();
332 }
333 if !self.stabilizing {
334 return Ok(StabilizeStatusV0::Settled {
335 wave: self.current_wave,
336 recomputed_node_count: 0,
337 });
338 }
339
340 let mut recomputed = 0;
341 while recomputed < maximum_recomputes {
342 let Some(node) = self.pop_next_scheduled() else {
343 self.stabilizing = false;
344 return Ok(StabilizeStatusV0::Settled {
345 wave: self.current_wave,
346 recomputed_node_count: recomputed,
347 });
348 };
349 if self.nodes[node.index()].needed_by == 0 {
350 self.nodes[node.index()].stale = true;
351 continue;
352 }
353 self.recompute(node);
354 recomputed += 1;
355 self.current_wave_recomputes += 1;
356 }
357
358 if self.queues.values().all(VecDeque::is_empty) {
359 self.stabilizing = false;
360 Ok(StabilizeStatusV0::Settled {
361 wave: self.current_wave,
362 recomputed_node_count: recomputed,
363 })
364 } else {
365 Ok(StabilizeStatusV0::Pending {
366 wave: self.current_wave,
367 recomputed_node_count: recomputed,
368 })
369 }
370 }
371
372 pub fn stabilize_until_settled(
373 &mut self,
374 maximum_recomputes: usize,
375 ) -> Result<StabilizeStatusV0, ReactiveEngineErrorV0> {
376 if maximum_recomputes == 0 {
377 return Err(ReactiveEngineErrorV0::ZeroStepBudget);
378 }
379 let mut remaining = maximum_recomputes;
380 loop {
381 let status = self.stabilize_step(remaining)?;
382 match status {
383 StabilizeStatusV0::Settled { .. } => return Ok(status),
384 StabilizeStatusV0::Pending {
385 recomputed_node_count,
386 ..
387 } => {
388 remaining = remaining.saturating_sub(recomputed_node_count);
389 if remaining == 0 {
390 return Ok(status);
391 }
392 }
393 }
394 }
395 }
396
397 pub fn drain_effect_receipts(&mut self) -> Vec<EffectReceiptV0> {
398 std::mem::take(&mut self.effect_receipts)
399 }
400
401 pub fn verify_delta_fold(&self, node: ReactiveNodeIdV0) -> Result<(), DeltaFoldParityErrorV0> {
402 if node.graph != self.graph_id {
403 return Err(DeltaFoldParityErrorV0::ForeignNodeId {
404 node_index: node.index(),
405 expected_graph: self.graph_id.value(),
406 actual_graph: node.graph.value(),
407 });
408 }
409 let Some(runtime) = self.nodes.get(node.index()) else {
410 return Err(DeltaFoldParityErrorV0::InvalidNode {
411 node_index: node.index(),
412 });
413 };
414 if !matches!(runtime.operation, NodeOperationV0::DeltaFold { .. }) {
415 return Err(DeltaFoldParityErrorV0::NotDeltaFold { node });
416 }
417 let ReactiveStateV0::Available(ReactiveValueV0::Digest(incremental_digest)) =
418 &runtime.state
419 else {
420 return Ok(());
421 };
422 let Some(rebuilt_entries) = self.rebuild_delta_fold_from_dependencies(node) else {
423 return Err(DeltaFoldParityErrorV0::NotDeltaFold { node });
424 };
425 let rebuilt_digest = full_delta_digest(&rebuilt_entries);
426 if *incremental_digest == rebuilt_digest && runtime.delta_entries == rebuilt_entries {
427 Ok(())
428 } else {
429 Err(DeltaFoldParityErrorV0::Diverged {
430 node,
431 incremental_digest: *incremental_digest,
432 rebuilt_digest,
433 })
434 }
435 }
436
437 fn rebuild_delta_fold_from_dependencies(
438 &self,
439 node: ReactiveNodeIdV0,
440 ) -> Option<BTreeMap<String, ReactiveStateV0>> {
441 let runtime = self.nodes.get(node.index())?;
442 let NodeOperationV0::DeltaFold { keys } = &runtime.operation else {
443 return None;
444 };
445 Some(
446 keys.iter()
447 .zip(&runtime.dependencies)
448 .map(|(key, dependency)| {
449 (key.clone(), self.nodes[dependency.index()].state.clone())
450 })
451 .collect(),
452 )
453 }
454
455 fn begin_wave(&mut self) {
456 if self.deferred_deposits.is_empty() && self.activation_nodes.is_empty() {
457 return;
458 }
459 self.current_wave = self.current_wave.saturating_add(1);
460 self.current_wave_recomputes = 0;
461 self.stabilizing = true;
462
463 let deposits = std::mem::take(&mut self.deferred_deposits);
464 for (node, next) in deposits {
465 let previous = self.nodes[node.index()].state.clone();
466 let policy = self.nodes[node.index()].change_policy;
467 self.nodes[node.index()].state = next.clone();
468 self.nodes[node.index()].stale = false;
469 if !policy.equivalent(&previous, &next) {
470 self.schedule_parents(node);
471 }
472 }
473
474 let activation_nodes = std::mem::take(&mut self.activation_nodes);
475 for node in activation_nodes {
476 if self.nodes[node.index()].needed_by > 0 && self.nodes[node.index()].stale {
477 self.schedule(node);
478 }
479 }
480
481 if self.queues.values().all(VecDeque::is_empty) {
482 self.stabilizing = false;
483 }
484 }
485
486 fn recompute(&mut self, node: ReactiveNodeIdV0) {
487 debug_assert_ne!(
488 self.nodes[node.index()].last_recomputed_wave,
489 Some(self.current_wave),
490 "a node must not recompute more than once in one wave"
491 );
492 let next = self.evaluate(node);
493 let previous = self.nodes[node.index()].state.clone();
494 let policy = self.nodes[node.index()].change_policy;
495 let changed = !policy.equivalent(&previous, &next);
496
497 self.nodes[node.index()].state = next.clone();
498 self.nodes[node.index()].stale = false;
499 self.nodes[node.index()].last_recomputed_wave = Some(self.current_wave);
500 self.nodes[node.index()].recompute_count += 1;
501
502 if !changed {
503 return;
504 }
505 if let NodeOperationV0::EffectBoundary { channel } = &self.nodes[node.index()].operation {
506 self.effect_receipts.push(EffectReceiptV0 {
507 channel: channel.clone(),
508 wave: self.current_wave,
509 state: next,
510 });
511 }
512 self.schedule_parents(node);
513 }
514
515 fn evaluate(&mut self, node: ReactiveNodeIdV0) -> ReactiveStateV0 {
516 let operation = self.nodes[node.index()].operation.clone();
517 let dependencies = self.nodes[node.index()].dependencies.clone();
518 match operation {
519 NodeOperationV0::Input | NodeOperationV0::AsyncResult => {
520 self.nodes[node.index()].state.clone()
521 }
522 NodeOperationV0::Map { operation } => {
523 operation(&self.nodes[dependencies[0].index()].state)
524 }
525 NodeOperationV0::Zip { operation } => operation(
526 &self.nodes[dependencies[0].index()].state,
527 &self.nodes[dependencies[1].index()].state,
528 ),
529 NodeOperationV0::Switch => match &self.nodes[dependencies[0].index()].state {
530 ReactiveStateV0::Available(ReactiveValueV0::Bool(false)) => {
531 self.nodes[dependencies[1].index()].state.clone()
532 }
533 ReactiveStateV0::Available(ReactiveValueV0::Bool(true)) => {
534 self.nodes[dependencies[2].index()].state.clone()
535 }
536 ReactiveStateV0::Available(_) => {
537 ReactiveStateV0::Unavailable(ReactiveUnavailableV0::new(
538 "switchSelectorTypeMismatch",
539 "a switch selector must carry a boolean value",
540 ))
541 }
542 ReactiveStateV0::Unavailable(unavailable) => {
543 ReactiveStateV0::Unavailable(unavailable.clone())
544 }
545 },
546 NodeOperationV0::DeltaFold { keys } => {
547 let mut entries = std::mem::take(&mut self.nodes[node.index()].delta_entries);
548 let dirty_dependencies =
549 std::mem::take(&mut self.nodes[node.index()].delta_dirty_dependencies);
550 let mut digest = match &self.nodes[node.index()].state {
551 ReactiveStateV0::Available(ReactiveValueV0::Digest(digest)) => *digest,
552 _ => [0; 32],
553 };
554 let refresh_all = entries.is_empty() || dirty_dependencies.is_empty();
555 for (key, dependency) in keys.iter().zip(dependencies) {
556 if !refresh_all && !dirty_dependencies.contains(&dependency) {
557 continue;
558 }
559 let next = self.nodes[dependency.index()].state.clone();
560 if let Some(previous) = entries.insert(key.clone(), next.clone()) {
561 xor_digest(&mut digest, &digest_entry(key, &previous));
562 }
563 xor_digest(&mut digest, &digest_entry(key, &next));
564 self.nodes[node.index()].delta_update_count += 1;
565 }
566 self.nodes[node.index()].delta_entries = entries;
567 ReactiveStateV0::Available(ReactiveValueV0::Digest(digest))
568 }
569 NodeOperationV0::EffectBoundary { .. } => {
570 self.nodes[dependencies[0].index()].state.clone()
571 }
572 }
573 }
574
575 fn increase_necessity(&mut self, node: ReactiveNodeIdV0) {
576 self.nodes[node.index()].needed_by += 1;
577 if self.nodes[node.index()].needed_by != 1 {
578 return;
579 }
580 for dependency in self.nodes[node.index()].dependencies.clone() {
581 self.increase_necessity(dependency);
582 }
583 if !matches!(
584 self.nodes[node.index()].operation,
585 NodeOperationV0::Input | NodeOperationV0::AsyncResult
586 ) && self.nodes[node.index()].stale
587 {
588 self.activation_nodes.insert(node);
589 }
590 }
591
592 fn decrease_necessity(&mut self, node: ReactiveNodeIdV0) {
593 debug_assert!(self.nodes[node.index()].needed_by > 0);
594 self.nodes[node.index()].needed_by -= 1;
595 if self.nodes[node.index()].needed_by != 0 {
596 return;
597 }
598 for dependency in self.nodes[node.index()].dependencies.clone() {
599 self.decrease_necessity(dependency);
600 }
601 }
602
603 fn schedule_parents(&mut self, node: ReactiveNodeIdV0) {
604 for parent in self.nodes[node.index()].parents.clone() {
605 if matches!(
606 self.nodes[parent.index()].operation,
607 NodeOperationV0::DeltaFold { .. }
608 ) {
609 self.nodes[parent.index()]
610 .delta_dirty_dependencies
611 .insert(node);
612 }
613 if self.nodes[parent.index()].needed_by > 0 {
614 self.schedule(parent);
615 } else {
616 self.nodes[parent.index()].stale = true;
617 }
618 }
619 }
620
621 fn schedule(&mut self, node: ReactiveNodeIdV0) {
622 if self.nodes[node.index()].scheduled_wave == Some(self.current_wave) {
623 return;
624 }
625 self.nodes[node.index()].scheduled_wave = Some(self.current_wave);
626 self.queues
627 .entry(self.nodes[node.index()].height)
628 .or_default()
629 .push_back(node);
630 }
631
632 fn pop_next_scheduled(&mut self) -> Option<ReactiveNodeIdV0> {
633 loop {
634 let mut entry = self.queues.first_entry()?;
635 let queue = entry.get_mut();
636 if let Some(node) = queue.pop_front() {
637 if queue.is_empty() {
638 entry.remove();
639 }
640 return Some(node);
641 }
642 entry.remove();
643 }
644 }
645
646 fn node(&self, node: ReactiveNodeIdV0) -> Result<&RuntimeNodeV0, ReactiveEngineErrorV0> {
647 if node.graph != self.graph_id {
648 return Err(ReactiveEngineErrorV0::ForeignNodeId {
649 node_index: node.index(),
650 expected_graph: self.graph_id.value(),
651 actual_graph: node.graph.value(),
652 });
653 }
654 self.nodes
655 .get(node.index())
656 .ok_or(ReactiveEngineErrorV0::InvalidNode {
657 node_index: node.index(),
658 })
659 }
660
661 #[cfg(test)]
662 pub(crate) fn corrupt_delta_fold_entry_for_test(
663 &mut self,
664 node: ReactiveNodeIdV0,
665 key: &str,
666 state: ReactiveStateV0,
667 ) {
668 self.nodes[node.index()]
669 .delta_entries
670 .insert(key.to_string(), state);
671 }
672
673 #[cfg(test)]
674 pub(crate) fn replace_state_without_scheduling_for_test(
675 &mut self,
676 node: ReactiveNodeIdV0,
677 state: ReactiveStateV0,
678 ) {
679 self.nodes[node.index()].state = state;
680 }
681}
682
683fn full_delta_digest(entries: &BTreeMap<String, ReactiveStateV0>) -> [u8; 32] {
684 let mut digest = [0; 32];
685 for (key, state) in entries {
686 xor_digest(&mut digest, &digest_entry(key, state));
687 }
688 digest
689}
690
691fn digest_entry(key: &str, state: &ReactiveStateV0) -> [u8; 32] {
692 let mut hasher = blake3::Hasher::new();
693 hasher.update(b"omena-reactive-delta-entry-v0");
694 hash_text(&mut hasher, key);
695 hash_state(&mut hasher, state);
696 *hasher.finalize().as_bytes()
697}
698
699fn hash_state(hasher: &mut blake3::Hasher, state: &ReactiveStateV0) {
700 match state {
701 ReactiveStateV0::Available(value) => {
702 hasher.update(&[0]);
703 hash_value(hasher, value);
704 }
705 ReactiveStateV0::Unavailable(unavailable) => {
706 hasher.update(&[1]);
707 hash_text(hasher, &unavailable.code);
708 hash_text(hasher, &unavailable.detail);
709 }
710 }
711}
712
713fn hash_value(hasher: &mut blake3::Hasher, value: &ReactiveValueV0) {
714 match value {
715 ReactiveValueV0::Unit => {
716 hasher.update(&[0]);
717 }
718 ReactiveValueV0::Bool(value) => {
719 hasher.update(&[1, u8::from(*value)]);
720 }
721 ReactiveValueV0::Counter(value) => {
722 hasher.update(&[2]);
723 hasher.update(&value.to_le_bytes());
724 }
725 ReactiveValueV0::Text(value) => {
726 hasher.update(&[3]);
727 hash_text(hasher, value);
728 }
729 ReactiveValueV0::StringSet(values) => {
730 hasher.update(&[4]);
731 hasher.update(&(values.len() as u64).to_le_bytes());
732 for value in values {
733 hash_text(hasher, value);
734 }
735 }
736 ReactiveValueV0::TextMap(values) => {
737 hasher.update(&[5]);
738 hasher.update(&(values.len() as u64).to_le_bytes());
739 for (key, value) in values {
740 hash_text(hasher, key);
741 hash_text(hasher, value);
742 }
743 }
744 ReactiveValueV0::Tuple(values) => {
745 hasher.update(&[6]);
746 hasher.update(&(values.len() as u64).to_le_bytes());
747 for value in values {
748 hash_value(hasher, value);
749 }
750 }
751 ReactiveValueV0::Digest(value) => {
752 hasher.update(&[7]);
753 hasher.update(value);
754 }
755 }
756}
757
758fn hash_text(hasher: &mut blake3::Hasher, value: &str) {
759 hasher.update(&(value.len() as u64).to_le_bytes());
760 hasher.update(value.as_bytes());
761}
762
763fn xor_digest(left: &mut [u8; 32], right: &[u8; 32]) {
764 for (left_byte, right_byte) in left.iter_mut().zip(right) {
765 *left_byte ^= right_byte;
766 }
767}
768
769#[cfg(test)]
770mod tests {
771 use std::collections::BTreeSet;
772
773 use super::*;
774 use crate::{ReactiveGraphBuilderV0, ReactiveValueV0};
775
776 fn exact_policy() -> ChangePolicyV0 {
777 ChangePolicyV0::exact("testSemanticValue")
778 }
779
780 fn counter_map(state: &ReactiveStateV0) -> ReactiveStateV0 {
781 match state {
782 ReactiveStateV0::Available(ReactiveValueV0::Counter(value)) => {
783 ReactiveStateV0::available(ReactiveValueV0::Counter(value.saturating_add(1)))
784 }
785 _ => ReactiveStateV0::unavailable("counterInputRequired", "expected a counter"),
786 }
787 }
788
789 fn counter_zip(left: &ReactiveStateV0, right: &ReactiveStateV0) -> ReactiveStateV0 {
790 match (left, right) {
791 (
792 ReactiveStateV0::Available(ReactiveValueV0::Counter(left)),
793 ReactiveStateV0::Available(ReactiveValueV0::Counter(right)),
794 ) => ReactiveStateV0::available(ReactiveValueV0::Counter(left.saturating_add(*right))),
795 _ => ReactiveStateV0::unavailable("counterInputsRequired", "expected two counters"),
796 }
797 }
798
799 fn tuple_revision(state: &ReactiveStateV0) -> ReactiveStateV0 {
800 match state {
801 ReactiveStateV0::Available(ReactiveValueV0::Tuple(values)) => values
802 .get(1)
803 .cloned()
804 .map(ReactiveStateV0::available)
805 .unwrap_or_else(|| {
806 ReactiveStateV0::unavailable(
807 "revisionMissing",
808 "the tuple must carry a revision",
809 )
810 }),
811 _ => ReactiveStateV0::unavailable("tupleRequired", "expected a tuple"),
812 }
813 }
814
815 fn always_different(_: &ReactiveStateV0, _: &ReactiveStateV0) -> bool {
816 false
817 }
818
819 fn text_field_only(previous: &ReactiveStateV0, next: &ReactiveStateV0) -> bool {
820 fn text(state: &ReactiveStateV0) -> Option<&str> {
821 let ReactiveStateV0::Available(ReactiveValueV0::Tuple(values)) = state else {
822 return None;
823 };
824 let Some(ReactiveValueV0::Text(text)) = values.first() else {
825 return None;
826 };
827 Some(text)
828 }
829 text(previous) == text(next)
830 }
831
832 fn revisioned_text(text: &str, revision: u64) -> ReactiveStateV0 {
833 ReactiveStateV0::available(ReactiveValueV0::Tuple(vec![
834 ReactiveValueV0::Text(text.to_string()),
835 ReactiveValueV0::Counter(revision),
836 ]))
837 }
838
839 fn settled(engine: &mut ReactiveEngineV0) -> Result<(), ReactiveEngineErrorV0> {
840 loop {
841 if matches!(
842 engine.stabilize_step(64)?,
843 StabilizeStatusV0::Settled { .. }
844 ) {
845 return Ok(());
846 }
847 }
848 }
849
850 #[test]
851 fn graph_exposes_all_static_node_kinds() -> Result<(), Box<dyn Error>> {
852 let mut graph = ReactiveGraphBuilderV0::new();
853 let input = graph.add_input(
854 ReactiveStateV0::available(ReactiveValueV0::Counter(1)),
855 exact_policy(),
856 );
857 let asynchronous = graph.add_async_result(
858 ReactiveStateV0::available(ReactiveValueV0::Counter(2)),
859 exact_policy(),
860 );
861 let mapped = graph.add_map(input, counter_map, exact_policy());
862 let zipped = graph.add_zip(mapped, asynchronous, counter_zip, exact_policy());
863 let selector = graph.add_input(
864 ReactiveStateV0::available(ReactiveValueV0::Bool(true)),
865 exact_policy(),
866 );
867 let switched = graph.add_switch(selector, mapped, zipped, exact_policy());
868 let folded = graph.add_delta_fold(
869 vec![
870 ("mapped".to_string(), mapped),
871 ("switched".to_string(), switched),
872 ],
873 exact_policy(),
874 )?;
875 let boundary = graph.add_effect_boundary(folded, "diagnostics", exact_policy());
876 let engine = graph.build()?;
877
878 let kinds = [
879 input,
880 mapped,
881 zipped,
882 switched,
883 folded,
884 asynchronous,
885 boundary,
886 ]
887 .into_iter()
888 .map(|node| engine.node_kind(node))
889 .collect::<Result<BTreeSet<_>, _>>()?;
890 assert_eq!(
891 kinds,
892 BTreeSet::from([
893 ReactiveNodeKindV0::Input,
894 ReactiveNodeKindV0::Map,
895 ReactiveNodeKindV0::Zip,
896 ReactiveNodeKindV0::Switch,
897 ReactiveNodeKindV0::DeltaFold,
898 ReactiveNodeKindV0::AsyncResult,
899 ReactiveNodeKindV0::EffectBoundary,
900 ])
901 );
902 Ok(())
903 }
904
905 #[test]
906 fn runtime_entry_points_reject_foreign_graph_node_ids() -> Result<(), Box<dyn Error>> {
907 let mut first_graph = ReactiveGraphBuilderV0::new();
908 let first_input = first_graph.add_input(
909 ReactiveStateV0::available(ReactiveValueV0::Counter(1)),
910 exact_policy(),
911 );
912 let first_second_input = first_graph.add_input(
913 ReactiveStateV0::available(ReactiveValueV0::Counter(2)),
914 exact_policy(),
915 );
916 let mut first_engine = first_graph.build()?;
917
918 let mut second_graph = ReactiveGraphBuilderV0::new();
919 let _second_input = second_graph.add_input(
920 ReactiveStateV0::available(ReactiveValueV0::Counter(100)),
921 exact_policy(),
922 );
923 let second_second_input = second_graph.add_input(
924 ReactiveStateV0::available(ReactiveValueV0::Counter(200)),
925 exact_policy(),
926 );
927 let _second_engine = second_graph.build()?;
928
929 for result in [
930 first_engine.node_kind(second_second_input).map(|_| ()),
931 first_engine.state(second_second_input).map(|_| ()),
932 first_engine.is_necessary(second_second_input).map(|_| ()),
933 first_engine.is_stale(second_second_input).map(|_| ()),
934 first_engine
935 .node_recompute_count(second_second_input)
936 .map(|_| ()),
937 first_engine
938 .delta_update_count(second_second_input)
939 .map(|_| ()),
940 first_engine.observe(second_second_input),
941 first_engine.unobserve(second_second_input),
942 first_engine.deposit(
943 second_second_input,
944 ReactiveStateV0::available(ReactiveValueV0::Counter(777)),
945 ),
946 ] {
947 assert!(matches!(
948 result,
949 Err(ReactiveEngineErrorV0::ForeignNodeId {
950 node_index: 1,
951 expected_graph,
952 actual_graph,
953 }) if expected_graph != actual_graph
954 ));
955 }
956 assert!(matches!(
957 first_engine.verify_delta_fold(second_second_input),
958 Err(DeltaFoldParityErrorV0::ForeignNodeId {
959 node_index: 1,
960 expected_graph,
961 actual_graph,
962 }) if expected_graph != actual_graph
963 ));
964 assert_eq!(
965 first_engine.state(first_second_input)?,
966 &ReactiveStateV0::available(ReactiveValueV0::Counter(2))
967 );
968 assert_eq!(
969 first_engine.state(first_input)?,
970 &ReactiveStateV0::available(ReactiveValueV0::Counter(1))
971 );
972 Ok(())
973 }
974
975 #[test]
976 fn deposits_arriving_mid_wave_wait_for_the_next_wave() -> Result<(), Box<dyn Error>> {
977 let mut graph = ReactiveGraphBuilderV0::new();
978 let input = graph.add_input(
979 ReactiveStateV0::available(ReactiveValueV0::Counter(0)),
980 exact_policy(),
981 );
982 let mapped = graph.add_map(input, counter_map, exact_policy());
983 let boundary = graph.add_effect_boundary(mapped, "result", exact_policy());
984 let mut engine = graph.build()?;
985 engine.observe(boundary)?;
986 engine.deposit(
987 input,
988 ReactiveStateV0::available(ReactiveValueV0::Counter(1)),
989 )?;
990
991 assert!(matches!(
992 engine.stabilize_step(1)?,
993 StabilizeStatusV0::Pending { .. }
994 ));
995 engine.deposit(
996 input,
997 ReactiveStateV0::available(ReactiveValueV0::Counter(2)),
998 )?;
999 settled(&mut engine)?;
1000 assert_eq!(
1001 engine.drain_effect_receipts(),
1002 vec![EffectReceiptV0 {
1003 channel: "result".to_string(),
1004 wave: 1,
1005 state: ReactiveStateV0::available(ReactiveValueV0::Counter(2)),
1006 }]
1007 );
1008
1009 settled(&mut engine)?;
1010 assert_eq!(
1011 engine.drain_effect_receipts(),
1012 vec![EffectReceiptV0 {
1013 channel: "result".to_string(),
1014 wave: 2,
1015 state: ReactiveStateV0::available(ReactiveValueV0::Counter(3)),
1016 }]
1017 );
1018 Ok(())
1019 }
1020
1021 #[test]
1022 fn delta_fold_is_checked_against_a_full_rebuild() -> Result<(), Box<dyn Error>> {
1023 let mut graph = ReactiveGraphBuilderV0::new();
1024 let first = graph.add_input(
1025 ReactiveStateV0::available(ReactiveValueV0::Counter(1)),
1026 exact_policy(),
1027 );
1028 let second = graph.add_input(
1029 ReactiveStateV0::available(ReactiveValueV0::Counter(2)),
1030 exact_policy(),
1031 );
1032 let fold = graph.add_delta_fold(
1033 vec![("first".to_string(), first), ("second".to_string(), second)],
1034 exact_policy(),
1035 )?;
1036 let mut engine = graph.build()?;
1037 engine.observe(fold)?;
1038 settled(&mut engine)?;
1039 engine.verify_delta_fold(fold)?;
1040 assert_eq!(engine.delta_update_count(fold)?, 2);
1041
1042 engine.deposit(
1043 first,
1044 ReactiveStateV0::available(ReactiveValueV0::Counter(3)),
1045 )?;
1046 settled(&mut engine)?;
1047 engine.verify_delta_fold(fold)?;
1048 assert_eq!(
1049 engine.delta_update_count(fold)?,
1050 3,
1051 "only the changed key should be revisited after the initial rebuild"
1052 );
1053
1054 engine.corrupt_delta_fold_entry_for_test(
1055 fold,
1056 "first",
1057 ReactiveStateV0::available(ReactiveValueV0::Counter(99)),
1058 );
1059 assert!(matches!(
1060 engine.verify_delta_fold(fold),
1061 Err(DeltaFoldParityErrorV0::Diverged {
1062 incremental_digest,
1063 rebuilt_digest,
1064 ..
1065 }) if incremental_digest == rebuilt_digest
1066 ));
1067 Ok(())
1068 }
1069
1070 #[test]
1071 fn delta_fold_detects_dependency_changes_missed_by_dirty_tracking() -> Result<(), Box<dyn Error>>
1072 {
1073 let mut graph = ReactiveGraphBuilderV0::new();
1074 let first = graph.add_input(
1075 ReactiveStateV0::available(ReactiveValueV0::Counter(1)),
1076 exact_policy(),
1077 );
1078 let second = graph.add_input(
1079 ReactiveStateV0::available(ReactiveValueV0::Counter(2)),
1080 exact_policy(),
1081 );
1082 let fold = graph.add_delta_fold(
1083 vec![("first".to_string(), first), ("second".to_string(), second)],
1084 exact_policy(),
1085 )?;
1086 let mut engine = graph.build()?;
1087 engine.observe(fold)?;
1088 settled(&mut engine)?;
1089
1090 engine.replace_state_without_scheduling_for_test(
1091 first,
1092 ReactiveStateV0::available(ReactiveValueV0::Counter(999)),
1093 );
1094
1095 assert!(matches!(
1096 engine.verify_delta_fold(fold),
1097 Err(DeltaFoldParityErrorV0::Diverged {
1098 incremental_digest,
1099 rebuilt_digest,
1100 ..
1101 }) if incremental_digest != rebuilt_digest
1102 ));
1103 Ok(())
1104 }
1105
1106 #[test]
1107 fn dependency_rebuild_does_not_read_incremental_entries() {
1108 let source = include_str!("engine.rs");
1109 let marker = "fn rebuild_delta_fold_from_dependencies";
1110 let start = source.find(marker).unwrap_or(source.len());
1111 assert!(
1112 start < source.len(),
1113 "dependency rebuild function is missing"
1114 );
1115 let after_start = &source[start..];
1116 let end = after_start
1117 .find("\n fn begin_wave")
1118 .unwrap_or(after_start.len());
1119 assert!(
1120 end < after_start.len(),
1121 "dependency rebuild function boundary is missing"
1122 );
1123 let function = &after_start[..end];
1124 assert!(
1125 !function.contains("delta_entries"),
1126 "dependency rebuild must not read incremental fold entries"
1127 );
1128 }
1129
1130 #[test]
1131 fn unavailable_nodes_do_not_poison_independent_branches() -> Result<(), Box<dyn Error>> {
1132 let mut graph = ReactiveGraphBuilderV0::new();
1133 let unavailable = graph.add_async_result(
1134 ReactiveStateV0::unavailable("queryCancelled", "the query was cancelled"),
1135 exact_policy(),
1136 );
1137 let healthy = graph.add_input(
1138 ReactiveStateV0::available(ReactiveValueV0::Counter(7)),
1139 exact_policy(),
1140 );
1141 let failed_map = graph.add_map(unavailable, counter_map, exact_policy());
1142 let healthy_map = graph.add_map(healthy, counter_map, exact_policy());
1143 let mut engine = graph.build()?;
1144 engine.observe(failed_map)?;
1145 engine.observe(healthy_map)?;
1146 settled(&mut engine)?;
1147
1148 assert!(matches!(
1149 engine.state(failed_map)?,
1150 ReactiveStateV0::Unavailable(_)
1151 ));
1152 assert_eq!(
1153 engine.state(healthy_map)?,
1154 &ReactiveStateV0::available(ReactiveValueV0::Counter(8))
1155 );
1156 Ok(())
1157 }
1158
1159 #[test]
1160 fn unobserved_nodes_stay_stale_until_needed() -> Result<(), Box<dyn Error>> {
1161 let mut graph = ReactiveGraphBuilderV0::new();
1162 let input = graph.add_input(
1163 ReactiveStateV0::available(ReactiveValueV0::Counter(1)),
1164 exact_policy(),
1165 );
1166 let mapped = graph.add_map(input, counter_map, exact_policy());
1167 let mut engine = graph.build()?;
1168 engine.deposit(
1169 input,
1170 ReactiveStateV0::available(ReactiveValueV0::Counter(2)),
1171 )?;
1172 settled(&mut engine)?;
1173 assert!(engine.is_stale(mapped)?);
1174 assert_eq!(engine.node_recompute_count(mapped)?, 0);
1175
1176 engine.observe(mapped)?;
1177 settled(&mut engine)?;
1178 assert!(!engine.is_stale(mapped)?);
1179 assert_eq!(engine.node_recompute_count(mapped)?, 1);
1180 Ok(())
1181 }
1182
1183 #[test]
1184 fn bounded_steps_recompute_each_node_at_most_once_per_wave() -> Result<(), Box<dyn Error>> {
1185 let mut graph = ReactiveGraphBuilderV0::new();
1186 let input = graph.add_input(
1187 ReactiveStateV0::available(ReactiveValueV0::Counter(0)),
1188 exact_policy(),
1189 );
1190 let first = graph.add_map(input, counter_map, exact_policy());
1191 let second = graph.add_map(first, counter_map, exact_policy());
1192 let boundary = graph.add_effect_boundary(second, "result", exact_policy());
1193 let mut engine = graph.build()?;
1194 engine.observe(boundary)?;
1195 engine.deposit(
1196 input,
1197 ReactiveStateV0::available(ReactiveValueV0::Counter(1)),
1198 )?;
1199
1200 let mut steps = 0;
1201 loop {
1202 steps += 1;
1203 if matches!(engine.stabilize_step(1)?, StabilizeStatusV0::Settled { .. }) {
1204 break;
1205 }
1206 }
1207 assert_eq!(steps, 3);
1208 assert_eq!(engine.node_recompute_count(first)?, 1);
1209 assert_eq!(engine.node_recompute_count(second)?, 1);
1210 assert_eq!(engine.node_recompute_count(boundary)?, 1);
1211 Ok(())
1212 }
1213
1214 #[test]
1215 fn semantic_policy_avoids_spurious_refires_for_equal_allocations() -> Result<(), Box<dyn Error>>
1216 {
1217 let mut exact_graph = ReactiveGraphBuilderV0::new();
1218 let exact_input = exact_graph.add_input(revisioned_text("same", 1), exact_policy());
1219 let exact_map = exact_graph.add_map(exact_input, tuple_revision, exact_policy());
1220 let mut exact_engine = exact_graph.build()?;
1221 exact_engine.observe(exact_map)?;
1222 settled(&mut exact_engine)?;
1223 exact_engine.deposit(exact_input, revisioned_text("same", 1))?;
1224 settled(&mut exact_engine)?;
1225 assert_eq!(exact_engine.node_recompute_count(exact_map)?, 1);
1226
1227 let mut identity_graph = ReactiveGraphBuilderV0::new();
1228 let identity_input = identity_graph.add_input(
1229 revisioned_text("same", 1),
1230 ChangePolicyV0::custom("allocationIdentity", always_different),
1231 );
1232 let identity_map = identity_graph.add_map(identity_input, tuple_revision, exact_policy());
1233 let mut identity_engine = identity_graph.build()?;
1234 identity_engine.observe(identity_map)?;
1235 settled(&mut identity_engine)?;
1236 identity_engine.deposit(identity_input, revisioned_text("same", 1))?;
1237 settled(&mut identity_engine)?;
1238 assert_eq!(identity_engine.node_recompute_count(identity_map)?, 2);
1239 Ok(())
1240 }
1241
1242 #[test]
1243 fn semantic_policy_preserves_updates_hidden_by_partial_equality() -> Result<(), Box<dyn Error>>
1244 {
1245 let mut exact_graph = ReactiveGraphBuilderV0::new();
1246 let exact_input = exact_graph.add_input(revisioned_text("stable", 1), exact_policy());
1247 let exact_map = exact_graph.add_map(exact_input, tuple_revision, exact_policy());
1248 let mut exact_engine = exact_graph.build()?;
1249 exact_engine.observe(exact_map)?;
1250 settled(&mut exact_engine)?;
1251 exact_engine.deposit(exact_input, revisioned_text("stable", 2))?;
1252 settled(&mut exact_engine)?;
1253 assert_eq!(
1254 exact_engine.state(exact_map)?,
1255 &ReactiveStateV0::available(ReactiveValueV0::Counter(2))
1256 );
1257
1258 let mut partial_graph = ReactiveGraphBuilderV0::new();
1259 let partial_input = partial_graph.add_input(
1260 revisioned_text("stable", 1),
1261 ChangePolicyV0::custom("textFieldOnly", text_field_only),
1262 );
1263 let partial_map = partial_graph.add_map(partial_input, tuple_revision, exact_policy());
1264 let mut partial_engine = partial_graph.build()?;
1265 partial_engine.observe(partial_map)?;
1266 settled(&mut partial_engine)?;
1267 partial_engine.deposit(partial_input, revisioned_text("stable", 2))?;
1268 settled(&mut partial_engine)?;
1269 assert_eq!(
1270 partial_engine.state(partial_map)?,
1271 &ReactiveStateV0::available(ReactiveValueV0::Counter(1))
1272 );
1273 Ok(())
1274 }
1275}