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] [net]
git-fetch-with-cli = true 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/*.vsix
/client/vscode/codemp.node /client/vscode/codemp.node
node_modules

View file

@ -3,20 +3,27 @@ name = "codemp-vscode"
version = "0.0.1" version = "0.0.1"
description = "VSCode extension for CodeMP" description = "VSCode extension for CodeMP"
edition = "2021" edition = "2021"
exclude = ["index.node"]
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [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 = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
uuid = { version = "1.3.1", features = ["v4"] } uuid = { version = "1.3.1", features = ["v4"] }
once_cell = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
rmpv = "1" rmpv = "1"
clap = { version = "4.2.1", features = ["derive"] }
async-trait = "0.1.68" 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", "name": "codemp-vscode",
"version": "0.0.1", "triples": {
"description": "VSCode extension for CodeMP", "defaults": true,
"main": "./out/extension.js", "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": { "engines": {
"vscode": "^1.32.0" "node": ">= 10"
}, },
"scripts": { "scripts": {
"build": "cargo-cp-artifact --artifact cdylib codemp-vscode codemp.node -- cargo build --release --message-format=json-render-diagnostics", "artifacts": "napi artifacts",
"install": "npm run build", "bench": "node -r @swc-node/register benchmark/bench.ts",
"test": "cargo test" "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": { "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": { "lint-staged": {
"commands": [ "*.@(js|ts|tsx)": [
{ "eslint -c .eslintrc.yml --fix"
"command": "codemp.connect", ],
"title": "Connect to CodeMP" "*.@(js|ts|tsx|yml|yaml|md|json)": [
}, "prettier --write"
{ ],
"command": "codemp.join", "*.toml": [
"title": "Join remote session" "taplo format"
},
{
"command": "codemp.share",
"title": "Share local session"
}
] ]
}, },
"activationEvents": [ "ava": {
"onCommand:codemp.connect", "require": [
"onCommand:codemp.join", "@swc-node/register"
"onCommand:codemp.share" ],
] "extensions": [
"ts"
],
"timeout": "2m",
"workerThreads": false,
"environmentVariables": {
"TS_NODE_PROJECT": "./tsconfig.json"
}
},
"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 std::sync::Arc;
use futures::prelude::*;
use neon::prelude::*; use napi::bindgen_prelude::*;
use once_cell::sync::OnceCell;
use codemp::{ use codemp::{
controller::cursor::{CursorSubscriber, CursorControllerHandle}, prelude::*,
controller::buffer::{OperationControllerHandle, OperationControllerSubscriber}, proto::{RowCol, CursorEvent},
client::CodempClient, buffer::factory::OperationFactory, ot::OperationSeq
factory::OperationFactory,
proto::buffer_client::BufferClient,
}; };
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> { #[derive(Debug)]
static RUNTIME: OnceCell<Runtime> = OnceCell::new(); struct JsCodempError(CodempError);
RUNTIME.get_or_try_init(|| { impl From::<JsCodempError> for napi::Error {
Runtime::new() fn from(value: JsCodempError) -> Self {
.or_else(|err| cx.throw_error(err.to_string())) napi::Error::new(Status::GenericFailure, &format!("CodempError: {:?}", value))
}) }
} }
fn tuple<'a, C: Context<'a>>(cx: &mut C, a: i32, b: i32) -> NeonResult<Handle<'a, JsArray>> { #[napi]
let obj = cx.empty_array(); pub fn connect(addr: String) -> napi::Result<()> {
let a_val = cx.number(a); CODEMP_INSTANCE.connect(&addr)
obj.set(cx, 0, a_val)?; .map_err(|e| JsCodempError(e).into())
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)
}),
}
});
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")?;
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())),
}
});
Ok(promise)
}
fn listen_client(mut cx: FunctionContext) -> JsResult<JsPromise> {
let this = cx.this();
let boxed : Handle<JsBox<ClientHandle>> = this.get(&mut cx, "boxed")?;
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> { /// CURSOR
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(); #[napi]
let (deferred, promise) = cx.promise(); pub fn join(session: String) -> napi::Result<JsCursorController> {
let channel = cx.channel(); let controller = CODEMP_INSTANCE.join(&session)
.map_err(|e| napi::Error::from(JsCodempError(e)))?;
runtime(&mut cx)?.spawn(async move { Ok(controller.into())
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> { #[napi]
let this = cx.this(); pub struct JsCursorController(Arc<CodempCursorController>);
let boxed : Handle<JsBox<OperationControllerJs>> = this.get(&mut cx, "boxed")?;
Ok(cx.string(boxed.0.content())) impl From::<Arc<CodempCursorController>> for JsCursorController {
fn from(value: Arc<CodempCursorController>) -> Self {
JsCursorController(value)
}
} }
fn callback_operation(mut cx: FunctionContext) -> JsResult<JsUndefined> { #[napi]
let this = cx.this(); impl JsCursorController {
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(); #[napi]
let channel = cx.channel(); pub async fn recv(&self) -> napi::Result<JsCursorEvent> {
Ok(
// TODO when garbage collecting OperationController stop this worker self.0.recv().await
runtime(&mut cx)?.spawn(async move { .map_err(|e| napi::Error::from(JsCodempError(e)))?
while let Some(edit) = rc.poll().await { .into()
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); #[napi]
impl Finalize for CursorEventsHandle {} 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)) };
fn callback_cursor(mut cx: FunctionContext) -> JsResult<JsUndefined> { self.0.send(pos).await
let this = cx.this(); .map_err(|e| napi::Error::from(JsCodempError(e)))
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(object)]
pub struct JsCursorEvent {
Ok(cx.undefined()) pub user: String,
pub buffer: String,
pub start: JsRowCol,
pub end: JsRowCol,
} }
fn send_cursor(mut cx: FunctionContext) -> JsResult<JsPromise> { impl From::<CursorEvent> for JsCursorEvent {
let this = cx.this(); fn from(value: CursorEvent) -> Self {
let boxed : Handle<JsBox<CursorEventsHandle>> = this.get(&mut cx, "boxed")?; let pos = value.position.unwrap_or_default();
let path = cx.argument::<JsString>(0)?.value(&mut cx); let start = pos.start.unwrap_or_default();
let start_obj = cx.argument::<JsArray>(1)?; let end = pos.end.unwrap_or_default();
let start = unpack_tuple(&mut cx, start_obj)?; JsCursorEvent {
let end_obj = cx.argument::<JsArray>(2)?; user: value.user,
let end = unpack_tuple(&mut cx, end_obj)?; buffer: pos.buffer,
start: JsRowCol { row: start.row, col: start.col },
let rc = boxed.0.clone(); end: JsRowCol { row: end.row, col: end.col },
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)
} }
#[neon::main] #[napi(object)]
fn main(mut cx: ModuleContext) -> NeonResult<()> { pub struct JsRowCol {
cx.export_function("connect", connect)?; pub row: i32,
pub col: i32
Ok(())
} }
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()
)
}