added more support for supabase-js
This commit is contained in:
23
functions/Cargo.toml
Normal file
23
functions/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "functions"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
wasmtime = "18.0.1"
|
||||
wasmtime-wasi = "18.0.1"
|
||||
wasi-common = "18.0.1"
|
||||
axum.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
common.workspace = true
|
||||
sqlx.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
chrono.workspace = true
|
||||
base64 = "0.22"
|
||||
uuid.workspace = true
|
||||
deno_core = "0.272.0"
|
||||
|
||||
189
functions/src/deno_runtime.rs
Normal file
189
functions/src/deno_runtime.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use anyhow::Result;
|
||||
use deno_core::{JsRuntime, RuntimeOptions, v8};
|
||||
use serde_json::Value;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct DenoRuntime {
|
||||
// We create a new runtime for each execution to ensure isolation
|
||||
// In a production environment, we might want to pool runtimes or use isolates more efficiently
|
||||
}
|
||||
|
||||
impl DenoRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub async fn execute(&self, code: String, payload: Option<Value>, headers: HashMap<String, String>) -> Result<(String, String, u16, HashMap<String, String>)> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
let result = Self::execute_inner(code, payload, headers).await;
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
});
|
||||
|
||||
rx.await.map_err(|_| anyhow::anyhow!("Deno execution thread panicked"))?
|
||||
}
|
||||
|
||||
async fn execute_inner(code: String, payload: Option<Value>, headers: HashMap<String, String>) -> Result<(String, String, u16, HashMap<String, String>)> {
|
||||
// Initialize JS Runtime
|
||||
let mut runtime = JsRuntime::new(RuntimeOptions::default());
|
||||
|
||||
// 1. Inject Preamble (Polyfills for Deno.serve, Request, Response, Headers)
|
||||
let preamble = r#"
|
||||
globalThis.console = {
|
||||
log: (...args) => {
|
||||
Deno.core.print(args.map(a => String(a)).join(" ") + "\n");
|
||||
},
|
||||
error: (...args) => {
|
||||
Deno.core.print("[ERROR] " + args.map(a => String(a)).join(" ") + "\n", true);
|
||||
}
|
||||
};
|
||||
|
||||
class Headers {
|
||||
constructor(init) {
|
||||
this.map = new Map();
|
||||
if (init) {
|
||||
if (init instanceof Headers) {
|
||||
init.forEach((v, k) => this.map.set(k.toLowerCase(), v));
|
||||
} else if (Array.isArray(init)) {
|
||||
init.forEach(([k, v]) => this.map.set(k.toLowerCase(), v));
|
||||
} else {
|
||||
Object.entries(init).forEach(([k, v]) => this.map.set(k.toLowerCase(), v));
|
||||
}
|
||||
}
|
||||
}
|
||||
get(key) { return this.map.get(key.toLowerCase()) || null; }
|
||||
set(key, value) { this.map.set(key.toLowerCase(), value); }
|
||||
has(key) { return this.map.has(key.toLowerCase()); }
|
||||
forEach(callback) { this.map.forEach(callback); }
|
||||
entries() { return this.map.entries(); }
|
||||
}
|
||||
globalThis.Headers = Headers;
|
||||
|
||||
globalThis.Deno = {
|
||||
serve: (handler) => {
|
||||
globalThis._handler = handler;
|
||||
},
|
||||
core: Deno.core,
|
||||
env: {
|
||||
get: (key) => {
|
||||
return globalThis._env ? globalThis._env[key] : null;
|
||||
},
|
||||
toObject: () => {
|
||||
return globalThis._env || {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class Response {
|
||||
constructor(body, init) {
|
||||
this.body = body;
|
||||
this.status = init?.status || 200;
|
||||
this.headers = new Headers(init?.headers);
|
||||
}
|
||||
async text() { return String(this.body); }
|
||||
async json() { return JSON.parse(this.body); }
|
||||
}
|
||||
globalThis.Response = Response;
|
||||
|
||||
class Request {
|
||||
constructor(url, init) {
|
||||
this.url = url;
|
||||
this.method = init?.method || "GET";
|
||||
this._body = init?.body;
|
||||
this.headers = new Headers(init?.headers);
|
||||
}
|
||||
async json() { return typeof this._body === 'string' ? JSON.parse(this._body) : this._body; }
|
||||
async text() { return typeof this._body === 'string' ? this._body : JSON.stringify(this._body); }
|
||||
}
|
||||
globalThis.Request = Request;
|
||||
"#;
|
||||
|
||||
runtime.execute_script("<preamble>", preamble.to_string())?;
|
||||
|
||||
// 2. Execute User Code
|
||||
runtime.execute_script("<user_script>", code.to_string())?;
|
||||
|
||||
// 3. Invoke Handler
|
||||
let payload_json = serde_json::to_string(&payload.unwrap_or(serde_json::json!({})))?;
|
||||
let headers_json = serde_json::to_string(&headers)?;
|
||||
|
||||
let invoke_script = format!(r#"
|
||||
(async () => {{
|
||||
if (!globalThis._handler) {{
|
||||
return {{ error: "No handler registered via Deno.serve" }};
|
||||
}}
|
||||
try {{
|
||||
const headers = {1};
|
||||
const req = new Request("http://localhost", {{
|
||||
method: "POST",
|
||||
body: {0},
|
||||
headers: headers
|
||||
}});
|
||||
const res = await globalThis._handler(req);
|
||||
const text = await res.text();
|
||||
|
||||
// Convert Headers to plain object for return
|
||||
const resHeaders = {{}};
|
||||
if (res.headers && typeof res.headers.forEach === 'function') {{
|
||||
res.headers.forEach((v, k) => resHeaders[k] = v);
|
||||
}}
|
||||
|
||||
return {{
|
||||
result: text,
|
||||
headers: resHeaders,
|
||||
status: res.status
|
||||
}};
|
||||
}} catch (e) {{
|
||||
return {{ error: String(e) }};
|
||||
}}
|
||||
}})()
|
||||
"#, payload_json, headers_json);
|
||||
|
||||
let result_val = runtime.execute_script("<invocation>", invoke_script)?;
|
||||
let result = runtime.resolve_value(result_val).await?;
|
||||
|
||||
let scope = &mut runtime.handle_scope();
|
||||
let local = v8::Local::new(scope, result);
|
||||
let deserialized_value: Value = deno_core::serde_v8::from_v8(scope, local)?;
|
||||
|
||||
let stdout = if let Some(res) = deserialized_value.get("result") {
|
||||
res.as_str().unwrap_or("").to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let stderr = if let Some(err) = deserialized_value.get("error") {
|
||||
err.as_str().unwrap_or("Unknown error").to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let status = if let Some(s) = deserialized_value.get("status") {
|
||||
s.as_u64().unwrap_or(200) as u16
|
||||
} else {
|
||||
200
|
||||
};
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
if let Some(h) = deserialized_value.get("headers") {
|
||||
if let Some(obj) = h.as_object() {
|
||||
for (k, v) in obj {
|
||||
if let Some(s) = v.as_str() {
|
||||
headers.insert(k.clone(), s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((stdout, stderr, status, headers))
|
||||
}
|
||||
}
|
||||
122
functions/src/handlers.rs
Normal file
122
functions/src/handlers.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{StatusCode, HeaderMap},
|
||||
response::{IntoResponse, Json},
|
||||
Extension,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use sqlx::PgPool;
|
||||
use base64::prelude::*;
|
||||
use crate::{FunctionsState, models::{DeployRequest, InvokeRequest, InvokeResponse, Function}};
|
||||
|
||||
pub async fn invoke_function(
|
||||
State(state): State<FunctionsState>,
|
||||
db: Option<Extension<PgPool>>,
|
||||
Path(name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<InvokeRequest>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!("Invoking function: {}", name);
|
||||
let db = db.map(|Extension(p)| p).unwrap_or_else(|| state.db.clone());
|
||||
|
||||
// Convert headers
|
||||
let mut header_map = HashMap::new();
|
||||
for (k, v) in headers.iter() {
|
||||
if let Ok(val) = v.to_str() {
|
||||
header_map.insert(k.as_str().to_string(), val.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Fetch function
|
||||
let func = sqlx::query_as::<_, Function>("SELECT * FROM functions.functions WHERE name = $1")
|
||||
.bind(&name)
|
||||
.fetch_optional(&db)
|
||||
.await;
|
||||
|
||||
let func = match func {
|
||||
Ok(Some(f)) => f,
|
||||
Ok(None) => {
|
||||
tracing::warn!("Function not found: {}", name);
|
||||
return (StatusCode::NOT_FOUND, "Function not found").into_response();
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("DB error fetching function: {}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Execute
|
||||
let result = if func.runtime == "deno" || func.runtime == "typescript" || func.runtime == "javascript" {
|
||||
let code = match String::from_utf8(func.code) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!("Invalid UTF-8 in Deno function code: {}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid function code".to_string()).into_response();
|
||||
}
|
||||
};
|
||||
state.deno_runtime.execute(code, payload.payload, header_map).await
|
||||
} else {
|
||||
// Assume WASM
|
||||
let payload_str = payload.payload.as_ref().map(|v| v.to_string());
|
||||
state.runtime.execute(&func.code, payload_str).await.map(|(out, err)| (out, err, 200, HashMap::new()))
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok((stdout, stderr, status, headers)) => {
|
||||
tracing::info!("Function executed successfully. Stdout len: {}, Stderr len: {}", stdout.len(), stderr.len());
|
||||
let resp = InvokeResponse {
|
||||
result: Some(stdout),
|
||||
error: if stderr.is_empty() { None } else { Some(stderr) },
|
||||
logs: vec![],
|
||||
status,
|
||||
headers: Some(headers),
|
||||
};
|
||||
Json(resp).into_response()
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Runtime execution error: {:?}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Runtime error: {:?}", e)).into_response()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn deploy_function(
|
||||
State(state): State<FunctionsState>,
|
||||
db: Option<Extension<PgPool>>,
|
||||
Json(payload): Json<DeployRequest>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!("Deploying function: {}", payload.name);
|
||||
let db = db.map(|Extension(p)| p).unwrap_or_else(|| state.db.clone());
|
||||
|
||||
// Decode base64
|
||||
let code = match BASE64_STANDARD.decode(&payload.code_base64) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!("Invalid base64: {}", e);
|
||||
return (StatusCode::BAD_REQUEST, format!("Invalid base64: {}", e)).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Store in DB
|
||||
let runtime = payload.runtime.unwrap_or("wasm".to_string());
|
||||
|
||||
let res = sqlx::query(
|
||||
"INSERT INTO functions.functions (name, code, runtime) VALUES ($1, $2, $3) ON CONFLICT (name) DO UPDATE SET code = $2, runtime = $3, updated_at = NOW() RETURNING id"
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&code)
|
||||
.bind(&runtime)
|
||||
.fetch_one(&db)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
tracing::info!("Function deployed successfully");
|
||||
(StatusCode::OK, "Function deployed").into_response()
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("DB error deploying function: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
},
|
||||
}
|
||||
}
|
||||
29
functions/src/lib.rs
Normal file
29
functions/src/lib.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use axum::{
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use common::Config;
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use runtime::WasmRuntime;
|
||||
use deno_runtime::DenoRuntime;
|
||||
|
||||
pub mod handlers;
|
||||
pub mod runtime;
|
||||
pub mod deno_runtime;
|
||||
pub mod models;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FunctionsState {
|
||||
pub db: PgPool,
|
||||
pub config: Config,
|
||||
pub runtime: Arc<WasmRuntime>,
|
||||
pub deno_runtime: Arc<DenoRuntime>,
|
||||
}
|
||||
|
||||
pub fn router(state: FunctionsState) -> Router {
|
||||
Router::new()
|
||||
.route("/:name", post(handlers::invoke_function))
|
||||
.route("/", post(handlers::deploy_function))
|
||||
.with_state(state)
|
||||
}
|
||||
35
functions/src/models.rs
Normal file
35
functions/src/models.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Function {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub code: Vec<u8>,
|
||||
pub runtime: String, // "wasm" or "deno"
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InvokeRequest {
|
||||
pub payload: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct InvokeResponse {
|
||||
pub result: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub logs: Vec<String>,
|
||||
pub status: u16,
|
||||
pub headers: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeployRequest {
|
||||
pub name: String,
|
||||
pub code_base64: String,
|
||||
pub runtime: Option<String>,
|
||||
}
|
||||
|
||||
85
functions/src/runtime.rs
Normal file
85
functions/src/runtime.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use anyhow::Result;
|
||||
use wasmtime::{Config, Engine, Linker, Module, Store};
|
||||
use wasmtime_wasi::WasiCtxBuilder;
|
||||
use wasi_common::WasiCtx;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WasmRuntime {
|
||||
engine: Engine,
|
||||
}
|
||||
|
||||
struct WasiState {
|
||||
ctx: WasiCtx,
|
||||
}
|
||||
|
||||
impl WasmRuntime {
|
||||
pub fn new() -> Result<Self> {
|
||||
let mut config = Config::new();
|
||||
config.async_support(true); // Enable async
|
||||
config.epoch_interruption(true); // Allow timeouts
|
||||
let engine = Engine::new(&config).map_err(|e| anyhow::anyhow!(e))?;
|
||||
Ok(Self { engine })
|
||||
}
|
||||
|
||||
pub async fn execute(&self, wasm: &[u8], payload: Option<String>) -> Result<(String, String)> {
|
||||
let start = std::time::Instant::now();
|
||||
let payload_size = payload.as_ref().map(|s| s.len()).unwrap_or(0);
|
||||
|
||||
let module = Module::new(&self.engine, wasm).map_err(|e| anyhow::anyhow!(e).context("Failed to compile WASM module"))?;
|
||||
|
||||
// Setup WASI
|
||||
let stdout = wasi_common::pipe::WritePipe::new_in_memory();
|
||||
let stderr = wasi_common::pipe::WritePipe::new_in_memory();
|
||||
|
||||
let mut builder = WasiCtxBuilder::new();
|
||||
builder
|
||||
.stdout(Box::new(stdout.clone()))
|
||||
.stderr(Box::new(stderr.clone()));
|
||||
|
||||
if let Some(p) = payload {
|
||||
builder.env("PAYLOAD", &p).map_err(|e| anyhow::anyhow!(e))?;
|
||||
}
|
||||
|
||||
let wasi = builder.build();
|
||||
|
||||
let mut store = Store::new(&self.engine, WasiState {
|
||||
ctx: wasi,
|
||||
});
|
||||
|
||||
store.set_epoch_deadline(1);
|
||||
|
||||
let mut linker = Linker::new(&self.engine);
|
||||
wasmtime_wasi::add_to_linker(&mut linker, |s: &mut WasiState| &mut s.ctx)
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
let instance = linker.instantiate_async(&mut store, &module).await
|
||||
.map_err(|e| anyhow::anyhow!(e).context("Failed to instantiate module"))?;
|
||||
|
||||
let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")
|
||||
.map_err(|e| anyhow::anyhow!(e).context("Failed to find _start function"))?;
|
||||
|
||||
start_func.call_async(&mut store, ()).await
|
||||
.map_err(|e| anyhow::anyhow!(e).context("Failed to execute function"))?;
|
||||
|
||||
// Drop store to release references to pipes
|
||||
drop(store);
|
||||
|
||||
// Capture output
|
||||
let out = stdout.try_into_inner().map_err(|_| anyhow::anyhow!("Failed to get stdout")).unwrap().into_inner();
|
||||
let err = stderr.try_into_inner().map_err(|_| anyhow::anyhow!("Failed to get stderr")).unwrap().into_inner();
|
||||
|
||||
let stdout_str = String::from_utf8_lossy(&out).to_string();
|
||||
let stderr_str = String::from_utf8_lossy(&err).to_string();
|
||||
|
||||
let duration = start.elapsed();
|
||||
tracing::info!(
|
||||
target: "function_metrics",
|
||||
execution_time_ms = duration.as_millis(),
|
||||
payload_size_bytes = payload_size,
|
||||
success = true,
|
||||
"Function executed successfully"
|
||||
);
|
||||
|
||||
Ok((stdout_str, stderr_str))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user