mod module;
mod worker;
#[cfg(feature = "transpile")]
pub mod transpile;
use std::path::Path;
use std::sync::Arc;
use abi_stable::sabi_trait::TD_Opaque;
use abi_stable::std_types::{RResult, RStr, RString};
use apiplant_abi::{BoxedFunction, Function, FunctionManifest, HostApi_TO, LogLevel};
use crossbeam_channel::{bounded, Sender};
use serde_json::Value;
use worker::{Job, Message};
pub const EXTENSION: &str = "js";
fn workers() -> usize {
std::env::var("APIPLANT_JS_WORKERS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|n| *n > 0)
.unwrap_or(2)
}
struct Pool {
jobs: Sender<Job>,
label: String,
}
impl Pool {
fn load(label: &str, code: &str) -> Result<(Pool, String), String> {
let (jobs, incoming) = bounded::<Job>(1024);
let mut manifest = None;
for worker in 0..workers() {
let declared = worker::spawn(
format!("{label}#{worker}"),
code.to_string(),
incoming.clone(),
)?;
manifest = manifest.or(declared);
}
let manifest = manifest.ok_or_else(|| {
"the module exports no `manifest`; add \
`export const manifest = [{ name: \"…\", permission: \"…\" }]`"
.to_string()
})?;
Ok((
Pool {
jobs,
label: label.to_string(),
},
manifest,
))
}
fn invoke(
&self,
name: &str,
input: &str,
host: &HostApi_TO<'_, abi_stable::std_types::RBox<()>>,
) -> Result<String, String> {
let (replies, incoming) = bounded::<Message>(1);
let job = Job {
name: name.to_string(),
input: input.to_string(),
replies,
};
if self.jobs.try_send(job).is_err() {
return Err(format!(
"{}javascript function `{name}` is overloaded; try again",
apiplant_abi::INTERNAL_ERROR_PREFIX
));
}
loop {
match incoming.recv() {
Ok(Message::Host {
kind,
payload,
answer,
}) => {
let _ = answer.send(serve(host, &kind, &payload));
}
Ok(Message::Done(result)) => return result,
Err(_) => {
tracing::error!(library = %self.label, function = %name, "javascript worker died");
return Err(format!(
"{}the javascript worker died",
apiplant_abi::INTERNAL_ERROR_PREFIX
));
}
}
}
}
}
fn serve(
host: &HostApi_TO<'_, abi_stable::std_types::RBox<()>>,
kind: &str,
payload: &str,
) -> String {
let in_band = |result: RResult<RString, RString>| match result {
RResult::ROk(reply) => reply.into_string(),
RResult::RErr(e) => serde_json::json!({ "error": e.as_str() }).to_string(),
};
match kind {
"query" => in_band(host.query(RStr::from_str(payload))),
"send_email" => in_band(host.send_email(RStr::from_str(payload))),
"cache" => in_band(host.cache(RStr::from_str(payload))),
"payments" => in_band(host.payments(RStr::from_str(payload))),
"ai" => in_band(host.ai(RStr::from_str(payload))),
"emit" => {
let chunk: String = serde_json::from_str(payload).unwrap_or_default();
serde_json::json!({ "delivered": host.emit(RStr::from_str(&chunk)) }).to_string()
}
"config" => host.config().into_string(),
"principal_id" => host.principal_id().into_string(),
"hook" => host.hook().into_string(),
"log" => {
let entry: Value = serde_json::from_str(payload).unwrap_or(Value::Null);
let message = entry.get("message").and_then(Value::as_str).unwrap_or("");
let level = match entry.get("level").and_then(Value::as_str) {
Some("trace") => LogLevel::Trace,
Some("debug") => LogLevel::Debug,
Some("warn") => LogLevel::Warn,
Some("error") => LogLevel::Error,
_ => LogLevel::Info,
};
host.log(level, RStr::from_str(message));
String::new()
}
other => serde_json::json!({ "error": format!("unknown host call `{other}`") }).to_string(),
}
}
struct JsFunction {
manifest: FunctionManifest,
pool: Arc<Pool>,
}
impl Function for JsFunction {
fn manifest(&self) -> FunctionManifest {
self.manifest.clone()
}
fn invoke(
&self,
host: HostApi_TO<'_, abi_stable::std_types::RBox<()>>,
input: RStr<'_>,
) -> RResult<RString, RString> {
let called = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.pool
.invoke(self.manifest.name.as_str(), input.as_str(), &host)
}));
match called {
Ok(Ok(output)) => RResult::ROk(output.into()),
Ok(Err(e)) => RResult::RErr(e.into()),
Err(_) => RResult::RErr(
format!(
"{}panic while invoking a javascript function",
apiplant_abi::INTERNAL_ERROR_PREFIX
)
.into(),
),
}
}
}
pub fn load(path: &Path) -> Result<Vec<BoxedFunction>, String> {
let code = std::fs::read_to_string(path).map_err(|e| format!("cannot read: {e}"))?;
let label = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "function".to_string());
let (pool, manifest) = Pool::load(&label, &code)?;
let pool = Arc::new(pool);
let entries: Vec<Value> = serde_json::from_str(&manifest)
.map_err(|e| format!("`manifest` is not valid JSON: {e}"))?;
if entries.is_empty() {
return Err("`manifest` is empty; it must describe at least one function".to_string());
}
let mut functions = Vec::with_capacity(entries.len());
for entry in &entries {
let manifest = apiplant_abi::manifest_from_json(entry)?;
functions.push(BoxedFunction::from_value(
JsFunction {
manifest,
pool: pool.clone(),
},
TD_Opaque,
));
}
Ok(functions)
}