Skip to main content

dataflow_wasm/
lib.rs

1//! WebAssembly bindings for dataflow-rs workflow engine.
2//!
3//! This crate provides WASM bindings that allow using dataflow-rs from JavaScript/TypeScript.
4//!
5//! # Usage
6//!
7//! ```javascript
8//! import init, { WasmEngine } from 'dataflow-wasm';
9//!
10//! await init();
11//!
12//! // Define workflows
13//! const workflows = JSON.stringify([{
14//!     id: "example",
15//!     name: "Example Workflow",
16//!     priority: 1,
17//!     tasks: [{
18//!         id: "parse_payload",
19//!         name: "Parse Payload",
20//!         function: {
21//!             name: "parse",
22//!             input: {}
23//!         }
24//!     }, {
25//!         id: "task1",
26//!         name: "Transform Data",
27//!         function: {
28//!             name: "map",
29//!             input: {
30//!                 mappings: [{
31//!                     path: "data.result",
32//!                     logic: { "var": "data.input" }
33//!                 }]
34//!             }
35//!         }
36//!     }]
37//! }]);
38//!
39//! // Create engine
40//! const engine = new WasmEngine(workflows);
41//!
42//! // Process a payload (raw string, parsed by the parse plugin)
43//! const payload = '{"input": "hello"}';
44//! const result = await engine.process(payload);
45//! console.log(JSON.parse(result));
46//! ```
47
48use dataflow_rs::{Engine, Message, Workflow};
49use serde_json::Value;
50use std::sync::Arc;
51use wasm_bindgen::prelude::*;
52use wasm_bindgen_futures::future_to_promise;
53
54/// Initialize the WASM module.
55///
56/// This is automatically called when the module loads.
57/// Sets up the panic hook for better error messages in the browser console.
58#[wasm_bindgen(start)]
59pub fn init() {
60    console_error_panic_hook::set_once();
61}
62
63/// A WebAssembly-compatible workflow engine.
64///
65/// Wraps the dataflow-rs Engine to provide async message processing
66/// that returns JavaScript Promises.
67#[wasm_bindgen]
68pub struct WasmEngine {
69    inner: Arc<Engine>,
70}
71
72#[wasm_bindgen]
73impl WasmEngine {
74    /// Create a new WasmEngine from a JSON array of workflow definitions.
75    ///
76    /// # Arguments
77    /// * `workflows_json` - JSON string containing an array of workflow definitions
78    ///
79    /// # Example
80    /// ```javascript
81    /// const workflows = JSON.stringify([{
82    ///     id: "workflow1",
83    ///     name: "My Workflow",
84    ///     priority: 1,
85    ///     tasks: [...]
86    /// }]);
87    /// const engine = new WasmEngine(workflows);
88    /// ```
89    #[wasm_bindgen(constructor)]
90    pub fn new(workflows_json: &str) -> Result<WasmEngine, String> {
91        let workflows_value: Value = serde_json::from_str(workflows_json)
92            .map_err(|e| format!("Invalid workflows JSON: {}", e))?;
93
94        let workflows_array = workflows_value
95            .as_array()
96            .ok_or_else(|| "Workflows must be a JSON array".to_string())?;
97
98        let mut workflows = Vec::with_capacity(workflows_array.len());
99        for (i, workflow_value) in workflows_array.iter().enumerate() {
100            let workflow_str = serde_json::to_string(workflow_value).map_err(|e| e.to_string())?;
101            let workflow = Workflow::from_json(&workflow_str)
102                .map_err(|e| format!("Invalid workflow at index {}: {}", i, e))?;
103            workflows.push(workflow);
104        }
105
106        let engine = Engine::new(workflows, None);
107        Ok(WasmEngine {
108            inner: Arc::new(engine),
109        })
110    }
111
112    /// Process a payload through the engine's workflows.
113    ///
114    /// This is an async operation that returns a Promise.
115    /// The payload is stored as a raw string and should be parsed by a parse plugin
116    /// in the workflow if JSON parsing is needed.
117    ///
118    /// # Arguments
119    /// * `payload` - Raw string payload to process (not parsed by the engine)
120    ///
121    /// # Returns
122    /// A Promise that resolves to the processed message as a JSON string
123    ///
124    /// # Example
125    /// ```javascript
126    /// const payload = '{"name": "John", "email": "john@example.com"}';
127    /// const result = await engine.process(payload);
128    /// const processed = JSON.parse(result);
129    /// console.log(processed.context.data);
130    /// ```
131    #[wasm_bindgen]
132    pub fn process(&self, payload: &str) -> js_sys::Promise {
133        // Store payload as a raw string - parsing is done by the parse plugin
134        let mut message = Message::from_value(&Value::String(payload.to_string()));
135
136        // Clone the Arc for the async block
137        let engine = Arc::clone(&self.inner);
138
139        future_to_promise(async move {
140            match engine.process_message(&mut message).await {
141                Ok(()) => serde_json::to_string(&message)
142                    .map(|s| JsValue::from_str(&s))
143                    .map_err(|e| JsValue::from_str(&e.to_string())),
144                Err(e) => Err(JsValue::from_str(&e.to_string())),
145            }
146        })
147    }
148
149    /// Process a payload with step-by-step execution tracing.
150    ///
151    /// This is an async operation that returns a Promise with the execution trace.
152    /// The trace contains message snapshots after each step, including which
153    /// workflows/tasks were executed or skipped.
154    /// The payload is stored as a raw string and should be parsed by a parse plugin.
155    ///
156    /// # Arguments
157    /// * `payload` - Raw string payload to process (not parsed by the engine)
158    ///
159    /// # Returns
160    /// A Promise that resolves to the execution trace as a JSON string
161    ///
162    /// # Example
163    /// ```javascript
164    /// const payload = '{"name": "John", "email": "john@example.com"}';
165    /// const trace = await engine.process_with_trace(payload);
166    /// const traceData = JSON.parse(trace);
167    /// console.log(traceData.steps); // Array of execution steps
168    /// ```
169    #[wasm_bindgen]
170    pub fn process_with_trace(&self, payload: &str) -> js_sys::Promise {
171        // Store payload as a raw string - parsing is done by the parse plugin
172        let mut message = Message::from_value(&Value::String(payload.to_string()));
173
174        // Clone the Arc for the async block
175        let engine = Arc::clone(&self.inner);
176
177        future_to_promise(async move {
178            match engine.process_message_with_trace(&mut message).await {
179                Ok(trace) => serde_json::to_string(&trace)
180                    .map(|s| JsValue::from_str(&s))
181                    .map_err(|e| JsValue::from_str(&e.to_string())),
182                Err(e) => Err(JsValue::from_str(&e.to_string())),
183            }
184        })
185    }
186
187    /// Get the number of workflows registered in the engine.
188    #[wasm_bindgen]
189    pub fn workflow_count(&self) -> usize {
190        self.inner.workflows().len()
191    }
192
193    /// Get the list of workflow IDs.
194    ///
195    /// # Returns
196    /// JSON array of workflow IDs as a string
197    #[wasm_bindgen]
198    pub fn workflow_ids(&self) -> String {
199        let ids: Vec<&String> = self.inner.workflows().keys().collect();
200        serde_json::to_string(&ids).unwrap_or_else(|_| "[]".to_string())
201    }
202}
203
204/// Process a payload through a one-off engine (convenience function).
205///
206/// Creates an engine with the given workflows and processes a single payload.
207/// Use WasmEngine class for better performance when processing multiple payloads.
208/// The payload is stored as a raw string and should be parsed by a parse plugin.
209///
210/// # Arguments
211/// * `workflows_json` - JSON string containing an array of workflow definitions
212/// * `payload` - Raw string payload to process (not parsed by the engine)
213///
214/// # Returns
215/// A Promise that resolves to the processed message as a JSON string
216///
217/// # Example
218/// ```javascript
219/// const payload = '{"name": "John", "email": "john@example.com"}';
220/// const result = await process_message(workflowsJson, payload);
221/// console.log(JSON.parse(result));
222/// ```
223#[wasm_bindgen]
224pub fn process_message(workflows_json: &str, payload: &str) -> js_sys::Promise {
225    let engine_result = WasmEngine::new(workflows_json);
226    match engine_result {
227        Ok(engine) => engine.process(payload),
228        Err(e) => future_to_promise(async move { Err(JsValue::from_str(&e)) }),
229    }
230}