1pub mod messaging;
9pub mod work_queue;
10
11use std::cell::{Cell, RefCell};
12use std::collections::HashSet;
13use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
14use std::rc::Rc;
15
16use serde::{de::DeserializeOwned, Serialize};
17use serde_json::Value;
18
19use crate::ctx::Ctx;
20use crate::graph::{Graph, GraphNodeOpts};
21use crate::identity::compound_tuple_key;
22use crate::node::{Core, Node};
23use crate::operators::Operator;
24
25#[derive(Debug, Clone, PartialEq)]
26pub struct ProcessCommand<T = crate::protocol::AnyValue> {
28 pub id: String,
30 pub command_type: String,
32 pub payload: T,
34 pub process_id: Option<String>,
36 pub correlation_id: Option<String>,
38 pub causation_id: Option<String>,
40}
41
42impl<T> ProcessCommand<T> {
43 pub fn new(id: impl Into<String>, command_type: impl Into<String>, payload: T) -> Self {
45 Self {
46 id: id.into(),
47 command_type: command_type.into(),
48 payload,
49 process_id: None,
50 correlation_id: None,
51 causation_id: None,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq)]
57pub struct ProcessEventDraft<T = crate::protocol::AnyValue> {
59 pub id: Option<String>,
61 pub event_type: String,
63 pub payload: T,
65 pub process_id: Option<String>,
67 pub correlation_id: Option<String>,
69 pub causation_id: Option<String>,
71}
72
73impl<T> ProcessEventDraft<T> {
74 pub fn new(event_type: impl Into<String>, payload: T) -> Self {
76 Self {
77 id: None,
78 event_type: event_type.into(),
79 payload,
80 process_id: None,
81 correlation_id: None,
82 causation_id: None,
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub struct ProcessEvent<T = crate::protocol::AnyValue> {
90 pub id: String,
92 pub event_type: String,
94 pub seq: u64,
96 pub cursor: u64,
98 pub command_id: String,
100 pub command_type: String,
102 pub payload: T,
104 pub timestamp_ms: u64,
106 pub process_id: Option<String>,
108 pub correlation_id: Option<String>,
110 pub causation_id: Option<String>,
112}
113
114#[derive(Debug, Clone, PartialEq)]
115pub struct ProcessEffectRequestDraft<T = crate::protocol::AnyValue> {
117 pub id: Option<String>,
119 pub effect_type: String,
121 pub payload: T,
123 pub process_id: Option<String>,
125 pub correlation_id: Option<String>,
127 pub causation_id: Option<String>,
129}
130
131impl<T> ProcessEffectRequestDraft<T> {
132 pub fn new(effect_type: impl Into<String>, payload: T) -> Self {
134 Self {
135 id: None,
136 effect_type: effect_type.into(),
137 payload,
138 process_id: None,
139 correlation_id: None,
140 causation_id: None,
141 }
142 }
143}
144
145#[derive(Debug, Clone, PartialEq)]
146pub struct ProcessEffectRequest<T = crate::protocol::AnyValue> {
148 pub id: String,
150 pub effect_type: String,
152 pub seq: u64,
154 pub cursor: u64,
156 pub command_id: String,
158 pub command_type: String,
160 pub payload: T,
162 pub timestamp_ms: u64,
164 pub process_id: Option<String>,
166 pub correlation_id: Option<String>,
168 pub causation_id: Option<String>,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum ProcessEffectOutcomeKind {
175 Result,
177 Failure,
179 Cancel,
181 Timeout,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum ProcessEffectCommandType {
188 Result,
190 Failure,
192 Cancel,
194 Timeout,
196}
197
198impl ProcessEffectCommandType {
199 pub fn as_str(self) -> &'static str {
201 match self {
202 Self::Result => "effect.result",
203 Self::Failure => "effect.failure",
204 Self::Cancel => "effect.cancel",
205 Self::Timeout => "effect.timeout",
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq)]
211pub struct ProcessEffectOutcome<TResult = crate::protocol::AnyValue> {
213 pub kind: ProcessEffectOutcomeKind,
215 pub effect_id: String,
217 pub effect_type: String,
219 pub value: Option<TResult>,
221 pub error: Option<String>,
223 pub reason: Option<String>,
225 pub command_id: Option<String>,
227 pub process_id: Option<String>,
229 pub correlation_id: Option<String>,
231 pub causation_id: Option<String>,
233}
234
235impl<TResult> ProcessEffectOutcome<TResult> {
236 pub fn result(
238 effect_id: impl Into<String>,
239 effect_type: impl Into<String>,
240 value: TResult,
241 ) -> Self {
242 Self {
243 kind: ProcessEffectOutcomeKind::Result,
244 effect_id: effect_id.into(),
245 effect_type: effect_type.into(),
246 value: Some(value),
247 error: None,
248 reason: None,
249 command_id: None,
250 process_id: None,
251 correlation_id: None,
252 causation_id: None,
253 }
254 }
255
256 pub fn failure(
258 effect_id: impl Into<String>,
259 effect_type: impl Into<String>,
260 error: impl Into<String>,
261 ) -> Self {
262 Self {
263 kind: ProcessEffectOutcomeKind::Failure,
264 effect_id: effect_id.into(),
265 effect_type: effect_type.into(),
266 value: None,
267 error: Some(error.into()),
268 reason: None,
269 command_id: None,
270 process_id: None,
271 correlation_id: None,
272 causation_id: None,
273 }
274 }
275
276 pub fn cancel(effect_id: impl Into<String>, effect_type: impl Into<String>) -> Self {
278 Self {
279 kind: ProcessEffectOutcomeKind::Cancel,
280 effect_id: effect_id.into(),
281 effect_type: effect_type.into(),
282 value: None,
283 error: None,
284 reason: None,
285 command_id: None,
286 process_id: None,
287 correlation_id: None,
288 causation_id: None,
289 }
290 }
291
292 pub fn timeout(
294 effect_id: impl Into<String>,
295 effect_type: impl Into<String>,
296 error: impl Into<String>,
297 ) -> Self {
298 Self {
299 kind: ProcessEffectOutcomeKind::Timeout,
300 effect_id: effect_id.into(),
301 effect_type: effect_type.into(),
302 value: None,
303 error: Some(error.into()),
304 reason: None,
305 command_id: None,
306 process_id: None,
307 correlation_id: None,
308 causation_id: None,
309 }
310 }
311
312 pub fn with_command_id(mut self, command_id: impl Into<String>) -> Self {
314 self.command_id = Some(command_id.into());
315 self
316 }
317
318 pub fn with_process_id(mut self, process_id: impl Into<String>) -> Self {
320 self.process_id = Some(process_id.into());
321 self
322 }
323
324 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
326 self.correlation_id = Some(correlation_id.into());
327 self
328 }
329
330 pub fn with_causation_id(mut self, causation_id: impl Into<String>) -> Self {
332 self.causation_id = Some(causation_id.into());
333 self
334 }
335
336 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
338 self.reason = Some(reason.into());
339 self
340 }
341}
342
343#[derive(Debug, Clone, PartialEq)]
344pub struct ProcessEffectCommandPayload<TResult = crate::protocol::AnyValue> {
346 pub kind: ProcessEffectOutcomeKind,
348 pub effect_id: String,
350 pub effect_type: String,
352 pub value: Option<TResult>,
354 pub error: Option<String>,
356 pub reason: Option<String>,
358 pub process_id: Option<String>,
360 pub correlation_id: Option<String>,
362 pub causation_id: Option<String>,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub enum ProcessEffectRunnerStatusState {
369 Requested,
371 Commanded,
373 Rejected,
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct ProcessEffectRunnerStatus {
380 pub state: ProcessEffectRunnerStatusState,
382 pub effect_id: Option<String>,
384 pub effect_type: Option<String>,
386 pub command_id: Option<String>,
388 pub command_type: Option<ProcessEffectCommandType>,
390 pub requested: u64,
392 pub commanded: u64,
394 pub rejected: u64,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum ProcessEffectRunnerErrorCode {
401 MalformedOutcome,
403}
404
405#[derive(Debug, Clone, PartialEq)]
406pub struct ProcessEffectRunnerError<TResult = crate::protocol::AnyValue> {
408 pub code: ProcessEffectRunnerErrorCode,
410 pub message: String,
412 pub outcome: Option<ProcessEffectOutcome<TResult>>,
414 pub effect_id: Option<String>,
416 pub effect_type: Option<String>,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum ProcessErrorCode {
423 MalformedCommand,
425 ReducerThrew,
427 ClockThrew,
429 MalformedState,
431 MalformedEvent,
433 MalformedEffect,
435}
436
437#[derive(Debug, Clone, PartialEq)]
438pub struct ProcessError<TCommand = crate::protocol::AnyValue> {
440 pub code: ProcessErrorCode,
442 pub message: String,
444 pub command: Option<ProcessCommand<TCommand>>,
446 pub cursor: ProcessCursor,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
451pub struct ProcessCursor {
453 pub event_seq: u64,
455 pub effect_seq: u64,
457 pub command_count: u64,
459 pub error_count: u64,
461 pub audit_seq: u64,
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
466pub struct ProcessStatus {
468 pub state: ProcessStatusState,
470 pub command_id: Option<String>,
472 pub command_type: Option<String>,
474 pub event_count: usize,
476 pub effect_count: usize,
478 pub error_code: Option<ProcessErrorCode>,
480 pub cursor: ProcessCursor,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub enum ProcessStatusState {
487 Accepted,
489 Rejected,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq)]
494pub struct ProcessAuditRecord {
496 pub seq: u64,
498 pub command_id: Option<String>,
500 pub command_type: Option<String>,
502 pub outcome: ProcessAuditOutcome,
504 pub event_ids: Vec<String>,
506 pub event_types: Vec<String>,
508 pub effect_ids: Vec<String>,
510 pub effect_types: Vec<String>,
512 pub error_code: Option<ProcessErrorCode>,
514 pub error_message: Option<String>,
516 pub cursor: ProcessCursor,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
521pub enum ProcessAuditOutcome {
523 Success,
525 Failure,
527}
528
529#[derive(Debug, Clone, PartialEq)]
530pub enum ProcessRuntimeFact<
532 TState = crate::protocol::AnyValue,
533 TEvent = crate::protocol::AnyValue,
534 TEffect = crate::protocol::AnyValue,
535 TCommand = crate::protocol::AnyValue,
536> {
537 State {
539 state: TState,
541 cursor: ProcessCursor,
543 },
544 Event(ProcessEvent<TEvent>),
546 EffectRequest(ProcessEffectRequest<TEffect>),
548 Status(ProcessStatus),
550 Error(ProcessError<TCommand>),
552 Audit(ProcessAuditRecord),
554 Cursor(ProcessCursor),
556}
557
558#[derive(Debug, Clone, PartialEq)]
559pub struct ProcessReduction<TState, TEvent, TEffect> {
561 pub state: TState,
563 pub events: Vec<ProcessEventDraft<TEvent>>,
565 pub effects: Vec<ProcessEffectRequestDraft<TEffect>>,
567}
568
569impl<TState, TEvent, TEffect> ProcessReduction<TState, TEvent, TEffect> {
570 pub fn new(state: TState) -> Self {
572 Self {
573 state,
574 events: Vec::new(),
575 effects: Vec::new(),
576 }
577 }
578
579 pub fn with_events(mut self, events: Vec<ProcessEventDraft<TEvent>>) -> Self {
581 self.events = events;
582 self
583 }
584
585 pub fn with_effects(mut self, effects: Vec<ProcessEffectRequestDraft<TEffect>>) -> Self {
587 self.effects = effects;
588 self
589 }
590}
591
592pub type ProcessReducerFn<TCommand, TState, TEvent, TEffect> =
594 dyn Fn(&ProcessCommand<TCommand>, TState) -> ProcessReduction<TState, TEvent, TEffect>;
595
596pub type ProcessReducer<TCommand, TState, TEvent, TEffect> =
598 Rc<ProcessReducerFn<TCommand, TState, TEvent, TEffect>>;
599
600#[derive(Clone)]
601pub struct ProcessBundleOptions<TCommand, TState, TEvent, TEffect> {
603 pub name: String,
605 pub initial_state: TState,
607 pub reduce: ProcessReducer<TCommand, TState, TEvent, TEffect>,
609 pub now: Rc<dyn Fn() -> u64>,
611}
612
613impl<TCommand, TState, TEvent, TEffect> ProcessBundleOptions<TCommand, TState, TEvent, TEffect> {
614 pub fn new(
616 initial_state: TState,
617 reduce: impl Fn(&ProcessCommand<TCommand>, TState) -> ProcessReduction<TState, TEvent, TEffect>
618 + 'static,
619 ) -> Self {
620 Self {
621 name: "process".to_owned(),
622 initial_state,
623 reduce: Rc::new(reduce),
624 now: Rc::new(|| 0),
625 }
626 }
627
628 pub fn named(mut self, name: impl Into<String>) -> Self {
630 self.name = name.into();
631 self
632 }
633
634 pub fn with_now(mut self, now: impl Fn() -> u64 + 'static) -> Self {
636 self.now = Rc::new(now);
637 self
638 }
639}
640
641#[derive(Clone)]
642pub struct ProcessBundle<
644 TCommand = crate::protocol::AnyValue,
645 TState = crate::protocol::AnyValue,
646 TEvent = crate::protocol::AnyValue,
647 TEffect = crate::protocol::AnyValue,
648> {
649 pub command: Node<ProcessCommand<TCommand>>,
651 pub state: Node<TState>,
653 pub events: Node<ProcessEvent<TEvent>>,
655 pub audit: Node<ProcessAuditRecord>,
657 pub effect_request: Node<ProcessEffectRequest<TEffect>>,
659 pub status: Node<ProcessStatus>,
661 pub error: Node<ProcessError<TCommand>>,
663 pub cursor: Node<ProcessCursor>,
665 command_sources: Rc<RefCell<Vec<Core>>>,
666 command_id: String,
667 _retains: Rc<Vec<ProcessRetain>>,
668}
669
670impl<TCommand: Clone + 'static, TState, TEvent, TEffect>
671 ProcessBundle<TCommand, TState, TEvent, TEffect>
672{
673 pub fn dispatch(&self, command: ProcessCommand<TCommand>) -> ProcessCommand<TCommand> {
675 self.command.set(command.clone());
676 command
677 }
678}
679
680struct ProcessRetain {
681 release: std::cell::RefCell<Option<Box<dyn FnOnce()>>>,
682}
683
684impl ProcessRetain {
685 fn new(release: Box<dyn FnOnce()>) -> Self {
686 Self {
687 release: std::cell::RefCell::new(Some(release)),
688 }
689 }
690}
691
692impl Drop for ProcessRetain {
693 fn drop(&mut self) {
694 if let Some(release) = self.release.borrow_mut().take() {
695 release();
696 }
697 }
698}
699
700pub struct ProcessEffectRunnerOptions<TCommand, TResult = crate::protocol::AnyValue> {
702 pub name: String,
704 pub outcomes: Vec<Node<ProcessEffectOutcome<TResult>>>,
706 command_payload: Rc<dyn Fn(ProcessEffectCommandPayload<TResult>) -> TCommand>,
707}
708
709impl<TResult: Clone + 'static>
710 ProcessEffectRunnerOptions<ProcessEffectCommandPayload<TResult>, TResult>
711{
712 pub fn new(outcomes: Vec<Node<ProcessEffectOutcome<TResult>>>) -> Self {
714 Self {
715 name: "processEffectRunner".to_owned(),
716 outcomes,
717 command_payload: Rc::new(|payload| payload),
718 }
719 }
720}
721
722impl<TCommand, TResult> ProcessEffectRunnerOptions<TCommand, TResult> {
723 pub fn named(mut self, name: impl Into<String>) -> Self {
725 self.name = name.into();
726 self
727 }
728
729 pub fn map_command_payload<TNext>(
731 self,
732 map: impl Fn(ProcessEffectCommandPayload<TResult>) -> TNext + 'static,
733 ) -> ProcessEffectRunnerOptions<TNext, TResult> {
734 ProcessEffectRunnerOptions {
735 name: self.name,
736 outcomes: self.outcomes,
737 command_payload: Rc::new(map),
738 }
739 }
740}
741
742#[derive(Debug, Clone, PartialEq)]
743enum ProcessEffectRunnerFact<TCommand, TResult = crate::protocol::AnyValue> {
744 Outcome(ProcessEffectOutcome<TResult>),
745 Command(
746 ProcessCommand<TCommand>,
747 ProcessEffectCommandPayload<TResult>,
748 ),
749 Error(ProcessEffectRunnerError<TResult>),
750}
751
752pub struct ProcessEffectRunnerBundle<
754 TCommand = ProcessEffectCommandPayload<crate::protocol::AnyValue>,
755 TEffect = crate::protocol::AnyValue,
756 TResult = crate::protocol::AnyValue,
757> {
758 pub requests: Node<ProcessEffectRequest<TEffect>>,
760 pub outcomes: Node<ProcessEffectOutcome<TResult>>,
762 pub commands: Node<ProcessCommand<TCommand>>,
764 pub status: Node<ProcessEffectRunnerStatus>,
766 pub errors: Node<ProcessEffectRunnerError<TResult>>,
768 runtime: Node<ProcessEffectRunnerFact<TCommand, TResult>>,
769 graph: Graph,
770 process_command: Node<ProcessCommand<TCommand>>,
771 process_command_id: String,
772 name: String,
773 command_sources: Rc<RefCell<Vec<Core>>>,
774 released: Cell<bool>,
775 retains: RefCell<Vec<ProcessRetain>>,
776}
777
778impl<TCommand: Clone + 'static, TEffect: Clone + 'static, TResult: Clone + 'static>
779 ProcessEffectRunnerBundle<TCommand, TEffect, TResult>
780{
781 pub fn release(&self) {
783 if self.released.get() {
784 return;
785 }
786 self.preflight_release();
787 detach_process_command_source(
788 &self.process_command,
789 &self.command_sources,
790 self.commands.erased(),
791 );
792 let active_retains = self.retains.replace(Vec::new());
793 drop(active_retains);
794 self.released.set(true);
795 self.graph.release_nodes(
796 &[
797 self.requests.erased(),
798 self.outcomes.erased(),
799 self.runtime.erased(),
800 self.commands.erased(),
801 self.status.erased(),
802 self.errors.erased(),
803 ],
804 "process_effect_runner release",
805 );
806 }
807
808 fn preflight_release(&self) {
809 let release_cores = [
810 self.requests.erased(),
811 self.outcomes.erased(),
812 self.runtime.erased(),
813 self.commands.erased(),
814 self.status.erased(),
815 self.errors.erased(),
816 ];
817 let release_ids = self.release_node_ids();
818 let release_id_set = release_ids.iter().cloned().collect::<HashSet<_>>();
819 for edge in self.graph.describe().edges {
820 if !release_id_set.contains(&edge.from) || release_id_set.contains(&edge.to) {
821 continue;
822 }
823 if edge.from == format!("{}/commands", self.name) && edge.to == self.process_command_id
824 {
825 continue;
826 }
827 panic!(
828 "process_effect_runner: cannot release '{}'; '{}' still depends on '{}' (D122)",
829 self.name, edge.to, edge.from
830 );
831 }
832 for (index, core) in release_cores.iter().enumerate() {
833 assert!(
834 core.runtime_is_quiescent_for_release(),
835 "process_effect_runner: cannot release '{}'; '{}' is not runtime-quiescent (D124)",
836 self.name,
837 release_ids[index]
838 );
839 let internal_subscribers = release_cores
840 .iter()
841 .filter(|dependent| dependent.is_active())
842 .flat_map(Core::deps)
843 .filter(|dep| dep.ptr_eq(core))
844 .count();
845 let retain_subscriber = 1;
846 let process_command_subscriber =
847 usize::from(release_ids[index] == format!("{}/commands", self.name));
848 assert!(
849 core.subscriber_count()
850 <= internal_subscribers + retain_subscriber + process_command_subscriber,
851 "process_effect_runner: cannot release '{}'; '{}' still has live subscribers (D124)",
852 self.name,
853 release_ids[index]
854 );
855 }
856 }
857
858 fn release_node_ids(&self) -> Vec<String> {
859 vec![
860 format!("{}/requests", self.name),
861 format!("{}/outcomes", self.name),
862 format!("{}/runtime", self.name),
863 format!("{}/commands", self.name),
864 format!("{}/status", self.name),
865 format!("{}/errors", self.name),
866 ]
867 }
868}
869
870pub fn process_bundle<
872 TCommand: Clone + 'static,
873 TState: Clone + Serialize + DeserializeOwned + 'static,
874 TEvent: Clone + 'static,
875 TEffect: Clone + 'static,
876>(
877 graph: &Graph,
878 opts: ProcessBundleOptions<TCommand, TState, TEvent, TEffect>,
879) -> ProcessBundle<TCommand, TState, TEvent, TEffect> {
880 assert!(
881 !opts.name.is_empty(),
882 "process_bundle: name must be non-empty"
883 );
884 let name = opts.name;
885 let reduce = opts.reduce;
886 let now = opts.now;
887 let command_sources = Rc::new(RefCell::new(Vec::new()));
888 let initial_state_json = state_to_json(&opts.initial_state)
889 .unwrap_or_else(|message| panic!("process_bundle: {message}"));
890
891 let command_id = format!("{name}/command");
892 let command = graph.init_node::<ProcessCommand<TCommand>>(
893 Operator::with_opts(
894 "processCommand",
895 no_terminal_opts(),
896 process_command_source_body::<TCommand>(0),
897 ),
898 Vec::new(),
899 meta_opts(command_id.clone(), "command"),
900 );
901
902 let runtime = graph.init_node::<ProcessRuntimeFact<TState, TEvent, TEffect, TCommand>>(
903 Operator::with_opts("processRuntime", no_terminal_opts(), {
904 move |ctx: &Ctx| {
905 ctx.state_persist(true);
906 let mut state = match ctx.state_get::<Value>() {
907 Some(value) => match RuntimeState::from_json(value.as_ref()) {
908 Ok(state) => state,
909 Err(message) => {
910 let mut recovery =
911 RuntimeState::cursor_recovery(initial_state_json.clone());
912 for command in ctx.batch::<ProcessCommand<TCommand>>(0) {
913 for fact in failure::<TCommand, TState, TEvent, TEffect>(
914 &mut recovery,
915 Some((*command).clone()),
916 ProcessErrorCode::MalformedState,
917 message.clone(),
918 ) {
919 ctx.emit(fact);
920 }
921 }
922 return;
923 }
924 },
925 None => RuntimeState::new(initial_state_json.clone()),
926 };
927 for command in ctx.batch::<ProcessCommand<TCommand>>(0) {
928 for fact in reduce_process_command_fact(
929 &mut state,
930 (*command).clone(),
931 reduce.as_ref(),
932 now.as_ref(),
933 ) {
934 ctx.emit(fact);
935 }
936 ctx.state_set(state.to_json());
937 }
938 }
939 }),
940 vec![command.erased()],
941 meta_opts(format!("{name}/runtime"), "runtime"),
942 );
943
944 let state = runtime_projection::<TCommand, TState, TEvent, TEffect, TState>(
945 graph,
946 &runtime,
947 &format!("{name}/state"),
948 "processState",
949 |fact| match fact {
950 ProcessRuntimeFact::State { state, .. } => Some(state.clone()),
951 _ => None,
952 },
953 );
954 let events = runtime_projection::<TCommand, TState, TEvent, TEffect, ProcessEvent<TEvent>>(
955 graph,
956 &runtime,
957 &format!("{name}/events"),
958 "processEvents",
959 |fact| match fact {
960 ProcessRuntimeFact::Event(event) => Some(event.clone()),
961 _ => None,
962 },
963 );
964 let audit = runtime_projection::<TCommand, TState, TEvent, TEffect, ProcessAuditRecord>(
965 graph,
966 &runtime,
967 &format!("{name}/audit"),
968 "processAudit",
969 |fact| match fact {
970 ProcessRuntimeFact::Audit(audit) => Some(audit.clone()),
971 _ => None,
972 },
973 );
974 let effect_request =
975 runtime_projection::<TCommand, TState, TEvent, TEffect, ProcessEffectRequest<TEffect>>(
976 graph,
977 &runtime,
978 &format!("{name}/effect_request"),
979 "processEffectRequest",
980 |fact| match fact {
981 ProcessRuntimeFact::EffectRequest(effect) => Some(effect.clone()),
982 _ => None,
983 },
984 );
985 let status = runtime_projection::<TCommand, TState, TEvent, TEffect, ProcessStatus>(
986 graph,
987 &runtime,
988 &format!("{name}/status"),
989 "processStatus",
990 |fact| match fact {
991 ProcessRuntimeFact::Status(status) => Some(status.clone()),
992 _ => None,
993 },
994 );
995 let error = runtime_projection::<TCommand, TState, TEvent, TEffect, ProcessError<TCommand>>(
996 graph,
997 &runtime,
998 &format!("{name}/error"),
999 "processError",
1000 |fact| match fact {
1001 ProcessRuntimeFact::Error(error) => Some(error.clone()),
1002 _ => None,
1003 },
1004 );
1005 let cursor = runtime_projection::<TCommand, TState, TEvent, TEffect, ProcessCursor>(
1006 graph,
1007 &runtime,
1008 &format!("{name}/cursor"),
1009 "processCursor",
1010 |fact| match fact {
1011 ProcessRuntimeFact::Cursor(cursor) => Some(cursor.clone()),
1012 _ => None,
1013 },
1014 );
1015
1016 let retains = Rc::new(vec![
1017 ProcessRetain::new(graph.retain(&runtime, &format!("{name}.process.runtime"))),
1018 ProcessRetain::new(graph.retain(&state, &format!("{name}.process.state"))),
1019 ProcessRetain::new(graph.retain(&events, &format!("{name}.process.events"))),
1020 ProcessRetain::new(graph.retain(&audit, &format!("{name}.process.audit"))),
1021 ProcessRetain::new(
1022 graph.retain(&effect_request, &format!("{name}.process.effect_request")),
1023 ),
1024 ProcessRetain::new(graph.retain(&status, &format!("{name}.process.status"))),
1025 ProcessRetain::new(graph.retain(&error, &format!("{name}.process.error"))),
1026 ProcessRetain::new(graph.retain(&cursor, &format!("{name}.process.cursor"))),
1027 ]);
1028
1029 ProcessBundle {
1030 command,
1031 state,
1032 events,
1033 audit,
1034 effect_request,
1035 status,
1036 error,
1037 cursor,
1038 command_sources,
1039 command_id,
1040 _retains: retains,
1041 }
1042}
1043
1044pub fn process_effect_runner<TCommand, TState, TEvent, TEffect, TResult>(
1046 graph: &Graph,
1047 process: &ProcessBundle<TCommand, TState, TEvent, TEffect>,
1048 opts: ProcessEffectRunnerOptions<TCommand, TResult>,
1049) -> ProcessEffectRunnerBundle<TCommand, TEffect, TResult>
1050where
1051 TCommand: Clone + 'static,
1052 TEffect: Clone + 'static,
1053 TResult: Clone + 'static,
1054{
1055 assert!(
1056 !opts.outcomes.is_empty(),
1057 "process_effect_runner: outcomes must contain at least one node"
1058 );
1059 let name = opts.name;
1060 let command_payload = opts.command_payload;
1061
1062 let requests = graph.init_node::<ProcessEffectRequest<TEffect>>(
1063 Operator::with_opts("processEffectRunnerRequests", no_terminal_opts(), |ctx| {
1064 for request in ctx.batch::<ProcessEffectRequest<TEffect>>(0) {
1065 ctx.emit((*request).clone());
1066 }
1067 }),
1068 vec![process.effect_request.erased()],
1069 effect_runner_meta_opts(format!("{name}/requests"), "requests"),
1070 );
1071
1072 let outcome_deps = opts.outcomes.iter().map(Node::erased).collect::<Vec<_>>();
1073 let runtime = graph.init_node::<ProcessEffectRunnerFact<TCommand, TResult>>(
1074 Operator::with_opts("processEffectRunner", no_terminal_opts(), {
1075 let outcome_count = outcome_deps.len();
1076 move |ctx: &Ctx| {
1077 for index in 0..outcome_count {
1078 for outcome in ctx.batch::<ProcessEffectOutcome<TResult>>(index) {
1079 match process_effect_outcome_command(
1080 outcome.as_ref(),
1081 command_payload.as_ref(),
1082 ) {
1083 Ok((command, payload)) => {
1084 ctx.emit(ProcessEffectRunnerFact::<TCommand, TResult>::Outcome(
1085 (*outcome).clone(),
1086 ));
1087 ctx.emit(ProcessEffectRunnerFact::Command(command, payload));
1088 }
1089 Err(error) => {
1090 ctx.emit(ProcessEffectRunnerFact::<TCommand, TResult>::Error(
1091 *error,
1092 ));
1093 }
1094 }
1095 }
1096 }
1097 }
1098 }),
1099 outcome_deps,
1100 effect_runner_meta_opts(format!("{name}/runtime"), "runtime"),
1101 );
1102
1103 let outcomes = effect_runner_projection::<TCommand, TResult, ProcessEffectOutcome<TResult>>(
1104 graph,
1105 &runtime,
1106 &format!("{name}/outcomes"),
1107 "processEffectRunnerOutcomes",
1108 |fact| match fact {
1109 ProcessEffectRunnerFact::Outcome(outcome) => Some(outcome.clone()),
1110 _ => None,
1111 },
1112 );
1113 let commands = effect_runner_projection::<TCommand, TResult, ProcessCommand<TCommand>>(
1114 graph,
1115 &runtime,
1116 &format!("{name}/commands"),
1117 "processEffectRunnerCommands",
1118 |fact| match fact {
1119 ProcessEffectRunnerFact::Command(command, _) => Some(command.clone()),
1120 _ => None,
1121 },
1122 );
1123 let errors = effect_runner_projection::<TCommand, TResult, ProcessEffectRunnerError<TResult>>(
1124 graph,
1125 &runtime,
1126 &format!("{name}/errors"),
1127 "processEffectRunnerErrors",
1128 |fact| match fact {
1129 ProcessEffectRunnerFact::Error(error) => Some(error.clone()),
1130 _ => None,
1131 },
1132 );
1133 let status = graph.init_node::<ProcessEffectRunnerStatus>(
1134 Operator::with_opts("processEffectRunnerStatus", no_terminal_opts(), |ctx| {
1135 let mut counters = ctx
1136 .state_get::<ProcessEffectRunnerCounters>()
1137 .map_or_else(ProcessEffectRunnerCounters::default, |state| {
1138 (*state).clone()
1139 });
1140 for request in ctx.batch::<ProcessEffectRequest<TEffect>>(0) {
1141 counters.requested += 1;
1142 ctx.emit(ProcessEffectRunnerStatus {
1143 state: ProcessEffectRunnerStatusState::Requested,
1144 effect_id: Some(request.id.clone()),
1145 effect_type: Some(request.effect_type.clone()),
1146 command_id: None,
1147 command_type: None,
1148 requested: counters.requested,
1149 commanded: counters.commanded,
1150 rejected: counters.rejected,
1151 });
1152 }
1153 for fact in ctx.batch::<ProcessEffectRunnerFact<TCommand, TResult>>(1) {
1154 match fact.as_ref() {
1155 ProcessEffectRunnerFact::Command(command, payload) => {
1156 counters.commanded += 1;
1157 ctx.emit(ProcessEffectRunnerStatus {
1158 state: ProcessEffectRunnerStatusState::Commanded,
1159 effect_id: Some(payload.effect_id.clone()),
1160 effect_type: Some(payload.effect_type.clone()),
1161 command_id: Some(command.id.clone()),
1162 command_type: Some(command_type_for_outcome(payload.kind)),
1163 requested: counters.requested,
1164 commanded: counters.commanded,
1165 rejected: counters.rejected,
1166 });
1167 }
1168 ProcessEffectRunnerFact::Error(error) => {
1169 counters.rejected += 1;
1170 ctx.emit(ProcessEffectRunnerStatus {
1171 state: ProcessEffectRunnerStatusState::Rejected,
1172 effect_id: error.effect_id.clone(),
1173 effect_type: error.effect_type.clone(),
1174 command_id: None,
1175 command_type: None,
1176 requested: counters.requested,
1177 commanded: counters.commanded,
1178 rejected: counters.rejected,
1179 });
1180 }
1181 ProcessEffectRunnerFact::Outcome(_) => {}
1182 }
1183 }
1184 ctx.state_set(counters);
1185 }),
1186 vec![requests.erased(), runtime.erased()],
1187 effect_runner_meta_opts(format!("{name}/status"), "status"),
1188 );
1189
1190 let attach = catch_unwind(AssertUnwindSafe(|| {
1191 attach_process_command_source_parts(
1192 &process.command,
1193 &process.command_sources,
1194 commands.erased(),
1195 );
1196 }));
1197 if let Err(panic) = attach {
1198 graph.release_nodes(
1199 &[
1200 requests.erased(),
1201 outcomes.erased(),
1202 runtime.erased(),
1203 commands.erased(),
1204 status.erased(),
1205 errors.erased(),
1206 ],
1207 "process_effect_runner failed command wiring",
1208 );
1209 resume_unwind(panic);
1210 }
1211 let retains = retain_effect_runner_nodes(
1212 graph,
1213 EffectRunnerNodeRefs {
1214 requests: &requests,
1215 outcomes: &outcomes,
1216 runtime: &runtime,
1217 commands: &commands,
1218 status: &status,
1219 errors: &errors,
1220 },
1221 &name,
1222 );
1223
1224 ProcessEffectRunnerBundle {
1225 requests,
1226 outcomes,
1227 commands,
1228 status,
1229 errors,
1230 runtime,
1231 graph: graph.clone(),
1232 process_command: process.command.clone(),
1233 process_command_id: process.command_id.clone(),
1234 name,
1235 command_sources: process.command_sources.clone(),
1236 released: Cell::new(false),
1237 retains: RefCell::new(retains),
1238 }
1239}
1240
1241#[derive(Clone, Default)]
1242struct ProcessEffectRunnerCounters {
1243 requested: u64,
1244 commanded: u64,
1245 rejected: u64,
1246}
1247
1248type ProcessEffectRunnerCommandResult<TCommand, TResult> = Result<
1249 (
1250 ProcessCommand<TCommand>,
1251 ProcessEffectCommandPayload<TResult>,
1252 ),
1253 Box<ProcessEffectRunnerError<TResult>>,
1254>;
1255
1256fn process_effect_outcome_command<TCommand, TResult: Clone>(
1257 outcome: &ProcessEffectOutcome<TResult>,
1258 map_payload: &dyn Fn(ProcessEffectCommandPayload<TResult>) -> TCommand,
1259) -> ProcessEffectRunnerCommandResult<TCommand, TResult> {
1260 validate_process_effect_outcome(outcome)?;
1261 let command_type = command_type_for_outcome(outcome.kind);
1262 let payload = ProcessEffectCommandPayload {
1263 kind: outcome.kind,
1264 effect_id: outcome.effect_id.clone(),
1265 effect_type: outcome.effect_type.clone(),
1266 value: outcome.value.clone(),
1267 error: outcome.error.clone(),
1268 reason: outcome.reason.clone(),
1269 process_id: outcome.process_id.clone(),
1270 correlation_id: outcome.correlation_id.clone(),
1271 causation_id: outcome.causation_id.clone(),
1272 };
1273 let command = ProcessCommand {
1274 id: outcome.command_id.clone().unwrap_or_else(|| {
1275 compound_tuple_key(
1276 "process-effect-command",
1277 &[&outcome.effect_id, command_type.as_str()],
1278 )
1279 }),
1280 command_type: command_type.as_str().to_owned(),
1281 payload: map_payload(payload.clone()),
1282 process_id: outcome.process_id.clone(),
1283 correlation_id: outcome.correlation_id.clone(),
1284 causation_id: outcome.causation_id.clone(),
1285 };
1286 Ok((command, payload))
1287}
1288
1289fn validate_process_effect_outcome<TResult>(
1290 outcome: &ProcessEffectOutcome<TResult>,
1291) -> Result<(), Box<ProcessEffectRunnerError<TResult>>>
1292where
1293 TResult: Clone,
1294{
1295 let reject = |message: String| {
1296 Box::new(ProcessEffectRunnerError {
1297 code: ProcessEffectRunnerErrorCode::MalformedOutcome,
1298 message,
1299 outcome: Some(outcome.clone()),
1300 effect_id: (!outcome.effect_id.is_empty()).then(|| outcome.effect_id.clone()),
1301 effect_type: (!outcome.effect_type.is_empty()).then(|| outcome.effect_type.clone()),
1302 })
1303 };
1304 if outcome.effect_id.is_empty() {
1305 return Err(reject(
1306 "process_effect_runner: outcome effect_id must be a non-empty string".to_owned(),
1307 ));
1308 }
1309 if outcome.effect_type.is_empty() {
1310 return Err(reject(
1311 "process_effect_runner: outcome effect_type must be a non-empty string".to_owned(),
1312 ));
1313 }
1314 if matches!(outcome.command_id.as_deref(), Some("")) {
1315 return Err(reject(
1316 "process_effect_runner: outcome command_id must be a non-empty string".to_owned(),
1317 ));
1318 }
1319 match outcome.kind {
1320 ProcessEffectOutcomeKind::Result if outcome.value.is_none() => Err(reject(
1321 "process_effect_runner: result outcome must carry value".to_owned(),
1322 )),
1323 ProcessEffectOutcomeKind::Failure if outcome.error.is_none() => Err(reject(
1324 "process_effect_runner: failure outcome must carry error".to_owned(),
1325 )),
1326 ProcessEffectOutcomeKind::Timeout if outcome.error.is_none() => Err(reject(
1327 "process_effect_runner: timeout outcome must carry error".to_owned(),
1328 )),
1329 ProcessEffectOutcomeKind::Result if outcome.error.is_some() || outcome.reason.is_some() => {
1330 Err(reject(
1331 "process_effect_runner: result outcome must not carry error or reason".to_owned(),
1332 ))
1333 }
1334 ProcessEffectOutcomeKind::Failure
1335 if outcome.value.is_some() || outcome.reason.is_some() =>
1336 {
1337 Err(reject(
1338 "process_effect_runner: failure outcome must not carry value or reason".to_owned(),
1339 ))
1340 }
1341 ProcessEffectOutcomeKind::Cancel if outcome.value.is_some() || outcome.error.is_some() => {
1342 Err(reject(
1343 "process_effect_runner: cancel outcome must not carry value or error".to_owned(),
1344 ))
1345 }
1346 ProcessEffectOutcomeKind::Timeout
1347 if outcome.value.is_some() || outcome.reason.is_some() =>
1348 {
1349 Err(reject(
1350 "process_effect_runner: timeout outcome must not carry value or reason".to_owned(),
1351 ))
1352 }
1353 _ => Ok(()),
1354 }
1355}
1356
1357fn command_type_for_outcome(kind: ProcessEffectOutcomeKind) -> ProcessEffectCommandType {
1358 match kind {
1359 ProcessEffectOutcomeKind::Result => ProcessEffectCommandType::Result,
1360 ProcessEffectOutcomeKind::Failure => ProcessEffectCommandType::Failure,
1361 ProcessEffectOutcomeKind::Cancel => ProcessEffectCommandType::Cancel,
1362 ProcessEffectOutcomeKind::Timeout => ProcessEffectCommandType::Timeout,
1363 }
1364}
1365
1366fn effect_runner_projection<TCommand, TResult, TOut>(
1367 graph: &Graph,
1368 runtime: &Node<ProcessEffectRunnerFact<TCommand, TResult>>,
1369 name: &str,
1370 factory: &'static str,
1371 select: impl Fn(&ProcessEffectRunnerFact<TCommand, TResult>) -> Option<TOut> + 'static,
1372) -> Node<TOut>
1373where
1374 TCommand: Clone + 'static,
1375 TResult: Clone + 'static,
1376 TOut: Clone + 'static,
1377{
1378 graph.init_node::<TOut>(
1379 Operator::with_opts(factory, no_terminal_opts(), move |ctx: &Ctx| {
1380 for fact in ctx.batch::<ProcessEffectRunnerFact<TCommand, TResult>>(0) {
1381 if let Some(selected) = select(fact.as_ref()) {
1382 ctx.emit(selected);
1383 }
1384 }
1385 }),
1386 vec![runtime.erased()],
1387 effect_runner_meta_opts(name.to_owned(), "projection"),
1388 )
1389}
1390
1391struct EffectRunnerNodeRefs<'a, TCommand, TEffect, TResult> {
1392 requests: &'a Node<ProcessEffectRequest<TEffect>>,
1393 outcomes: &'a Node<ProcessEffectOutcome<TResult>>,
1394 runtime: &'a Node<ProcessEffectRunnerFact<TCommand, TResult>>,
1395 commands: &'a Node<ProcessCommand<TCommand>>,
1396 status: &'a Node<ProcessEffectRunnerStatus>,
1397 errors: &'a Node<ProcessEffectRunnerError<TResult>>,
1398}
1399
1400fn retain_effect_runner_nodes<TCommand, TEffect, TResult>(
1401 graph: &Graph,
1402 nodes: EffectRunnerNodeRefs<'_, TCommand, TEffect, TResult>,
1403 name: &str,
1404) -> Vec<ProcessRetain>
1405where
1406 TCommand: 'static,
1407 TEffect: 'static,
1408 TResult: 'static,
1409{
1410 vec![
1411 ProcessRetain::new(graph.retain(nodes.requests, &format!("{name}.effect_runner.requests"))),
1412 ProcessRetain::new(graph.retain(nodes.outcomes, &format!("{name}.effect_runner.outcomes"))),
1413 ProcessRetain::new(graph.retain(nodes.runtime, &format!("{name}.effect_runner.runtime"))),
1414 ProcessRetain::new(graph.retain(nodes.commands, &format!("{name}.effect_runner.commands"))),
1415 ProcessRetain::new(graph.retain(nodes.status, &format!("{name}.effect_runner.status"))),
1416 ProcessRetain::new(graph.retain(nodes.errors, &format!("{name}.effect_runner.errors"))),
1417 ]
1418}
1419
1420fn attach_process_command_source_parts<TCommand>(
1421 command: &Node<ProcessCommand<TCommand>>,
1422 sources: &Rc<RefCell<Vec<Core>>>,
1423 source: Core,
1424) where
1425 TCommand: Clone + 'static,
1426{
1427 let previous = sources.borrow().clone();
1428 {
1429 let mut current = sources.borrow_mut();
1430 if !current.iter().any(|candidate| candidate.ptr_eq(&source)) {
1431 current.push(source.clone());
1432 }
1433 }
1434 let command_sources = sources.borrow().clone();
1435 let source_count = command_sources.len();
1436 let rewire = catch_unwind(AssertUnwindSafe(|| {
1437 command.replace_deps(
1438 command_sources,
1439 process_command_source_body::<TCommand>(source_count),
1440 );
1441 }));
1442 if let Err(panic) = rewire {
1443 *sources.borrow_mut() = previous.clone();
1444 command.replace_deps(
1445 previous,
1446 process_command_source_body::<TCommand>(sources.borrow().len()),
1447 );
1448 resume_unwind(panic);
1449 }
1450}
1451
1452fn detach_process_command_source<TCommand>(
1453 command: &Node<ProcessCommand<TCommand>>,
1454 sources: &Rc<RefCell<Vec<Core>>>,
1455 source: Core,
1456) where
1457 TCommand: Clone + 'static,
1458{
1459 let previous = sources.borrow().clone();
1460 if !previous.iter().any(|candidate| candidate.ptr_eq(&source)) {
1461 return;
1462 }
1463 let next = previous
1464 .iter()
1465 .filter(|candidate| !candidate.ptr_eq(&source))
1466 .cloned()
1467 .collect::<Vec<_>>();
1468 *sources.borrow_mut() = next.clone();
1469 let rewire = catch_unwind(AssertUnwindSafe(|| {
1470 command.replace_deps(
1471 next,
1472 process_command_source_body::<TCommand>(sources.borrow().len()),
1473 );
1474 }));
1475 if let Err(panic) = rewire {
1476 *sources.borrow_mut() = previous.clone();
1477 command.replace_deps(
1478 previous,
1479 process_command_source_body::<TCommand>(sources.borrow().len()),
1480 );
1481 resume_unwind(panic);
1482 }
1483}
1484
1485fn process_command_source_body<TCommand: Clone + 'static>(
1486 source_count: usize,
1487) -> impl Fn(&Ctx) + 'static {
1488 move |ctx: &Ctx| {
1489 for index in 0..source_count {
1490 for command in ctx.batch::<ProcessCommand<TCommand>>(index) {
1491 ctx.emit((*command).clone());
1492 }
1493 }
1494 }
1495}
1496
1497#[derive(Debug)]
1498struct RuntimeState {
1499 event_seq: u64,
1500 effect_seq: u64,
1501 command_count: u64,
1502 error_count: u64,
1503 audit_seq: u64,
1504 seen_event_ids: Vec<String>,
1505 seen_effect_ids: Vec<String>,
1506 state: Value,
1507}
1508
1509impl RuntimeState {
1510 fn new(state: Value) -> Self {
1511 Self {
1512 event_seq: 0,
1513 effect_seq: 0,
1514 command_count: 0,
1515 error_count: 0,
1516 audit_seq: 0,
1517 seen_event_ids: Vec::new(),
1518 seen_effect_ids: Vec::new(),
1519 state,
1520 }
1521 }
1522
1523 fn from_json(value: &Value) -> Result<Self, String> {
1524 Ok(Self {
1525 event_seq: json_required_u64(value, "eventSeq")?,
1526 effect_seq: json_required_u64(value, "effectSeq")?,
1527 command_count: json_required_u64(value, "commandCount")?,
1528 error_count: json_required_u64(value, "errorCount")?,
1529 audit_seq: json_required_u64(value, "auditSeq")?,
1530 seen_event_ids: json_required_string_array(value, "seenEventIds")?,
1531 seen_effect_ids: json_required_string_array(value, "seenEffectIds")?,
1532 state: value
1533 .get("state")
1534 .cloned()
1535 .ok_or_else(|| "process_bundle: runtime state missing 'state'".to_owned())?,
1536 })
1537 }
1538
1539 fn cursor_recovery(state: Value) -> Self {
1540 Self::new(state)
1541 }
1542
1543 fn to_json(&self) -> Value {
1544 serde_json::json!({
1545 "eventSeq": self.event_seq,
1546 "effectSeq": self.effect_seq,
1547 "commandCount": self.command_count,
1548 "errorCount": self.error_count,
1549 "auditSeq": self.audit_seq,
1550 "seenEventIds": self.seen_event_ids,
1551 "seenEffectIds": self.seen_effect_ids,
1552 "state": self.state,
1553 })
1554 }
1555}
1556
1557fn reduce_process_command_fact<
1558 TCommand: Clone + 'static,
1559 TState: Clone + Serialize + DeserializeOwned + 'static,
1560 TEvent: Clone + 'static,
1561 TEffect: Clone + 'static,
1562>(
1563 state: &mut RuntimeState,
1564 command: ProcessCommand<TCommand>,
1565 reduce: &ProcessReducerFn<TCommand, TState, TEvent, TEffect>,
1566 now: &dyn Fn() -> u64,
1567) -> Vec<ProcessRuntimeFact<TState, TEvent, TEffect, TCommand>> {
1568 state.command_count += 1;
1569 if command.id.is_empty() {
1570 return failure(
1571 state,
1572 Some(command),
1573 ProcessErrorCode::MalformedCommand,
1574 "process_bundle: command id must be non-empty".to_owned(),
1575 );
1576 }
1577 if command.command_type.is_empty() {
1578 return failure(
1579 state,
1580 Some(command),
1581 ProcessErrorCode::MalformedCommand,
1582 "process_bundle: command type must be non-empty".to_owned(),
1583 );
1584 }
1585 let reducer_state = match state_from_json::<TState>(&state.state) {
1586 Ok(state) => state,
1587 Err(message) => {
1588 return failure(
1589 state,
1590 Some(command),
1591 ProcessErrorCode::MalformedState,
1592 message,
1593 );
1594 }
1595 };
1596 let reduction = match catch_unwind(AssertUnwindSafe(|| reduce(&command, reducer_state))) {
1597 Ok(reduction) => reduction,
1598 Err(panic) => {
1599 if is_graph_runtime_panic(&panic) {
1600 resume_unwind(panic);
1601 }
1602 return failure(
1603 state,
1604 Some(command),
1605 ProcessErrorCode::ReducerThrew,
1606 panic_message(&panic),
1607 );
1608 }
1609 };
1610 let next_state_json = match state_to_json(&reduction.state) {
1611 Ok(state) => state,
1612 Err(message) => {
1613 return failure(
1614 state,
1615 Some(command),
1616 ProcessErrorCode::MalformedState,
1617 message,
1618 );
1619 }
1620 };
1621 let visible_state = match state_from_json::<TState>(&next_state_json) {
1622 Ok(state) => state,
1623 Err(message) => {
1624 return failure(
1625 state,
1626 Some(command),
1627 ProcessErrorCode::MalformedState,
1628 message,
1629 );
1630 }
1631 };
1632 let prepared_events = match prepare_events(&command, &reduction.events, state) {
1633 Ok(events) => events,
1634 Err(message) => {
1635 return failure(
1636 state,
1637 Some(command),
1638 ProcessErrorCode::MalformedEvent,
1639 message,
1640 );
1641 }
1642 };
1643 let prepared_effects = match prepare_effects(&command, &reduction.effects, state) {
1644 Ok(effects) => effects,
1645 Err(message) => {
1646 return failure(
1647 state,
1648 Some(command),
1649 ProcessErrorCode::MalformedEffect,
1650 message,
1651 );
1652 }
1653 };
1654 let timestamp_ms = match process_timestamp(now) {
1655 Ok(timestamp_ms) => timestamp_ms,
1656 Err(message) => {
1657 return failure(state, Some(command), ProcessErrorCode::ClockThrew, message);
1658 }
1659 };
1660
1661 state.state = next_state_json;
1662 let mut facts = vec![ProcessRuntimeFact::State {
1663 state: visible_state,
1664 cursor: cursor_of(state),
1665 }];
1666
1667 let mut events = Vec::new();
1668 for draft in prepared_events {
1669 state.event_seq += 1;
1670 state.seen_event_ids.push(draft.id.clone());
1671 let event = ProcessEvent {
1672 id: draft.id,
1673 event_type: draft.event_type,
1674 seq: state.event_seq,
1675 cursor: state.event_seq,
1676 command_id: command.id.clone(),
1677 command_type: command.command_type.clone(),
1678 payload: draft.payload,
1679 timestamp_ms,
1680 process_id: draft.process_id,
1681 correlation_id: draft.correlation_id,
1682 causation_id: draft.causation_id,
1683 };
1684 facts.push(ProcessRuntimeFact::Event(event.clone()));
1685 events.push(event);
1686 }
1687
1688 let mut effects = Vec::new();
1689 for draft in prepared_effects {
1690 state.effect_seq += 1;
1691 state.seen_effect_ids.push(draft.id.clone());
1692 let effect = ProcessEffectRequest {
1693 id: draft.id,
1694 effect_type: draft.effect_type,
1695 seq: state.effect_seq,
1696 cursor: state.effect_seq,
1697 command_id: command.id.clone(),
1698 command_type: command.command_type.clone(),
1699 payload: draft.payload,
1700 timestamp_ms,
1701 process_id: draft.process_id,
1702 correlation_id: draft.correlation_id,
1703 causation_id: draft.causation_id,
1704 };
1705 facts.push(ProcessRuntimeFact::EffectRequest(effect.clone()));
1706 effects.push(effect);
1707 }
1708
1709 facts.push(ProcessRuntimeFact::Status(ProcessStatus {
1710 state: ProcessStatusState::Accepted,
1711 command_id: Some(command.id.clone()),
1712 command_type: Some(command.command_type.clone()),
1713 event_count: events.len(),
1714 effect_count: effects.len(),
1715 error_code: None,
1716 cursor: cursor_of(state),
1717 }));
1718 facts.push(ProcessRuntimeFact::Audit(audit_record::<
1719 TCommand,
1720 TEvent,
1721 TEffect,
1722 >(
1723 state,
1724 Some(&command),
1725 ProcessAuditOutcome::Success,
1726 &events,
1727 &effects,
1728 None,
1729 None,
1730 )));
1731 facts.push(ProcessRuntimeFact::Cursor(cursor_of(state)));
1732 facts
1733}
1734
1735#[derive(Clone)]
1736struct PreparedEvent<T> {
1737 id: String,
1738 event_type: String,
1739 payload: T,
1740 process_id: Option<String>,
1741 correlation_id: Option<String>,
1742 causation_id: Option<String>,
1743}
1744
1745#[derive(Clone)]
1746struct PreparedEffect<T> {
1747 id: String,
1748 effect_type: String,
1749 payload: T,
1750 process_id: Option<String>,
1751 correlation_id: Option<String>,
1752 causation_id: Option<String>,
1753}
1754
1755fn prepare_events<TCommand, TEvent: Clone>(
1756 command: &ProcessCommand<TCommand>,
1757 drafts: &[ProcessEventDraft<TEvent>],
1758 state: &RuntimeState,
1759) -> Result<Vec<PreparedEvent<TEvent>>, String> {
1760 let mut seen = Vec::<String>::new();
1761 let mut prepared = Vec::new();
1762 for (index, draft) in drafts.iter().enumerate() {
1763 if draft.event_type.is_empty() {
1764 return Err("process_bundle: event draft must have a non-empty type".to_owned());
1765 }
1766 let id = draft
1767 .id
1768 .as_ref()
1769 .filter(|id| !id.is_empty())
1770 .cloned()
1771 .unwrap_or_else(|| {
1772 compound_tuple_key(
1773 "process-event",
1774 &[
1775 &command.id,
1776 &(state.event_seq + index as u64 + 1).to_string(),
1777 ],
1778 )
1779 });
1780 if seen.contains(&id) || state.seen_event_ids.contains(&id) {
1781 return Err(format!("process_bundle: duplicate event '{id}'"));
1782 }
1783 seen.push(id.clone());
1784 prepared.push(PreparedEvent {
1785 id,
1786 event_type: draft.event_type.clone(),
1787 payload: draft.payload.clone(),
1788 process_id: draft.process_id.clone(),
1789 correlation_id: draft.correlation_id.clone(),
1790 causation_id: draft.causation_id.clone(),
1791 });
1792 }
1793 Ok(prepared)
1794}
1795
1796fn prepare_effects<TCommand, TEffect: Clone>(
1797 command: &ProcessCommand<TCommand>,
1798 drafts: &[ProcessEffectRequestDraft<TEffect>],
1799 state: &RuntimeState,
1800) -> Result<Vec<PreparedEffect<TEffect>>, String> {
1801 let mut seen = Vec::<String>::new();
1802 let mut prepared = Vec::new();
1803 for (index, draft) in drafts.iter().enumerate() {
1804 if draft.effect_type.is_empty() {
1805 return Err("process_bundle: effect draft must have a non-empty type".to_owned());
1806 }
1807 let id = draft
1808 .id
1809 .as_ref()
1810 .filter(|id| !id.is_empty())
1811 .cloned()
1812 .unwrap_or_else(|| {
1813 compound_tuple_key(
1814 "process-effect",
1815 &[
1816 &command.id,
1817 &(state.effect_seq + index as u64 + 1).to_string(),
1818 ],
1819 )
1820 });
1821 if seen.contains(&id) || state.seen_effect_ids.contains(&id) {
1822 return Err(format!("process_bundle: duplicate effect '{id}'"));
1823 }
1824 seen.push(id.clone());
1825 prepared.push(PreparedEffect {
1826 id,
1827 effect_type: draft.effect_type.clone(),
1828 payload: draft.payload.clone(),
1829 process_id: draft.process_id.clone(),
1830 correlation_id: draft.correlation_id.clone(),
1831 causation_id: draft.causation_id.clone(),
1832 });
1833 }
1834 Ok(prepared)
1835}
1836
1837fn failure<
1838 TCommand: Clone + 'static,
1839 TState: Clone + 'static,
1840 TEvent: Clone + 'static,
1841 TEffect: Clone + 'static,
1842>(
1843 state: &mut RuntimeState,
1844 command: Option<ProcessCommand<TCommand>>,
1845 code: ProcessErrorCode,
1846 message: String,
1847) -> Vec<ProcessRuntimeFact<TState, TEvent, TEffect, TCommand>> {
1848 state.error_count += 1;
1849 let cursor = cursor_of(state);
1850 vec![
1851 ProcessRuntimeFact::Error(ProcessError {
1852 code,
1853 message: message.clone(),
1854 command: command.clone(),
1855 cursor: cursor.clone(),
1856 }),
1857 ProcessRuntimeFact::Status(ProcessStatus {
1858 state: ProcessStatusState::Rejected,
1859 command_id: command.as_ref().map(|command| command.id.clone()),
1860 command_type: command.as_ref().map(|command| command.command_type.clone()),
1861 event_count: 0,
1862 effect_count: 0,
1863 error_code: Some(code),
1864 cursor: cursor.clone(),
1865 }),
1866 ProcessRuntimeFact::Audit(audit_record::<TCommand, TEvent, TEffect>(
1867 state,
1868 command.as_ref(),
1869 ProcessAuditOutcome::Failure,
1870 &[],
1871 &[],
1872 Some(code),
1873 Some(message),
1874 )),
1875 ProcessRuntimeFact::Cursor(cursor_of(state)),
1876 ]
1877}
1878
1879fn audit_record<TCommand, TEvent, TEffect>(
1880 state: &mut RuntimeState,
1881 command: Option<&ProcessCommand<TCommand>>,
1882 outcome: ProcessAuditOutcome,
1883 events: &[ProcessEvent<TEvent>],
1884 effects: &[ProcessEffectRequest<TEffect>],
1885 error_code: Option<ProcessErrorCode>,
1886 error_message: Option<String>,
1887) -> ProcessAuditRecord {
1888 state.audit_seq += 1;
1889 ProcessAuditRecord {
1890 seq: state.audit_seq,
1891 command_id: command.map(|command| command.id.clone()),
1892 command_type: command.map(|command| command.command_type.clone()),
1893 outcome,
1894 event_ids: events.iter().map(|event| event.id.clone()).collect(),
1895 event_types: events
1896 .iter()
1897 .map(|event| event.event_type.clone())
1898 .collect(),
1899 effect_ids: effects.iter().map(|effect| effect.id.clone()).collect(),
1900 effect_types: effects
1901 .iter()
1902 .map(|effect| effect.effect_type.clone())
1903 .collect(),
1904 error_code,
1905 error_message,
1906 cursor: cursor_of(state),
1907 }
1908}
1909
1910fn runtime_projection<
1911 TCommand: Clone + 'static,
1912 TState: Clone + 'static,
1913 TEvent: Clone + 'static,
1914 TEffect: Clone + 'static,
1915 TOut: Clone + 'static,
1916>(
1917 graph: &Graph,
1918 runtime: &Node<ProcessRuntimeFact<TState, TEvent, TEffect, TCommand>>,
1919 name: &str,
1920 factory: &'static str,
1921 select: impl Fn(&ProcessRuntimeFact<TState, TEvent, TEffect, TCommand>) -> Option<TOut> + 'static,
1922) -> Node<TOut> {
1923 graph.init_node::<TOut>(
1924 Operator::with_opts(factory, no_terminal_opts(), move |ctx: &Ctx| {
1925 for fact in ctx.batch::<ProcessRuntimeFact<TState, TEvent, TEffect, TCommand>>(0) {
1926 if let Some(selected) = select(fact.as_ref()) {
1927 ctx.emit(selected);
1928 }
1929 }
1930 }),
1931 vec![runtime.erased()],
1932 GraphNodeOpts::named(name),
1933 )
1934}
1935
1936fn no_terminal_opts() -> crate::node::NodeOpts {
1937 crate::node::NodeOpts {
1938 partial: true,
1939 complete_when_deps_complete: false,
1940 error_when_deps_error: false,
1941 ..Default::default()
1942 }
1943}
1944
1945fn meta_opts(name: String, role: &'static str) -> GraphNodeOpts {
1946 let mut opts = GraphNodeOpts::named(name);
1947 opts.meta.insert("process".to_owned(), role.to_owned());
1948 opts.meta.insert("d".to_owned(), "D136".to_owned());
1949 opts
1950}
1951
1952fn effect_runner_meta_opts(name: String, role: &'static str) -> GraphNodeOpts {
1953 let mut opts = GraphNodeOpts::named(name);
1954 opts.meta
1955 .insert("process".to_owned(), format!("effect-runner:{role}"));
1956 opts.meta.insert("d".to_owned(), "D156".to_owned());
1957 opts
1958}
1959
1960fn cursor_of(state: &RuntimeState) -> ProcessCursor {
1961 ProcessCursor {
1962 event_seq: state.event_seq,
1963 effect_seq: state.effect_seq,
1964 command_count: state.command_count,
1965 error_count: state.error_count,
1966 audit_seq: state.audit_seq,
1967 }
1968}
1969
1970fn state_to_json<TState: Serialize>(state: &TState) -> Result<Value, String> {
1971 serde_json::to_value(state).map_err(|error| format!("state must serialize to JSON ({error})"))
1972}
1973
1974fn state_from_json<TState: DeserializeOwned>(state: &Value) -> Result<TState, String> {
1975 serde_json::from_value(state.clone())
1976 .map_err(|error| format!("state must deserialize from JSON ({error})"))
1977}
1978
1979fn json_required_u64(value: &Value, key: &str) -> Result<u64, String> {
1980 value.get(key).and_then(Value::as_u64).ok_or_else(|| {
1981 format!("process_bundle: runtime state field '{key}' must be an unsigned integer")
1982 })
1983}
1984
1985fn json_required_string_array(value: &Value, key: &str) -> Result<Vec<String>, String> {
1986 let Some(items) = value.get(key).and_then(Value::as_array) else {
1987 return Err(format!(
1988 "process_bundle: runtime state field '{key}' must be an array of strings"
1989 ));
1990 };
1991 let mut out = Vec::new();
1992 for item in items {
1993 let Some(item) = item.as_str() else {
1994 return Err(format!(
1995 "process_bundle: runtime state field '{key}' must be an array of strings"
1996 ));
1997 };
1998 out.push(item.to_owned());
1999 }
2000 Ok(out)
2001}
2002
2003fn process_timestamp(now: &dyn Fn() -> u64) -> Result<u64, String> {
2004 catch_unwind(AssertUnwindSafe(now))
2005 .map_err(|panic| format!("process_bundle: now() threw: {}", panic_message(&panic)))
2006}
2007
2008fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
2009 if let Some(message) = panic.downcast_ref::<&str>() {
2010 return (*message).to_owned();
2011 }
2012 if let Some(message) = panic.downcast_ref::<String>() {
2013 return message.clone();
2014 }
2015 "panic".to_owned()
2016}
2017
2018fn is_graph_runtime_panic(panic: &Box<dyn std::any::Any + Send>) -> bool {
2019 let message = panic_message(panic);
2020 message.contains("R-reentrancy")
2021 || message.contains("R-rewire")
2022 || message.contains("R-graph-domain")
2023 || message.contains("D22")
2024 || message.contains("D37")
2025 || message.contains("feedback cycle")
2026 || message.contains("different graph")
2027 || message.contains("cross-graph")
2028 || message.contains("wire bridge")
2029 || message.contains("mid-fn topology mutation")
2030 || message.contains("reentrant dep mutation")
2031}
2032
2033#[cfg(test)]
2034mod tests {
2035 use super::*;
2036 use serde_json::json;
2037
2038 #[test]
2039 fn runtime_state_rejects_corrupt_checkpoint_fields() {
2040 let valid = json!({
2041 "eventSeq": 1,
2042 "effectSeq": 2,
2043 "commandCount": 3,
2044 "errorCount": 0,
2045 "auditSeq": 3,
2046 "seenEventIds": ["event-1"],
2047 "seenEffectIds": ["effect-1"],
2048 "state": { "total": 4 },
2049 });
2050 assert!(RuntimeState::from_json(&valid).is_ok());
2051
2052 let bad_counter = json!({
2053 "eventSeq": "1",
2054 "effectSeq": 2,
2055 "commandCount": 3,
2056 "errorCount": 0,
2057 "auditSeq": 3,
2058 "seenEventIds": ["event-1"],
2059 "seenEffectIds": ["effect-1"],
2060 "state": { "total": 4 },
2061 });
2062 let err = RuntimeState::from_json(&bad_counter).expect_err("bad counter fails");
2063 assert!(err.contains("eventSeq"));
2064
2065 let bad_seen_ids = json!({
2066 "eventSeq": 1,
2067 "effectSeq": 2,
2068 "commandCount": 3,
2069 "errorCount": 0,
2070 "auditSeq": 3,
2071 "seenEventIds": ["event-1", 7],
2072 "seenEffectIds": ["effect-1"],
2073 "state": { "total": 4 },
2074 });
2075 let err = RuntimeState::from_json(&bad_seen_ids).expect_err("bad id array fails");
2076 assert!(err.contains("seenEventIds"));
2077
2078 let missing_state = json!({
2079 "eventSeq": 1,
2080 "effectSeq": 2,
2081 "commandCount": 3,
2082 "errorCount": 0,
2083 "auditSeq": 3,
2084 "seenEventIds": ["event-1"],
2085 "seenEffectIds": ["effect-1"],
2086 });
2087 let err = RuntimeState::from_json(&missing_state).expect_err("missing state fails");
2088 assert!(err.contains("missing 'state'"));
2089 }
2090}