feat:added js glue

This commit is contained in:
frelodev 2024-03-10 12:42:56 +01:00
parent 830ef1fa9b
commit c748f49941
11 changed files with 502 additions and 9 deletions

14
.gitignore vendored
View file

@ -1,12 +1,10 @@
/target /target
Cargo.lock Cargo.lock
# vscode extension build files
/client/vscode/node_modules/
/client/vscode/*.vsix
/client/vscode/codemp.node
.cargo .cargo
.vscode/
.vscode/ # js
node_modules/
package-lock.json
index.d.ts
index.node

View file

@ -39,14 +39,21 @@ log = { version = "0.4.21", optional = true }
mlua = { version = "0.9.6", features = ["module", "luajit", "send"], optional = true } mlua = { version = "0.9.6", features = ["module", "luajit", "send"], optional = true }
thiserror = { version = "1.0.57", optional = true } thiserror = { version = "1.0.57", optional = true }
derive_more = { version = "0.99.17", optional = true } derive_more = { version = "0.99.17", optional = true }
# glue (js)
rmpv = { version = "1", optional = true }
napi = { version = "2", features = ["full"], optional = true }
napi-derive = { version="2", optional = true}
futures = { version = "0.3.28", optional = true }
[build-dependencies] [build-dependencies]
# glue (java) # glue (java)
flapigen = { version = "0.6.0", optional = true } flapigen = { version = "0.6.0", optional = true }
rifgen = { git = "https://github.com/Kofituo/rifgen.git", rev = "d27d9785b2febcf5527f1deb6a846be5d583f7d7", optional = true } rifgen = { git = "https://github.com/Kofituo/rifgen.git", rev = "d27d9785b2febcf5527f1deb6a846be5d583f7d7", optional = true }
# glue (js)
napi-build = { version = "2", optional = true }
[features] [features]
default = [] default = []
lua = ["mlua", "thiserror", "derive_more", "lazy_static", "tracing-subscriber"] lua = ["mlua", "thiserror", "derive_more", "lazy_static", "tracing-subscriber"]
java = ["lazy_static", "jni", "jni-sys", "flapigen", "rifgen", "log"] java = ["lazy_static", "jni", "jni-sys", "flapigen", "rifgen", "log"]
java-artifact = ["java"] # also builds the jar java-artifact = ["java"] # also builds the jar
js = ["napi-build", "tracing-subscriber", "rmpv", "napi", "napi-derive", "futures"]

View file

@ -1,3 +1,6 @@
#[cfg(feature = "js")]
extern crate napi_build;
/// The main method of the buildscript, required by some glue modules. /// The main method of the buildscript, required by some glue modules.
fn main() { fn main() {
#[cfg(feature = "java")] { #[cfg(feature = "java")] {
@ -74,6 +77,10 @@ fn main() {
println!("cargo:rerun-if-changed={}", generated_glue_file.display()); println!("cargo:rerun-if-changed={}", generated_glue_file.display());
} }
} }
#[cfg(feature = "js")] {
napi_build::setup();
}
} }
#[cfg(feature = "java")] #[cfg(feature = "java")]

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"@napi-rs/cli": "^2.18.0"
}
}

110
src/ffi/js/buffer.rs Normal file
View file

@ -0,0 +1,110 @@
use std::sync::Arc;
use napi::threadsafe_function::{ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};
use napi_derive::napi;
use crate::api::Controller;
use crate::ffi::js::JsCodempError;
/// BUFFER
#[napi(object)]
pub struct JsTextChange {
pub span: JsRange,
pub content: String,
}
#[napi(object)]
pub struct JsRange{
pub start: i32,
pub end: i32,
}
impl From::<crate::api::TextChange> for JsTextChange {
fn from(value: crate::api::TextChange) -> Self {
JsTextChange {
// TODO how is x.. represented ? span.end can never be None
span: JsRange { start: value.span.start as i32, end: value.span.end as i32 },
content: value.content,
}
}
}
impl From::<Arc<crate::buffer::Controller>> for JsBufferController {
fn from(value: Arc<crate::buffer::Controller>) -> Self {
JsBufferController(value)
}
}
#[napi]
pub struct JsBufferController(Arc<crate::buffer::Controller>);
/*#[napi]
pub fn delta(string : String, start: i64, txt: String, end: i64 ) -> Option<JsCodempOperationSeq> {
Some(JsCodempOperationSeq(string.diff(start as usize, &txt, end as usize)?))
}*/
#[napi]
impl JsBufferController {
#[napi(ts_args_type = "fun: (event: JsTextChange) => void")]
pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{
let tsfn : ThreadsafeFunction<crate::api::TextChange, Fatal> =
fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<crate::api::TextChange>| {
Ok(vec![JsTextChange::from(ctx.value)])
}
)?;
let _controller = self.0.clone();
tokio::spawn(async move {
//tokio::time::sleep(std::time::Duration::from_secs(1)).await;
loop {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
match _controller.recv().await {
Ok(event) => {
tsfn.call(event, ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error
},
Err(crate::Error::Deadlocked) => continue,
Err(e) => break tracing::warn!("error receiving: {}", e),
}
}
});
Ok(())
}
#[napi]
pub fn content(&self) -> String {
self.0.content()
}
#[napi]
pub fn get_name(&self) -> String {
self.0.name().to_string()
}
#[napi]
pub async fn recv(&self) -> napi::Result<JsTextChange> {
Ok(
self.0.recv().await
.map_err(|e| napi::Error::from(JsCodempError(e)))?
.into()
)
}
#[napi]
pub fn send(&self, op: JsTextChange) -> napi::Result<()> {
// TODO might be nice to take ownership of the opseq
let new_text_change = crate::api::TextChange {
span: op.span.start as usize .. op.span.end as usize,
content: op.content,
};
Ok(self.0.send(new_text_change).map_err(JsCodempError)?)
}
}

45
src/ffi/js/client.rs Normal file
View file

@ -0,0 +1,45 @@
use napi_derive::napi;
use crate::ffi::js::JsCodempError;
use crate::ffi::js::workspace::JsWorkspace;
#[napi]
/// main codemp client session
pub struct JsCodempClient(tokio::sync::RwLock<crate::Client>);
#[napi]
/// connect to codemp servers and return a client session
pub async fn connect(addr: Option<String>) -> napi::Result<JsCodempClient>{
let client = crate::Client::new(addr.as_deref().unwrap_or("http://codemp.alemi.dev:50053"))
.await
.map_err(JsCodempError)?;
Ok(JsCodempClient(tokio::sync::RwLock::new(client)))
}
#[napi]
impl JsCodempClient {
#[napi]
/// login against AuthService with provided credentials, optionally requesting access to a workspace
pub async fn login(&self, username: String, password: String, workspace_id: Option<String>) -> napi::Result<()> {
self.0.read().await.login(username, password, workspace_id).await.map_err(JsCodempError)?;
Ok(())
}
#[napi]
/// join workspace with given id (will start its cursor controller)
pub async fn join_workspace(&self, workspace: String) -> napi::Result<JsWorkspace> {
Ok(JsWorkspace::from(self.0.write().await.join_workspace(&workspace).await.map_err(JsCodempError)?))
}
#[napi]
/// get workspace with given id, if it exists
pub async fn get_workspace(&self, workspace: String) -> Option<JsWorkspace> {
self.0.read().await.get_workspace(&workspace).map(|w| JsWorkspace::from(w))
}
#[napi]
/// return current sessions's user id
pub async fn user_id(&self) -> String {
self.0.read().await.user_id().to_string()
}
}

90
src/ffi/js/cursor.rs Normal file
View file

@ -0,0 +1,90 @@
use std::sync::Arc;
use napi_derive::napi;
use uuid::Uuid;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy};
use crate::api::Controller;
use crate::ffi::js::JsCodempError;
#[napi]
pub struct JsCursorController(Arc<crate::cursor::Controller>);
impl From::<Arc<crate::cursor::Controller>> for JsCursorController {
fn from(value: Arc<crate::cursor::Controller>) -> Self {
JsCursorController(value)
}
}
#[napi]
impl JsCursorController {
#[napi(ts_args_type = "fun: (event: JsCursorEvent) => void")]
pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{
let tsfn : ThreadsafeFunction<codemp_proto::cursor::CursorEvent, ErrorStrategy::Fatal> =
fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<codemp_proto::cursor::CursorEvent>| {
Ok(vec![JsCursorEvent::from(ctx.value)])
}
)?;
let _controller = self.0.clone();
tokio::spawn(async move {
loop {
match _controller.recv().await {
Ok(event) => {
tsfn.call(event.clone(), ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error
},
Err(crate::Error::Deadlocked) => continue,
Err(e) => break tracing::warn!("error receiving: {}", e),
}
}
});
Ok(())
}
#[napi]
pub fn send(&self, buffer: String, start: (i32, i32), end: (i32, i32)) -> napi::Result<()> {
let pos = codemp_proto::cursor::CursorPosition {
buffer: buffer.into(),
start: codemp_proto::cursor::RowCol::from(start),
end: codemp_proto::cursor::RowCol::from(end),
};
Ok(self.0.send(pos).map_err(JsCodempError)?)
}
}
#[derive(Debug)]
#[napi(object)]
pub struct JsCursorEvent {
pub user: String,
pub buffer: String,
pub start: JsRowCol,
pub end: JsRowCol,
}
impl From::<codemp_proto::cursor::CursorEvent> for JsCursorEvent {
fn from(value: codemp_proto::cursor::CursorEvent) -> Self {
let pos = value.position;
let start = pos.start;
let end = pos.end;
JsCursorEvent {
user: Uuid::from(value.user).to_string(),
buffer: pos.buffer.into(),
start: JsRowCol { row: start.row, col: start.col },
end: JsRowCol { row: end.row, col: end.col },
}
}
}
#[derive(Debug)]
#[napi(object)]
pub struct JsRowCol {
pub row: i32,
pub col: i32
}
impl From::<codemp_proto::cursor::RowCol> for JsRowCol {
fn from(value: codemp_proto::cursor::RowCol) -> Self {
JsRowCol { row: value.row, col: value.col }
}
}

68
src/ffi/js/mod.rs Normal file
View file

@ -0,0 +1,68 @@
#![deny(clippy::all)]
pub mod client;
pub mod workspace;
pub mod cursor;
pub mod buffer;
pub mod op_cache;
#[derive(Debug)]
struct JsCodempError(crate::Error);
impl From::<JsCodempError> for napi::Error {
fn from(value: JsCodempError) -> Self {
napi::Error::new(napi::Status::GenericFailure, &format!("CodempError: {:?}", value))
}
}
use napi_derive::napi;
#[napi]
pub struct JsLogger(std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<String>>>);
#[napi]
impl JsLogger {
#[napi(constructor)]
pub fn new(debug: Option<bool>) -> JsLogger {
let (tx, rx) = tokio::sync::mpsc::channel(256);
let level = if debug.unwrap_or(false) { tracing::Level::DEBUG } else {tracing::Level::INFO }; //TODO: study this tracing subscriber and customize it
let format = tracing_subscriber::fmt::format()
.with_level(true)
.with_target(true)
.with_thread_ids(false)
.with_thread_names(false)
.with_ansi(false)
.with_file(false)
.with_line_number(false)
.with_source_location(false)
.compact();
tracing_subscriber::fmt()
.event_format(format)
.with_max_level(level)
.with_writer(std::sync::Mutex::new(JsLoggerProducer(tx)))
.init();
JsLogger(std::sync::Arc::new(tokio::sync::Mutex::new(rx)))
}
#[napi]
pub async fn message(&self) -> Option<String> {
self.0
.lock()
.await
.recv()
.await
}
}
#[derive(Debug, Clone)]
struct JsLoggerProducer(tokio::sync::mpsc::Sender<String>);
impl std::io::Write for JsLoggerProducer {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
// TODO this is a LOSSY logger!!
let _ = self.0.try_send(String::from_utf8_lossy(buf).to_string()); // ignore: logger disconnected or with full buffer
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
}

104
src/ffi/js/op_cache.rs Normal file
View file

@ -0,0 +1,104 @@
use std::collections::HashMap;
use napi_derive::napi;
pub type OpTuple = (String, u32, String, u32); // buf_path, start, text, end
#[napi]
pub struct OpCache {
store: HashMap<OpTuple, i32>
}
#[napi]
impl OpCache {
#[napi(constructor)]
pub fn new() -> Self {
OpCache {
store: HashMap::new()
}
}
#[napi]
pub fn to_string(&self) -> String {
self.store.iter()
.map(|(k, v)| format!("{}x Op(@{} {}:{} '{}')", k.0, v, k.1, k.3, k.2))
.collect::<Vec<String>>()
.join(", ")
}
#[napi]
pub fn put(&mut self, buf: String, start: u32, text: String, end: u32) -> i32 {
let op = (buf, start, text, end);
match self.store.get_mut(&op) {
Some(val) => {
if *val < 0 { *val = 0 }
*val += 1;
*val
},
None => {
self.store.insert(op, 1);
return 1;
}
}
}
#[napi]
pub fn get(&mut self, buf: String, start: u32, text: String, end: u32) -> bool {
let op = (buf, start, text, end);
match self.store.get_mut(&op) {
Some(val) => {
*val -= 1;
*val >= 0
}
None => {
tracing::warn!("never seen this op: {:?}", op);
self.store.insert(op, -1);
false
},
}
}
}
//a
//consume a
//a
#[cfg(test)]
mod test {
#[test]
fn opcache_put_increments_internal_counter() {
let mut op = super::OpCache::new();
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1); // 1: did not already contain it
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 2); // 2: already contained it
}
#[test]
fn op_cache_get_checks_count() {
let mut op = super::OpCache::new();
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1);
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
}
#[test]
fn op_cache_get_works_for_multiple_puts() {
let mut op = super::OpCache::new();
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1);
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 2);
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
}
#[test]
fn op_cache_different_keys(){
let mut op = super::OpCache::new();
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1);
assert_eq!(op.get("workspace".into(), 0, "hi".into(), 0), false);
assert_eq!(op.put("workspace".into(), 0, "hi".into(), 0), 1);
assert_eq!(op.get("workspace".into(), 0, "hi".into(), 0), true);
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
}
}

56
src/ffi/js/workspace.rs Normal file
View file

@ -0,0 +1,56 @@
use std::sync::Arc;
use napi_derive::napi;
use crate::ffi::js::{JsCodempError, buffer::JsBufferController, cursor::JsCursorController};
#[napi]
/// a reference to a codemp workspace
pub struct JsWorkspace(Arc<crate::Workspace>);
impl From<Arc<crate::Workspace>> for JsWorkspace {
fn from(value: Arc<crate::Workspace>) -> Self {
JsWorkspace(value)
}
}
#[napi]
impl JsWorkspace {
#[napi]
pub fn id(&self) -> String {
self.0.id()
}
#[napi]
pub fn filetree(&self) -> Vec<String> {
self.0.filetree()
}
#[napi]
pub fn cursor(&self) -> JsCursorController {
JsCursorController::from(self.0.cursor())
}
#[napi]
pub fn buffer_by_name(&self, path: String) -> Option<JsBufferController> {
self.0.buffer_by_name(&path).map(|b| JsBufferController::from(b))
}
#[napi]
pub async fn create(&self, path: String) -> napi::Result<()> {
Ok(self.0.create(&path).await.map_err(JsCodempError)?)
}
#[napi]
pub async fn attach(&self, path: String) -> napi::Result<JsBufferController> {
Ok(JsBufferController::from(self.0.attach(&path).await.map_err(JsCodempError)?))
}
/*#[napi]
pub async fn delete(&self, path: String) -> napi::Result<>{
self.0.delete(&path)
}*/
}

View file

@ -3,3 +3,6 @@ pub mod java;
#[cfg(feature = "lua")] #[cfg(feature = "lua")]
pub mod lua; pub mod lua;
#[cfg(feature = "js")]
pub mod js;