Skip to main content

javascript_mel/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3
4#[cfg(any(
5    all(feature = "real", feature = "mock"),
6    not(any(feature = "real", feature = "mock"))
7))]
8compile_error!("One of the two features 'real' or 'mock' must be enabled");
9
10mod engine;
11
12use async_lock::Mutex;
13use engine::Engine;
14use json_mel::*;
15use melodium_core::*;
16use melodium_macro::{check, mel_model, mel_package, mel_treatment};
17use std::sync::{Arc, Weak};
18
19#[derive(Debug)]
20/// Provides a JavaScript/ECMAScript execution engine.
21///
22/// The engine is initialised with `code`, which may define functions, set up variables, or
23/// perform any one-time setup useful for later processing. Use the `process` treatment to run
24/// JavaScript against data at track time.
25///
26/// - `stack_size_limit`: maximum stack size available to JavaScript code, in bytes.
27/// - `recursion_limit`: maximum call-stack depth.
28/// - `loop_iteration_limit`: maximum iterations any single loop may perform.
29/// - `strict`: enables strict mode; can also be enabled per-script with `"use strict"`.
30/// - `code`: JavaScript source loaded once at engine initialisation.
31#[mel_model(
32    param stack_size_limit u64 1024
33    param recursion_limit u64 400
34    param loop_iteration_limit u64 4294967295
35    param strict bool false
36    param {content(javascript)} code string ""
37    initialize initialize
38    shutdown shutdown
39)]
40pub struct JavaScriptEngine {
41    model: Weak<JavaScriptEngineModel>,
42    engine: Mutex<Option<Engine>>,
43}
44
45impl JavaScriptEngine {
46    fn new(model: Weak<JavaScriptEngineModel>) -> Self {
47        Self {
48            model,
49            engine: Mutex::new(None),
50        }
51    }
52
53    fn initialize(&self) {
54        let model = self.model.upgrade().unwrap();
55
56        let engine = Engine::new(
57            model.get_stack_size_limit(),
58            model.get_recursion_limit(),
59            model.get_loop_iteration_limit(),
60            model.get_strict(),
61            model.get_code(),
62        );
63
64        *futures_lite::future::block_on(model.inner().engine.lock()) = Some(engine);
65    }
66
67    fn shutdown(&self) {
68        let model = self.model.upgrade().unwrap();
69        let engine = futures_lite::future::block_on(model.inner().engine.lock());
70        if let Some(engine) = engine.as_ref() {
71            engine.stop();
72        }
73    }
74
75    pub async fn process(
76        &self,
77        value: serde_json::Value,
78        code: String,
79    ) -> Result<Result<serde_json::Value, String>, ()> {
80        if let Some(engine) = self.engine.lock().await.as_ref() {
81            let res = engine.process(value, code).await;
82            res
83        } else {
84            Err(())
85        }
86    }
87
88    pub(crate) fn engine(&self) -> &Mutex<Option<Engine>> {
89        &self.engine
90    }
91}
92
93/// Executes JavaScript code on values.
94///
95/// For every incoming `value`, `code` is executed as-is within `engine`.
96/// Inside the `code` part the incoming value is reffered as globally-accessible `value` variable.
97/// `code` can return any JS object convertible into JSON data.
98///
99/// If `code` not actually processable JavaScript code, or its return value not convertible into JSON, a none `result` value is send.
100#[mel_treatment(
101    model engine JavaScriptEngine
102    input value Stream<Json>
103    output result Stream<Option<Json>>
104)]
105pub async fn process(#[mel(content(javascript))] code: string) {
106    let engine = JavaScriptEngineModel::into(engine);
107
108    'main: while let Ok(values) = value
109        .recv_many()
110        .await
111        .map(|values| Into::<Vec<Value>>::into(values))
112    {
113        for value in values {
114            if let Value::Data(value) = value {
115                match value.downcast_arc::<Json>() {
116                    Ok(value) => {
117                        let processed;
118                        if let Some(engine) = engine.inner().engine().lock().await.as_ref() {
119                            processed = engine.process(value.0.clone(), code.clone()).await;
120                        } else {
121                            break;
122                        }
123
124                        match processed {
125                            Ok(Ok(value)) => {
126                                check!(
127                                    result
128                                        .send_one_as(Some(Arc::new(Json(value)) as Arc<dyn Data>))
129                                        .await
130                                );
131                            }
132                            Ok(Err(_err)) => {
133                                check!(result.send_one_as(Option::<Arc<dyn Data>>::None).await);
134                            }
135                            Err(_) => {
136                                break 'main;
137                            }
138                        }
139                    }
140                    Err(_) => break 'main,
141                }
142            }
143        }
144    }
145}
146
147mel_package!();