1pub mod messaging;
9pub mod work_queue;
10
11use std::collections::{HashMap, HashSet};
12use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
13use std::rc::Rc;
14
15use serde_json::{json, Value};
16
17use crate::ctx::Ctx;
18use crate::graph::{Graph, GraphNodeOpts};
19use crate::identity::compound_tuple_key;
20use crate::node::Node;
21use crate::operators::Operator;
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct CqrsCommand<T = crate::protocol::AnyValue> {
26 pub id: String,
28 pub command_type: String,
30 pub payload: T,
32 pub aggregate_id: Option<String>,
34 pub correlation_id: Option<String>,
36 pub causation_id: Option<String>,
38}
39
40impl<T> CqrsCommand<T> {
41 pub fn new(id: impl Into<String>, command_type: impl Into<String>, payload: T) -> Self {
43 Self {
44 id: id.into(),
45 command_type: command_type.into(),
46 payload,
47 aggregate_id: None,
48 correlation_id: None,
49 causation_id: None,
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
55pub struct CqrsEventDraft<T = crate::protocol::AnyValue> {
57 pub id: Option<String>,
59 pub event_type: String,
61 pub payload: T,
63 pub aggregate_id: Option<String>,
65 pub correlation_id: Option<String>,
67 pub causation_id: Option<String>,
69}
70
71impl<T> CqrsEventDraft<T> {
72 pub fn new(event_type: impl Into<String>, payload: T) -> Self {
74 Self {
75 id: None,
76 event_type: event_type.into(),
77 payload,
78 aggregate_id: None,
79 correlation_id: None,
80 causation_id: None,
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq)]
86pub struct CqrsEvent<T = crate::protocol::AnyValue> {
88 pub id: String,
90 pub event_type: String,
92 pub seq: u64,
94 pub cursor: u64,
96 pub runtime_cursor: CqrsCursor,
98 pub command_id: String,
100 pub command_type: String,
102 pub payload: T,
104 pub timestamp_ms: u64,
106 pub aggregate_id: Option<String>,
108 pub correlation_id: Option<String>,
110 pub causation_id: Option<String>,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum CqrsErrorCode {
117 MalformedCommand,
119 DuplicateCommand,
121 UnknownCommand,
123 HandlerThrew,
125 ClockThrew,
127 MalformedEvent,
129 UnknownEvent,
131 DuplicateEvent,
133}
134
135#[derive(Debug, Clone, PartialEq)]
136pub struct CqrsError<TCommand = crate::protocol::AnyValue> {
138 pub code: CqrsErrorCode,
140 pub message: String,
142 pub command: Option<CqrsCommand<TCommand>>,
144 pub cursor: CqrsCursor,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct CqrsDedupeSnapshot {
151 pub command_ids_retained: usize,
153 pub event_ids_retained: usize,
155 pub command_ids_evicted: u64,
157 pub event_ids_evicted: u64,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct CqrsCursor {
164 pub event_seq: u64,
166 pub command_count: u64,
168 pub error_count: u64,
170 pub audit_seq: u64,
172 pub dedupe: Option<CqrsDedupeSnapshot>,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct CqrsStatus {
179 pub state: CqrsStatusState,
181 pub command_id: Option<String>,
183 pub command_type: Option<String>,
185 pub event_count: usize,
187 pub error_code: Option<CqrsErrorCode>,
189 pub cursor: CqrsCursor,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum CqrsStatusState {
196 Accepted,
198 Rejected,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct CqrsAuditRecord {
205 pub seq: u64,
207 pub command_id: Option<String>,
209 pub command_type: Option<String>,
211 pub outcome: CqrsAuditOutcome,
213 pub event_ids: Vec<String>,
215 pub event_types: Vec<String>,
217 pub error_code: Option<CqrsErrorCode>,
219 pub error_message: Option<String>,
221 pub cursor: CqrsCursor,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum CqrsAuditOutcome {
228 Success,
230 Failure,
232}
233
234#[derive(Debug, Clone, PartialEq)]
235pub enum CqrsRuntimeFact<TCommand = crate::protocol::AnyValue, TEvent = crate::protocol::AnyValue> {
237 Event(CqrsEvent<TEvent>),
239 Status(CqrsStatus),
241 Error(CqrsError<TCommand>),
243 Audit(CqrsAuditRecord),
245 Cursor(CqrsCursor),
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum CqrsDedupeWindow {
255 Unbounded,
257 Bounded {
259 max_entries: usize,
261 },
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub struct CqrsDedupePolicy {
267 pub commands: CqrsDedupeWindow,
269 pub events: CqrsDedupeWindow,
271}
272
273impl Default for CqrsDedupePolicy {
274 fn default() -> Self {
275 Self {
276 commands: CqrsDedupeWindow::Unbounded,
277 events: CqrsDedupeWindow::Unbounded,
278 }
279 }
280}
281
282impl CqrsDedupePolicy {
283 pub fn bounded(command_max_entries: usize, event_max_entries: usize) -> Self {
285 Self {
286 commands: CqrsDedupeWindow::Bounded {
287 max_entries: command_max_entries,
288 },
289 events: CqrsDedupeWindow::Bounded {
290 max_entries: event_max_entries,
291 },
292 }
293 }
294
295 fn bounded_any(self) -> bool {
296 matches!(self.commands, CqrsDedupeWindow::Bounded { .. })
297 || matches!(self.events, CqrsDedupeWindow::Bounded { .. })
298 }
299}
300
301pub type CqrsCommandHandler<TCommand, TEvent> =
303 Rc<dyn Fn(&CqrsCommand<TCommand>) -> Vec<CqrsEventDraft<TEvent>>>;
304
305#[derive(Clone)]
306pub struct CqrsCommandHandlerDefinition<TCommand, TEvent> {
308 pub command_type: String,
310 pub handle: CqrsCommandHandler<TCommand, TEvent>,
312}
313
314pub fn cqrs_command_handler<TCommand, TEvent>(
316 command_type: impl Into<String>,
317 handle: impl Fn(&CqrsCommand<TCommand>) -> Vec<CqrsEventDraft<TEvent>> + 'static,
318) -> CqrsCommandHandlerDefinition<TCommand, TEvent> {
319 let command_type = command_type.into();
320 assert!(
321 !command_type.is_empty(),
322 "cqrs_command_handler: command type must be non-empty"
323 );
324 CqrsCommandHandlerDefinition {
325 command_type,
326 handle: Rc::new(handle),
327 }
328}
329
330#[derive(Clone)]
331pub struct CqrsOptions<TCommand, TEvent> {
333 pub name: String,
335 pub handlers: Vec<CqrsCommandHandlerDefinition<TCommand, TEvent>>,
337 pub events: Option<Vec<String>>,
339 pub now: Rc<dyn Fn() -> u64>,
341 pub dedupe: CqrsDedupePolicy,
343}
344
345impl<TCommand, TEvent> Default for CqrsOptions<TCommand, TEvent> {
346 fn default() -> Self {
347 Self {
348 name: "cqrs".to_owned(),
349 handlers: Vec::new(),
350 events: None,
351 now: Rc::new(|| 0),
352 dedupe: CqrsDedupePolicy::default(),
353 }
354 }
355}
356
357impl<TCommand, TEvent> CqrsOptions<TCommand, TEvent> {
358 pub fn named(name: impl Into<String>) -> Self {
360 Self {
361 name: name.into(),
362 ..Self::default()
363 }
364 }
365
366 pub fn with_handlers(
368 mut self,
369 handlers: Vec<CqrsCommandHandlerDefinition<TCommand, TEvent>>,
370 ) -> Self {
371 self.handlers = handlers;
372 self
373 }
374
375 pub fn with_events(mut self, events: impl IntoIterator<Item = impl Into<String>>) -> Self {
377 self.events = Some(events.into_iter().map(Into::into).collect());
378 self
379 }
380
381 pub fn with_now(mut self, now: impl Fn() -> u64 + 'static) -> Self {
383 self.now = Rc::new(now);
384 self
385 }
386
387 pub fn with_dedupe(mut self, dedupe: CqrsDedupePolicy) -> Self {
389 self.dedupe = dedupe;
390 self
391 }
392}
393
394#[derive(Clone)]
395pub struct CqrsBundle<TCommand = crate::protocol::AnyValue, TEvent = crate::protocol::AnyValue> {
397 pub command: Node<CqrsCommand<TCommand>>,
399 pub runtime: Node<CqrsRuntimeFact<TCommand, TEvent>>,
401 pub events: Node<CqrsEvent<TEvent>>,
403 pub status: Node<CqrsStatus>,
405 pub errors: Node<CqrsError<TCommand>>,
407 pub audit: Node<CqrsAuditRecord>,
409 pub cursor: Node<CqrsCursor>,
411 _retains: Rc<Vec<CqrsRetain>>,
412}
413
414impl<TCommand: Clone + 'static, TEvent: Clone + 'static> CqrsBundle<TCommand, TEvent> {
415 pub fn dispatch(&self, command: CqrsCommand<TCommand>) -> CqrsCommand<TCommand> {
417 self.command.set(command.clone());
418 command
419 }
420}
421
422struct CqrsRetain {
423 release: std::cell::RefCell<Option<Box<dyn FnOnce()>>>,
424}
425
426impl CqrsRetain {
427 fn new(release: Box<dyn FnOnce()>) -> Self {
428 Self {
429 release: std::cell::RefCell::new(Some(release)),
430 }
431 }
432}
433
434impl Drop for CqrsRetain {
435 fn drop(&mut self) {
436 if let Some(release) = self.release.borrow_mut().take() {
437 release();
438 }
439 }
440}
441
442pub fn cqrs<TCommand: Clone + 'static, TEvent: Clone + 'static>(
444 graph: &Graph,
445) -> CqrsBundle<TCommand, TEvent> {
446 cqrs_with_options(graph, CqrsOptions::default())
447}
448
449pub fn cqrs_with_options<TCommand: Clone + 'static, TEvent: Clone + 'static>(
451 graph: &Graph,
452 opts: CqrsOptions<TCommand, TEvent>,
453) -> CqrsBundle<TCommand, TEvent> {
454 let name = opts.name;
455 let handlers = Rc::new(normalize_handlers(opts.handlers));
456 let known_events = Rc::new(normalize_events(opts.events));
457 let now = opts.now;
458 let dedupe = opts.dedupe;
459
460 let command = graph.init_node::<CqrsCommand<TCommand>>(
461 Operator::with_opts("cqrsCommand", no_terminal_opts(), |_| {}),
462 Vec::new(),
463 GraphNodeOpts::named(format!("{name}/command")),
464 );
465
466 let runtime_seed = RuntimeState::default().to_json();
467 let runtime = graph.init_node::<CqrsRuntimeFact<TCommand, TEvent>>(
468 Operator::with_opts("cqrsRuntime", no_terminal_opts(), {
469 let handlers = handlers.clone();
470 let known_events = known_events.clone();
471 let now = now.clone();
472 move |ctx: &Ctx| {
473 let mut state = ctx
474 .state_get::<Value>()
475 .map(|value| RuntimeState::from_json(value.as_ref()))
476 .unwrap_or_else(|| RuntimeState::from_json(&runtime_seed));
477 ctx.state_persist(true);
478 for command in ctx.batch::<CqrsCommand<TCommand>>(0) {
479 for fact in reduce_command_fact(
480 &mut state,
481 (*command).clone(),
482 handlers.as_ref(),
483 known_events.as_ref(),
484 now.as_ref(),
485 dedupe,
486 ) {
487 ctx.emit(fact);
488 }
489 }
490 ctx.state_set(state.to_json());
491 }
492 }),
493 vec![command.erased()],
494 {
495 let mut node_opts = GraphNodeOpts::named(format!("{name}/runtime"));
496 node_opts
497 .meta
498 .insert("dedupe".to_owned(), dedupe_meta(dedupe));
499 node_opts
500 },
501 );
502
503 let events = runtime_projection::<TCommand, TEvent, CqrsEvent<TEvent>>(
504 graph,
505 &runtime,
506 &format!("{name}/events"),
507 "cqrsEvents",
508 |fact| match fact {
509 CqrsRuntimeFact::Event(event) => Some(event.clone()),
510 _ => None,
511 },
512 );
513 let status = runtime_projection::<TCommand, TEvent, CqrsStatus>(
514 graph,
515 &runtime,
516 &format!("{name}/status"),
517 "cqrsStatus",
518 |fact| match fact {
519 CqrsRuntimeFact::Status(status) => Some(status.clone()),
520 _ => None,
521 },
522 );
523 let errors = runtime_projection::<TCommand, TEvent, CqrsError<TCommand>>(
524 graph,
525 &runtime,
526 &format!("{name}/errors"),
527 "cqrsErrors",
528 |fact| match fact {
529 CqrsRuntimeFact::Error(error) => Some(error.clone()),
530 _ => None,
531 },
532 );
533 let audit = runtime_projection::<TCommand, TEvent, CqrsAuditRecord>(
534 graph,
535 &runtime,
536 &format!("{name}/audit"),
537 "cqrsAudit",
538 |fact| match fact {
539 CqrsRuntimeFact::Audit(audit) => Some(audit.clone()),
540 _ => None,
541 },
542 );
543 let cursor = runtime_projection::<TCommand, TEvent, CqrsCursor>(
544 graph,
545 &runtime,
546 &format!("{name}/cursor"),
547 "cqrsCursor",
548 |fact| match fact {
549 CqrsRuntimeFact::Cursor(cursor) => Some(cursor.clone()),
550 _ => None,
551 },
552 );
553
554 let retains = Rc::new(vec![
555 CqrsRetain::new(graph.retain(&runtime, &format!("{name}.cqrs.runtime"))),
556 CqrsRetain::new(graph.retain(&events, &format!("{name}.cqrs.events"))),
557 CqrsRetain::new(graph.retain(&status, &format!("{name}.cqrs.status"))),
558 CqrsRetain::new(graph.retain(&errors, &format!("{name}.cqrs.errors"))),
559 CqrsRetain::new(graph.retain(&audit, &format!("{name}.cqrs.audit"))),
560 CqrsRetain::new(graph.retain(&cursor, &format!("{name}.cqrs.cursor"))),
561 ]);
562
563 CqrsBundle {
564 command,
565 runtime,
566 events,
567 status,
568 errors,
569 audit,
570 cursor,
571 _retains: retains,
572 }
573}
574
575pub type CqrsProjectionReducer<TState, TEvent> = Rc<dyn Fn(TState, &CqrsEvent<TEvent>) -> TState>;
577
578#[derive(Clone)]
579pub struct CqrsProjectionOptions<TState, TEvent> {
581 pub name: String,
583 pub events: Option<Vec<String>>,
585 pub initial: TState,
587 pub reducer: CqrsProjectionReducer<TState, TEvent>,
589}
590
591#[derive(Debug, Clone, PartialEq)]
592pub enum CqrsProjectionFrame<TState> {
594 Value {
596 state: TState,
598 event_id: String,
600 cursor: CqrsCursor,
602 },
603 Error(CqrsProjectionError),
605}
606
607#[derive(Debug, Clone, PartialEq, Eq)]
608pub struct CqrsProjectionError {
610 pub code: CqrsProjectionErrorCode,
612 pub message: String,
614 pub event_id: String,
616 pub event_type: String,
618 pub cursor: CqrsCursor,
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623pub enum CqrsProjectionErrorCode {
625 ProjectionThrew,
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
630pub struct CqrsProjectionStatus {
632 pub state: CqrsProjectionStatusState,
634 pub event_id: String,
636 pub event_type: Option<String>,
638 pub cursor: CqrsCursor,
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq)]
643pub enum CqrsProjectionStatusState {
645 Updated,
647 Errored,
649}
650
651#[derive(Clone)]
652pub struct CqrsProjection<TState> {
654 pub frames: Node<CqrsProjectionFrame<TState>>,
656 pub value: Node<TState>,
658 pub status: Node<CqrsProjectionStatus>,
660 pub errors: Node<CqrsProjectionError>,
662 _retains: Rc<Vec<CqrsRetain>>,
663}
664
665pub fn cqrs_projection<TState: Clone + 'static, TEvent: Clone + 'static>(
667 graph: &Graph,
668 source: &CqrsBundle<impl Clone + 'static, TEvent>,
669 opts: CqrsProjectionOptions<TState, TEvent>,
670) -> CqrsProjection<TState> {
671 let name = opts.name;
672 let event_filter = Rc::new(normalize_events(opts.events));
673 let reducer = opts.reducer;
674 let initial = opts.initial;
675 let frames = graph.init_node::<CqrsProjectionFrame<TState>>(
676 Operator::with_opts("cqrsProjection", no_terminal_opts(), {
677 move |ctx: &Ctx| {
678 let mut state = ctx
679 .state_get::<ProjectionState<TState>>()
680 .map(|value| (*value).clone())
681 .unwrap_or_else(|| ProjectionState {
682 value: initial.clone(),
683 });
684 ctx.state_persist(true);
685 for event in ctx.batch::<CqrsEvent<TEvent>>(0) {
686 if let Some(filter) = event_filter.as_ref() {
687 if !filter.contains(&event.event_type) {
688 continue;
689 }
690 }
691 let event_for_reduce = (*event).clone();
692 let reduced = catch_unwind(AssertUnwindSafe(|| {
693 (reducer)(state.value.clone(), &event_for_reduce)
694 }));
695 match reduced {
696 Ok(next) => {
697 state.value = next.clone();
698 ctx.state_set(state.clone());
699 ctx.emit(CqrsProjectionFrame::Value {
700 state: next,
701 event_id: event.id.clone(),
702 cursor: event.runtime_cursor.clone(),
703 });
704 }
705 Err(panic) => {
706 if is_graph_runtime_panic(&panic) {
707 resume_unwind(panic);
708 }
709 ctx.emit(CqrsProjectionFrame::<TState>::Error(CqrsProjectionError {
710 code: CqrsProjectionErrorCode::ProjectionThrew,
711 message: panic_message(&panic),
712 event_id: event.id.clone(),
713 event_type: event.event_type.clone(),
714 cursor: event.runtime_cursor.clone(),
715 }));
716 return;
717 }
718 }
719 }
720 }
721 }),
722 vec![source.events.erased()],
723 GraphNodeOpts::named(name.clone()),
724 );
725 let value = graph.init_node::<TState>(
726 Operator::with_opts("cqrsProjectionValue", no_terminal_opts(), |ctx: &Ctx| {
727 for frame in ctx.batch::<CqrsProjectionFrame<TState>>(0) {
728 if let CqrsProjectionFrame::Value { state, .. } = frame.as_ref() {
729 ctx.emit(state.clone());
730 }
731 }
732 }),
733 vec![frames.erased()],
734 GraphNodeOpts::named(format!("{name}/value")),
735 );
736 let status = graph.init_node::<CqrsProjectionStatus>(
737 Operator::with_opts("cqrsProjectionStatus", no_terminal_opts(), |ctx: &Ctx| {
738 for frame in ctx.batch::<CqrsProjectionFrame<TState>>(0) {
739 match frame.as_ref() {
740 CqrsProjectionFrame::Value {
741 event_id, cursor, ..
742 } => ctx.emit(CqrsProjectionStatus {
743 state: CqrsProjectionStatusState::Updated,
744 event_id: event_id.clone(),
745 event_type: None,
746 cursor: cursor.clone(),
747 }),
748 CqrsProjectionFrame::Error(error) => ctx.emit(CqrsProjectionStatus {
749 state: CqrsProjectionStatusState::Errored,
750 event_id: error.event_id.clone(),
751 event_type: Some(error.event_type.clone()),
752 cursor: error.cursor.clone(),
753 }),
754 }
755 }
756 }),
757 vec![frames.erased()],
758 GraphNodeOpts::named(format!("{name}/status")),
759 );
760 let errors = graph.init_node::<CqrsProjectionError>(
761 Operator::with_opts("cqrsProjectionErrors", no_terminal_opts(), |ctx: &Ctx| {
762 for frame in ctx.batch::<CqrsProjectionFrame<TState>>(0) {
763 if let CqrsProjectionFrame::Error(error) = frame.as_ref() {
764 ctx.emit(error.clone());
765 }
766 }
767 }),
768 vec![frames.erased()],
769 GraphNodeOpts::named(format!("{name}/errors")),
770 );
771 let retains = Rc::new(vec![
772 CqrsRetain::new(graph.retain(&frames, &format!("{name}.cqrsProjection.frames"))),
773 CqrsRetain::new(graph.retain(&value, &format!("{name}.cqrsProjection.value"))),
774 CqrsRetain::new(graph.retain(&status, &format!("{name}.cqrsProjection.status"))),
775 CqrsRetain::new(graph.retain(&errors, &format!("{name}.cqrsProjection.errors"))),
776 ]);
777 CqrsProjection {
778 frames,
779 value,
780 status,
781 errors,
782 _retains: retains,
783 }
784}
785
786#[derive(Clone)]
787struct ProjectionState<T> {
788 value: T,
789}
790
791#[derive(Clone, Default)]
792struct RuntimeState {
793 event_seq: u64,
794 command_count: u64,
795 error_count: u64,
796 audit_seq: u64,
797 seen_command_ids: Vec<String>,
798 seen_event_ids: Vec<String>,
799 command_dedupe_evicted: u64,
800 event_dedupe_evicted: u64,
801}
802
803impl RuntimeState {
804 fn from_json(value: &Value) -> Self {
805 Self {
806 event_seq: json_u64(value, "eventSeq"),
807 command_count: json_u64(value, "commandCount"),
808 error_count: json_u64(value, "errorCount"),
809 audit_seq: json_u64(value, "auditSeq"),
810 seen_command_ids: json_string_array(value, "seenCommandIds"),
811 seen_event_ids: json_string_array(value, "seenEventIds"),
812 command_dedupe_evicted: json_u64(value, "commandDedupeEvicted"),
813 event_dedupe_evicted: json_u64(value, "eventDedupeEvicted"),
814 }
815 }
816
817 fn to_json(&self) -> Value {
818 json!({
819 "eventSeq": self.event_seq,
820 "commandCount": self.command_count,
821 "errorCount": self.error_count,
822 "auditSeq": self.audit_seq,
823 "seenCommandIds": self.seen_command_ids,
824 "seenEventIds": self.seen_event_ids,
825 "commandDedupeEvicted": self.command_dedupe_evicted,
826 "eventDedupeEvicted": self.event_dedupe_evicted,
827 })
828 }
829}
830
831fn reduce_command_fact<TCommand: Clone + 'static, TEvent: Clone + 'static>(
832 state: &mut RuntimeState,
833 command: CqrsCommand<TCommand>,
834 handlers: &HashMap<String, CqrsCommandHandler<TCommand, TEvent>>,
835 known_events: &Option<HashSet<String>>,
836 now: &dyn Fn() -> u64,
837 dedupe: CqrsDedupePolicy,
838) -> Vec<CqrsRuntimeFact<TCommand, TEvent>> {
839 state.command_count += 1;
840 if command.id.is_empty() {
841 return failure(
842 state,
843 Some(command),
844 CqrsErrorCode::MalformedCommand,
845 "cqrs: command id must be non-empty".to_owned(),
846 dedupe,
847 );
848 }
849 if command.command_type.is_empty() {
850 return failure(
851 state,
852 Some(command),
853 CqrsErrorCode::MalformedCommand,
854 "cqrs: command type must be non-empty".to_owned(),
855 dedupe,
856 );
857 }
858 if state.seen_command_ids.contains(&command.id) {
859 return failure(
860 state,
861 Some(command.clone()),
862 CqrsErrorCode::DuplicateCommand,
863 format!("cqrs: duplicate command '{}'", command.id),
864 dedupe,
865 );
866 }
867 state.seen_command_ids.push(command.id.clone());
868 trim_dedupe_window(
869 &mut state.seen_command_ids,
870 dedupe_max_entries(dedupe.commands),
871 &mut state.command_dedupe_evicted,
872 );
873
874 let Some(handler) = handlers.get(&command.command_type) else {
875 return failure(
876 state,
877 Some(command.clone()),
878 CqrsErrorCode::UnknownCommand,
879 format!("cqrs: unknown command '{}'", command.command_type),
880 dedupe,
881 );
882 };
883 let drafts = match catch_unwind(AssertUnwindSafe(|| (handler)(&command))) {
884 Ok(drafts) => drafts,
885 Err(panic) => {
886 if is_graph_runtime_panic(&panic) {
887 resume_unwind(panic);
888 }
889 return failure(
890 state,
891 Some(command.clone()),
892 CqrsErrorCode::HandlerThrew,
893 panic_message(&panic),
894 dedupe,
895 );
896 }
897 };
898 let prepared = match prepare_events(&command, drafts, state, known_events) {
899 Ok(prepared) => prepared,
900 Err((code, message)) => return failure(state, Some(command), code, message, dedupe),
901 };
902 let mut facts = Vec::new();
903 let timestamp_ms = match cqrs_timestamp(now) {
904 Ok(timestamp_ms) => timestamp_ms,
905 Err(message) => {
906 return failure(
907 state,
908 Some(command),
909 CqrsErrorCode::ClockThrew,
910 message,
911 dedupe,
912 );
913 }
914 };
915 let mut events = Vec::new();
916 for draft in prepared {
917 state.event_seq += 1;
918 state.seen_event_ids.push(draft.id.clone());
919 trim_dedupe_window(
920 &mut state.seen_event_ids,
921 dedupe_max_entries(dedupe.events),
922 &mut state.event_dedupe_evicted,
923 );
924 let event = CqrsEvent {
925 id: draft.id,
926 event_type: draft.event_type,
927 seq: state.event_seq,
928 cursor: state.event_seq,
929 runtime_cursor: cursor_of(state, dedupe),
930 command_id: command.id.clone(),
931 command_type: command.command_type.clone(),
932 payload: draft.payload,
933 timestamp_ms,
934 aggregate_id: draft.aggregate_id,
935 correlation_id: draft.correlation_id,
936 causation_id: draft.causation_id,
937 };
938 facts.push(CqrsRuntimeFact::Event(event.clone()));
939 events.push(event);
940 }
941 facts.push(CqrsRuntimeFact::Status(CqrsStatus {
942 state: CqrsStatusState::Accepted,
943 command_id: Some(command.id.clone()),
944 command_type: Some(command.command_type.clone()),
945 event_count: events.len(),
946 error_code: None,
947 cursor: cursor_of(state, dedupe),
948 }));
949 facts.push(CqrsRuntimeFact::Audit(audit_record(
950 state,
951 Some(&command),
952 CqrsAuditOutcome::Success,
953 &events,
954 None,
955 None,
956 dedupe,
957 )));
958 facts.push(CqrsRuntimeFact::Cursor(cursor_of(state, dedupe)));
959 facts
960}
961
962#[derive(Clone)]
963struct PreparedEvent<T> {
964 id: String,
965 event_type: String,
966 payload: T,
967 aggregate_id: Option<String>,
968 correlation_id: Option<String>,
969 causation_id: Option<String>,
970}
971
972fn prepare_events<TCommand, TEvent: Clone + 'static>(
973 command: &CqrsCommand<TCommand>,
974 drafts: Vec<CqrsEventDraft<TEvent>>,
975 state: &RuntimeState,
976 known_events: &Option<HashSet<String>>,
977) -> Result<Vec<PreparedEvent<TEvent>>, (CqrsErrorCode, String)> {
978 let mut seen_in_command = HashSet::new();
979 let mut prepared = Vec::new();
980 for (index, draft) in drafts.into_iter().enumerate() {
981 if draft.event_type.is_empty() {
982 return Err((
983 CqrsErrorCode::MalformedEvent,
984 "cqrs: event draft must have a non-empty type".to_owned(),
985 ));
986 }
987 if let Some(known_events) = known_events {
988 if !known_events.contains(&draft.event_type) {
989 return Err((
990 CqrsErrorCode::UnknownEvent,
991 format!("cqrs: unknown event '{}'", draft.event_type),
992 ));
993 }
994 }
995 let id = draft
996 .id
997 .clone()
998 .filter(|id| !id.is_empty())
999 .unwrap_or_else(|| {
1000 compound_tuple_key("cqrs-event", &[&command.id, &(index + 1).to_string()])
1001 });
1002 if state.seen_event_ids.contains(&id) || seen_in_command.contains(&id) {
1003 return Err((
1004 CqrsErrorCode::DuplicateEvent,
1005 format!("cqrs: duplicate event '{id}'"),
1006 ));
1007 }
1008 seen_in_command.insert(id.clone());
1009 prepared.push(PreparedEvent {
1010 id,
1011 event_type: draft.event_type,
1012 payload: draft.payload,
1013 aggregate_id: draft.aggregate_id,
1014 correlation_id: draft.correlation_id,
1015 causation_id: draft.causation_id,
1016 });
1017 }
1018 Ok(prepared)
1019}
1020
1021fn failure<TCommand: Clone + 'static, TEvent: Clone + 'static>(
1022 state: &mut RuntimeState,
1023 command: Option<CqrsCommand<TCommand>>,
1024 code: CqrsErrorCode,
1025 message: String,
1026 dedupe: CqrsDedupePolicy,
1027) -> Vec<CqrsRuntimeFact<TCommand, TEvent>> {
1028 state.error_count += 1;
1029 let cursor = cursor_of(state, dedupe);
1030 vec![
1031 CqrsRuntimeFact::Error(CqrsError {
1032 code,
1033 message: message.clone(),
1034 command: command.clone(),
1035 cursor: cursor.clone(),
1036 }),
1037 CqrsRuntimeFact::Status(CqrsStatus {
1038 state: CqrsStatusState::Rejected,
1039 command_id: command.as_ref().map(|command| command.id.clone()),
1040 command_type: command.as_ref().map(|command| command.command_type.clone()),
1041 event_count: 0,
1042 error_code: Some(code),
1043 cursor: cursor.clone(),
1044 }),
1045 CqrsRuntimeFact::Audit(audit_record::<TCommand, TEvent>(
1046 state,
1047 command.as_ref(),
1048 CqrsAuditOutcome::Failure,
1049 &[],
1050 Some(code),
1051 Some(message),
1052 dedupe,
1053 )),
1054 CqrsRuntimeFact::Cursor(cursor_of(state, dedupe)),
1055 ]
1056}
1057
1058fn audit_record<TCommand, TEvent>(
1059 state: &mut RuntimeState,
1060 command: Option<&CqrsCommand<TCommand>>,
1061 outcome: CqrsAuditOutcome,
1062 events: &[CqrsEvent<TEvent>],
1063 error_code: Option<CqrsErrorCode>,
1064 error_message: Option<String>,
1065 dedupe: CqrsDedupePolicy,
1066) -> CqrsAuditRecord {
1067 state.audit_seq += 1;
1068 CqrsAuditRecord {
1069 seq: state.audit_seq,
1070 command_id: command.map(|command| command.id.clone()),
1071 command_type: command.map(|command| command.command_type.clone()),
1072 outcome,
1073 event_ids: events.iter().map(|event| event.id.clone()).collect(),
1074 event_types: events
1075 .iter()
1076 .map(|event| event.event_type.clone())
1077 .collect(),
1078 error_code,
1079 error_message,
1080 cursor: cursor_of(state, dedupe),
1081 }
1082}
1083
1084fn runtime_projection<TCommand: Clone + 'static, TEvent: Clone + 'static, TOut: Clone + 'static>(
1085 graph: &Graph,
1086 runtime: &Node<CqrsRuntimeFact<TCommand, TEvent>>,
1087 name: &str,
1088 factory: &'static str,
1089 select: impl Fn(&CqrsRuntimeFact<TCommand, TEvent>) -> Option<TOut> + 'static,
1090) -> Node<TOut> {
1091 graph.init_node::<TOut>(
1092 Operator::with_opts(factory, no_terminal_opts(), move |ctx: &Ctx| {
1093 for fact in ctx.batch::<CqrsRuntimeFact<TCommand, TEvent>>(0) {
1094 if let Some(selected) = select(fact.as_ref()) {
1095 ctx.emit(selected);
1096 }
1097 }
1098 }),
1099 vec![runtime.erased()],
1100 GraphNodeOpts::named(name),
1101 )
1102}
1103
1104fn normalize_handlers<TCommand, TEvent>(
1105 definitions: Vec<CqrsCommandHandlerDefinition<TCommand, TEvent>>,
1106) -> HashMap<String, CqrsCommandHandler<TCommand, TEvent>> {
1107 let mut handlers = HashMap::new();
1108 for definition in definitions {
1109 assert!(
1110 !definition.command_type.is_empty(),
1111 "cqrs: handler type must be non-empty"
1112 );
1113 assert!(
1114 !handlers.contains_key(&definition.command_type),
1115 "cqrs: duplicate handler '{}'",
1116 definition.command_type
1117 );
1118 handlers.insert(definition.command_type, definition.handle);
1119 }
1120 handlers
1121}
1122
1123fn normalize_events(events: Option<Vec<String>>) -> Option<HashSet<String>> {
1124 let events = events?;
1125 let mut known = HashSet::new();
1126 for event in events {
1127 assert!(!event.is_empty(), "cqrs.events: values must be non-empty");
1128 assert!(known.insert(event.clone()), "cqrs.events: duplicate value");
1129 }
1130 Some(known)
1131}
1132
1133fn no_terminal_opts() -> crate::node::NodeOpts {
1134 crate::node::NodeOpts {
1135 complete_when_deps_complete: false,
1136 error_when_deps_error: false,
1137 ..Default::default()
1138 }
1139}
1140
1141fn cursor_of(state: &RuntimeState, dedupe: CqrsDedupePolicy) -> CqrsCursor {
1142 CqrsCursor {
1143 event_seq: state.event_seq,
1144 command_count: state.command_count,
1145 error_count: state.error_count,
1146 audit_seq: state.audit_seq,
1147 dedupe: if dedupe.bounded_any() {
1148 Some(CqrsDedupeSnapshot {
1149 command_ids_retained: state.seen_command_ids.len(),
1150 event_ids_retained: state.seen_event_ids.len(),
1151 command_ids_evicted: state.command_dedupe_evicted,
1152 event_ids_evicted: state.event_dedupe_evicted,
1153 })
1154 } else {
1155 None
1156 },
1157 }
1158}
1159
1160fn dedupe_max_entries(window: CqrsDedupeWindow) -> Option<usize> {
1161 match window {
1162 CqrsDedupeWindow::Unbounded => None,
1163 CqrsDedupeWindow::Bounded { max_entries } => Some(max_entries),
1164 }
1165}
1166
1167fn dedupe_meta(dedupe: CqrsDedupePolicy) -> String {
1168 match (dedupe.commands, dedupe.events) {
1169 (CqrsDedupeWindow::Unbounded, CqrsDedupeWindow::Unbounded) => "unbounded".to_owned(),
1170 (commands, events) => format!("commands={commands:?};events={events:?}"),
1171 }
1172}
1173
1174fn trim_dedupe_window(ids: &mut Vec<String>, max_entries: Option<usize>, evicted_total: &mut u64) {
1175 let Some(max_entries) = max_entries else {
1176 return;
1177 };
1178 if ids.len() <= max_entries {
1179 return;
1180 }
1181 let evicted = ids.len() - max_entries;
1182 ids.drain(0..evicted);
1183 *evicted_total += evicted as u64;
1184}
1185
1186fn json_u64(value: &Value, key: &str) -> u64 {
1187 value.get(key).and_then(Value::as_u64).unwrap_or(0)
1188}
1189
1190fn json_string_array(value: &Value, key: &str) -> Vec<String> {
1191 value
1192 .get(key)
1193 .and_then(Value::as_array)
1194 .map(|items| {
1195 items
1196 .iter()
1197 .filter_map(Value::as_str)
1198 .map(str::to_owned)
1199 .collect()
1200 })
1201 .unwrap_or_default()
1202}
1203
1204fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
1205 if let Some(message) = panic.downcast_ref::<&str>() {
1206 return (*message).to_owned();
1207 }
1208 if let Some(message) = panic.downcast_ref::<String>() {
1209 return message.clone();
1210 }
1211 "panic".to_owned()
1212}
1213
1214fn cqrs_timestamp(now: &dyn Fn() -> u64) -> Result<u64, String> {
1215 catch_unwind(AssertUnwindSafe(now))
1216 .map_err(|panic| format!("cqrs: now() threw: {}", panic_message(&panic)))
1217}
1218
1219fn is_graph_runtime_panic(panic: &Box<dyn std::any::Any + Send>) -> bool {
1220 let message = panic_message(panic);
1221 message.contains("R-reentrancy")
1222 || message.contains("R-rewire")
1223 || message.contains("R-graph-domain")
1224 || message.contains("D22")
1225 || message.contains("D37")
1226 || message.contains("feedback cycle")
1227 || message.contains("different graph")
1228 || message.contains("cross-graph")
1229 || message.contains("wire bridge")
1230 || message.contains("mid-fn topology mutation")
1231 || message.contains("reentrant dep mutation")
1232}