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}