Skip to main content

graphrefly/
cqrs.rs

1//! Graph-visible CQRS application infrastructure (D142 / B67).
2//!
3//! Commands are ordinary DATA facts on a graph-owned command node. Runtime facts,
4//! events, status, errors, audit, cursor, and projections are ordinary graph
5//! nodes with declared deps; there is no Graph subclass, hidden EventEmitter, or
6//! storage-owned restore path.
7
8pub 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)]
24/// `CqrsCommand` data container.
25pub struct CqrsCommand<T = crate::protocol::AnyValue> {
26    /// `id` field for id.
27    pub id: String,
28    /// `command_type` field for command type.
29    pub command_type: String,
30    /// `payload` field for payload.
31    pub payload: T,
32    /// `aggregate_id` field for aggregate id.
33    pub aggregate_id: Option<String>,
34    /// `correlation_id` field for correlation id.
35    pub correlation_id: Option<String>,
36    /// `causation_id` field for causation id.
37    pub causation_id: Option<String>,
38}
39
40impl<T> CqrsCommand<T> {
41    /// Creates or computes `new`.
42    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)]
55/// `CqrsEventDraft` data container.
56pub struct CqrsEventDraft<T = crate::protocol::AnyValue> {
57    /// `id` field for id.
58    pub id: Option<String>,
59    /// `event_type` field for event type.
60    pub event_type: String,
61    /// `payload` field for payload.
62    pub payload: T,
63    /// `aggregate_id` field for aggregate id.
64    pub aggregate_id: Option<String>,
65    /// `correlation_id` field for correlation id.
66    pub correlation_id: Option<String>,
67    /// `causation_id` field for causation id.
68    pub causation_id: Option<String>,
69}
70
71impl<T> CqrsEventDraft<T> {
72    /// Creates or computes `new`.
73    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)]
86/// `CqrsEvent` data container.
87pub struct CqrsEvent<T = crate::protocol::AnyValue> {
88    /// `id` field for id.
89    pub id: String,
90    /// `event_type` field for event type.
91    pub event_type: String,
92    /// `seq` field for seq.
93    pub seq: u64,
94    /// `cursor` field for cursor.
95    pub cursor: u64,
96    /// `runtime_cursor` field for runtime cursor.
97    pub runtime_cursor: CqrsCursor,
98    /// `command_id` field for command id.
99    pub command_id: String,
100    /// `command_type` field for command type.
101    pub command_type: String,
102    /// `payload` field for payload.
103    pub payload: T,
104    /// `timestamp_ms` field for timestamp ms.
105    pub timestamp_ms: u64,
106    /// `aggregate_id` field for aggregate id.
107    pub aggregate_id: Option<String>,
108    /// `correlation_id` field for correlation id.
109    pub correlation_id: Option<String>,
110    /// `causation_id` field for causation id.
111    pub causation_id: Option<String>,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115/// `CqrsErrorCode` variants.
116pub enum CqrsErrorCode {
117    /// `MalformedCommand` variant.
118    MalformedCommand,
119    /// `DuplicateCommand` variant.
120    DuplicateCommand,
121    /// `UnknownCommand` variant.
122    UnknownCommand,
123    /// `HandlerThrew` variant.
124    HandlerThrew,
125    /// `ClockThrew` variant.
126    ClockThrew,
127    /// `MalformedEvent` variant.
128    MalformedEvent,
129    /// `UnknownEvent` variant.
130    UnknownEvent,
131    /// `DuplicateEvent` variant.
132    DuplicateEvent,
133}
134
135#[derive(Debug, Clone, PartialEq)]
136/// `CqrsError` data container.
137pub struct CqrsError<TCommand = crate::protocol::AnyValue> {
138    /// `code` field for code.
139    pub code: CqrsErrorCode,
140    /// `message` field for message.
141    pub message: String,
142    /// `command` field for command.
143    pub command: Option<CqrsCommand<TCommand>>,
144    /// `cursor` field for cursor.
145    pub cursor: CqrsCursor,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
149/// `CqrsDedupeSnapshot` data container.
150pub struct CqrsDedupeSnapshot {
151    /// `command_ids_retained` field for command ids retained.
152    pub command_ids_retained: usize,
153    /// `event_ids_retained` field for event ids retained.
154    pub event_ids_retained: usize,
155    /// `command_ids_evicted` field for command ids evicted.
156    pub command_ids_evicted: u64,
157    /// `event_ids_evicted` field for event ids evicted.
158    pub event_ids_evicted: u64,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162/// `CqrsCursor` data container.
163pub struct CqrsCursor {
164    /// `event_seq` field for event seq.
165    pub event_seq: u64,
166    /// `command_count` field for command count.
167    pub command_count: u64,
168    /// `error_count` field for error count.
169    pub error_count: u64,
170    /// `audit_seq` field for audit seq.
171    pub audit_seq: u64,
172    /// `dedupe` field for dedupe.
173    pub dedupe: Option<CqrsDedupeSnapshot>,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177/// `CqrsStatus` data container.
178pub struct CqrsStatus {
179    /// `state` field for state.
180    pub state: CqrsStatusState,
181    /// `command_id` field for command id.
182    pub command_id: Option<String>,
183    /// `command_type` field for command type.
184    pub command_type: Option<String>,
185    /// `event_count` field for event count.
186    pub event_count: usize,
187    /// `error_code` field for error code.
188    pub error_code: Option<CqrsErrorCode>,
189    /// `cursor` field for cursor.
190    pub cursor: CqrsCursor,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194/// `CqrsStatusState` variants.
195pub enum CqrsStatusState {
196    /// `Accepted` variant.
197    Accepted,
198    /// `Rejected` variant.
199    Rejected,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203/// `CqrsAuditRecord` data container.
204pub struct CqrsAuditRecord {
205    /// `seq` field for seq.
206    pub seq: u64,
207    /// `command_id` field for command id.
208    pub command_id: Option<String>,
209    /// `command_type` field for command type.
210    pub command_type: Option<String>,
211    /// `outcome` field for outcome.
212    pub outcome: CqrsAuditOutcome,
213    /// `event_ids` field for event ids.
214    pub event_ids: Vec<String>,
215    /// `event_types` field for event types.
216    pub event_types: Vec<String>,
217    /// `error_code` field for error code.
218    pub error_code: Option<CqrsErrorCode>,
219    /// `error_message` field for error message.
220    pub error_message: Option<String>,
221    /// `cursor` field for cursor.
222    pub cursor: CqrsCursor,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226/// `CqrsAuditOutcome` variants.
227pub enum CqrsAuditOutcome {
228    /// `Success` variant.
229    Success,
230    /// `Failure` variant.
231    Failure,
232}
233
234#[derive(Debug, Clone, PartialEq)]
235/// `CqrsRuntimeFact` variants.
236pub enum CqrsRuntimeFact<TCommand = crate::protocol::AnyValue, TEvent = crate::protocol::AnyValue> {
237    /// `Event` variant.
238    Event(CqrsEvent<TEvent>),
239    /// `Status` variant.
240    Status(CqrsStatus),
241    /// `Error` variant.
242    Error(CqrsError<TCommand>),
243    /// `Audit` variant.
244    Audit(CqrsAuditRecord),
245    /// `Cursor` variant.
246    Cursor(CqrsCursor),
247}
248
249/// D151 membership-based duplicate-recognition window for CQRS ids.
250///
251/// This is passive CQRS vocabulary, not a shared idempotency reducer engine:
252/// commands and events each own their own id membership window.
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum CqrsDedupeWindow {
255    /// `Unbounded` variant.
256    Unbounded,
257    /// `Bounded` variant.
258    Bounded {
259        /// `max_entries` field for `Bounded`.
260        max_entries: usize,
261    },
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265/// `CqrsDedupePolicy` data container.
266pub struct CqrsDedupePolicy {
267    /// `commands` field for commands.
268    pub commands: CqrsDedupeWindow,
269    /// `events` field for events.
270    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    /// Creates or computes `bounded`.
284    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
301/// `CqrsCommandHandler` type alias.
302pub type CqrsCommandHandler<TCommand, TEvent> =
303    Rc<dyn Fn(&CqrsCommand<TCommand>) -> Vec<CqrsEventDraft<TEvent>>>;
304
305#[derive(Clone)]
306/// `CqrsCommandHandlerDefinition` data container.
307pub struct CqrsCommandHandlerDefinition<TCommand, TEvent> {
308    /// `command_type` field for command type.
309    pub command_type: String,
310    /// `handle` field for handle.
311    pub handle: CqrsCommandHandler<TCommand, TEvent>,
312}
313
314/// Creates or computes `cqrs_command_handler`.
315pub 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)]
331/// `CqrsOptions` data container.
332pub struct CqrsOptions<TCommand, TEvent> {
333    /// `name` field for name.
334    pub name: String,
335    /// `handlers` field for handlers.
336    pub handlers: Vec<CqrsCommandHandlerDefinition<TCommand, TEvent>>,
337    /// `events` field for events.
338    pub events: Option<Vec<String>>,
339    /// `now` field for now.
340    pub now: Rc<dyn Fn() -> u64>,
341    /// `dedupe` field for dedupe.
342    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    /// Creates or computes `named`.
359    pub fn named(name: impl Into<String>) -> Self {
360        Self {
361            name: name.into(),
362            ..Self::default()
363        }
364    }
365
366    /// Updates or reads `with_handlers`.
367    pub fn with_handlers(
368        mut self,
369        handlers: Vec<CqrsCommandHandlerDefinition<TCommand, TEvent>>,
370    ) -> Self {
371        self.handlers = handlers;
372        self
373    }
374
375    /// Updates or reads `with_events`.
376    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    /// Updates or reads `with_now`.
382    pub fn with_now(mut self, now: impl Fn() -> u64 + 'static) -> Self {
383        self.now = Rc::new(now);
384        self
385    }
386
387    /// Updates or reads `with_dedupe`.
388    pub fn with_dedupe(mut self, dedupe: CqrsDedupePolicy) -> Self {
389        self.dedupe = dedupe;
390        self
391    }
392}
393
394#[derive(Clone)]
395/// `CqrsBundle` data container.
396pub struct CqrsBundle<TCommand = crate::protocol::AnyValue, TEvent = crate::protocol::AnyValue> {
397    /// `command` field for command.
398    pub command: Node<CqrsCommand<TCommand>>,
399    /// `runtime` field for runtime.
400    pub runtime: Node<CqrsRuntimeFact<TCommand, TEvent>>,
401    /// `events` field for events.
402    pub events: Node<CqrsEvent<TEvent>>,
403    /// `status` field for status.
404    pub status: Node<CqrsStatus>,
405    /// `errors` field for errors.
406    pub errors: Node<CqrsError<TCommand>>,
407    /// `audit` field for audit.
408    pub audit: Node<CqrsAuditRecord>,
409    /// `cursor` field for cursor.
410    pub cursor: Node<CqrsCursor>,
411    _retains: Rc<Vec<CqrsRetain>>,
412}
413
414impl<TCommand: Clone + 'static, TEvent: Clone + 'static> CqrsBundle<TCommand, TEvent> {
415    /// Updates or reads `dispatch`.
416    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
442/// Creates or computes `cqrs`.
443pub fn cqrs<TCommand: Clone + 'static, TEvent: Clone + 'static>(
444    graph: &Graph,
445) -> CqrsBundle<TCommand, TEvent> {
446    cqrs_with_options(graph, CqrsOptions::default())
447}
448
449/// Creates or computes `cqrs_with_options`.
450pub 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
575/// `CqrsProjectionReducer` type alias.
576pub type CqrsProjectionReducer<TState, TEvent> = Rc<dyn Fn(TState, &CqrsEvent<TEvent>) -> TState>;
577
578#[derive(Clone)]
579/// `CqrsProjectionOptions` data container.
580pub struct CqrsProjectionOptions<TState, TEvent> {
581    /// `name` field for name.
582    pub name: String,
583    /// `events` field for events.
584    pub events: Option<Vec<String>>,
585    /// `initial` field for initial.
586    pub initial: TState,
587    /// `reducer` field for reducer.
588    pub reducer: CqrsProjectionReducer<TState, TEvent>,
589}
590
591#[derive(Debug, Clone, PartialEq)]
592/// `CqrsProjectionFrame` variants.
593pub enum CqrsProjectionFrame<TState> {
594    /// `Value` variant.
595    Value {
596        /// `state` field for state.
597        state: TState,
598        /// `event_id` field for event id.
599        event_id: String,
600        /// `cursor` field for cursor.
601        cursor: CqrsCursor,
602    },
603    /// `Error` variant.
604    Error(CqrsProjectionError),
605}
606
607#[derive(Debug, Clone, PartialEq, Eq)]
608/// `CqrsProjectionError` data container.
609pub struct CqrsProjectionError {
610    /// `code` field for code.
611    pub code: CqrsProjectionErrorCode,
612    /// `message` field for message.
613    pub message: String,
614    /// `event_id` field for event id.
615    pub event_id: String,
616    /// `event_type` field for event type.
617    pub event_type: String,
618    /// `cursor` field for cursor.
619    pub cursor: CqrsCursor,
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623/// `CqrsProjectionErrorCode` variants.
624pub enum CqrsProjectionErrorCode {
625    /// `ProjectionThrew` variant.
626    ProjectionThrew,
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
630/// `CqrsProjectionStatus` data container.
631pub struct CqrsProjectionStatus {
632    /// `state` field for state.
633    pub state: CqrsProjectionStatusState,
634    /// `event_id` field for event id.
635    pub event_id: String,
636    /// `event_type` field for event type.
637    pub event_type: Option<String>,
638    /// `cursor` field for cursor.
639    pub cursor: CqrsCursor,
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq)]
643/// `CqrsProjectionStatusState` variants.
644pub enum CqrsProjectionStatusState {
645    /// `Updated` variant.
646    Updated,
647    /// `Errored` variant.
648    Errored,
649}
650
651#[derive(Clone)]
652/// `CqrsProjection` data container.
653pub struct CqrsProjection<TState> {
654    /// `frames` field for frames.
655    pub frames: Node<CqrsProjectionFrame<TState>>,
656    /// `value` field for value.
657    pub value: Node<TState>,
658    /// `status` field for status.
659    pub status: Node<CqrsProjectionStatus>,
660    /// `errors` field for errors.
661    pub errors: Node<CqrsProjectionError>,
662    _retains: Rc<Vec<CqrsRetain>>,
663}
664
665/// Creates or computes `cqrs_projection`.
666pub 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}