Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
+20
View File
@@ -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 = []
+6
View File
@@ -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)
}
}
+110
View File
@@ -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)
}
}
+72
View File
@@ -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()
}
}
+156
View File
@@ -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,
}
}
}
+8
View File
@@ -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::*;