Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions apps/oxlint/src/js_plugins/external_linter.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::sync::{Arc, atomic::Ordering, mpsc::channel};
use std::{
sync::{Arc, atomic::Ordering, mpsc::channel},
time::Duration,
};

use napi::{
Status,
Expand Down Expand Up @@ -326,8 +329,40 @@ fn wrap_create_workspace(cb: JsCreateWorkspaceCb) -> ExternalLinterCreateWorkspa
}

/// Wrap `destroyWorkspace` JS callback as a normal Rust function.
///
/// The JS-side `destroyWorkspace` function is synchronous, but it's wrapped in a `ThreadsafeFunction`,
/// so cannot be called synchronously. Use an `mpsc::channel` to wait for the result from JS side.
///
/// Uses a timeout to prevent indefinite blocking during shutdown, which can cause issues
/// in multi-root workspace scenarios where multiple workspaces are being destroyed concurrently.
fn wrap_destroy_workspace(cb: JsDestroyWorkspaceCb) -> ExternalLinterDestroyWorkspaceCb {
Arc::new(Box::new(move |workspace_uri| {
let _ = cb.call(workspace_uri, ThreadsafeFunctionCallMode::NonBlocking);
let (tx, rx) = channel();

// Send data to JS
let status = cb.call_with_return_value(
workspace_uri,
ThreadsafeFunctionCallMode::NonBlocking,
move |result, _env| {
// Ignore send errors - the receiver may have timed out
let _ = tx.send(result);
Ok(())
},
);

if status == Status::Ok {
// Use a timeout to prevent blocking indefinitely during shutdown.
// If JS side doesn't respond within the timeout, we proceed with shutdown anyway.
match rx.recv_timeout(Duration::from_secs(5)) {
// Destroying workspace succeeded
Ok(Ok(()))
// Timeout or sender dropped - proceed with shutdown
| Err(_) => Ok(()),
// `destroyWorkspace` threw an error
Ok(Err(err)) => Err(format!("`destroyWorkspace` threw an error: {err}")),
}
} else {
Err(format!("Failed to schedule `destroyWorkspace` callback: {status:?}"))
}
}))
}
6 changes: 5 additions & 1 deletion apps/oxlint/src/lsp/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,11 @@ impl ToolBuilder for ServerLinterBuilder {
fn shutdown(&self, root_uri: &Uri) {
// Destroy JS workspace
if let Some(external_linter) = &self.external_linter {
(external_linter.destroy_workspace)(root_uri.as_str().to_string());
let res = (external_linter.destroy_workspace)(root_uri.as_str().to_string());

if let Err(err) = res {
error!("Failed to destroy JS workspace:\n{err}\n");
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/oxc_linter/src/external_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use crate::{
pub type ExternalLinterCreateWorkspaceCb =
Arc<Box<dyn Fn(String) -> Result<(), String> + Send + Sync>>;

pub type ExternalLinterDestroyWorkspaceCb = Arc<Box<dyn Fn(String) + Send + Sync>>;
pub type ExternalLinterDestroyWorkspaceCb =
Arc<Box<dyn Fn(String) -> Result<(), String> + Send + Sync>>;

pub type ExternalLinterLoadPluginCb = Arc<
Box<
Expand Down
Loading