use rustc_hash::{FxHashMap, FxHashSet};
use serde::Serialize;
use serde_json::json;
use wasm_bindgen::prelude::*;
use crate::error::{self, codes};
use crate::models::{DFGNode, DirectlyFollowsRelation, DFG};
use crate::state;
use crate::utilities::to_js_str;
#[derive(Debug, Clone)]
pub struct IncrementalDFG {
pub edges: FxHashMap<(u32, u32), u64>,
pub node_counts: FxHashMap<u32, u64>,
pub total_events: u64,
pub start_activities: FxHashSet<u32>,
pub end_activities: FxHashSet<u32>,
pub last_activity: Option<u32>,
}
impl IncrementalDFG {
pub fn new() -> Self {
IncrementalDFG {
edges: FxHashMap::default(),
node_counts: FxHashMap::default(),
total_events: 0,
start_activities: FxHashSet::default(),
end_activities: FxHashSet::default(),
last_activity: None,
}
}
pub fn process_event(&mut self, activity: u32, is_trace_start: bool) {
if is_trace_start {
self.start_activities.insert(activity);
self.last_activity = Some(activity);
} else if let Some(prev) = self.last_activity {
*self.edges.entry((prev, activity)).or_insert(0) += 1;
self.last_activity = Some(activity);
} else {
self.start_activities.insert(activity);
self.last_activity = Some(activity);
}
*self.node_counts.entry(activity).or_insert(0) += 1;
self.total_events += 1;
}
pub fn end_trace(&mut self) {
if let Some(last) = self.last_activity.take() {
self.end_activities.insert(last);
}
}
pub fn snapshot(&self) -> DFG {
let mut dfg = DFG::new();
dfg.nodes = self
.node_counts
.iter()
.map(|(&id, &count)| {
let label = format!("activity_{}", id);
DFGNode {
id: label.clone(),
label,
frequency: count as usize,
}
})
.collect();
dfg.edges = self
.edges
.iter()
.map(|(&(from, to), &freq)| DirectlyFollowsRelation {
from: format!("activity_{}", from),
to: format!("activity_{}", to),
frequency: freq as usize,
})
.collect();
for &id in &self.start_activities {
dfg.start_activities.insert(format!("activity_{}", id), 0);
}
for &id in &self.end_activities {
dfg.end_activities.insert(format!("activity_{}", id), 0);
}
dfg
}
pub fn merge(&mut self, other: &IncrementalDFG) {
for (&key, &count) in &other.edges {
*self.edges.entry(key).or_insert(0) += count;
}
for (&key, &count) in &other.node_counts {
*self.node_counts.entry(key).or_insert(0) += count;
}
self.start_activities.extend(&other.start_activities);
self.end_activities.extend(&other.end_activities);
self.total_events += other.total_events;
}
pub fn clear(&mut self) {
self.edges.clear();
self.node_counts.clear();
self.total_events = 0;
self.start_activities.clear();
self.end_activities.clear();
self.last_activity = None;
}
pub fn stats(&self) -> (u64, u64, u64) {
(
self.total_events,
self.node_counts.len() as u64,
self.edges.len() as u64,
)
}
}
impl Default for IncrementalDFG {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct StreamingDFG {
vocab_map: FxHashMap<String, u32>,
vocab: Vec<String>,
inner: IncrementalDFG,
}
impl StreamingDFG {
pub fn new() -> Self {
StreamingDFG {
vocab_map: FxHashMap::default(),
vocab: Vec::new(),
inner: IncrementalDFG::new(),
}
}
#[inline]
fn intern(&mut self, activity: &str) -> u32 {
if let Some(&id) = self.vocab_map.get(activity) {
return id;
}
let id = self.vocab.len() as u32;
self.vocab.push(activity.to_owned());
self.vocab_map.insert(activity.to_owned(), id);
id
}
pub fn process_event(&mut self, activity: &str) {
let id = self.intern(activity);
let is_trace_start = self.inner.last_activity.is_none();
self.inner.process_event(id, is_trace_start);
}
pub fn end_trace(&mut self) {
self.inner.end_trace();
}
pub fn snapshot(&self) -> DFG {
let mut dfg = DFG::new();
dfg.nodes = self
.inner
.node_counts
.iter()
.filter_map(|(&id, &count)| {
let label = self.vocab.get(id as usize).cloned()?;
Some(DFGNode {
id: label.clone(),
label,
frequency: count as usize,
})
})
.collect();
dfg.edges = self
.inner
.edges
.iter()
.filter_map(|(&(from, to), &freq)| {
let from_label = self.vocab.get(from as usize).cloned()?;
let to_label = self.vocab.get(to as usize).cloned()?;
Some(DirectlyFollowsRelation {
from: from_label,
to: to_label,
frequency: freq as usize,
})
})
.collect();
for &id in &self.inner.start_activities {
if let Some(name) = self.vocab.get(id as usize) {
dfg.start_activities.insert(name.clone(), 0);
}
}
for &id in &self.inner.end_activities {
if let Some(name) = self.vocab.get(id as usize) {
dfg.end_activities.insert(name.clone(), 0);
}
}
dfg
}
pub fn event_count(&self) -> u64 {
self.inner.total_events
}
}
impl Default for StreamingDFG {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Serialize)]
struct StreamingDfgStats {
total_events: u64,
unique_activities: u64,
unique_edges: u64,
}
#[wasm_bindgen]
pub fn new_streaming_dfg() -> Result<String, JsValue> {
let app_state = state::get_or_init_state();
let idfg = IncrementalDFG::new();
app_state.store_object(state::StoredObject::IncrementalDFG(idfg))
}
#[wasm_bindgen]
pub fn streaming_dfg_process_event(handle: &str, activity_id: u32) -> Result<(), JsValue> {
let app_state = state::get_or_init_state();
let aid = activity_id;
app_state.with_object_mut(handle, |opt| match opt {
Some(state::StoredObject::IncrementalDFG(idfg)) => {
let is_trace_start = idfg.last_activity.is_none();
idfg.process_event(aid, is_trace_start);
Ok(())
}
Some(_) => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("Object {} is not an IncrementalDFG", handle),
)),
None => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("No object found for handle: {}", handle),
)),
})
}
#[wasm_bindgen]
pub fn streaming_dfg_end_trace(handle: &str) -> Result<(), JsValue> {
let app_state = state::get_or_init_state();
app_state.with_object_mut(handle, |opt| match opt {
Some(state::StoredObject::IncrementalDFG(idfg)) => {
idfg.end_trace();
Ok(())
}
Some(_) => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("Object {} is not an IncrementalDFG", handle),
)),
None => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("No object found for handle: {}", handle),
)),
})
}
#[wasm_bindgen]
pub fn incremental_dfg_snapshot(handle: &str) -> Result<String, JsValue> {
let app_state = state::get_or_init_state();
app_state.with_object(handle, |opt| match opt {
Some(state::StoredObject::IncrementalDFG(idfg)) => {
let dfg = idfg.snapshot();
serde_json::to_string(&dfg).map_err(|e| {
error::wasm_err(
codes::INTERNAL_ERROR,
format!("JSON serialization failed: {}", e),
)
})
}
Some(_) => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("Object {} is not an IncrementalDFG", handle),
)),
None => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("No object found for handle: {}", handle),
)),
})
}
#[wasm_bindgen]
pub fn incremental_dfg_stats(handle: &str) -> Result<String, JsValue> {
let app_state = state::get_or_init_state();
app_state.with_object(handle, |opt| match opt {
Some(state::StoredObject::IncrementalDFG(idfg)) => {
let (total, activities, edges) = idfg.stats();
let stats = StreamingDfgStats {
total_events: total,
unique_activities: activities,
unique_edges: edges,
};
serde_json::to_string(&stats).map_err(|e| {
error::wasm_err(
codes::INTERNAL_ERROR,
format!("JSON serialization failed: {}", e),
)
})
}
Some(_) => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("Object {} is not an IncrementalDFG", handle),
)),
None => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("No object found for handle: {}", handle),
)),
})
}
#[wasm_bindgen]
pub fn streaming_dfg_string_new() -> Result<String, JsValue> {
let app_state = state::get_or_init_state();
let sdfg = StreamingDFG::new();
app_state.store_object(state::StoredObject::StreamingDFG(sdfg))
}
#[wasm_bindgen]
pub fn streaming_dfg_string_event(handle: &str, activity: &str) -> Result<JsValue, JsValue> {
state::get_or_init_state().with_object_mut(handle, |obj| match obj {
Some(state::StoredObject::StreamingDFG(sdfg)) => {
sdfg.process_event(activity);
to_js_str(&json!({
"ok": true,
"event_count": sdfg.event_count(),
}))
}
Some(_) => Err(crate::error::js_val("Object is not a StreamingDFG")),
None => Err(crate::error::js_val(&format!(
"StreamingDFG '{}' not found",
handle
))),
})
}
#[wasm_bindgen]
pub fn streaming_dfg_string_end_trace(handle: &str) -> Result<JsValue, JsValue> {
state::get_or_init_state().with_object_mut(handle, |obj| match obj {
Some(state::StoredObject::StreamingDFG(sdfg)) => {
sdfg.end_trace();
to_js_str(&json!({ "ok": true }))
}
Some(_) => Err(crate::error::js_val("Object is not a StreamingDFG")),
None => Err(crate::error::js_val(&format!(
"StreamingDFG '{}' not found",
handle
))),
})
}
#[wasm_bindgen]
pub fn streaming_dfg_string_snapshot(handle: &str) -> Result<String, JsValue> {
let app_state = state::get_or_init_state();
app_state.with_object(handle, |opt| match opt {
Some(state::StoredObject::StreamingDFG(sdfg)) => {
let dfg = sdfg.snapshot();
serde_json::to_string(&dfg).map_err(|e| {
error::wasm_err(
codes::INTERNAL_ERROR,
format!("JSON serialization failed: {}", e),
)
})
}
Some(_) => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("Object {} is not a StreamingDFG", handle),
)),
None => Err(error::wasm_err(
codes::INVALID_HANDLE,
format!("No object found for handle: {}", handle),
)),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn unique_key(prefix: &str) -> String {
format!("{}:{:?}", prefix, std::thread::current().id())
}
#[test]
fn test_empty_dfg() {
let _key = unique_key("test_empty_dfg");
let idfg = IncrementalDFG::new();
let (events, activities, edges) = idfg.stats();
assert_eq!(events, 0, "new DFG should have 0 total events");
assert_eq!(activities, 0, "new DFG should have 0 unique activities");
assert_eq!(edges, 0, "new DFG should have 0 unique edges");
}
#[test]
fn test_single_event() {
let _key = unique_key("test_single_event");
let mut idfg = IncrementalDFG::new();
idfg.process_event(1, true);
let (events, activities, edges) = idfg.stats();
assert_eq!(events, 1, "should have 1 total event");
assert_eq!(activities, 1, "should have 1 unique activity");
assert_eq!(edges, 0, "single event should have 0 edges");
assert_eq!(idfg.node_counts.get(&1), Some(&1));
assert!(idfg.start_activities.contains(&1));
}
#[test]
fn test_edge_creation() {
let _key = unique_key("test_edge_creation");
let mut idfg = IncrementalDFG::new();
idfg.process_event(1, true);
idfg.process_event(2, false);
idfg.end_trace();
let (events, activities, edges) = idfg.stats();
assert_eq!(events, 2);
assert_eq!(activities, 2);
assert_eq!(edges, 1, "two events in same trace should create 1 edge");
assert_eq!(idfg.edges.get(&(1, 2)), Some(&1));
assert!(idfg.start_activities.contains(&1));
assert!(idfg.end_activities.contains(&2));
}
#[test]
fn test_trace_boundaries() {
let _key = unique_key("test_trace_boundaries");
let mut idfg = IncrementalDFG::new();
idfg.process_event(1, true);
idfg.process_event(2, false);
idfg.end_trace();
idfg.process_event(3, true);
idfg.process_event(4, false);
idfg.end_trace();
let (_events, _activities, edges) = idfg.stats();
assert_eq!(edges, 2, "should have exactly 2 edges");
assert_eq!(idfg.edges.get(&(1, 2)), Some(&1));
assert_eq!(idfg.edges.get(&(3, 4)), Some(&1));
assert_eq!(
idfg.edges.get(&(2, 3)),
None,
"B->C should not exist (different traces)"
);
assert_eq!(idfg.edges.get(&(2, 4)), None);
assert!(idfg.start_activities.contains(&1));
assert!(idfg.start_activities.contains(&3));
assert!(idfg.end_activities.contains(&2));
assert!(idfg.end_activities.contains(&4));
}
#[test]
fn test_merge() {
let _key = unique_key("test_merge");
let mut a = IncrementalDFG::new();
a.process_event(1, true);
a.process_event(2, false);
a.end_trace();
let mut b = IncrementalDFG::new();
b.process_event(1, true);
b.process_event(3, false);
b.end_trace();
a.merge(&b);
let (events, activities, edges) = a.stats();
assert_eq!(events, 4, "merged should have 4 total events");
assert_eq!(
activities, 3,
"merged should have 3 unique activities (1, 2, 3)"
);
assert_eq!(edges, 2, "merged should have 2 unique edges (1->2, 1->3)");
assert_eq!(a.edges.get(&(1, 2)), Some(&1));
assert_eq!(a.edges.get(&(1, 3)), Some(&1));
assert!(a.start_activities.contains(&1));
assert!(a.end_activities.contains(&2));
assert!(a.end_activities.contains(&3));
}
#[test]
fn test_start_end_activities() {
let _key = unique_key("test_start_end_activities");
let mut idfg = IncrementalDFG::new();
idfg.process_event(1, true);
idfg.process_event(2, false);
idfg.process_event(3, false);
idfg.end_trace();
idfg.process_event(1, true);
idfg.process_event(3, false);
idfg.process_event(2, false);
idfg.end_trace();
assert!(idfg.start_activities.contains(&1));
assert!(
!idfg.end_activities.contains(&1),
"A is never the last activity"
);
assert!(!idfg.start_activities.contains(&2), "B is never first");
assert!(idfg.end_activities.contains(&2));
assert!(!idfg.start_activities.contains(&3), "C is never first");
assert!(idfg.end_activities.contains(&3));
assert_eq!(idfg.node_counts.get(&1), Some(&2), "A appears twice");
assert_eq!(idfg.node_counts.get(&2), Some(&2), "B appears twice");
assert_eq!(idfg.node_counts.get(&3), Some(&2), "C appears twice");
}
#[test]
fn test_streaming_wrapper() {
let _key = unique_key("test_streaming_wrapper");
let mut sdfg = StreamingDFG::new();
sdfg.process_event("A");
sdfg.process_event("B");
sdfg.process_event("C");
sdfg.end_trace();
sdfg.process_event("A");
sdfg.process_event("D");
sdfg.end_trace();
assert_eq!(sdfg.event_count(), 5);
let dfg = sdfg.snapshot();
assert_eq!(dfg.nodes.len(), 4, "should have 4 unique activities");
let labels: Vec<&str> = dfg.nodes.iter().map(|n| n.label.as_str()).collect();
assert!(labels.contains(&"A"));
assert!(labels.contains(&"B"));
assert!(labels.contains(&"C"));
assert!(labels.contains(&"D"));
assert_eq!(dfg.edges.len(), 3);
assert!(dfg.edges.iter().any(|e| e.from == "A" && e.to == "B"));
assert!(dfg.edges.iter().any(|e| e.from == "B" && e.to == "C"));
assert!(dfg.edges.iter().any(|e| e.from == "A" && e.to == "D"));
assert!(dfg.start_activities.contains_key("A"));
assert!(dfg.end_activities.contains_key("C"));
assert!(dfg.end_activities.contains_key("D"));
}
#[test]
fn test_large_stream() {
let _key = unique_key("test_large_stream");
let mut idfg = IncrementalDFG::new();
const N: u32 = 10_000;
idfg.process_event(0, true);
for i in 1..N {
idfg.process_event(i, false);
}
idfg.end_trace();
let (events, activities, edges) = idfg.stats();
assert_eq!(events, N as u64, "should have exactly 10K events");
assert_eq!(activities, N as u64, "should have 10K unique activities");
assert_eq!(edges, (N - 1) as u64, "should have 9999 edges");
assert_eq!(idfg.edges.get(&(0, 1)), Some(&1));
assert_eq!(idfg.edges.get(&((N - 2), (N - 1))), Some(&1));
assert!(idfg.start_activities.contains(&0));
assert!(idfg.end_activities.contains(&(N - 1)));
assert!(!idfg.end_activities.contains(&0));
assert!(!idfg.start_activities.contains(&(N - 1)));
}
#[test]
fn test_clear() {
let _key = unique_key("test_clear");
let mut idfg = IncrementalDFG::new();
idfg.process_event(1, true);
idfg.process_event(2, false);
idfg.end_trace();
assert_eq!(idfg.total_events, 2);
idfg.clear();
let (events, activities, edges) = idfg.stats();
assert_eq!(events, 0);
assert_eq!(activities, 0);
assert_eq!(edges, 0);
assert!(idfg.last_activity.is_none());
}
#[test]
fn test_multiple_traces_same_activity() {
let _key = unique_key("test_multiple_traces_same_activity");
let mut idfg = IncrementalDFG::new();
for _ in 0..3 {
idfg.process_event(1, true);
idfg.process_event(1, false);
idfg.end_trace();
}
let (events, activities, edges) = idfg.stats();
assert_eq!(events, 6, "3 traces * 2 events = 6");
assert_eq!(activities, 1, "only activity 1");
assert_eq!(edges, 1, "only one unique edge: 1->1");
assert_eq!(
idfg.edges.get(&(1, 1)),
Some(&3),
"edge 1->1 should have count 3"
);
assert_eq!(idfg.node_counts.get(&1), Some(&6));
}
}