Updated Javascript Glue with Napi

Co-authored-by: alemi.dev <me@alemi.dev>
This commit is contained in:
frelodev 2024-08-07 23:06:33 +02:00
parent e4b6e82485
commit ffa8d8ea82
9 changed files with 125 additions and 144 deletions

View file

@ -42,8 +42,8 @@ derive_more = { version = "0.99.17", optional = true }
# glue (js) # glue (js)
rmpv = { version = "1", optional = true } rmpv = { version = "1", optional = true }
napi = { version = "2", features = ["full"], optional = true } napi = { version = "2.16", features = ["full"], optional = true }
napi-derive = { version="2", optional = true} napi-derive = { version="2.16", optional = true}
futures = { version = "0.3.28", optional = true } futures = { version = "0.3.28", optional = true }
# glue (python) # glue (python)
@ -60,7 +60,7 @@ napi-build = { version = "2", optional = true }
pyo3-build-config = { version = "0.19.2", optional = true } pyo3-build-config = { version = "0.19.2", optional = true }
[features] [features]
default = [] default = ["js"]
lua = ["mlua", "derive_more", "lazy_static", "tracing-subscriber"] lua = ["mlua", "derive_more", "lazy_static", "tracing-subscriber"]
java = ["lazy_static", "jni", "tracing-subscriber"] java = ["lazy_static", "jni", "tracing-subscriber"]
java-artifact = ["java"] # also builds the jar java-artifact = ["java"] # also builds the jar

View file

@ -30,11 +30,15 @@ pub struct Op(pub(crate) woot::crdt::Op);
/// to delete a the fourth character we should send a /// to delete a the fourth character we should send a
/// `TextChange { span: 3..4, content: "".into() }` /// `TextChange { span: 3..4, content: "".into() }`
/// ///
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "python", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct TextChange { pub struct TextChange {
/// range of text change, as char indexes in buffer previous state /// range start of text change, as char indexes in buffer previous state
pub span: std::ops::Range<usize>, pub start: u32,
/// range end of text change, as char indexes in buffer previous state
pub end: u32,
/// new content of text inside span /// new content of text inside span
pub content: String, pub content: String,
} }
@ -65,11 +69,16 @@ impl TextChange {
let end_after = after.len() - end; let end_after = after.len() - end;
TextChange { TextChange {
span: start..end_before, start: start as u32,
end: end_before as u32,
content: after[start..end_after].to_string(), content: after[start..end_after].to_string(),
} }
} }
pub fn span(&self) -> std::ops::Range<usize> {
self.start as usize .. self.end as usize
}
/// consume the [TextChange], transforming it into a Vec of [Op] /// consume the [TextChange], transforming it into a Vec of [Op]
pub fn transform(self, woot: &Woot) -> WootResult<Vec<Op>> { pub fn transform(self, woot: &Woot) -> WootResult<Vec<Op>> {
let mut out = Vec::new(); let mut out = Vec::new();
@ -77,19 +86,19 @@ impl TextChange {
return Ok(out); return Ok(out);
} // no-op } // no-op
let view = woot.view(); let view = woot.view();
let Some(span) = view.get(self.span.clone()) else { let Some(span) = view.get(self.span()) else {
return Err(crate::woot::WootError::OutOfBounds); return Err(crate::woot::WootError::OutOfBounds);
}; };
let diff = similar::TextDiff::from_chars(span, &self.content); let diff = similar::TextDiff::from_chars(span, &self.content);
for (i, diff) in diff.iter_all_changes().enumerate() { for (i, diff) in diff.iter_all_changes().enumerate() {
match diff.tag() { match diff.tag() {
similar::ChangeTag::Equal => {} similar::ChangeTag::Equal => {}
similar::ChangeTag::Delete => match woot.delete_one(self.span.start + i) { similar::ChangeTag::Delete => match woot.delete_one(self.span().start + i) {
Err(e) => tracing::error!("could not create deletion: {}", e), Err(e) => tracing::error!("could not create deletion: {}", e),
Ok(op) => out.push(Op(op)), Ok(op) => out.push(Op(op)),
}, },
similar::ChangeTag::Insert => { similar::ChangeTag::Insert => {
match woot.insert(self.span.start + i, diff.value()) { match woot.insert(self.span().start + i, diff.value()) {
Ok(ops) => { Ok(ops) => {
for op in ops { for op in ops {
out.push(Op(op)) out.push(Op(op))
@ -105,7 +114,7 @@ impl TextChange {
/// returns true if this TextChange deletes existing text /// returns true if this TextChange deletes existing text
pub fn is_deletion(&self) -> bool { pub fn is_deletion(&self) -> bool {
!self.span.is_empty() !self.span().is_empty()
} }
/// returns true if this TextChange adds new text /// returns true if this TextChange adds new text
@ -120,9 +129,9 @@ impl TextChange {
/// applies this text change to given text, returning a new string /// applies this text change to given text, returning a new string
pub fn apply(&self, txt: &str) -> String { pub fn apply(&self, txt: &str) -> String {
let pre_index = std::cmp::min(self.span.start, txt.len()); let pre_index = std::cmp::min(self.span().start, txt.len());
let pre = txt.get(..pre_index).unwrap_or("").to_string(); let pre = txt.get(..pre_index).unwrap_or("").to_string();
let post = txt.get(self.span.end..).unwrap_or("").to_string(); let post = txt.get(self.span().end..).unwrap_or("").to_string();
format!("{}{}{}", pre, self.content, post) format!("{}{}{}", pre, self.content, post)
} }
@ -144,7 +153,7 @@ mod tests {
"sphinx of black quartz, judge my vow", "sphinx of black quartz, judge my vow",
"sphinx of quartz, judge my vow", "sphinx of quartz, judge my vow",
); );
assert_eq!(change.span, 10..16); assert_eq!(change.span(), 10..16);
assert_eq!(change.content, ""); assert_eq!(change.content, "");
} }
@ -154,7 +163,7 @@ mod tests {
"sphinx of quartz, judge my vow", "sphinx of quartz, judge my vow",
"sphinx of black quartz, judge my vow", "sphinx of black quartz, judge my vow",
); );
assert_eq!(change.span, 10..10); assert_eq!(change.span(), 10..10);
assert_eq!(change.content, "black "); assert_eq!(change.content, "black ");
} }
@ -164,14 +173,14 @@ mod tests {
"sphinx of black quartz, judge my vow", "sphinx of black quartz, judge my vow",
"sphinx who watches the desert, judge my vow", "sphinx who watches the desert, judge my vow",
); );
assert_eq!(change.span, 7..22); assert_eq!(change.span(), 7..22);
assert_eq!(change.content, "who watches the desert"); assert_eq!(change.content, "who watches the desert");
} }
#[test] #[test]
fn textchange_apply_works_for_insertions() { fn textchange_apply_works_for_insertions() {
let change = super::TextChange { let change = super::TextChange {
span: 5..5, start: 5, end: 5,
content: " cruel".to_string(), content: " cruel".to_string(),
}; };
let result = change.apply("hello world!"); let result = change.apply("hello world!");
@ -181,7 +190,7 @@ mod tests {
#[test] #[test]
fn textchange_apply_works_for_deletions() { fn textchange_apply_works_for_deletions() {
let change = super::TextChange { let change = super::TextChange {
span: 5..11, start: 5, end: 11,
content: "".to_string(), content: "".to_string(),
}; };
let result = change.apply("hello cruel world!"); let result = change.apply("hello cruel world!");
@ -191,7 +200,7 @@ mod tests {
#[test] #[test]
fn textchange_apply_works_for_replacements() { fn textchange_apply_works_for_replacements() {
let change = super::TextChange { let change = super::TextChange {
span: 5..11, start: 5, end: 11,
content: " not very pleasant".to_string(), content: " not very pleasant".to_string(),
}; };
let result = change.apply("hello cruel world!"); let result = change.apply("hello cruel world!");
@ -201,7 +210,7 @@ mod tests {
#[test] #[test]
fn textchange_apply_never_panics() { fn textchange_apply_never_panics() {
let change = super::TextChange { let change = super::TextChange {
span: 100..110, start: 100, end: 110,
content: "a very long string \n which totally matters".to_string(), content: "a very long string \n which totally matters".to_string(),
}; };
let result = change.apply("a short text"); let result = change.apply("a short text");
@ -220,7 +229,7 @@ mod tests {
#[test] #[test]
fn empty_textchange_doesnt_alter_buffer() { fn empty_textchange_doesnt_alter_buffer() {
let change = super::TextChange { let change = super::TextChange {
span: 42..42, start: 42, end: 42,
content: "".to_string(), content: "".to_string(),
}; };
let result = change.apply("some important text"); let result = change.apply("some important text");

View file

@ -22,6 +22,7 @@ use crate::api::TextChange;
/// upon dropping this handle will stop the associated worker /// upon dropping this handle will stop the associated worker
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "python", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi)]
pub struct BufferController(Arc<BufferControllerInner>); pub struct BufferController(Arc<BufferControllerInner>);
#[derive(Debug)] #[derive(Debug)]

View file

@ -30,6 +30,7 @@ use codemp_proto::cursor::{CursorEvent, CursorPosition};
/// upon dropping this handle will stop the associated worker /// upon dropping this handle will stop the associated worker
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "python", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi)]
pub struct CursorController(Arc<CursorControllerInner>); pub struct CursorController(Arc<CursorControllerInner>);
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,56 +1,25 @@
use std::sync::Arc; use std::sync::Arc;
use napi::threadsafe_function::{ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi::threadsafe_function::{ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};
use napi_derive::napi; use napi_derive::napi;
use crate::api::Controller; use crate::api::TextChange;
use crate::ffi::js::JsCodempError; use crate::ffi::js::JsCodempError;
use crate::api::Controller;
use crate::prelude::*;
/// BUFFER
#[napi(object)]
pub struct JsTextChange {
pub span: JsRange,
pub content: String,
}
#[napi(object)]
pub struct JsRange{
pub start: i32,
pub end: i32,
}
impl From::<crate::api::TextChange> for JsTextChange { impl From<crate::Error> for napi::Error {
fn from(value: crate::api::TextChange) -> Self { fn from(value: crate::Error) -> Self {
JsTextChange { let msg = format!("{value}");
// TODO how is x.. represented ? span.end can never be None match value {
span: JsRange { start: value.span.start as i32, end: value.span.end as i32 }, crate::Error::Deadlocked => napi::Error::new(napi::Status::WouldDeadlock, msg),
content: value.content, _ => napi::Error::new(napi::Status::GenericFailure, msg),
} }
} }
} }
impl From::<Arc<crate::buffer::Controller>> for JsBufferController {
fn from(value: Arc<crate::buffer::Controller>) -> Self {
JsBufferController(value)
}
}
#[napi] #[napi]
pub struct JsBufferController(Arc<crate::buffer::Controller>); impl CodempBufferController {
/*#[napi]
pub fn delta(string : String, start: i64, txt: String, end: i64 ) -> Option<JsCodempOperationSeq> {
Some(JsCodempOperationSeq(string.diff(start as usize, &txt, end as usize)?))
}*/
#[napi]
impl JsBufferController {
#[napi(ts_args_type = "fun: (event: JsTextChange) => void")] #[napi(ts_args_type = "fun: (event: JsTextChange) => void")]
@ -58,10 +27,10 @@ impl JsBufferController {
let tsfn : ThreadsafeFunction<crate::api::TextChange, Fatal> = let tsfn : ThreadsafeFunction<crate::api::TextChange, Fatal> =
fun.create_threadsafe_function(0, fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<crate::api::TextChange>| { |ctx : ThreadSafeCallContext<crate::api::TextChange>| {
Ok(vec![JsTextChange::from(ctx.value)]) Ok(vec![ctx.value])
} }
)?; )?;
let _controller = self.0.clone(); let _controller = self.clone();
tokio::spawn(async move { tokio::spawn(async move {
//tokio::time::sleep(std::time::Duration::from_secs(1)).await; //tokio::time::sleep(std::time::Duration::from_secs(1)).await;
loop { loop {
@ -79,32 +48,13 @@ impl JsBufferController {
} }
#[napi] #[napi(js_name = "recv")]
pub fn content(&self) -> String { pub async fn jsrecv(&self) -> napi::Result<TextChange> {
self.0.content() Ok(self.recv().await?.into())
} }
#[napi] #[napi]
pub fn get_name(&self) -> String { pub fn send(&self, op: TextChange) -> napi::Result<()> {
self.0.name().to_string() Ok(self.send(op)?)
}
#[napi]
pub async fn recv(&self) -> napi::Result<JsTextChange> {
Ok(
self.0.recv().await
.map_err(|e| napi::Error::from(JsCodempError(e)))?
.into()
)
}
#[napi]
pub fn send(&self, op: JsTextChange) -> napi::Result<()> {
// TODO might be nice to take ownership of the opseq
let new_text_change = crate::api::TextChange {
span: op.span.start as usize .. op.span.end as usize,
content: op.content,
};
Ok(self.0.send(new_text_change).map_err(JsCodempError)?)
} }
} }

View file

@ -1,6 +1,5 @@
use napi_derive::napi; use napi_derive::napi;
use crate::ffi::js::JsCodempError; use crate::ffi::js::JsCodempError;
use crate::ffi::js::workspace::JsWorkspace;
#[napi] #[napi]
/// main codemp client session /// main codemp client session
@ -10,8 +9,7 @@ pub struct JsCodempClient(tokio::sync::RwLock<crate::Client>);
/// connect to codemp servers and return a client session /// connect to codemp servers and return a client session
pub async fn connect(addr: Option<String>) -> napi::Result<JsCodempClient>{ pub async fn connect(addr: Option<String>) -> napi::Result<JsCodempClient>{
let client = crate::Client::new(addr.as_deref().unwrap_or("http://codemp.alemi.dev:50053")) let client = crate::Client::new(addr.as_deref().unwrap_or("http://codemp.alemi.dev:50053"))
.await .await?;
.map_err(JsCodempError)?;
Ok(JsCodempClient(tokio::sync::RwLock::new(client))) Ok(JsCodempClient(tokio::sync::RwLock::new(client)))
} }
@ -21,14 +19,14 @@ impl JsCodempClient {
#[napi] #[napi]
/// login against AuthService with provided credentials, optionally requesting access to a workspace /// login against AuthService with provided credentials, optionally requesting access to a workspace
pub async fn login(&self, username: String, password: String, workspace_id: Option<String>) -> napi::Result<()> { pub async fn login(&self, username: String, password: String, workspace_id: Option<String>) -> napi::Result<()> {
self.0.read().await.login(username, password, workspace_id).await.map_err(JsCodempError)?; self.0.read().await.login(username, password, workspace_id).await?;
Ok(()) Ok(())
} }
#[napi] #[napi]
/// join workspace with given id (will start its cursor controller) /// join workspace with given id (will start its cursor controller)
pub async fn join_workspace(&self, workspace: String) -> napi::Result<JsWorkspace> { pub async fn join_workspace(&self, workspace: String) -> napi::Result<JsWorkspace> {
Ok(JsWorkspace::from(self.0.write().await.join_workspace(&workspace).await.map_err(JsCodempError)?)) Ok(JsWorkspace::from(self.0.write().await.join_workspace(&workspace).await?))
} }
#[napi] #[napi]

View file

@ -3,34 +3,64 @@ use napi_derive::napi;
use uuid::Uuid; use uuid::Uuid;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy};
use crate::api::Controller; use crate::api::Controller;
use crate::ffi::js::JsCodempError; use crate::prelude::*;
#[napi]
pub struct JsCursorController(Arc<crate::cursor::Controller>);
impl From::<Arc<crate::cursor::Controller>> for JsCursorController {
fn from(value: Arc<crate::cursor::Controller>) -> Self {
JsCursorController(value) #[napi(js_name = "Cursor")]
pub struct JsCursor {
/// range of text change, as char indexes in buffer previous state
pub start_row: i32,
pub start_col: i32,
pub end_row: i32,
pub end_col: i32,
pub buffer: String,
pub user: Option<String>,
}
impl From<JsCursor> for CodempCursor {
fn from(value: JsCursor) -> Self {
CodempCursor {
start : (value.start_row, value.start_col),
end: (value.end_row, value.end_col),
buffer: value.buffer,
user: value.user.map(|x| uuid::Uuid::parse_str(&x).expect("invalid uuid")),
}
}
}
impl From<CodempCursor> for JsCursor {
fn from(value: CodempCursor) -> Self {
JsCursor {
start_row : value.start.0,
start_col : value.start.1,
end_row : value.end.0,
end_col: value.end.1,
buffer: value.buffer,
user: value.user.map(|x| x.to_string())
}
} }
} }
#[napi] #[napi]
impl JsCursorController { impl CodempCursorController {
#[napi(ts_args_type = "fun: (event: JsCursorEvent) => void")] #[napi(ts_args_type = "fun: (event: JsCursorEvent) => void")]
pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{
let tsfn : ThreadsafeFunction<codemp_proto::cursor::CursorEvent, ErrorStrategy::Fatal> = let tsfn : ThreadsafeFunction<JsCursor, ErrorStrategy::Fatal> =
fun.create_threadsafe_function(0, fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<codemp_proto::cursor::CursorEvent>| { |ctx : ThreadSafeCallContext<JsCursor>| {
Ok(vec![JsCursorEvent::from(ctx.value)]) Ok(vec![ctx.value])
} }
)?; )?;
let _controller = self.0.clone(); let _controller = self.clone();
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
match _controller.recv().await { match _controller.recv().await {
Ok(event) => { Ok(event) => {
tsfn.call(event.clone(), ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error tsfn.call(event.into(), ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error
}, },
Err(crate::Error::Deadlocked) => continue, Err(crate::Error::Deadlocked) => continue,
Err(e) => break tracing::warn!("error receiving: {}", e), Err(e) => break tracing::warn!("error receiving: {}", e),
@ -41,13 +71,8 @@ impl JsCursorController {
} }
#[napi] #[napi]
pub fn send(&self, buffer: String, start: (i32, i32), end: (i32, i32)) -> napi::Result<()> { pub fn send(&self, pos: &CodempCursorController) -> napi::Result<()> {
let pos = codemp_proto::cursor::CursorPosition { Ok(self.send(pos)?)
buffer: buffer.into(),
start: codemp_proto::cursor::RowCol::from(start),
end: codemp_proto::cursor::RowCol::from(end),
};
Ok(self.0.send(pos).map_err(JsCodempError)?)
} }
} }

View file

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

View file

@ -21,6 +21,9 @@ use tokio::sync::mpsc;
use tonic::Streaming; use tonic::Streaming;
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "js")]
use napi_derive::napi;
//TODO may contain more info in the future //TODO may contain more info in the future
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UserInfo { pub struct UserInfo {
@ -29,6 +32,7 @@ pub struct UserInfo {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass)] #[cfg_attr(feature = "python", pyo3::pyclass)]
#[cfg_attr(feature = "js", napi)]
pub struct Workspace(Arc<WorkspaceInner>); pub struct Workspace(Arc<WorkspaceInner>);
#[derive(Debug)] #[derive(Debug)]
@ -227,27 +231,32 @@ impl Workspace {
} }
/// get the id of the workspace /// get the id of the workspace
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn id(&self) -> String { pub fn id(&self) -> String {
self.0.id.clone() self.0.id.clone()
} }
/// return a reference to current cursor controller, if currently in a workspace /// return a reference to current cursor controller, if currently in a workspace
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn cursor(&self) -> cursor::Controller { pub fn cursor(&self) -> cursor::Controller {
self.0.cursor.clone() self.0.cursor.clone()
} }
/// get a new reference to a buffer controller, if any is active to given path /// get a new reference to a buffer controller, if any is active to given path
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn buffer_by_name(&self, path: &str) -> Option<buffer::Controller> { pub fn buffer_by_name(&self, path: &str) -> Option<buffer::Controller> {
self.0.buffers.get(path).map(|x| x.clone()) self.0.buffers.get(path).map(|x| x.clone())
} }
/// get a list of all the currently attached to buffers /// get a list of all the currently attached to buffers
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn buffer_list(&self) -> Vec<String> { pub fn buffer_list(&self) -> Vec<String> {
self.0.buffers.iter().map(|elem| elem.key().clone()).collect() self.0.buffers.iter().map(|elem| elem.key().clone()).collect()
} }
/// get the currently cached "filetree" /// get the currently cached "filetree"
// #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn filetree(&self) -> Vec<String> { pub fn filetree(&self) -> Vec<String> {
self.0.filetree.iter().map(|f| f.clone()).collect() self.0.filetree.iter().map(|f| f.clone()).collect()
} }
} }