Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "galaxy_js"
|
||||
authors = ["Warp Team <dev@warp.dev>"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bincode.workspace = true
|
||||
cfg-if.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
rquickjs.workspace = true
|
||||
|
||||
[features]
|
||||
test-util = []
|
||||
@@ -0,0 +1,6 @@
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(target_family = "wasm"))] {
|
||||
mod native;
|
||||
pub use native::{IntoWarpJs, FromWarpJs, util};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Traits for converting Rust values into `rquickjs`-compatible values on native (non-wasm)
|
||||
//! platforms.
|
||||
pub mod util;
|
||||
|
||||
use rquickjs::{Ctx, FromJs, IntoJs, Value};
|
||||
|
||||
use crate::JsFunctionRegistry;
|
||||
|
||||
/// Trait to be implemented for converting JS values into Rust values.
|
||||
///
|
||||
/// This is similar to `rquickjs`'s native `FromJs` trait, except it enables registering JS
|
||||
/// functions in the given `JsFunctionRegistry` so these functions can be called arbitrarily.
|
||||
pub trait FromWarpJs<'js>: Sized {
|
||||
fn from_galaxy_js(
|
||||
ctx: Ctx<'js>,
|
||||
value: Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Self>;
|
||||
}
|
||||
|
||||
/// Trait to be implemented for converting Rust values into JS values.
|
||||
///
|
||||
/// This is a wrapper trait over `rquickjs`'s `IntoJs` trait, which is required due to a
|
||||
/// shortcoming of the Rust compiler that's surfaced by attempting to provide a blanket
|
||||
/// implementation of `super::js_function::CallableJsFunction` over all `T` that implement
|
||||
/// `IntoJs`. `rquickjs` contains recursive blanket implementations of `IntoJs` for generic types
|
||||
/// like `Vec`, which causes issues when the Rust compiler attempts to generate `CallableJsFunction`
|
||||
/// for monomorphized `TypedJsFunction`s (which have generic type params).
|
||||
pub trait IntoWarpJs<'js>: Sized {
|
||||
fn into_galaxy_js(self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>>;
|
||||
}
|
||||
|
||||
impl<'js, T> FromWarpJs<'js> for Vec<T>
|
||||
where
|
||||
T: FromWarpJs<'js>,
|
||||
{
|
||||
fn from_galaxy_js(
|
||||
ctx: Ctx<'js>,
|
||||
value: Value<'js>,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<Vec<T>> {
|
||||
if value.is_array() {
|
||||
let values = value.get::<Vec<Value>>()?;
|
||||
Ok(values
|
||||
.into_iter()
|
||||
.flat_map(|value| T::from_galaxy_js(ctx, value, js_function_registry).ok())
|
||||
.collect())
|
||||
} else {
|
||||
Err(rquickjs::Error::FromJs {
|
||||
from: "object",
|
||||
to: "Vec<T>",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for String {
|
||||
fn from_galaxy_js(
|
||||
ctx: Ctx<'js>,
|
||||
value: Value<'js>,
|
||||
_js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<String> {
|
||||
String::from_js(ctx, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for bool {
|
||||
fn from_galaxy_js(
|
||||
ctx: Ctx<'js>,
|
||||
value: Value<'js>,
|
||||
_js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<bool> {
|
||||
bool::from_js(ctx, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> FromWarpJs<'js> for i32 {
|
||||
fn from_galaxy_js(
|
||||
ctx: Ctx<'js>,
|
||||
value: Value<'js>,
|
||||
_js_function_registry: &mut JsFunctionRegistry,
|
||||
) -> rquickjs::Result<i32> {
|
||||
i32::from_js(ctx, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> IntoWarpJs<'js> for String {
|
||||
fn into_galaxy_js(self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
|
||||
self.into_js(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> IntoWarpJs<'js> for Vec<String> {
|
||||
fn into_galaxy_js(self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
|
||||
self.into_js(ctx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! This module contains utilities for extracting properties from JS objects in Rust.
|
||||
//!
|
||||
//! Where possible, these utilities should be used rather than the rquickjs APIs directly to ensure
|
||||
//! JS objects are converted into Rust with consistent semantics.
|
||||
use rquickjs::{Ctx, Object, Result, Value};
|
||||
|
||||
use super::{FromWarpJs, JsFunctionRegistry};
|
||||
|
||||
/// Returns the value for an optional property that maybe set to either a single `T` or an array of
|
||||
/// `T`'s in Javascript.
|
||||
///
|
||||
/// This is different from calling get_optional::Vec<String>() because it handles a values of type
|
||||
/// `T` in addition to `T[]`.
|
||||
///
|
||||
/// For example, calling get_one_or_more_optional::<String>(object, "foo"):
|
||||
///
|
||||
/// { "foo": "bar" } -> Returns Ok(["bar"])
|
||||
/// { "foo": ["bar", "baz"] } -> Returns Ok(["bar", "baz"])
|
||||
/// {} -> Returns Ok([])
|
||||
pub fn get_one_or_more_optional<'js, T>(
|
||||
object: &Object<'js>,
|
||||
field_name: &str,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
ctx: Ctx<'js>,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
T: FromWarpJs<'js>,
|
||||
{
|
||||
if object.contains_key(field_name)? {
|
||||
get_one_or_more_required(object, field_name, js_function_registry, ctx)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value for a required property that maybe set to either a single `T` _or_ an array of
|
||||
/// `T`'s in Javascript.
|
||||
///
|
||||
/// This is different from calling get_required::Vec<String>() because it handles a values of type
|
||||
/// `T` in addition to `T[]`.
|
||||
///
|
||||
/// For example, calling get_one_or_more_required::<String>(object, "foo"):
|
||||
///
|
||||
/// { "foo": "bar" } -> Returns Ok(["bar"])
|
||||
/// { "foo": ["bar", "baz"] } -> Returns Ok(["bar", "baz"])
|
||||
/// {} -> Returns Err([])
|
||||
pub fn get_one_or_more_required<'js, T>(
|
||||
object: &Object<'js>,
|
||||
field_name: &str,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
ctx: Ctx<'js>,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
T: FromWarpJs<'js>,
|
||||
{
|
||||
let value: Value = object.get(field_name)?;
|
||||
if value.is_array() {
|
||||
Vec::<T>::from_galaxy_js(ctx, value, js_function_registry)
|
||||
} else {
|
||||
Ok(vec![T::from_galaxy_js(ctx, value, js_function_registry)?])
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value for a required property of type `T`.
|
||||
///
|
||||
/// For example, calling get_required::<String>(object, "foo"):
|
||||
///
|
||||
/// { "foo": "bar" } -> Returns Ok(["bar"])
|
||||
/// { "foo": 13 } -> Returns Err(..)
|
||||
/// {} -> Returns Err(..)
|
||||
pub fn get_required<'js, T>(
|
||||
object: &Object<'js>,
|
||||
field_name: &str,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
ctx: Ctx<'js>,
|
||||
) -> Result<T>
|
||||
where
|
||||
T: FromWarpJs<'js>,
|
||||
{
|
||||
let value = object.get(field_name)?;
|
||||
T::from_galaxy_js(ctx, value, js_function_registry)
|
||||
}
|
||||
|
||||
/// Returns the value for an optional property of type `T`.
|
||||
///
|
||||
/// For example, calling get_optional::<String>(object, "foo"):
|
||||
///
|
||||
/// { "foo": "bar" } -> Returns Ok(Some("bar"))
|
||||
/// { "foo": 13 } -> Returns Err(..)
|
||||
/// {} -> Returns Ok(None)
|
||||
pub fn get_optional<'js, T>(
|
||||
object: &Object<'js>,
|
||||
field_name: &str,
|
||||
js_function_registry: &mut JsFunctionRegistry,
|
||||
ctx: Ctx<'js>,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
T: FromWarpJs<'js>,
|
||||
{
|
||||
if object.contains_key(field_name)? {
|
||||
Ok(Some(get_required::<T>(
|
||||
object,
|
||||
field_name,
|
||||
js_function_registry,
|
||||
ctx,
|
||||
)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//! This module contains abstractions for registering and calling plugin JS functions.
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(target_family = "wasm"))] {
|
||||
mod native;
|
||||
pub use native::*;
|
||||
}
|
||||
}
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Struct to pass serialized function input and output, encapsulating the details of the actual
|
||||
/// serialization.
|
||||
///
|
||||
/// This is required to pass function inputs/outputs across process boundaries, as is done to call
|
||||
/// JS functions from the rust app process to be executed by the plugin host process.
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedJsValue(Vec<u8>);
|
||||
|
||||
impl SerializedJsValue {
|
||||
pub fn from_value<T: Serialize>(input: T) -> Result<Self, bincode::Error> {
|
||||
Ok(Self(bincode::serialize::<T>(&input)?))
|
||||
}
|
||||
|
||||
pub fn to_value<T: DeserializeOwned>(&self) -> Result<T, bincode::Error> {
|
||||
bincode::deserialize::<T>(&self.0[..])
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique "ref" to a registered JS function parameterized by the function's input and output
|
||||
/// types.
|
||||
///
|
||||
/// `I` is the type of the function's input, which must implement `IntoWarpJs` and be
|
||||
/// deserializable.
|
||||
/// `O` is the type of the function's return value, which must implement `FromWarpJs` and be
|
||||
/// serializable.
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct TypedJsFunctionRef<I, O> {
|
||||
pub id: JsFunctionId,
|
||||
_input_marker: PhantomData<I>,
|
||||
_output_marker: PhantomData<O>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-util")]
|
||||
impl<I, O> TypedJsFunctionRef<I, O> {
|
||||
pub fn new_for_test() -> Self {
|
||||
Self {
|
||||
id: JsFunctionId::new(),
|
||||
_input_marker: PhantomData,
|
||||
_output_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique ID for a plugin function defined in JS.
|
||||
///
|
||||
/// This is expected to be unique across every JS function across all registered plugins.
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct JsFunctionId(Uuid);
|
||||
|
||||
impl JsFunctionId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for JsFunctionId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
use std::{collections::HashMap, marker::PhantomData, sync::Arc};
|
||||
|
||||
use rquickjs::{Ctx, Function, Persistent};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use crate::{FromWarpJs, IntoWarpJs, JsFunctionId, SerializedJsValue, TypedJsFunctionRef};
|
||||
|
||||
impl<I, O> TypedJsFunctionRef<I, O>
|
||||
where
|
||||
I: for<'a> IntoWarpJs<'a> + DeserializeOwned + Clone + 'static,
|
||||
O: for<'a> FromWarpJs<'a> + Serialize + Clone + 'static,
|
||||
{
|
||||
fn new(id: JsFunctionId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
_input_marker: PhantomData,
|
||||
_output_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread-local registry for JS plugin functions.
|
||||
///
|
||||
/// This exposes methods to register/persist a JS function (e.g. an `rquickjs::Function`) and
|
||||
/// retrieve a corresponding `CallableJsFunction` given a `JsFunctionId`.
|
||||
///
|
||||
/// Internally, functions are wrapped with `TypedJsFunction<I, O>`, which preserves type
|
||||
/// information for the function's input and output. `TypedJsFunction`s are referred to via the
|
||||
/// `CallableJsFunction` trait, which makes it possible to store a collection of polymorphic
|
||||
/// `TypedJsFunction`s in a single collection.
|
||||
#[derive(Default)]
|
||||
pub struct JsFunctionRegistry {
|
||||
function_map: HashMap<JsFunctionId, Arc<dyn CallableJsFunction>>,
|
||||
on_registered_js_function_callback: Option<Box<dyn Fn(JsFunctionId)>>,
|
||||
}
|
||||
|
||||
impl JsFunctionRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn on_registered_js_function(mut self, callback: impl Fn(JsFunctionId) + 'static) -> Self {
|
||||
self.on_registered_js_function_callback = Some(Box::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers the given function and returns its a `TypedJsFunctionRef` that may be used to
|
||||
/// call the function.
|
||||
///
|
||||
/// This must be called with specified type parameters, where `I` is the type of the function's
|
||||
/// input and `O` is the type of the functions output.
|
||||
///
|
||||
/// `I` and `O` must have corresponding `IntoWarpJs`/`FromWarpJs` implementations so they
|
||||
/// can be converted to/from JavaScript values.
|
||||
pub fn register_js_function<'js, I, O>(
|
||||
&mut self,
|
||||
js_function: Function<'js>,
|
||||
ctx: Ctx<'js>,
|
||||
) -> TypedJsFunctionRef<I, O>
|
||||
where
|
||||
I: for<'a> IntoWarpJs<'a> + DeserializeOwned + Clone + 'static,
|
||||
O: for<'a> FromWarpJs<'a> + Serialize + Clone + 'static,
|
||||
{
|
||||
let persisted_function = Persistent::save(ctx, js_function);
|
||||
let function_id: JsFunctionId = JsFunctionId::new();
|
||||
let js_function = TypedJsFunction::<I, O>::new(persisted_function);
|
||||
self.function_map.insert(function_id, Arc::new(js_function));
|
||||
|
||||
if let Some(on_registered_js_function_callback) = &self.on_registered_js_function_callback {
|
||||
on_registered_js_function_callback(function_id);
|
||||
}
|
||||
|
||||
TypedJsFunctionRef::<I, O>::new(function_id)
|
||||
}
|
||||
|
||||
/// Returns the `CallableJsFunction` corresponding to the given `id`, if any.
|
||||
///
|
||||
/// Note that this returns an owned `CallableJsFunction` (backed by an owned
|
||||
/// `TypedJsFunction`), which is necessary because any use of the function (e.g. calling it)
|
||||
/// consumes the function type itself.
|
||||
pub fn get_function(&self, id: &JsFunctionId) -> Option<Arc<dyn CallableJsFunction>> {
|
||||
self.function_map.get(id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum JsFunctionError {
|
||||
#[error("Could not serialize function input to bytes: {0:?}")]
|
||||
Serialization(bincode::Error),
|
||||
#[error("Could not deserialize function output from bytes: {0:?}")]
|
||||
Deserialization(bincode::Error),
|
||||
#[error("Error occurred in quickjs: {0:?}")]
|
||||
QuickJs(#[from] rquickjs::Error),
|
||||
}
|
||||
|
||||
/// Helper trait that allows us to treat `TypedJsFunction`s, which may have different generic type
|
||||
/// parameters, uniformly.
|
||||
///
|
||||
/// This is akin to the `AnyView` and `AnyModel` traits used by the UI framework to similarly store
|
||||
/// `View` callbacks that are actually parameterized by the type of the actual `View`
|
||||
/// implementation.
|
||||
pub trait CallableJsFunction {
|
||||
/// Calls the wrapped JS function with the given `input` deserialized into the appropriate
|
||||
/// `IntoWarpJs`-implemmenting Rust type, and returns the serialized bytes representation of
|
||||
/// the function's output (which is of a Rust type that implemenets `FromWarpJs`).
|
||||
fn call(
|
||||
&self,
|
||||
input: SerializedJsValue,
|
||||
function_registry: &mut JsFunctionRegistry,
|
||||
ctx: Ctx<'_>,
|
||||
) -> Result<SerializedJsValue, JsFunctionError>;
|
||||
}
|
||||
|
||||
impl<I, O> CallableJsFunction for TypedJsFunction<I, O>
|
||||
where
|
||||
I: for<'a> IntoWarpJs<'a> + DeserializeOwned + Clone + 'static,
|
||||
O: for<'a> FromWarpJs<'a> + Serialize + Clone + 'static,
|
||||
{
|
||||
fn call(
|
||||
&self,
|
||||
input: SerializedJsValue,
|
||||
function_registry: &mut JsFunctionRegistry,
|
||||
ctx: Ctx<'_>,
|
||||
) -> Result<SerializedJsValue, JsFunctionError> {
|
||||
let input: I = input.to_value().map_err(JsFunctionError::Deserialization)?;
|
||||
let input_value = input.into_galaxy_js(ctx)?;
|
||||
let func = self.js_function.clone().restore(ctx)?;
|
||||
let output = O::from_galaxy_js(ctx, func.call((input_value,))?, function_registry)?;
|
||||
SerializedJsValue::from_value(output).map_err(JsFunctionError::Serialization)
|
||||
}
|
||||
}
|
||||
|
||||
/// A typed wrapper around a "raw" JS function (e.g. an `rquickjs::Function`).
|
||||
///
|
||||
/// `I` is the type of the function's input, which must implement `IntoWarpJs`.
|
||||
/// `O` is the type of the function's return value, which must implement `FromWarpJs`.
|
||||
#[derive(Clone)]
|
||||
struct TypedJsFunction<I, O> {
|
||||
js_function: Persistent<Function<'static>>,
|
||||
_input_marker: PhantomData<I>,
|
||||
_output_marker: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<I, O> TypedJsFunction<I, O>
|
||||
where
|
||||
I: for<'a> IntoWarpJs<'a> + DeserializeOwned + 'static,
|
||||
O: for<'a> FromWarpJs<'a> + Serialize + 'static,
|
||||
{
|
||||
fn new(js_function: Persistent<Function<'static>>) -> Self {
|
||||
Self {
|
||||
js_function,
|
||||
_input_marker: PhantomData,
|
||||
_output_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! This crate contains helper abstractions for dealing with JavaScript values and functions from
|
||||
//! Rust.
|
||||
mod convert;
|
||||
mod js_function;
|
||||
|
||||
#[cfg_attr(target_family = "wasm", allow(unused_imports))]
|
||||
pub use convert::*;
|
||||
pub use js_function::*;
|
||||
Reference in New Issue
Block a user