feat: switched from neon to napi

Co-authored-by: alemi <me@alemi.dev>
This commit is contained in:
frelodev 2023-08-19 18:43:22 +02:00
parent 6681a53da9
commit 7f9422103a
6 changed files with 247 additions and 262 deletions

View file

@ -1,2 +1,6 @@
[net]
git-fetch-with-cli = true
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
rustflags = ["-C", "target-feature=-crt-static"]

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ Cargo.lock
/client/vscode/*.vsix
/client/vscode/codemp.node
node_modules

View file

@ -3,20 +3,27 @@ name = "codemp-vscode"
version = "0.0.1"
description = "VSCode extension for CodeMP"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
[dependencies]
codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.3" }
# codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.3" }
codemp = { path = "../../lib", features = ["global"]}
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1.3.1", features = ["v4"] }
once_cell = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rmpv = "1"
clap = { version = "4.2.1", features = ["derive"] }
async-trait = "0.1.68"
neon = { version = "0.10.1", default-features = false, features = ["channel-api", "napi-6", "promise-api"] }
napi = { version = "2", features = ["full"] }
napi-derive = "2"
futures = "0.3.28"
tokio = {version = "1.32.0", features = ["full"] }
[build-dependencies]
napi-build = "2"
[profile.release]
lto = true

5
build.rs Normal file
View file

@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}

View file

@ -1,38 +1,96 @@
{
"name": "@codemp/vscode",
"version": "0.2.0",
"description": "codemp bindings for vscode plugin",
"main": "index.js",
"files": [
"index.js"
],
"napi": {
"name": "codemp-vscode",
"version": "0.0.1",
"description": "VSCode extension for CodeMP",
"main": "./out/extension.js",
"triples": {
"defaults": true,
"additional": [
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"i686-pc-windows-msvc",
"armv7-unknown-linux-gnueabihf",
"aarch64-apple-darwin",
"aarch64-linux-android",
"x86_64-unknown-freebsd",
"aarch64-unknown-linux-musl",
"aarch64-pc-windows-msvc",
"armv7-linux-androideabi"
]
}
},
"engines": {
"vscode": "^1.32.0"
"node": ">= 10"
},
"scripts": {
"build": "cargo-cp-artifact --artifact cdylib codemp-vscode codemp.node -- cargo build --release --message-format=json-render-diagnostics",
"install": "npm run build",
"test": "cargo test"
"artifacts": "napi artifacts",
"bench": "node -r @swc-node/register benchmark/bench.ts",
"build": "napi build --platform --release --pipe \"prettier -w\"",
"build:debug": "napi build --platform --pipe \"prettier -w\"",
"format": "run-p format:prettier format:rs format:toml",
"format:prettier": "prettier . -w",
"format:toml": "taplo format",
"format:rs": "cargo fmt",
"lint": "eslint . -c ./.eslintrc.yml",
"prepublishOnly": "napi prepublish -t npm",
"test": "ava",
"version": "napi version"
},
"devDependencies": {
"cargo-cp-artifact": "^0.1"
"@napi-rs/cli": "^2.14.6",
"@swc-node/register": "^1.5.5",
"@swc/core": "^1.3.32",
"@taplo/cli": "^0.5.2",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"ava": "^5.1.1",
"benny": "^3.7.1",
"chalk": "^5.2.0",
"eslint": "^8.33.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
},
"contributes": {
"commands": [
{
"command": "codemp.connect",
"title": "Connect to CodeMP"
"lint-staged": {
"*.@(js|ts|tsx)": [
"eslint -c .eslintrc.yml --fix"
],
"*.@(js|ts|tsx|yml|yaml|md|json)": [
"prettier --write"
],
"*.toml": [
"taplo format"
]
},
{
"command": "codemp.join",
"title": "Join remote session"
},
{
"command": "codemp.share",
"title": "Share local session"
"ava": {
"require": [
"@swc-node/register"
],
"extensions": [
"ts"
],
"timeout": "2m",
"workerThreads": false,
"environmentVariables": {
"TS_NODE_PROJECT": "./tsconfig.json"
}
]
},
"activationEvents": [
"onCommand:codemp.connect",
"onCommand:codemp.join",
"onCommand:codemp.share"
]
"prettier": {
"printWidth": 120,
"semi": false,
"trailingComma": "all",
"singleQuote": true,
"arrowParens": "always"
},
"packageManager": "yarn@3.6.2"
}

View file

@ -1,268 +1,177 @@
#![deny(clippy::all)]
use std::sync::Arc;
use neon::prelude::*;
use once_cell::sync::OnceCell;
use futures::prelude::*;
use napi::bindgen_prelude::*;
use codemp::{
controller::cursor::{CursorSubscriber, CursorControllerHandle},
controller::buffer::{OperationControllerHandle, OperationControllerSubscriber},
client::CodempClient,
factory::OperationFactory,
proto::buffer_client::BufferClient,
prelude::*,
proto::{RowCol, CursorEvent},
buffer::factory::OperationFactory, ot::OperationSeq
};
use codemp::tokio::{runtime::Runtime, sync::Mutex};
use napi_derive::napi;
use napi::tokio::{self, fs};
fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> {
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
#[derive(Debug)]
struct JsCodempError(CodempError);
RUNTIME.get_or_try_init(|| {
Runtime::new()
.or_else(|err| cx.throw_error(err.to_string()))
})
}
fn tuple<'a, C: Context<'a>>(cx: &mut C, a: i32, b: i32) -> NeonResult<Handle<'a, JsArray>> {
let obj = cx.empty_array();
let a_val = cx.number(a);
obj.set(cx, 0, a_val)?;
let b_val = cx.number(b);
obj.set(cx, 1, b_val)?;
Ok(obj)
}
fn unpack_tuple<'a, C: Context<'a>>(cx: &mut C, arr: Handle<'a, JsArray>) -> NeonResult<(i32, i32)> {
Ok((
arr.get::<JsNumber, _, u32>(cx, 0)?.value(cx) as i32,
arr.get::<JsNumber, _, u32>(cx, 1)?.value(cx) as i32,
))
}
struct ClientHandle(Arc<Mutex<CodempClient>>);
impl Finalize for ClientHandle {}
fn connect(mut cx: FunctionContext) -> JsResult<JsPromise> {
let host = cx.argument::<JsString>(0).ok().map(|x| x.value(&mut cx));
let (deferred, promise) = cx.promise();
let channel = cx.channel();
runtime(&mut cx)?.spawn(async move {
match BufferClient::connect(host.unwrap_or("".into())).await {
Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::<String, neon::handle::Handle<JsString>>(format!("{}", e))),
Ok(c) => deferred.settle_with(&channel, |mut cx| {
let obj = cx.empty_object();
let boxed_value = cx.boxed(ClientHandle(Arc::new(Mutex::new(c.into()))));
obj.set(&mut cx, "boxed", boxed_value)?;
let method_create = JsFunction::new(&mut cx, create_client)?;
obj.set(&mut cx, "create", method_create)?;
let method_listen = JsFunction::new(&mut cx, listen_client)?;
obj.set(&mut cx, "listen", method_listen)?;
let method_attach = JsFunction::new(&mut cx, attach_client)?;
obj.set(&mut cx, "attach", method_attach)?;
Ok(obj)
}),
impl From::<JsCodempError> for napi::Error {
fn from(value: JsCodempError) -> Self {
napi::Error::new(Status::GenericFailure, &format!("CodempError: {:?}", value))
}
});
Ok(promise)
}
fn create_client(mut cx: FunctionContext) -> JsResult<JsPromise> {
let path = cx.argument::<JsString>(0)?.value(&mut cx);
let content = cx.argument::<JsString>(1).ok().map(|x| x.value(&mut cx));
let this = cx.this();
let boxed : Handle<JsBox<ClientHandle>> = this.get(&mut cx, "boxed")?;
#[napi]
pub fn connect(addr: String) -> napi::Result<()> {
CODEMP_INSTANCE.connect(&addr)
.map_err(|e| JsCodempError(e).into())
}
let rc = boxed.0.clone();
let (deferred, promise) = cx.promise();
let channel = cx.channel();
runtime(&mut cx)?.spawn(async move {
match rc.lock().await.create(path, content).await {
Ok(accepted) => deferred.settle_with(&channel, move |mut cx| Ok(cx.boolean(accepted))),
Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::<String, neon::handle::Handle<JsString>>(e.to_string())),
/// CURSOR
#[napi]
pub fn join(session: String) -> napi::Result<JsCursorController> {
let controller = CODEMP_INSTANCE.join(&session)
.map_err(|e| napi::Error::from(JsCodempError(e)))?;
Ok(controller.into())
}
#[napi]
pub struct JsCursorController(Arc<CodempCursorController>);
impl From::<Arc<CodempCursorController>> for JsCursorController {
fn from(value: Arc<CodempCursorController>) -> Self {
JsCursorController(value)
}
});
Ok(promise)
}
fn listen_client(mut cx: FunctionContext) -> JsResult<JsPromise> {
let this = cx.this();
let boxed : Handle<JsBox<ClientHandle>> = this.get(&mut cx, "boxed")?;
#[napi]
impl JsCursorController {
let rc = boxed.0.clone();
let (deferred, promise) = cx.promise();
let channel = cx.channel();
runtime(&mut cx)?.spawn(async move {
match rc.lock().await.listen().await {
Ok(controller) => {
deferred.settle_with(&channel, move |mut cx| {
let obj = cx.empty_object();
let boxed_value = cx.boxed(CursorEventsHandle(controller));
obj.set(&mut cx, "boxed", boxed_value)?;
let callback_method = JsFunction::new(&mut cx, callback_cursor)?;
obj.set(&mut cx, "callback", callback_method)?;
let send_method = JsFunction::new(&mut cx, send_cursor)?;
obj.set(&mut cx, "send", send_method)?;
Ok(obj)
})
},
Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::<String, neon::handle::Handle<JsString>>(e.to_string())),
}
});
Ok(promise)
}
fn attach_client(mut cx: FunctionContext) -> JsResult<JsPromise> {
let this = cx.this();
let boxed : Handle<JsBox<ClientHandle>> = this.get(&mut cx, "boxed")?;
let path = cx.argument::<JsString>(0)?.value(&mut cx);
let rc = boxed.0.clone();
let (deferred, promise) = cx.promise();
let channel = cx.channel();
runtime(&mut cx)?.spawn(async move {
match rc.lock().await.attach(path).await {
Ok(controller) => {
deferred.settle_with(&channel, move |mut cx| {
let obj = cx.empty_object();
let boxed_value = cx.boxed(OperationControllerJs(controller));
obj.set(&mut cx, "boxed", boxed_value)?;
let apply_method = JsFunction::new(&mut cx, apply_operation)?;
obj.set(&mut cx, "apply", apply_method)?;
let content_method = JsFunction::new(&mut cx, content_operation)?;
obj.set(&mut cx, "content", content_method)?;
let callback_method = JsFunction::new(&mut cx, callback_operation)?;
obj.set(&mut cx, "callback", callback_method)?;
Ok(obj)
})
},
Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::<String, neon::handle::Handle<JsString>>(e.to_string())),
}
});
Ok(promise)
}
struct OperationControllerJs(OperationControllerHandle);
impl Finalize for OperationControllerJs {}
fn apply_operation(mut cx: FunctionContext) -> JsResult<JsPromise> {
let this = cx.this();
let boxed : Handle<JsBox<OperationControllerJs>> = this.get(&mut cx, "boxed")?;
let skip = cx.argument::<JsNumber>(0)?.value(&mut cx).round() as usize;
let text = cx.argument::<JsString>(1)?.value(&mut cx);
let tail = cx.argument::<JsNumber>(2)?.value(&mut cx).round() as usize;
let rc = boxed.0.clone();
let (deferred, promise) = cx.promise();
let channel = cx.channel();
runtime(&mut cx)?.spawn(async move {
if let Some(op) = rc.delta(skip, text.as_str(), tail) {
rc.apply(op).await;
deferred.settle_with(&channel, move |mut cx| Ok(cx.boolean(true)));
} else {
deferred.settle_with(&channel, move |mut cx| Ok(cx.undefined()));
}
});
Ok(promise)
}
fn content_operation(mut cx: FunctionContext) -> JsResult<JsString> {
let this = cx.this();
let boxed : Handle<JsBox<OperationControllerJs>> = this.get(&mut cx, "boxed")?;
Ok(cx.string(boxed.0.content()))
}
fn callback_operation(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let this = cx.this();
let boxed : Handle<JsBox<OperationControllerJs>> = this.get(&mut cx, "boxed")?;
let callback = Arc::new(cx.argument::<JsFunction>(0)?.root(&mut cx));
let mut rc = boxed.0.clone();
let channel = cx.channel();
// TODO when garbage collecting OperationController stop this worker
runtime(&mut cx)?.spawn(async move {
while let Some(edit) = rc.poll().await {
let cb = callback.clone();
channel.send(move |mut cx| {
cb.to_inner(&mut cx)
.call_with(&cx)
.arg(cx.number(edit.span.start as i32))
.arg(cx.number(edit.span.end as i32))
.arg(cx.string(edit.content))
.apply::<JsUndefined, _>(&mut cx)?;
Ok(())
});
}
});
Ok(cx.undefined())
}
struct CursorEventsHandle(CursorControllerHandle);
impl Finalize for CursorEventsHandle {}
fn callback_cursor(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let this = cx.this();
let boxed : Handle<JsBox<CursorEventsHandle>> = this.get(&mut cx, "boxed")?;
let callback = Arc::new(cx.argument::<JsFunction>(0)?.root(&mut cx));
let mut rc = boxed.0.clone();
let channel = cx.channel();
// TODO when garbage collecting OperationController stop this worker
runtime(&mut cx)?.spawn(async move {
while let Some(op) = rc.poll().await {
let cb = callback.clone();
channel.send(move |mut cx| {
cb.to_inner(&mut cx)
.call_with(&cx)
.arg(cx.string(&op.user))
.arg(cx.string(&op.buffer))
.arg(tuple(&mut cx, op.start().row, op.start().col)?)
.arg(tuple(&mut cx, op.end().row, op.end().col)?)
.apply::<JsUndefined, _>(&mut cx)?;
Ok(())
});
#[napi]
pub async fn recv(&self) -> napi::Result<JsCursorEvent> {
Ok(
self.0.recv().await
.map_err(|e| napi::Error::from(JsCodempError(e)))?
.into()
)
}
});
Ok(cx.undefined())
#[napi]
pub async fn send(&self, buffer: String, start: (i32, i32), end: (i32, i32)) -> napi::Result<()> {
let pos = CodempCursorPosition { buffer, start: Some(RowCol::from(start)), end: Some(RowCol::from(end)) };
self.0.send(pos).await
.map_err(|e| napi::Error::from(JsCodempError(e)))
}
}
fn send_cursor(mut cx: FunctionContext) -> JsResult<JsPromise> {
let this = cx.this();
let boxed : Handle<JsBox<CursorEventsHandle>> = this.get(&mut cx, "boxed")?;
let path = cx.argument::<JsString>(0)?.value(&mut cx);
let start_obj = cx.argument::<JsArray>(1)?;
let start = unpack_tuple(&mut cx, start_obj)?;
let end_obj = cx.argument::<JsArray>(2)?;
let end = unpack_tuple(&mut cx, end_obj)?;
let rc = boxed.0.clone();
let (deferred, promise) = cx.promise();
let channel = cx.channel();
runtime(&mut cx)?.spawn(async move {
rc.send(&path, start.into(), end.into()).await;
deferred.settle_with(&channel, |mut cx| Ok(cx.undefined()))
});
Ok(promise)
#[napi(object)]
pub struct JsCursorEvent {
pub user: String,
pub buffer: String,
pub start: JsRowCol,
pub end: JsRowCol,
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("connect", connect)?;
Ok(())
impl From::<CursorEvent> for JsCursorEvent {
fn from(value: CursorEvent) -> Self {
let pos = value.position.unwrap_or_default();
let start = pos.start.unwrap_or_default();
let end = pos.end.unwrap_or_default();
JsCursorEvent {
user: value.user,
buffer: pos.buffer,
start: JsRowCol { row: start.row, col: start.col },
end: JsRowCol { row: end.row, col: end.col },
}
}
}
#[napi(object)]
pub struct JsRowCol {
pub row: i32,
pub col: i32
}
impl From::<RowCol> for JsRowCol {
fn from(value: RowCol) -> Self {
JsRowCol { row: value.row, col: value.col }
}
}
/// BUFFER
#[napi(object)]
pub struct JsTextChange {
pub span: JSRange,
pub content: String,
}
#[napi(object)]
pub struct JSRange{
pub start: i32,
pub end: Option<i32>,
}
impl From::<CodempTextChange> for JsTextChange {
fn from(value: CodempTextChange) -> Self {
JsTextChange {
// TODO how is x.. represented ? span.end can never be None
span: JSRange { start: value.span.start as i32, end: Some(value.span.end as i32) },
content: value.content,
}
}
}
impl From::<Arc<CodempBufferController>> for JsBufferController {
fn from(value: Arc<CodempBufferController>) -> Self {
JsBufferController(value)
}
}
#[napi]
pub struct JsBufferController(Arc<CodempBufferController>);
#[napi]
impl JsBufferController {
#[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 async fn send(&self, op: OperationSeq) -> napi::Result<()> {
self.0.send(op).await
.map_err(|e| napi::Error::from(JsCodempError(e)))
}
}
#[napi]
pub fn create(path: String, content: Option<String>) -> napi::Result<()> {
CODEMP_INSTANCE.create(&path, content.as_deref())
.map_err(|e| napi::Error::from(JsCodempError(e)))
}
#[napi]
pub fn attach(path: String) -> napi::Result<JsBufferController> {
Ok(
CODEMP_INSTANCE.attach(&path)
.map_err(|e| napi::Error::from(JsCodempError(e)))?
.into()
)
}