Merge branch 'dev' into feat/workspace-receiver

This commit is contained in:
əlemi 2024-10-03 03:52:42 +02:00 committed by GitHub
commit 535de6c2fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 790 additions and 378 deletions

View file

@ -11,7 +11,7 @@ authors = [
]
license = "GPL-3.0-only"
edition = "2021"
version = "0.7.2"
version = "0.7.3"
exclude = ["dist/*"]
[lib]

View file

@ -5,7 +5,7 @@
[![Crates.io Version](https://img.shields.io/crates/v/codemp)](https://crates.io/crates/codemp)
[![Gitter Chat](https://img.shields.io/gitter/room/hexedtech/codemp)](https://gitter.im/hexedtech/codemp)
[![GitHub last commit](https://img.shields.io/github/last-commit/hexedtech/codemp)](https://github.com/hexedtech/codemp/commits/dev/)
[![GitHub commits since tagged version](https://img.shields.io/github/commits-since/hexedtech/codemp/v0.7.2)](https://github.com/hexedtech/codemp/releases/tag/v0.7.2)
[![GitHub commits since tagged version](https://img.shields.io/github/commits-since/hexedtech/codemp/v0.7.3)](https://github.com/hexedtech/codemp/releases/tag/v0.7.3)
> `codemp` is a **collaborative** text editing solution to work remotely.

View file

@ -5,7 +5,7 @@ plugins {
}
group = 'mp.code'
version = '0.7.2'
version = '0.7.3'
java {
sourceCompatibility = targetCompatibility = JavaVersion.VERSION_11

View file

@ -1,6 +1,6 @@
{
"name": "@codemp/native",
"version": "0.7.2",
"version": "0.7.3",
"description": "code multiplexer -- javascript bindings",
"keywords": [
"codemp",
@ -35,8 +35,8 @@
}
},
"optionalDependencies": {
"@codemp/native-win32-x64-msvc": "0.7.2",
"@codemp/native-darwin-arm64": "0.7.2",
"@codemp/native-linux-x64-gnu": "0.7.2"
"@codemp/native-win32-x64-msvc": "0.7.3",
"@codemp/native-darwin-arm64": "0.7.3",
"@codemp/native-linux-x64-gnu": "0.7.3"
}
}

View file

@ -18,6 +18,9 @@ local NilPromise = {}
--- block until promise is ready
function NilPromise:await() end
--- cancel promise execution
function NilPromise:cancel() end
---@param cb fun() callback to invoke
---invoke callback asynchronously as soon as promise is ready
function NilPromise:and_then(cb) end
@ -30,6 +33,9 @@ local StringPromise = {}
--- @return string
function StringPromise:await() end
--- cancel promise execution
function StringPromise:cancel() end
---@param cb fun(x: string) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function StringPromise:and_then(cb) end
@ -40,6 +46,8 @@ local StringArrayPromise = {}
--- block until promise is ready and return value
--- @return string[]
function StringArrayPromise:await() end
--- cancel promise execution
function StringArrayPromise:cancel() end
---@param cb fun(x: string[]) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function StringArrayPromise:and_then(cb) end
@ -50,6 +58,8 @@ local ClientPromise = {}
--- block until promise is ready and return value
--- @return Client
function ClientPromise:await() end
--- cancel promise execution
function ClientPromise:cancel() end
---@param cb fun(x: Client) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function ClientPromise:and_then(cb) end
@ -60,6 +70,8 @@ local WorkspacePromise = {}
--- block until promise is ready and return value
--- @return Workspace
function WorkspacePromise:await() end
--- cancel promise execution
function WorkspacePromise:cancel() end
---@param cb fun(x: Workspace) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function WorkspacePromise:and_then(cb) end
@ -70,6 +82,8 @@ local WorkspaceEventPromise = {}
--- block until promise is ready and return value
--- @return WorkspaceEvent
function WorkspaceEventPromise:await() end
--- cancel promise execution
function WorkspaceEventPromise:cancel() end
---@param cb fun(x: WorkspaceEvent) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function WorkspaceEventPromise:and_then(cb) end
@ -90,6 +104,8 @@ local BufferControllerPromise = {}
--- block until promise is ready and return value
--- @return BufferController
function BufferControllerPromise:await() end
--- cancel promise execution
function BufferControllerPromise:cancel() end
---@param cb fun(x: BufferController) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function BufferControllerPromise:and_then(cb) end
@ -100,6 +116,8 @@ local CursorPromise = {}
--- block until promise is ready and return value
--- @return Cursor
function CursorPromise:await() end
--- cancel promise execution
function CursorPromise:cancel() end
---@param cb fun(x: Cursor) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function CursorPromise:and_then(cb) end
@ -110,6 +128,8 @@ local MaybeCursorPromise = {}
--- block until promise is ready and return value
--- @return Cursor | nil
function MaybeCursorPromise:await() end
--- cancel promise execution
function MaybeCursorPromise:cancel() end
---@param cb fun(x: Cursor | nil) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function MaybeCursorPromise:and_then(cb) end
@ -120,6 +140,8 @@ local TextChangePromise = {}
--- block until promise is ready and return value
--- @return TextChange
function TextChangePromise:await() end
--- cancel promise execution
function TextChangePromise:cancel() end
---@param cb fun(x: TextChange) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function TextChangePromise:and_then(cb) end
@ -130,6 +152,8 @@ local MaybeTextChangePromise = {}
--- block until promise is ready and return value
--- @return TextChange | nil
function MaybeTextChangePromise:await() end
--- cancel promise execution
function MaybeTextChangePromise:cancel() end
---@param cb fun(x: TextChange | nil) callback to invoke
---invoke callback asynchronously as soon as promise is ready
function MaybeTextChangePromise:and_then(cb) end

View file

@ -1,9 +1,9 @@
package = "codemp"
version = "0.7.2-1"
version = "0.7.3-1"
source = {
url = "git+https://github.com/hexedtech/codemp",
tag = "v0.7.2",
tag = "v0.7.3",
}
dependencies = {

View file

@ -1,6 +1,6 @@
[project]
name = "codemp"
version = "0.7.2"
version = "0.7.3"
description = "code multiplexer"
requires-python = ">=3.8"
license = "GPL-3.0-only"

View file

@ -75,7 +75,6 @@ pub trait AsyncReceiver<T : Sized + Send + Sync> : Sized + Send + Sync {
async fn try_recv(&self) -> ControllerResult<Option<T>>;
}
/// Type wrapper for Boxed dynamic callback.
pub struct ControllerCallback<T>(pub Box<dyn Sync + Send + Fn(T)>);

View file

@ -19,9 +19,9 @@ pub mod event;
/// data structure for remote users
pub mod user;
pub use controller::Controller;
pub use change::TextChange;
pub use config::Config;
pub use controller::Controller;
pub use cursor::Cursor;
pub use event::Event;
pub use user::User;

View file

@ -34,7 +34,12 @@ struct BufferWorker {
}
impl BufferController {
pub(crate) fn spawn(user_id: Uuid, path: &str, tx: mpsc::Sender<Operation>, rx: Streaming<BufferEvent>) -> Self {
pub(crate) fn spawn(
user_id: Uuid,
path: &str,
tx: mpsc::Sender<Operation>,
rx: Streaming<BufferEvent>,
) -> Self {
let init = diamond_types::LocalVersion::default();
let (latest_version_tx, latest_version_rx) = watch::channel(init.clone());
@ -75,17 +80,21 @@ impl BufferController {
timer: Timer::new(10), // TODO configurable!
};
tokio::spawn(async move {
BufferController::work(worker, tx, rx).await
});
tokio::spawn(async move { BufferController::work(worker, tx, rx).await });
BufferController(controller)
}
async fn work(mut worker: BufferWorker, tx: mpsc::Sender<Operation>, mut rx: Streaming<BufferEvent>) {
async fn work(
mut worker: BufferWorker,
tx: mpsc::Sender<Operation>,
mut rx: Streaming<BufferEvent>,
) {
tracing::debug!("controller worker started");
loop {
if worker.controller.upgrade().is_none() { break };
if worker.controller.upgrade().is_none() {
break;
};
// block until one of these is ready
tokio::select! {

View file

@ -182,7 +182,10 @@ impl Client {
/// Leave the [`Workspace`] with the given name.
pub fn leave_workspace(&self, id: &str) -> bool {
self.0.workspaces.remove(id).is_some()
match self.0.workspaces.remove(id) {
None => true,
Some(x) => x.1.consume(),
}
}
/// Gets a [`Workspace`] handle by name.

View file

@ -4,8 +4,11 @@ use tokio::sync::{mpsc, oneshot, watch};
use tonic::Streaming;
use uuid::Uuid;
use crate::{api::{controller::ControllerCallback, Cursor, User}, ext::IgnorableError};
use codemp_proto::cursor::{CursorPosition, CursorEvent};
use crate::{
api::{controller::ControllerCallback, Cursor, User},
ext::IgnorableError,
};
use codemp_proto::cursor::{CursorEvent, CursorPosition};
use super::controller::{CursorController, CursorControllerInner};
@ -21,7 +24,11 @@ struct CursorWorker {
}
impl CursorController {
pub(crate) fn spawn(user_map: Arc<dashmap::DashMap<Uuid, User>>, tx: mpsc::Sender<CursorPosition>, rx: Streaming<CursorEvent>) -> Self {
pub(crate) fn spawn(
user_map: Arc<dashmap::DashMap<Uuid, User>>,
tx: mpsc::Sender<CursorPosition>,
rx: Streaming<CursorEvent>,
) -> Self {
// TODO we should tweak the channel buffer size to better propagate backpressure
let (op_tx, op_rx) = mpsc::channel(64);
let (stream_tx, stream_rx) = mpsc::channel(1);
@ -52,10 +59,16 @@ impl CursorController {
CursorController(controller)
}
async fn work(mut worker: CursorWorker, tx: mpsc::Sender<CursorPosition>, mut rx: Streaming<CursorEvent>) {
async fn work(
mut worker: CursorWorker,
tx: mpsc::Sender<CursorPosition>,
mut rx: Streaming<CursorEvent>,
) {
loop {
tracing::debug!("cursor worker polling");
if worker.controller.upgrade().is_none() { break }; // clean exit: all controllers dropped
if worker.controller.upgrade().is_none() {
break;
}; // clean exit: all controllers dropped
tokio::select! {
biased;

View file

@ -61,4 +61,3 @@ impl From<tokio::sync::oneshot::error::RecvError> for ControllerError {
/// Wraps [std::result::Result] with a [ControllerError].
pub type ControllerResult<T> = std::result::Result<T, ControllerError>;

View file

@ -16,7 +16,7 @@ use tokio::sync::mpsc;
pub async fn select_buffer(
buffers: &[crate::buffer::Controller],
timeout: Option<std::time::Duration>,
runtime: &tokio::runtime::Runtime
runtime: &tokio::runtime::Runtime,
) -> ControllerResult<Option<crate::buffer::Controller>> {
let (tx, mut rx) = mpsc::unbounded_channel();
let mut tasks = Vec::new();
@ -46,7 +46,7 @@ pub async fn select_buffer(
t.abort();
}
return Ok(x);
},
}
}
}
}
@ -104,11 +104,13 @@ pub trait IgnorableError {
}
impl<T, E> IgnorableError for std::result::Result<T, E>
where E : std::fmt::Debug {
where
E: std::fmt::Debug,
{
/// Logs the error as a warning and returns a unit.
fn unwrap_or_warn(self, msg: &str) {
match self {
Ok(_) => {},
Ok(_) => {}
Err(e) => tracing::warn!("{}: {:?}", msg, e),
}
}

View file

@ -1,7 +1,10 @@
use jni::{objects::JObject, JNIEnv};
use jni_toolbox::jni;
use crate::{api::{controller::{AsyncReceiver, AsyncSender}, TextChange}, errors::ControllerError};
use crate::{
api::{controller::{AsyncReceiver, AsyncSender}, TextChange},
errors::ControllerError,
};
use super::null_check;
@ -19,7 +22,9 @@ fn get_content(controller: &mut crate::buffer::Controller) -> Result<String, Con
/// Try to fetch a [TextChange], or return null if there's nothing.
#[jni(package = "mp.code", class = "BufferController")]
fn try_recv(controller: &mut crate::buffer::Controller) -> Result<Option<TextChange>, ControllerError> {
fn try_recv(
controller: &mut crate::buffer::Controller,
) -> Result<Option<TextChange>, ControllerError> {
super::tokio().block_on(controller.try_recv())
}
@ -31,23 +36,34 @@ fn recv(controller: &mut crate::buffer::Controller) -> Result<TextChange, Contro
/// Send a [TextChange] to the server.
#[jni(package = "mp.code", class = "BufferController")]
fn send(controller: &mut crate::buffer::Controller, change: TextChange) -> Result<(), ControllerError> {
fn send(
controller: &mut crate::buffer::Controller,
change: TextChange,
) -> Result<(), ControllerError> {
super::tokio().block_on(controller.send(change))
}
/// Register a callback for buffer changes.
#[jni(package = "mp.code", class = "BufferController")]
fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::buffer::Controller, cb: JObject<'local>) {
fn callback<'local>(
env: &mut JNIEnv<'local>,
controller: &mut crate::buffer::Controller,
cb: JObject<'local>,
) {
null_check!(env, cb, {});
let Ok(cb_ref) = env.new_global_ref(cb) else {
env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!")
env.throw_new(
"mp/code/exceptions/JNIException",
"Failed to pin callback reference!",
)
.expect("Failed to throw exception!");
return;
};
controller.callback(move |controller: crate::buffer::Controller| {
let jvm = super::jvm();
let mut env = jvm.attach_current_thread_permanently()
let mut env = jvm
.attach_current_thread_permanently()
.expect("failed attaching to main JVM thread");
if let Err(e) = env.with_local_frame(5, |env| {
use jni_toolbox::IntoJavaObject;
@ -56,7 +72,7 @@ fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::buffer::Co
&cb_ref,
"accept",
"(Ljava/lang/Object;)V",
&[jni::objects::JValueGen::Object(&jcontroller)]
&[jni::objects::JValueGen::Object(&jcontroller)],
) {
tracing::error!("error invoking callback: {e:?}");
};

View file

@ -1,5 +1,10 @@
use crate::{
api::Config,
client::Client,
errors::{ConnectionError, RemoteError},
Workspace,
};
use jni_toolbox::jni;
use crate::{api::Config, client::Client, errors::{ConnectionError, RemoteError}, Workspace};
/// Connect using the given credentials to the default server, and return a [Client] to interact with it.
#[jni(package = "mp.code", class = "Client", ptr)]
@ -33,13 +38,21 @@ fn delete_workspace(client: &mut Client, workspace: String) -> Result<(), Remote
/// Invite another user to an owned workspace.
#[jni(package = "mp.code", class = "Client")]
fn invite_to_workspace(client: &mut Client, workspace: String, user: String) -> Result<(), RemoteError> {
fn invite_to_workspace(
client: &mut Client,
workspace: String,
user: String,
) -> Result<(), RemoteError> {
super::tokio().block_on(client.invite_to_workspace(workspace, user))
}
/// List available workspaces.
#[jni(package = "mp.code", class = "Client")]
fn list_workspaces(client: &mut Client, owned: bool, invited: bool) -> Result<Vec<String>, RemoteError> {
fn list_workspaces(
client: &mut Client,
owned: bool,
invited: bool,
) -> Result<Vec<String>, RemoteError> {
super::tokio().block_on(client.list_workspaces(owned, invited))
}

View file

@ -1,6 +1,13 @@
use crate::{
api::{Controller, Cursor},
errors::ControllerError,
};
use jni::{objects::JObject, JNIEnv};
use jni_toolbox::jni;
use crate::{api::{controller::{AsyncSender, AsyncReceiver}, Cursor}, errors::ControllerError};
use crate::{
api::{controller::{AsyncSender, AsyncReceiver}, Cursor},
errors::ControllerError
};
use super::null_check;
@ -24,17 +31,25 @@ fn send(controller: &mut crate::cursor::Controller, cursor: Cursor) -> Result<()
/// Register a callback for cursor changes.
#[jni(package = "mp.code", class = "CursorController")]
fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::cursor::Controller, cb: JObject<'local>) {
fn callback<'local>(
env: &mut JNIEnv<'local>,
controller: &mut crate::cursor::Controller,
cb: JObject<'local>,
) {
null_check!(env, cb, {});
let Ok(cb_ref) = env.new_global_ref(cb) else {
env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!")
env.throw_new(
"mp/code/exceptions/JNIException",
"Failed to pin callback reference!",
)
.expect("Failed to throw exception!");
return;
};
controller.callback(move |controller: crate::cursor::Controller| {
let jvm = super::jvm();
let mut env = jvm.attach_current_thread_permanently()
let mut env = jvm
.attach_current_thread_permanently()
.expect("failed attaching to main JVM thread");
if let Err(e) = env.with_local_frame(5, |env| {
use jni_toolbox::IntoJavaObject;
@ -43,7 +58,7 @@ fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::cursor::Co
&cb_ref,
"accept",
"(Ljava/lang/Object;)V",
&[jni::objects::JValueGen::Object(&jcontroller)]
&[jni::objects::JValueGen::Object(&jcontroller)],
) {
tracing::error!("error invoking callback: {e:?}");
};

View file

@ -1,19 +1,19 @@
pub mod client;
pub mod workspace;
pub mod cursor;
pub mod buffer;
pub mod client;
pub mod cursor;
pub mod ext;
pub mod workspace;
/// Gets or creates the relevant [tokio::runtime::Runtime].
fn tokio() -> &'static tokio::runtime::Runtime {
use std::sync::OnceLock;
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(||
RT.get_or_init(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("could not create tokio runtime")
)
})
}
/// A static reference to [jni::JavaVM] that is set on JNI load.
@ -27,10 +27,7 @@ pub(crate) fn jvm() -> std::sync::Arc<jni::JavaVM> {
/// Called upon initialisation of the JVM.
#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn JNI_OnLoad(
vm: jni::JavaVM,
_: *mut std::ffi::c_void
) -> jni::sys::jint {
pub extern "system" fn JNI_OnLoad(vm: jni::JavaVM, _: *mut std::ffi::c_void) -> jni::sys::jint {
unsafe { JVM = Some(std::sync::Arc::new(vm)) };
jni::sys::JNI_VERSION_1_1
}
@ -48,7 +45,11 @@ pub(crate) fn setup_logger(debug: bool, path: Option<String>) {
.with_source_location(false)
.compact();
let level = if debug { tracing::Level::DEBUG } else {tracing::Level::INFO };
let level = if debug {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let builder = tracing_subscriber::fmt()
.event_format(format)
@ -58,13 +59,16 @@ pub(crate) fn setup_logger(debug: bool, path: Option<String>) {
let logfile = std::fs::File::create(path).expect("failed creating logfile");
builder.with_writer(std::sync::Mutex::new(logfile)).init();
} else {
builder.with_writer(std::sync::Mutex::new(std::io::stdout())).init();
builder
.with_writer(std::sync::Mutex::new(std::io::stdout()))
.init();
}
}
/// Performs a null check on the given variable and throws a NullPointerException on the Java side
/// if it is null. Finally, it returns with the given default value.
macro_rules! null_check { // TODO replace
macro_rules! null_check {
// TODO replace
($env: ident, $var: ident, $return: expr) => {
if $var.is_null() {
let mut message = stringify!($var).to_string();
@ -80,9 +84,14 @@ pub(crate) use null_check;
impl jni_toolbox::JniToolboxError for crate::errors::ConnectionError {
fn jclass(&self) -> String {
match self {
crate::errors::ConnectionError::Transport(_) => "mp/code/exceptions/ConnectionTransportException",
crate::errors::ConnectionError::Remote(_) => "mp/code/exceptions/ConnectionRemoteException"
}.to_string()
crate::errors::ConnectionError::Transport(_) => {
"mp/code/exceptions/ConnectionTransportException"
}
crate::errors::ConnectionError::Remote(_) => {
"mp/code/exceptions/ConnectionRemoteException"
}
}
.to_string()
}
}
@ -95,9 +104,14 @@ impl jni_toolbox::JniToolboxError for crate::errors::RemoteError {
impl jni_toolbox::JniToolboxError for crate::errors::ControllerError {
fn jclass(&self) -> String {
match self {
crate::errors::ControllerError::Stopped => "mp/code/exceptions/ControllerStoppedException",
crate::errors::ControllerError::Unfulfilled => "mp/code/exceptions/ControllerUnfulfilledException",
}.to_string()
crate::errors::ControllerError::Stopped => {
"mp/code/exceptions/ControllerStoppedException"
}
crate::errors::ControllerError::Unfulfilled => {
"mp/code/exceptions/ControllerUnfulfilledException"
}
}
.to_string()
}
}
@ -106,12 +120,17 @@ macro_rules! into_java_ptr_class {
($type: ty, $jclass: literal) => {
impl<'j> jni_toolbox::IntoJavaObject<'j> for $type {
const CLASS: &'static str = $jclass;
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let class = env.find_class(Self::CLASS)?;
env.new_object(
class,
"(J)V",
&[jni::objects::JValueGen::Long(Box::into_raw(Box::new(self)) as jni::sys::jlong)]
&[jni::objects::JValueGen::Long(
Box::into_raw(Box::new(self)) as jni::sys::jlong
)],
)
}
}
@ -125,7 +144,10 @@ into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController");
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::User {
const CLASS: &'static str = "mp/code/data/User";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let id_field = self.id.into_java_object(env)?;
let name_field = env.new_string(self.name)?;
let class = env.find_class(Self::CLASS)?;
@ -134,15 +156,18 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::User {
"(Ljava/util/UUID;Ljava/lang/String;)V",
&[
jni::objects::JValueGen::Object(&id_field),
jni::objects::JValueGen::Object(&name_field)
]
jni::objects::JValueGen::Object(&name_field),
],
)
}
}
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event {
const CLASS: &'static str = "mp/code/Workspace$Event";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let (ordinal, arg) = match self {
crate::api::Event::UserJoin(arg) => (0, env.new_string(arg)?),
crate::api::Event::UserLeave(arg) => (1, env.new_string(arg)?),
@ -150,12 +175,10 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event {
};
let type_class = env.find_class("mp/code/Workspace$Event$Type")?;
let variants: jni::objects::JObjectArray = env.call_method(
type_class,
"getEnumConstants",
"()[Ljava/lang/Object;",
&[]
)?.l()?.into();
let variants: jni::objects::JObjectArray = env
.call_method(type_class, "getEnumConstants", "()[Ljava/lang/Object;", &[])?
.l()?
.into();
let event_type = env.get_object_array_element(variants, ordinal)?;
let event_class = env.find_class(Self::CLASS)?;
@ -164,23 +187,32 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event {
"(Lmp/code/Workspace$Event$Type;Ljava/lang/String;)V",
&[
jni::objects::JValueGen::Object(&event_type),
jni::objects::JValueGen::Object(&arg)
]
jni::objects::JValueGen::Object(&arg),
],
)
}
}
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange {
const CLASS: &'static str = "mp/code/data/TextChange";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let content = env.new_string(self.content)?;
let hash_class = env.find_class("java/util/OptionalLong")?;
let hash = if let Some(h) = self.hash {
env.call_static_method(hash_class, "of", "(J)Ljava/util/OptionalLong;", &[jni::objects::JValueGen::Long(h)])
env.call_static_method(
hash_class,
"of",
"(J)Ljava/util/OptionalLong;",
&[jni::objects::JValueGen::Long(h)],
)
} else {
env.call_static_method(hash_class, "empty", "()Ljava/util/OptionalLong;", &[])
}?.l()?;
}?
.l()?;
let class = env.find_class(Self::CLASS)?;
env.new_object(
@ -190,15 +222,18 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange {
jni::objects::JValueGen::Long(self.start.into()),
jni::objects::JValueGen::Long(self.end.into()),
jni::objects::JValueGen::Object(&content),
jni::objects::JValueGen::Object(&hash)
]
jni::objects::JValueGen::Object(&hash),
],
)
}
}
impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Cursor {
const CLASS: &'static str = "mp/code/data/Cursor";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let class = env.find_class("mp/code/data/Cursor")?;
let buffer = env.new_string(&self.buffer)?;
let user = if let Some(user) = self.user {
@ -216,8 +251,8 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Cursor {
jni::objects::JValueGen::Int(self.end.0),
jni::objects::JValueGen::Int(self.end.1),
jni::objects::JValueGen::Object(&buffer),
jni::objects::JValueGen::Object(&user)
]
jni::objects::JValueGen::Object(&user),
],
)
}
}
@ -226,7 +261,10 @@ macro_rules! from_java_ptr {
($type: ty) => {
impl<'j> jni_toolbox::FromJava<'j> for &mut $type {
type From = jni::sys::jobject;
fn from_java(_env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result<Self, jni::errors::Error> {
fn from_java(
_env: &mut jni::JNIEnv<'j>,
value: Self::From,
) -> Result<Self, jni::errors::Error> {
Ok(unsafe { Box::leak(Box::from_raw(value as *mut $type)) })
}
}
@ -240,9 +278,14 @@ from_java_ptr!(crate::buffer::Controller);
impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
type From = jni::objects::JObject<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, config: Self::From) -> Result<Self, jni::errors::Error> {
fn from_java(
env: &mut jni::JNIEnv<'j>,
config: Self::From,
) -> Result<Self, jni::errors::Error> {
let username = {
let jfield = env.get_field(&config, "username", "Ljava/lang/String;")?.l()?;
let jfield = env
.get_field(&config, "username", "Ljava/lang/String;")?
.l()?;
if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Username can never be null!"));
}
@ -250,7 +293,9 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
};
let password = {
let jfield = env.get_field(&config, "password", "Ljava/lang/String;")?.l()?;
let jfield = env
.get_field(&config, "password", "Ljava/lang/String;")?
.l()?;
if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Password can never be null!"));
}
@ -258,9 +303,13 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
};
let host = {
let jfield = env.get_field(&config, "host", "Ljava/util/Optional;")?.l()?;
let jfield = env
.get_field(&config, "host", "Ljava/util/Optional;")?
.l()?;
if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
let field = env.call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?.l()?;
let field = env
.call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?
.l()?;
Some(unsafe { env.get_string_unchecked(&field.into()) }?.into())
} else {
None
@ -268,7 +317,9 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
};
let port = {
let jfield = env.get_field(&config, "port", "Ljava/util/OptionalInt;")?.l()?;
let jfield = env
.get_field(&config, "port", "Ljava/util/OptionalInt;")?
.l()?;
if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
let ivalue = env.call_method(&jfield, "getAsInt", "()I", &[])?.i()?;
Some(ivalue.clamp(0, 65535) as u16)
@ -278,35 +329,55 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
};
let tls = {
let jfield = env.get_field(&config, "host", "Ljava/util/Optional;")?.l()?;
let jfield = env
.get_field(&config, "host", "Ljava/util/Optional;")?
.l()?;
if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
let field = env.call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?.l()?;
let bool_true = env.get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;")?.l()?;
Some(env.call_method(
let field = env
.call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?
.l()?;
let bool_true = env
.get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;")?
.l()?;
Some(
env.call_method(
field,
"equals",
"(Ljava/lang/Object;)Z",
&[jni::objects::JValueGen::Object(&bool_true)]
)?.z()?) // what a joke
&[jni::objects::JValueGen::Object(&bool_true)],
)?
.z()?,
) // what a joke
} else {
None
}
};
Ok(Self { username, password, host, port, tls })
Ok(Self {
username,
password,
host,
port,
tls,
})
}
}
impl<'j> jni_toolbox::FromJava<'j> for crate::api::Cursor {
type From = jni::objects::JObject<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, cursor: Self::From) -> Result<Self, jni::errors::Error> {
fn from_java(
env: &mut jni::JNIEnv<'j>,
cursor: Self::From,
) -> Result<Self, jni::errors::Error> {
let start_row = env.get_field(&cursor, "startRow", "I")?.i()?;
let start_col = env.get_field(&cursor, "startCol", "I")?.i()?;
let end_row = env.get_field(&cursor, "endRow", "I")?.i()?;
let end_col = env.get_field(&cursor, "endCol", "I")?.i()?;
let buffer = {
let jfield = env.get_field(&cursor, "buffer", "Ljava/lang/String;")?.l()?;
let jfield = env
.get_field(&cursor, "buffer", "Ljava/lang/String;")?
.l()?;
if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Buffer can never be null!"));
}
@ -322,18 +393,34 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Cursor {
}
};
Ok(Self { start: (start_row, start_col), end: (end_row, end_col), buffer, user })
Ok(Self {
start: (start_row, start_col),
end: (end_row, end_col),
buffer,
user,
})
}
}
impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange {
type From = jni::objects::JObject<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, change: Self::From) -> Result<Self, jni::errors::Error> {
let start = env.get_field(&change, "start", "J")?.j()?.clamp(0, u32::MAX.into()) as u32;
let end = env.get_field(&change, "end", "J")?.j()?.clamp(0, u32::MAX.into()) as u32;
fn from_java(
env: &mut jni::JNIEnv<'j>,
change: Self::From,
) -> Result<Self, jni::errors::Error> {
let start = env
.get_field(&change, "start", "J")?
.j()?
.clamp(0, u32::MAX.into()) as u32;
let end = env
.get_field(&change, "end", "J")?
.j()?
.clamp(0, u32::MAX.into()) as u32;
let content = {
let jfield = env.get_field(&change, "content", "Ljava/lang/String;")?.l()?;
let jfield = env
.get_field(&change, "content", "Ljava/lang/String;")?
.l()?;
if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Content can never be null!"));
}
@ -341,13 +428,20 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange {
};
let hash = {
let jfield = env.get_field(&change, "hash", "Ljava/util/OptionalLong;")?.l()?;
let jfield = env
.get_field(&change, "hash", "Ljava/util/OptionalLong;")?
.l()?;
if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
Some(env.call_method(&jfield, "getAsLong", "()J", &[])?.j()?)
} else {
None
}
};
Ok(Self { start, end, content, hash })
Ok(Self {
start,
end,
content,
hash,
})
}
}

View file

@ -1,6 +1,9 @@
use jni::{objects::JObject, JNIEnv};
use crate::{
api::controller::AsyncReceiver,
errors::{ConnectionError, ControllerError, RemoteError},
Workspace,
};
use jni_toolbox::jni;
use crate::{api::controller::AsyncReceiver, errors::{ConnectionError, ControllerError, RemoteError}, ffi::java::null_check, Workspace};
/// Get the workspace id.
#[jni(package = "mp.code", class = "Workspace")]
@ -46,7 +49,10 @@ fn create_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteEr
/// Attach to a buffer and return a pointer to its [crate::buffer::Controller].
#[jni(package = "mp.code", class = "Workspace")]
fn attach_to_buffer(workspace: &mut Workspace, path: String) -> Result<crate::buffer::Controller, ConnectionError> {
fn attach_to_buffer(
workspace: &mut Workspace,
path: String,
) -> Result<crate::buffer::Controller, ConnectionError> {
super::tokio().block_on(workspace.attach(&path))
}
@ -70,7 +76,10 @@ fn fetch_users(workspace: &mut Workspace) -> Result<(), RemoteError> {
/// List users attached to a buffer.
#[jni(package = "mp.code", class = "Workspace")]
fn list_buffer_users(workspace: &mut Workspace, path: String) -> Result<Vec<crate::api::User>, RemoteError> {
fn list_buffer_users(
workspace: &mut Workspace,
path: String,
) -> Result<Vec<crate::api::User>, RemoteError> {
super::tokio().block_on(workspace.list_buffer_users(&path))
}

View file

@ -3,60 +3,73 @@ use napi_derive::napi;
use crate::api::TextChange;
use crate::api::controller::{AsyncReceiver, AsyncSender};
use crate::buffer::controller::BufferController;
use napi::threadsafe_function::{
ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use napi_derive::napi;
#[napi]
impl BufferController {
#[napi(js_name = "callback", ts_args_type = "fun: (event: BufferController) => void")]
/// Register a callback to be invoked every time a new event is available to consume
/// There can only be one callback registered at any given time.
#[napi(
js_name = "callback",
ts_args_type = "fun: (event: BufferController) => void"
)]
pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> {
let tsfn : ThreadsafeFunction<crate::buffer::controller::BufferController, Fatal> =
fun.create_threadsafe_function(0,
let tsfn: ThreadsafeFunction<crate::buffer::controller::BufferController, Fatal> = fun
.create_threadsafe_function(
0,
|ctx: ThreadSafeCallContext<crate::buffer::controller::BufferController>| {
Ok(vec![ctx.value])
}
},
)?;
self.callback(move |controller: BufferController| {
tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error
tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking);
//check this with tracing also we could use Ok(event) to get the error
// If it blocks the main thread too many time we have to change this
});
Ok(())
}
/// Remove registered buffer callback
#[napi(js_name = "clear_callback")]
pub fn js_clear_callback(&self) {
self.clear_callback();
}
/// Get buffer path
#[napi(js_name = "get_path")]
pub fn js_path(&self) -> &str {
self.path()
}
/// Block until next buffer event without returning it
#[napi(js_name = "poll")]
pub async fn js_poll(&self) -> napi::Result<()> {
Ok(self.poll().await?)
}
/// Return next buffer event if present
#[napi(js_name = "try_recv")]
pub async fn js_try_recv(&self) -> napi::Result<Option<TextChange>> {
Ok(self.try_recv().await?)
}
/// Wait for next buffer event and return it
#[napi(js_name = "recv")]
pub async fn js_recv(&self) -> napi::Result<TextChange> {
Ok(self.recv().await?)
}
/// Send a buffer update to workspace
#[napi(js_name = "send")]
pub async fn js_send(&self, op: TextChange) -> napi::Result<()> {
Ok(self.send(op).await?)
}
/// Return buffer whole content
#[napi(js_name = "content")]
pub async fn js_content(&self) -> napi::Result<String> {
Ok(self.content().await?)

View file

@ -1,5 +1,30 @@
use napi_derive::napi;
use crate::{Client, Workspace};
use napi_derive::napi;
#[napi(object, js_name = "User")]
pub struct JsUser {
pub uuid: String,
pub name: String,
}
impl TryFrom<JsUser> for crate::api::User {
type Error = <uuid::Uuid as std::str::FromStr>::Err;
fn try_from(value: JsUser) -> Result<Self, Self::Error> {
Ok(Self {
id: value.uuid.parse()?,
name: value.name,
})
}
}
impl From<crate::api::User> for JsUser {
fn from(value: crate::api::User) -> Self {
Self {
uuid: value.id.to_string(),
name: value.name,
}
}
}
#[napi]
/// connect to codemp servers and return a client session
@ -23,13 +48,21 @@ impl Client {
#[napi(js_name = "list_workspaces")]
/// list available workspaces
pub async fn js_list_workspaces(&self, owned: bool, invited: bool) -> napi::Result<Vec<String>> {
pub async fn js_list_workspaces(
&self,
owned: bool,
invited: bool,
) -> napi::Result<Vec<String>> {
Ok(self.list_workspaces(owned, invited).await?)
}
#[napi(js_name = "invite_to_workspace")]
/// invite user to given workspace, if able to
pub async fn js_invite_to_workspace(&self, workspace: String, user: String) -> napi::Result<()> {
pub async fn js_invite_to_workspace(
&self,
workspace: String,
user: String,
) -> napi::Result<()> {
Ok(self.invite_to_workspace(workspace, user).await?)
}
@ -51,10 +84,10 @@ impl Client {
self.get_workspace(&workspace)
}
#[napi(js_name = "user_id")]
#[napi(js_name = "user")]
/// return current sessions's user id
pub fn js_user_id(&self) -> String {
self.user().id.to_string()
pub fn js_user(&self) -> JsUser {
self.user().clone().into()
}
#[napi(js_name = "active_workspaces")]

View file

@ -3,7 +3,11 @@ use napi_derive::napi;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode};
use crate::api::controller::{AsyncReceiver, AsyncSender};
use crate::cursor::controller::CursorController;
use napi::threadsafe_function::ErrorStrategy::Fatal;
use napi::threadsafe_function::{
ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use napi_derive::napi;
#[napi(object, js_name = "Cursor")]
pub struct JsCursor {
@ -26,6 +30,7 @@ impl From<JsCursor> for crate::api::Cursor {
}
}
}
impl From<crate::api::Cursor> for JsCursor {
fn from(value: crate::api::Cursor) -> Self {
JsCursor {
@ -34,48 +39,55 @@ impl From<crate::api::Cursor> for JsCursor {
end_row: value.end.0,
end_col: value.end.1,
buffer: value.buffer,
user: value.user.map(|x| x.to_string())
}
user: value.user.map(|x| x.to_string()),
}
}
}
#[napi]
impl CursorController {
#[napi(js_name = "callback", ts_args_type = "fun: (event: CursorController) => void")]
/// Register a callback to be called on receive.
/// There can only be one callback registered at any given time.
#[napi(
js_name = "callback",
ts_args_type = "fun: (event: CursorController) => void"
)]
pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> {
let tsfn : ThreadsafeFunction<crate::cursor::controller::CursorController, Fatal> =
fun.create_threadsafe_function(0,
let tsfn: ThreadsafeFunction<crate::cursor::controller::CursorController, Fatal> = fun
.create_threadsafe_function(
0,
|ctx: ThreadSafeCallContext<crate::cursor::controller::CursorController>| {
Ok(vec![ctx.value])
}
},
)?;
self.callback(move |controller: CursorController| {
tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error
tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking);
//check this with tracing also we could use Ok(event) to get the error
// If it blocks the main thread too many time we have to change this
});
Ok(())
}
/// Clear the registered callback
#[napi(js_name = "clear_callback")]
pub fn js_clear_callback(&self) {
self.clear_callback();
}
/// Send a new cursor event to remote
#[napi(js_name = "send")]
pub async fn js_send(&self, pos: JsCursor) -> napi::Result<()> {
Ok(self.send(crate::api::Cursor::from(pos)).await?)
}
/// Get next cursor event if available without blocking
#[napi(js_name = "try_recv")]
pub async fn js_try_recv(&self) -> napi::Result<Option<JsCursor>> {
Ok(self.try_recv().await?
.map(JsCursor::from))
Ok(self.try_recv().await?.map(JsCursor::from))
}
/// Block until next
#[napi(js_name = "recv")]
pub async fn js_recv(&self) -> napi::Result<JsCursor> {
Ok(self.recv().await?.into())

View file

@ -1,11 +1,12 @@
use napi_derive::napi;
/// Hash function
#[napi(js_name = "hash")]
pub fn js_hash(data: String) -> i64 {
crate::ext::hash(data)
}
/// Get the current version of the client
#[napi(js_name = "version")]
pub fn js_version() -> String {
crate::version()

View file

@ -1,9 +1,8 @@
pub mod client;
pub mod workspace;
pub mod cursor;
pub mod buffer;
pub mod client;
pub mod cursor;
pub mod ext;
pub mod workspace;
impl From<crate::errors::ConnectionError> for napi::Error {
fn from(value: crate::errors::ConnectionError) -> Self {
@ -33,7 +32,11 @@ 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 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)
@ -55,11 +58,7 @@ impl JsLogger {
#[napi]
pub async fn message(&self) -> Option<String> {
self.0
.lock()
.await
.recv()
.await
self.0.lock().await.recv().await
}
}
@ -73,5 +72,7 @@ impl std::io::Write for JsLoggerProducer {
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

View file

@ -6,6 +6,7 @@ use crate::buffer::controller::BufferController;
use crate::cursor::controller::CursorController;
use crate::api::controller::AsyncReceiver;
#[napi(object, js_name = "Event")]
pub struct JsEvent {
pub r#type: String,
@ -15,50 +16,73 @@ pub struct JsEvent {
impl From<crate::api::Event> for JsEvent {
fn from(value: crate::api::Event) -> Self {
match value {
crate::api::Event::FileTreeUpdated(value) => Self { r#type: "filetree".into(), value },
crate::api::Event::UserJoin(value) => Self { r#type: "join".into(), value },
crate::api::Event::UserLeave(value) => Self { r#type: "leave".into(), value },
crate::api::Event::FileTreeUpdated(value) => Self {
r#type: "filetree".into(),
value,
},
crate::api::Event::UserJoin(value) => Self {
r#type: "join".into(),
value,
},
crate::api::Event::UserLeave(value) => Self {
r#type: "leave".into(),
value,
},
}
}
}
#[napi]
impl Workspace {
/// Get the unique workspace id
#[napi(js_name = "id")]
pub fn js_id(&self) -> String {
self.id()
}
/// List all available buffers in this workspace
#[napi(js_name = "filetree")]
pub fn js_filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
self.filetree(filter, strict)
}
/// List all user names currently in this workspace
#[napi(js_name = "user_list")]
pub fn js_user_list(&self) -> Vec<String> {
self.user_list()
}
/// List all currently active buffers
#[napi(js_name = "buffer_list")]
pub fn js_buffer_list(&self) -> Vec<String> {
self.buffer_list()
}
/// Get workspace's Cursor Controller
#[napi(js_name = "cursor")]
pub fn js_cursor(&self) -> CursorController {
self.cursor()
}
/// Get a buffer controller by its name (path)
#[napi(js_name = "buffer_by_name")]
pub fn js_buffer_by_name(&self, path: String) -> Option<BufferController> {
self.buffer_by_name(&path)
}
/// Create a new buffer in the current workspace
#[napi(js_name = "create")]
pub async fn js_create(&self, path: String) -> napi::Result<()> {
Ok(self.create(&path).await?)
}
/// Attach to a workspace buffer, starting a BufferController
#[napi(js_name = "attach")]
pub async fn js_attach(&self, path: String) -> napi::Result<BufferController> {
Ok(self.attach(&path).await?)
}
/// Delete a buffer from workspace
#[napi(js_name = "delete")]
pub async fn js_delete(&self, path: String) -> napi::Result<()> {
Ok(self.delete(&path).await?)
@ -81,7 +105,7 @@ impl Workspace {
}
#[napi(js_name = "clear_callback")]
pub fn js_clear_callbacl(&self) -> napi::Result<()> {
pub fn js_clear_callback(&self) -> napi::Result<()> {
self.clear_callback();
Ok(())
}
@ -103,4 +127,34 @@ impl Workspace {
Ok(())
}
/// Detach from an active buffer, stopping its underlying worker
/// this method returns true if no reference or last reference was held, false if there are still
/// dangling references to clear
#[napi(js_name = "detach")]
pub async fn js_detach(&self, path: String) -> bool {
self.detach(&path)
}
/// Re-fetch remote buffer list
#[napi(js_name = "fetch_buffers")]
pub async fn js_fetch_buffers(&self) -> napi::Result<()> {
Ok(self.fetch_buffers().await?)
}
/// Re-fetch the list of all users in the workspace.
#[napi(js_name = "fetch_users")]
pub async fn js_fetch_users(&self) -> napi::Result<()> {
Ok(self.fetch_users().await?)
}
/// List users attached to a specific buffer
#[napi(js_name = "list_buffer_users")]
pub async fn js_list_buffer_users(&self, path: String) -> napi::Result<Vec<JsUser>> {
Ok(self
.list_buffer_users(&path)
.await?
.into_iter()
.map(JsUser::from)
.collect())
}
}

View file

@ -1,28 +1,41 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde;
impl LuaUserData for CodempBufferController {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
methods.add_method("send", |_, this, (change,): (CodempTextChange,)|
a_sync! { this => this.send(change).await? }
methods.add_method(
"send",
|_, this, (change,): (CodempTextChange,)| a_sync! { this => this.send(change).await? },
);
methods.add_method("try_recv", |_, this, ()| a_sync! { this => this.try_recv().await? });
methods.add_method(
"try_recv",
|_, this, ()| a_sync! { this => this.try_recv().await? },
);
methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? });
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
methods.add_method("content", |_, this, ()| a_sync! { this => this.content().await? });
methods.add_method(
"content",
|_, this, ()| a_sync! { this => this.content().await? },
);
methods.add_method("clear_callback", |_, this, ()| { this.clear_callback(); Ok(()) });
methods.add_method("clear_callback", |_, this, ()| {
this.clear_callback();
Ok(())
});
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
this.callback(move |controller: CodempBufferController| super::ext::callback().invoke(cb.clone(), controller));
this.callback(move |controller: CodempBufferController| {
super::ext::callback().invoke(cb.clone(), controller)
});
Ok(())
});
}
@ -40,7 +53,9 @@ impl LuaUserData for CodempTextChange {
}
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
methods.add_method("apply", |_, this, (txt,): (String,)| Ok(this.apply(&txt)));
}
}

View file

@ -1,6 +1,6 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde;
@ -13,22 +13,28 @@ impl LuaUserData for CodempClient {
}
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
methods.add_method("refresh", |_, this, ()|
a_sync! { this => this.refresh().await? }
methods.add_method(
"refresh",
|_, this, ()| a_sync! { this => this.refresh().await? },
);
methods.add_method("join_workspace", |_, this, (ws,):(String,)|
a_sync! { this => this.join_workspace(ws).await? }
methods.add_method(
"join_workspace",
|_, this, (ws,): (String,)| a_sync! { this => this.join_workspace(ws).await? },
);
methods.add_method("create_workspace", |_, this, (ws,):(String,)|
a_sync! { this => this.create_workspace(ws).await? }
methods.add_method(
"create_workspace",
|_, this, (ws,): (String,)| a_sync! { this => this.create_workspace(ws).await? },
);
methods.add_method("delete_workspace", |_, this, (ws,):(String,)|
a_sync! { this => this.delete_workspace(ws).await? }
methods.add_method(
"delete_workspace",
|_, this, (ws,): (String,)| a_sync! { this => this.delete_workspace(ws).await? },
);
methods.add_method("invite_to_workspace", |_, this, (ws,user):(String,String)|
@ -39,11 +45,13 @@ impl LuaUserData for CodempClient {
a_sync! { this => this.list_workspaces(owned.unwrap_or(true), invited.unwrap_or(true)).await? }
);
methods.add_method("leave_workspace", |_, this, (ws,):(String,)|
methods.add_method("leave_workspace", |_, this, (ws,): (String,)| {
Ok(this.leave_workspace(&ws))
);
});
methods.add_method("get_workspace", |_, this, (ws,):(String,)| Ok(this.get_workspace(&ws)));
methods.add_method("get_workspace", |_, this, (ws,): (String,)| {
Ok(this.get_workspace(&ws))
});
}
}

View file

@ -1,6 +1,6 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde;
@ -8,20 +8,29 @@ use super::ext::lua_tuple;
impl LuaUserData for CodempCursorController {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
methods.add_method("send", |_, this, (cursor,):(CodempCursor,)|
a_sync! { this => this.send(cursor).await? }
methods.add_method(
"send",
|_, this, (cursor,): (CodempCursor,)| a_sync! { this => this.send(cursor).await? },
);
methods.add_method("try_recv", |_, this, ()|
a_sync! { this => this.try_recv().await? }
methods.add_method(
"try_recv",
|_, this, ()| a_sync! { this => this.try_recv().await? },
);
methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? });
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? });
methods.add_method("clear_callback", |_, this, ()| { this.clear_callback(); Ok(()) });
methods.add_method("clear_callback", |_, this, ()| {
this.clear_callback();
Ok(())
});
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
this.callback(move |controller: CodempCursorController| super::ext::callback().invoke(cb.clone(), controller));
this.callback(move |controller: CodempCursorController| {
super::ext::callback().invoke(cb.clone(), controller)
});
Ok(())
});
}
@ -30,7 +39,9 @@ impl LuaUserData for CodempCursorController {
from_lua_serde! { CodempCursor }
impl LuaUserData for CodempCursor {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
}
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {

View file

@ -1,15 +1,15 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
pub(crate) fn tokio() -> &'static tokio::runtime::Runtime {
use std::sync::OnceLock;
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(||
RT.get_or_init(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("could not create tokio runtime")
)
})
}
macro_rules! a_sync {
@ -32,32 +32,39 @@ macro_rules! a_sync {
pub(crate) use a_sync;
pub(crate) struct Promise(pub(crate) Option<tokio::task::JoinHandle<LuaResult<super::callback::CallbackArg>>>);
pub(crate) struct Promise(
pub(crate) Option<tokio::task::JoinHandle<LuaResult<super::callback::CallbackArg>>>,
);
impl LuaUserData for Promise {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("ready", |_, this|
fields.add_field_method_get("ready", |_, this| {
Ok(this.0.as_ref().map_or(true, |x| x.is_finished()))
);
});
}
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// TODO: await MUST NOT be used in callbacks!!
methods.add_method_mut("await", |_, this, ()| match this.0.take() {
None => Err(LuaError::runtime("Promise already awaited")),
Some(x) => {
tokio()
.block_on(x)
.map_err(LuaError::runtime)?
},
Some(x) => tokio().block_on(x).map_err(LuaError::runtime)?,
});
methods.add_method_mut("and_then", |_, this, (cb,):(LuaFunction,)| match this.0.take() {
methods.add_method_mut("cancel", |_, this, ()| match this.0.take() {
None => Err(LuaError::runtime("Promise already awaited")),
Some(x) => {
tokio()
.spawn(async move {
x.abort();
Ok(())
},
});
methods.add_method_mut("and_then", |_, this, (cb,): (LuaFunction,)| {
match this.0.take() {
None => Err(LuaError::runtime("Promise already awaited")),
Some(x) => {
tokio().spawn(async move {
match x.await {
Err(e) => tracing::error!("could not join promise to run callback: {e}"),
Err(e) => {
tracing::error!("could not join promise to run callback: {e}")
}
Ok(res) => match res {
Err(e) => super::callback().failure(e),
Ok(val) => super::callback().invoke(cb, val),
@ -65,7 +72,8 @@ impl LuaUserData for Promise {
}
});
Ok(())
},
}
}
});
}
}
@ -89,12 +97,16 @@ pub(crate) fn setup_driver(_: &Lua, (block,):(Option<bool>,)) -> LuaResult<Optio
}
#[derive(Debug)]
pub(crate) struct Driver(pub(crate) tokio::sync::mpsc::UnboundedSender<()>, Option<std::thread::JoinHandle<()>>);
pub(crate) struct Driver(
pub(crate) tokio::sync::mpsc::UnboundedSender<()>,
Option<std::thread::JoinHandle<()>>,
);
impl LuaUserData for Driver {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_method_mut("stop", |_, this, ()| {
match this.1.take() {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
methods.add_method_mut("stop", |_, this, ()| match this.1.take() {
None => Ok(false),
Some(handle) => {
if this.0.send(()).is_err() {
@ -104,10 +116,7 @@ impl LuaUserData for Driver {
Err(e) => Err(LuaError::runtime(format!("runtime thread panicked: {e:?}"))),
Ok(()) => Ok(true),
}
},
}
});
}
}

View file

@ -1,7 +1,7 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*;
use crate::ext::IgnorableError;
use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
pub(crate) fn callback() -> &'static CallbackChannel<LuaCallback> {
static CHANNEL: std::sync::OnceLock<CallbackChannel<LuaCallback>> = std::sync::OnceLock::new();
@ -10,7 +10,7 @@ pub(crate) fn callback() -> &'static CallbackChannel<LuaCallback> {
pub(crate) struct CallbackChannel<T> {
tx: std::sync::Arc<tokio::sync::mpsc::UnboundedSender<T>>,
rx: std::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<T>>
rx: std::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<T>>,
}
impl Default for CallbackChannel<LuaCallback> {
@ -26,12 +26,16 @@ impl Default for CallbackChannel<LuaCallback> {
impl CallbackChannel<LuaCallback> {
pub(crate) fn invoke(&self, cb: LuaFunction, arg: impl Into<CallbackArg>) {
self.tx.send(LuaCallback::Invoke(cb, arg.into()))
self.tx
.send(LuaCallback::Invoke(cb, arg.into()))
.unwrap_or_warn("error scheduling callback")
}
pub(crate) fn failure(&self, err: impl std::error::Error) {
self.tx.send(LuaCallback::Fail(format!("promise failed with error: {err:?}")))
self.tx
.send(LuaCallback::Fail(format!(
"promise failed with error: {err:?}"
)))
.unwrap_or_warn("error scheduling callback failure")
}
@ -40,12 +44,12 @@ impl CallbackChannel<LuaCallback> {
Err(e) => {
tracing::debug!("backing off from callback mutex: {e}");
None
},
}
Ok(mut lock) => match lock.try_recv() {
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
tracing::error!("callback channel closed");
None
},
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => None,
Ok(cb) => Some(cb),
},

View file

@ -1,7 +1,7 @@
use std::{io::Write, sync::Mutex};
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use tokio::sync::mpsc;
#[derive(Debug, Clone)]
@ -12,12 +12,21 @@ impl Write for LuaLoggerProducer {
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
// TODO can we make this less verbose?
pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>)) -> LuaResult<bool> {
let level = if debug.unwrap_or_default() { tracing::Level::DEBUG } else {tracing::Level::INFO };
pub(crate) fn setup_tracing(
_: &Lua,
(printer, debug): (LuaValue, Option<bool>),
) -> LuaResult<bool> {
let level = if debug.unwrap_or_default() {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let format = tracing_subscriber::fmt::format()
.with_level(true)
.with_target(true)
@ -36,16 +45,15 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>))
| LuaValue::Thread(_)
| LuaValue::UserData(_)
| LuaValue::Error(_) => return Err(LuaError::BindError), // TODO full BadArgument type??
LuaValue::Nil => {
tracing_subscriber::fmt()
LuaValue::Nil => tracing_subscriber::fmt()
.event_format(format)
.with_max_level(level)
.with_writer(std::sync::Mutex::new(std::io::stderr()))
.try_init()
.is_ok()
},
.is_ok(),
LuaValue::String(path) => {
let logfile = std::fs::File::create(path.to_string_lossy()).map_err(|e| LuaError::RuntimeError(e.to_string()))?;
let logfile = std::fs::File::create(path.to_string_lossy())
.map_err(|e| LuaError::RuntimeError(e.to_string()))?;
tracing_subscriber::fmt()
.event_format(format)
.with_max_level(level)
@ -53,7 +61,7 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>))
.with_ansi(false)
.try_init()
.is_ok()
},
}
LuaValue::Function(cb) => {
let (tx, mut rx) = mpsc::unbounded_channel();
let res = tracing_subscriber::fmt()
@ -71,7 +79,7 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>))
});
}
res
},
}
};
Ok(success)

View file

@ -2,8 +2,8 @@ pub mod a_sync;
pub mod callback;
pub mod log;
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
pub(crate) use a_sync::tokio;
pub(crate) use callback::callback;

View file

@ -1,42 +1,64 @@
mod client;
mod workspace;
mod cursor;
mod buffer;
mod client;
mod cursor;
mod ext;
mod workspace;
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
// define multiple entrypoints, so this library can have multiple names and still work
#[mlua::lua_module(name = "codemp")] fn entry_1(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) }
#[mlua::lua_module(name = "libcodemp")] fn entry_2(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) }
#[mlua::lua_module(name = "codemp_native")] fn entry_3(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) }
#[mlua::lua_module(name = "codemp_lua")] fn entry_4(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) }
#[mlua::lua_module(name = "codemp")]
fn entry_1(lua: &Lua) -> LuaResult<LuaTable> {
entrypoint(lua)
}
#[mlua::lua_module(name = "libcodemp")]
fn entry_2(lua: &Lua) -> LuaResult<LuaTable> {
entrypoint(lua)
}
#[mlua::lua_module(name = "codemp_native")]
fn entry_3(lua: &Lua) -> LuaResult<LuaTable> {
entrypoint(lua)
}
#[mlua::lua_module(name = "codemp_lua")]
fn entry_4(lua: &Lua) -> LuaResult<LuaTable> {
entrypoint(lua)
}
fn entrypoint(lua: &Lua) -> LuaResult<LuaTable> {
let exports = lua.create_table()?;
// entrypoint
exports.set("connect", lua.create_function(|_, (config,):(CodempConfig,)|
ext::a_sync::a_sync! { => CodempClient::connect(config).await? }
)?)?;
exports.set(
"connect",
lua.create_function(
|_, (config,): (CodempConfig,)| ext::a_sync::a_sync! { => CodempClient::connect(config).await? },
)?,
)?;
// utils
exports.set("hash", lua.create_function(|_, (txt,):(String,)|
Ok(crate::ext::hash(txt))
)?)?;
exports.set(
"hash",
lua.create_function(|_, (txt,): (String,)| Ok(crate::ext::hash(txt)))?,
)?;
exports.set("version", lua.create_function(|_, ()|
Ok(crate::version())
)?)?;
exports.set(
"version",
lua.create_function(|_, ()| Ok(crate::version()))?,
)?;
// runtime
exports.set("setup_driver", lua.create_function(ext::a_sync::setup_driver)?)?;
exports.set("poll_callback", lua.create_function(|lua, ()| {
exports.set(
"setup_driver",
lua.create_function(ext::a_sync::setup_driver)?,
)?;
exports.set(
"poll_callback",
lua.create_function(|lua, ()| {
let mut val = LuaMultiValue::new();
match ext::callback().recv() {
None => {},
None => {}
Some(ext::callback::LuaCallback::Invoke(cb, arg)) => {
val.push_back(LuaValue::Function(cb));
val.push_back(arg.into_lua(lua)?);
@ -44,30 +66,34 @@ fn entrypoint(lua: &Lua) -> LuaResult<LuaTable> {
Some(ext::callback::LuaCallback::Fail(msg)) => {
val.push_back(false.into_lua(lua)?);
val.push_back(msg.into_lua(lua)?);
},
}
}
Ok(val)
})?)?;
})?,
)?;
// logging
exports.set("setup_tracing", lua.create_function(ext::log::setup_tracing)?)?;
exports.set(
"setup_tracing",
lua.create_function(ext::log::setup_tracing)?,
)?;
Ok(exports)
}
impl From::<crate::errors::ConnectionError> for LuaError {
impl From<crate::errors::ConnectionError> for LuaError {
fn from(value: crate::errors::ConnectionError) -> Self {
LuaError::runtime(value.to_string())
}
}
impl From::<crate::errors::RemoteError> for LuaError {
impl From<crate::errors::RemoteError> for LuaError {
fn from(value: crate::errors::RemoteError) -> Self {
LuaError::runtime(value.to_string())
}
}
impl From::<crate::errors::ControllerError> for LuaError {
impl From<crate::errors::ControllerError> for LuaError {
fn from(value: crate::errors::ControllerError) -> Self {
LuaError::runtime(value.to_string())
}

View file

@ -1,40 +1,52 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde;
impl LuaUserData for CodempWorkspace {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_method("create", |_, this, (name,):(String,)|
a_sync! { this => this.create(&name).await? }
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
methods.add_method(
"create",
|_, this, (name,): (String,)| a_sync! { this => this.create(&name).await? },
);
methods.add_method("attach", |_, this, (name,):(String,)|
a_sync! { this => this.attach(&name).await? }
methods.add_method(
"attach",
|_, this, (name,): (String,)| a_sync! { this => this.attach(&name).await? },
);
methods.add_method("detach", |_, this, (name,):(String,)|
methods.add_method("detach", |_, this, (name,): (String,)| {
Ok(this.detach(&name))
});
methods.add_method(
"delete",
|_, this, (name,): (String,)| a_sync! { this => this.delete(&name).await? },
);
methods.add_method("delete", |_, this, (name,):(String,)|
a_sync! { this => this.delete(&name).await? }
);
methods.add_method("get_buffer", |_, this, (name,):(String,)| Ok(this.buffer_by_name(&name)));
methods.add_method("get_buffer", |_, this, (name,): (String,)| {
Ok(this.buffer_by_name(&name))
});
methods.add_method("fetch_buffers", |_, this, ()|
a_sync! { this => this.fetch_buffers().await? }
);
methods.add_method("fetch_users", |_, this, ()|
a_sync! { this => this.fetch_users().await? }
methods.add_method(
"fetch_users",
|_, this, ()| a_sync! { this => this.fetch_users().await? },
);
methods.add_method("filetree", |_, this, (filter, strict,):(Option<String>, Option<bool>,)|
methods.add_method(
"filetree",
|_, this, (filter, strict): (Option<String>, Option<bool>)| {
Ok(this.filetree(filter.as_deref(), strict.unwrap_or(false)))
},
);
methods.add_method("user_list", |_, this, ()|
@ -62,6 +74,7 @@ impl LuaUserData for CodempWorkspace {
this.clear_callback();
Ok(())
});
}
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
@ -75,7 +88,9 @@ impl LuaUserData for CodempWorkspace {
from_lua_serde! { CodempEvent }
impl LuaUserData for CodempEvent {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this)));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{:?}", this))
});
}
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
@ -87,8 +102,7 @@ impl LuaUserData for CodempEvent {
fields.add_field_method_get("value", |_, this| match this {
CodempEvent::FileTreeUpdated(x)
| CodempEvent::UserJoin(x)
| CodempEvent::UserLeave(x)
=> Ok(x.clone()),
| CodempEvent::UserLeave(x) => Ok(x.clone()),
});
}
}

View file

@ -1,5 +1,5 @@
use codemp_proto::{
common::Token, buffer::buffer_client::BufferClient, cursor::cursor_client::CursorClient,
buffer::buffer_client::BufferClient, common::Token, cursor::cursor_client::CursorClient,
workspace::workspace_client::WorkspaceClient,
};
use tonic::{
@ -14,10 +14,7 @@ type AuthedService = InterceptedService<Channel, WorkspaceInterceptor>;
#[derive(Debug, Clone)]
pub struct SessionInterceptor(pub tokio::sync::watch::Receiver<codemp_proto::common::Token>);
impl tonic::service::Interceptor for SessionInterceptor {
fn call(
&mut self,
mut request: tonic::Request<()>,
) -> tonic::Result<tonic::Request<()>> {
fn call(&mut self, mut request: tonic::Request<()>) -> tonic::Result<tonic::Request<()>> {
if let Ok(token) = self.0.borrow().token.parse() {
request.metadata_mut().insert("session", token);
}

View file

@ -13,8 +13,6 @@ pub use crate::api::{
};
pub use crate::{
client::Client as CodempClient,
workspace::Workspace as CodempWorkspace,
cursor::Controller as CodempCursorController,
buffer::Controller as CodempBufferController,
buffer::Controller as CodempBufferController, client::Client as CodempClient,
cursor::Controller as CodempCursorController, workspace::Workspace as CodempWorkspace,
};

View file

@ -134,6 +134,11 @@ impl Workspace {
Ok(ws)
}
/// drop arc, return true if was last
pub(crate) fn consume(self) -> bool {
Arc::into_inner(self.0).is_some()
}
/// Create a new buffer in the current workspace.
pub async fn create(&self, path: &str) -> RemoteResult<()> {
let mut workspace_client = self.0.services.ws();
@ -191,7 +196,7 @@ impl Workspace {
Some((_name, controller)) => match Arc::into_inner(controller.0) {
None => false, // dangling ref! we can't drop this
Some(_) => true, // dropping it now
}
},
}
}
@ -261,7 +266,6 @@ impl Workspace {
}))
.await?;
self.0.filetree.remove(path);
Ok(())
@ -308,7 +312,7 @@ impl Workspace {
/// A filter may be applied, and it may be strict (equality check) or not (starts_with check).
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
self.0
let mut tree = self.0
.filetree
.iter()
.filter(|f| {
@ -321,7 +325,9 @@ impl Workspace {
})
})
.map(|f| f.clone())
.collect()
.collect::<Vec<String>>();
tree.sort();
tree
}
pub(crate) fn run_actor(
@ -333,13 +339,18 @@ impl Workspace {
let weak = Arc::downgrade(&self.0);
let name = self.id();
tokio::spawn(async move {
tracing::debug!("workspace worker starting");
loop {
// TODO can we stop responsively rather than poll for Arc being dropped?
if weak.upgrade().is_none() { break };
if weak.upgrade().is_none() {
break;
};
let Some(res) = tokio::select!(
x = stream.message() => Some(x),
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => None,
) else { continue };
) else {
continue;
};
match res {
Err(e) => break tracing::error!("workspace '{}' stream closed: {}", name, e),
Ok(None) => break tracing::info!("leaving workspace {}", name),
@ -376,6 +387,7 @@ impl Workspace {
}
}
}
tracing::debug!("workspace worker stopping");
});
}
}