feat(y-octo): import y-octo monorepo (#11750)

This commit is contained in:
Brooooooklyn
2025-04-21 02:51:15 +00:00
parent e3973538e8
commit 95dbda24fc
127 changed files with 17319 additions and 18 deletions
+160
View File
@@ -0,0 +1,160 @@
use napi::{bindgen_prelude::Array as JsArray, Env, JsUnknown, ValueType};
use y_octo::{Any, Array, Value};
use super::*;
#[napi]
pub struct YArray {
pub(crate) array: Array,
}
#[napi]
impl YArray {
#[allow(clippy::new_without_default)]
#[napi(constructor)]
pub fn new() -> Self {
unimplemented!()
}
pub(crate) fn inner_new(array: Array) -> Self {
Self { array }
}
#[napi(getter)]
pub fn length(&self) -> i64 {
self.array.len() as i64
}
#[napi(getter)]
pub fn is_empty(&self) -> bool {
self.array.is_empty()
}
#[napi(ts_generic_types = "T = unknown", ts_return_type = "T")]
pub fn get(&self, env: Env, index: i64) -> Result<MixedYType> {
if let Some(value) = self.array.get(index as u64) {
match value {
Value::Any(any) => get_js_unknown_from_any(env, any).map(MixedYType::D),
Value::Array(array) => Ok(MixedYType::A(YArray::inner_new(array))),
Value::Map(map) => Ok(MixedYType::B(YMap::inner_new(map))),
Value::Text(text) => Ok(MixedYType::C(YText::inner_new(text))),
_ => env.get_null().map(|v| v.into_unknown()).map(MixedYType::D),
}
.map_err(anyhow::Error::from)
} else {
Ok(MixedYType::D(env.get_null()?.into_unknown()))
}
}
#[napi(
ts_args_type = "index: number, value: YArray | YMap | YText | boolean | number | string | \
Record<string, any> | null | undefined"
)]
pub fn insert(&mut self, index: i64, value: MixedRefYType) -> Result<()> {
match value {
MixedRefYType::A(array) => self
.array
.insert(index as u64, array.array.clone())
.map_err(anyhow::Error::from),
MixedRefYType::B(map) => self
.array
.insert(index as u64, map.map.clone())
.map_err(anyhow::Error::from),
MixedRefYType::C(text) => self
.array
.insert(index as u64, text.text.clone())
.map_err(anyhow::Error::from),
MixedRefYType::D(unknown) => match unknown.get_type() {
Ok(value_type) => match value_type {
ValueType::Undefined | ValueType::Null => self
.array
.insert(index as u64, Any::Null)
.map_err(anyhow::Error::from),
ValueType::Boolean => match unknown.coerce_to_bool().and_then(|v| v.get_value()) {
Ok(boolean) => self
.array
.insert(index as u64, boolean)
.map_err(anyhow::Error::from),
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to boolean")),
},
ValueType::Number => match unknown.coerce_to_number().and_then(|v| v.get_double()) {
Ok(number) => self
.array
.insert(index as u64, number)
.map_err(anyhow::Error::from),
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to number")),
},
ValueType::String => {
match unknown
.coerce_to_string()
.and_then(|v| v.into_utf8())
.and_then(|s| s.as_str().map(|s| s.to_string()))
{
Ok(string) => self
.array
.insert(index as u64, string)
.map_err(anyhow::Error::from),
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to string")),
}
}
ValueType::Object => match unknown
.coerce_to_object()
.and_then(|o| o.get_array_length().map(|l| (o, l)))
{
Ok((object, length)) => {
for i in 0..length {
if let Ok(any) = object
.get_element::<JsUnknown>(i)
.and_then(get_any_from_js_unknown)
{
self
.array
.insert(index as u64 + i as u64, Value::Any(any))
.map_err(anyhow::Error::from)?;
}
}
Ok(())
}
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to object")),
},
ValueType::BigInt => Err(anyhow::Error::msg("BigInt values are not supported")),
ValueType::Symbol => Err(anyhow::Error::msg("Symbol values are not supported")),
ValueType::Function => Err(anyhow::Error::msg("Function values are not supported")),
ValueType::External => Err(anyhow::Error::msg("External values are not supported")),
ValueType::Unknown => Err(anyhow::Error::msg("Unknown values are not supported")),
},
Err(e) => Err(anyhow::Error::from(e)),
},
}
}
#[napi]
pub fn remove(&mut self, index: i64, len: i64) -> Result<()> {
self
.array
.remove(index as u64, len as u64)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn to_json(&self, env: Env) -> Result<JsArray> {
let mut js_array = env.create_array(0)?;
for value in self.array.iter() {
js_array.insert(get_js_unknown_from_value(env, value)?)?;
}
Ok(js_array)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_array_init() {
let doc = Doc::new(None);
let array = doc.get_or_create_array("array".into()).unwrap();
assert_eq!(array.length(), 0);
}
}
+176
View File
@@ -0,0 +1,176 @@
use napi::{
bindgen_prelude::{Buffer, Uint8Array},
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
};
use y_octo::{CrdtRead, Doc as YDoc, History, RawDecoder, StateVector};
use super::*;
#[napi]
pub struct Doc {
doc: YDoc,
}
#[napi]
impl Doc {
#[napi(constructor)]
pub fn new(client_id: Option<i64>) -> Self {
Self {
doc: if let Some(client_id) = client_id {
YDoc::with_client(client_id as u64)
} else {
YDoc::default()
},
}
}
#[napi(getter)]
pub fn client_id(&self) -> i64 {
self.doc.client() as i64
}
#[napi(getter)]
pub fn guid(&self) -> &str {
self.doc.guid()
}
#[napi(getter)]
pub fn keys(&self) -> Vec<String> {
self.doc.keys()
}
#[napi]
pub fn get_or_create_array(&self, key: String) -> Result<YArray> {
self
.doc
.get_or_create_array(key)
.map(YArray::inner_new)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn get_or_create_text(&self, key: String) -> Result<YText> {
self
.doc
.get_or_create_text(key)
.map(YText::inner_new)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn get_or_create_map(&self, key: String) -> Result<YMap> {
self
.doc
.get_or_create_map(key)
.map(YMap::inner_new)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn create_array(&self) -> Result<YArray> {
self
.doc
.create_array()
.map(YArray::inner_new)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn create_text(&self) -> Result<YText> {
self
.doc
.create_text()
.map(YText::inner_new)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn create_map(&self) -> Result<YMap> {
self
.doc
.create_map()
.map(YMap::inner_new)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn apply_update(&mut self, update: &[u8]) -> Result<()> {
self.doc.apply_update_from_binary_v1(update)?;
Ok(())
}
#[napi]
pub fn encode_state_as_update_v1(&self, state: Option<&[u8]>) -> Result<Uint8Array> {
let result = match state {
Some(state) => {
let mut decoder = RawDecoder::new(state);
let state = StateVector::read(&mut decoder)?;
self.doc.encode_state_as_update_v1(&state)
}
None => self.doc.encode_update_v1(),
};
result.map(|v| v.into()).map_err(anyhow::Error::from)
}
#[napi]
pub fn gc(&self) -> Result<()> {
self.doc.gc().map_err(anyhow::Error::from)
}
#[napi(ts_args_type = "callback: (result: Uint8Array) => void")]
pub fn on_update(&mut self, callback: ThreadsafeFunction<Buffer>) -> Result<()> {
let callback = move |update: &[u8], _h: &[History]| {
callback.call(
Ok(update.to_vec().into()),
ThreadsafeFunctionCallMode::Blocking,
);
};
self.doc.subscribe(Box::new(callback));
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_doc_client() {
let client_id = 1;
let doc = Doc::new(Some(client_id));
assert_eq!(doc.client_id(), 1);
}
#[test]
fn test_doc_guid() {
let doc = Doc::new(None);
assert_eq!(doc.guid().len(), 21);
}
#[test]
fn test_create_array() {
let doc = Doc::new(None);
let array = doc.get_or_create_array("array".into()).unwrap();
assert_eq!(array.length(), 0);
}
#[test]
fn test_create_text() {
let doc = Doc::new(None);
let text = doc.get_or_create_text("text".into()).unwrap();
assert_eq!(text.len(), 0);
}
#[test]
fn test_keys() {
let doc = Doc::new(None);
doc.get_or_create_array("array".into()).unwrap();
doc.get_or_create_text("text".into()).unwrap();
doc.get_or_create_map("map".into()).unwrap();
let mut keys = doc.keys();
keys.sort();
assert_eq!(keys, vec!["array", "map", "text"]);
}
}
+17
View File
@@ -0,0 +1,17 @@
use anyhow::Result;
use napi_derive::napi;
mod array;
mod doc;
mod map;
mod text;
mod utils;
pub use array::YArray;
pub use doc::Doc;
pub use map::YMap;
pub use text::YText;
use utils::{
get_any_from_js_object, get_any_from_js_unknown, get_js_unknown_from_any,
get_js_unknown_from_value, MixedRefYType, MixedYType,
};
+134
View File
@@ -0,0 +1,134 @@
use napi::{Env, JsObject, ValueType};
use y_octo::{Any, Map, Value};
use super::*;
#[napi]
pub struct YMap {
pub(crate) map: Map,
}
#[napi]
impl YMap {
#[allow(clippy::new_without_default)]
#[napi(constructor)]
pub fn new() -> Self {
unimplemented!()
}
pub(crate) fn inner_new(map: Map) -> Self {
Self { map }
}
#[napi(getter)]
pub fn length(&self) -> i64 {
self.map.len() as i64
}
#[napi(getter)]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[napi(ts_generic_types = "T = unknown", ts_return_type = "T")]
pub fn get(&self, env: Env, key: String) -> Result<MixedYType> {
if let Some(value) = self.map.get(&key) {
match value {
Value::Any(any) => get_js_unknown_from_any(env, any).map(MixedYType::D),
Value::Array(array) => Ok(MixedYType::A(YArray::inner_new(array))),
Value::Map(map) => Ok(MixedYType::B(YMap::inner_new(map))),
Value::Text(text) => Ok(MixedYType::C(YText::inner_new(text))),
_ => env.get_null().map(|v| v.into_unknown()).map(MixedYType::D),
}
.map_err(anyhow::Error::from)
} else {
Ok(MixedYType::D(env.get_null()?.into_unknown()))
}
}
#[napi(
ts_args_type = "key: string, value: YArray | YMap | YText | boolean | number | string | \
Record<string, any> | null | undefined"
)]
pub fn set(&mut self, key: String, value: MixedRefYType) -> Result<()> {
match value {
MixedRefYType::A(array) => self
.map
.insert(key, array.array.clone())
.map_err(anyhow::Error::from),
MixedRefYType::B(map) => self
.map
.insert(key, map.map.clone())
.map_err(anyhow::Error::from),
MixedRefYType::C(text) => self
.map
.insert(key, text.text.clone())
.map_err(anyhow::Error::from),
MixedRefYType::D(unknown) => match unknown.get_type() {
Ok(value_type) => match value_type {
ValueType::Undefined | ValueType::Null => {
self.map.insert(key, Any::Null).map_err(anyhow::Error::from)
}
ValueType::Boolean => match unknown.coerce_to_bool().and_then(|v| v.get_value()) {
Ok(boolean) => self.map.insert(key, boolean).map_err(anyhow::Error::from),
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to boolean")),
},
ValueType::Number => match unknown.coerce_to_number().and_then(|v| v.get_double()) {
Ok(number) => self.map.insert(key, number).map_err(anyhow::Error::from),
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to number")),
},
ValueType::String => {
match unknown
.coerce_to_string()
.and_then(|v| v.into_utf8())
.and_then(|s| s.as_str().map(|s| s.to_string()))
{
Ok(string) => self.map.insert(key, string).map_err(anyhow::Error::from),
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to string")),
}
}
ValueType::Object => match unknown.coerce_to_object().and_then(get_any_from_js_object) {
Ok(any) => self
.map
.insert(key, Value::Any(any))
.map_err(anyhow::Error::from),
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to object")),
},
ValueType::BigInt => Err(anyhow::Error::msg("BigInt values are not supported")),
ValueType::Symbol => Err(anyhow::Error::msg("Symbol values are not supported")),
ValueType::Function => Err(anyhow::Error::msg("Function values are not supported")),
ValueType::External => Err(anyhow::Error::msg("External values are not supported")),
ValueType::Unknown => Err(anyhow::Error::msg("Unknown values are not supported")),
},
Err(e) => Err(anyhow::Error::from(e)),
},
}
}
#[napi]
pub fn remove(&mut self, key: String) {
self.map.remove(&key);
}
#[napi]
pub fn to_json(&self, env: Env) -> Result<JsObject> {
let mut js_object = env.create_object()?;
for (key, value) in self.map.iter() {
js_object.set(key, get_js_unknown_from_value(env, value))?;
}
Ok(js_object)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_map_init() {
let doc = Doc::new(None);
let text = doc.get_or_create_map("map".into()).unwrap();
assert_eq!(text.length(), 0);
}
}
+82
View File
@@ -0,0 +1,82 @@
use y_octo::Text;
use super::*;
#[napi]
pub struct YText {
pub(crate) text: Text,
}
#[napi]
impl YText {
#[allow(clippy::new_without_default)]
#[napi(constructor)]
pub fn new() -> Self {
unimplemented!()
}
pub(crate) fn inner_new(text: Text) -> Self {
Self { text }
}
#[napi(getter)]
pub fn len(&self) -> i64 {
self.text.len() as i64
}
#[napi(getter)]
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[napi]
pub fn insert(&mut self, index: i64, str: String) -> Result<()> {
self
.text
.insert(index as u64, str)
.map_err(anyhow::Error::from)
}
#[napi]
pub fn remove(&mut self, index: i64, len: i64) -> Result<()> {
self
.text
.remove(index as u64, len as u64)
.map_err(anyhow::Error::from)
}
#[napi(getter)]
pub fn length(&self) -> i64 {
self.text.len() as i64
}
#[allow(clippy::inherent_to_string)]
#[napi]
pub fn to_string(&self) -> String {
self.text.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_init() {
let doc = Doc::new(None);
let text = doc.get_or_create_text("text".into()).unwrap();
assert_eq!(text.len(), 0);
}
#[test]
fn test_text_edit() {
let doc = Doc::new(None);
let mut text = doc.get_or_create_text("text".into()).unwrap();
text.insert(0, "hello".into()).unwrap();
assert_eq!(text.to_string(), "hello");
text.insert(5, " world".into()).unwrap();
assert_eq!(text.to_string(), "hello world");
text.remove(5, 6).unwrap();
assert_eq!(text.to_string(), "hello");
}
}
+117
View File
@@ -0,0 +1,117 @@
use napi::{bindgen_prelude::Either4, Env, Error, JsObject, JsUnknown, Result, Status, ValueType};
use y_octo::{AHashMap, Any, HashMapExt, Value};
use super::*;
pub type MixedYType = Either4<YArray, YMap, YText, JsUnknown>;
pub type MixedRefYType<'a> = Either4<&'a YArray, &'a YMap, &'a YText, JsUnknown>;
pub fn get_js_unknown_from_any(env: Env, any: Any) -> Result<JsUnknown> {
match any {
Any::Null | Any::Undefined => env.get_null().map(|v| v.into_unknown()),
Any::True => env.get_boolean(true).map(|v| v.into_unknown()),
Any::False => env.get_boolean(false).map(|v| v.into_unknown()),
Any::Integer(number) => env.create_int32(number).map(|v| v.into_unknown()),
Any::BigInt64(number) => env.create_int64(number).map(|v| v.into_unknown()),
Any::Float32(number) => env.create_double(number.0 as f64).map(|v| v.into_unknown()),
Any::Float64(number) => env.create_double(number.0).map(|v| v.into_unknown()),
Any::String(string) => env.create_string(string.as_str()).map(|v| v.into_unknown()),
Any::Array(array) => {
let mut js_array = env.create_array_with_length(array.len())?;
for (i, value) in array.into_iter().enumerate() {
js_array.set_element(i as u32, get_js_unknown_from_any(env, value)?)?;
}
Ok(js_array.into_unknown())
}
_ => env.get_null().map(|v| v.into_unknown()),
}
}
#[allow(deprecated)]
// Wait for NAPI-RS External::into_unknown to be stabilized
pub fn get_js_unknown_from_value(env: Env, value: Value) -> Result<JsUnknown> {
match value {
Value::Any(any) => get_js_unknown_from_any(env, any),
Value::Array(array) => env
.create_external(YArray::inner_new(array), None)
.map(|o| o.into_unknown()),
Value::Map(map) => env
.create_external(YMap::inner_new(map), None)
.map(|o| o.into_unknown()),
Value::Text(text) => env
.create_external(YText::inner_new(text), None)
.map(|o| o.into_unknown()),
_ => env.get_null().map(|v| v.into_unknown()),
}
}
pub fn get_any_from_js_object(object: JsObject) -> Result<Any> {
if let Ok(length) = object.get_array_length() {
let mut array = Vec::with_capacity(length as usize);
for i in 0..length {
if let Ok(value) = object.get_element::<JsUnknown>(i) {
array.push(get_any_from_js_unknown(value)?);
}
}
Ok(Any::Array(array))
} else {
let mut map = AHashMap::new();
let keys = object.get_property_names()?;
if let Ok(length) = keys.get_array_length() {
for i in 0..length {
if let Ok((obj, key)) = keys.get_element::<JsUnknown>(i).and_then(|o| {
o.coerce_to_string().and_then(|obj| {
obj
.into_utf8()
.and_then(|s| s.as_str().map(|s| (obj, s.to_string())))
})
}) {
if let Ok(value) = object.get_property::<_, JsUnknown>(obj) {
println!("key: {}", key);
map.insert(key, get_any_from_js_unknown(value)?);
}
}
}
}
Ok(Any::Object(map))
}
}
pub fn get_any_from_js_unknown(js_unknown: JsUnknown) -> Result<Any> {
match js_unknown.get_type()? {
ValueType::Undefined | ValueType::Null => Ok(Any::Null),
ValueType::Boolean => Ok(
js_unknown
.coerce_to_bool()
.and_then(|v| v.get_value())?
.into(),
),
ValueType::Number => Ok(
js_unknown
.coerce_to_number()
.and_then(|v| v.get_double())
.map(|v| v.into())?,
),
ValueType::String => Ok(
js_unknown
.coerce_to_string()
.and_then(|v| v.into_utf8())
.and_then(|s| s.as_str().map(|s| s.to_string()))?
.into(),
),
ValueType::Object => {
if let Ok(object) = js_unknown.coerce_to_object() {
get_any_from_js_object(object)
} else {
Err(Error::new(
Status::InvalidArg,
"Failed to coerce value to object",
))
}
}
_ => Err(Error::new(
Status::InvalidArg,
"Failed to coerce value to any",
)),
}
}