11use std:: {
22 path:: Path ,
3- sync:: { Arc , Mutex } ,
3+ sync:: { Arc , RwLock } ,
44} ;
55
66use napi:: {
@@ -73,21 +73,24 @@ pub type JsSortTailwindClassesCb = ThreadsafeFunction<
7373/// Holds raw ThreadsafeFunctions wrapped in Option for cleanup.
7474/// The TSFNs can be explicitly dropped via `cleanup()` to prevent
7575/// use-after-free during V8 cleanup on Node.js exit.
76+ ///
77+ /// Uses `RwLock` instead of `Mutex` for better performance: the wrapper functions
78+ /// only read from the Option (common path), while only `cleanup()` needs write access.
7679#[ derive( Clone ) ]
7780struct TsfnHandles {
78- init : Arc < Mutex < Option < JsInitExternalFormatterCb > > > ,
79- format_embedded : Arc < Mutex < Option < JsFormatEmbeddedCb > > > ,
80- format_file : Arc < Mutex < Option < JsFormatFileCb > > > ,
81- sort_tailwind : Arc < Mutex < Option < JsSortTailwindClassesCb > > > ,
81+ init : Arc < RwLock < Option < JsInitExternalFormatterCb > > > ,
82+ format_embedded : Arc < RwLock < Option < JsFormatEmbeddedCb > > > ,
83+ format_file : Arc < RwLock < Option < JsFormatFileCb > > > ,
84+ sort_tailwind : Arc < RwLock < Option < JsSortTailwindClassesCb > > > ,
8285}
8386
8487impl TsfnHandles {
8588 /// Drop all ThreadsafeFunctions to prevent use-after-free during V8 cleanup.
8689 fn cleanup ( & self ) {
87- let _ = self . init . lock ( ) . unwrap ( ) . take ( ) ;
88- let _ = self . format_embedded . lock ( ) . unwrap ( ) . take ( ) ;
89- let _ = self . format_file . lock ( ) . unwrap ( ) . take ( ) ;
90- let _ = self . sort_tailwind . lock ( ) . unwrap ( ) . take ( ) ;
90+ let _ = self . init . write ( ) . unwrap ( ) . take ( ) ;
91+ let _ = self . format_embedded . write ( ) . unwrap ( ) . take ( ) ;
92+ let _ = self . format_file . write ( ) . unwrap ( ) . take ( ) ;
93+ let _ = self . sort_tailwind . write ( ) . unwrap ( ) . take ( ) ;
9194 }
9295}
9396
@@ -145,11 +148,11 @@ impl ExternalFormatter {
145148 format_file_cb : JsFormatFileCb ,
146149 sort_tailwindcss_classes_cb : JsSortTailwindClassesCb ,
147150 ) -> Self {
148- // Wrap TSFNs in Arc<Mutex <Option<...>>> so they can be explicitly dropped
149- let init_handle = Arc :: new ( Mutex :: new ( Some ( init_cb) ) ) ;
150- let format_embedded_handle = Arc :: new ( Mutex :: new ( Some ( format_embedded_cb) ) ) ;
151- let format_file_handle = Arc :: new ( Mutex :: new ( Some ( format_file_cb) ) ) ;
152- let sort_tailwind_handle = Arc :: new ( Mutex :: new ( Some ( sort_tailwindcss_classes_cb) ) ) ;
151+ // Wrap TSFNs in Arc<RwLock <Option<...>>> so they can be explicitly dropped
152+ let init_handle = Arc :: new ( RwLock :: new ( Some ( init_cb) ) ) ;
153+ let format_embedded_handle = Arc :: new ( RwLock :: new ( Some ( format_embedded_cb) ) ) ;
154+ let format_file_handle = Arc :: new ( RwLock :: new ( Some ( format_file_cb) ) ) ;
155+ let sort_tailwind_handle = Arc :: new ( RwLock :: new ( Some ( sort_tailwindcss_classes_cb) ) ) ;
153156
154157 // Create handles struct for cleanup
155158 let handles = TsfnHandles {
@@ -243,10 +246,10 @@ impl ExternalFormatter {
243246 // Therefore, just provides a dummy external formatter that consistently returns errors.
244247 Self {
245248 handles : TsfnHandles {
246- init : Arc :: new ( Mutex :: new ( None ) ) ,
247- format_embedded : Arc :: new ( Mutex :: new ( None ) ) ,
248- format_file : Arc :: new ( Mutex :: new ( None ) ) ,
249- sort_tailwind : Arc :: new ( Mutex :: new ( None ) ) ,
249+ init : Arc :: new ( RwLock :: new ( None ) ) ,
250+ format_embedded : Arc :: new ( RwLock :: new ( None ) ) ,
251+ format_file : Arc :: new ( RwLock :: new ( None ) ) ,
252+ sort_tailwind : Arc :: new ( RwLock :: new ( None ) ) ,
250253 } ,
251254 init : Arc :: new ( |_| Err ( "Dummy init called" . to_string ( ) ) ) ,
252255 format_embedded : Arc :: new ( |_, _, _| Err ( "Dummy format_embedded called" . to_string ( ) ) ) ,
@@ -289,11 +292,11 @@ fn language_to_prettier_parser(language: &str) -> Option<&'static str> {
289292
290293/// Wrap JS `initExternalFormatter` callback as a normal Rust function.
291294fn wrap_init_external_formatter (
292- cb_handle : Arc < Mutex < Option < JsInitExternalFormatterCb > > > ,
295+ cb_handle : Arc < RwLock < Option < JsInitExternalFormatterCb > > > ,
293296) -> InitExternalFormatterCallback {
294297 Arc :: new ( move |num_threads : usize | {
295298 debug_span ! ( "oxfmt::external::init" , num_threads = num_threads) . in_scope ( || {
296- let guard = cb_handle. lock ( ) . unwrap ( ) ;
299+ let guard = cb_handle. read ( ) . unwrap ( ) ;
297300 let Some ( cb) = guard. as_ref ( ) else {
298301 return Err ( "JS callback unavailable (environment shutting down)" . to_string ( ) ) ;
299302 } ;
@@ -320,11 +323,11 @@ fn wrap_init_external_formatter(
320323
321324/// Wrap JS `formatEmbeddedCode` callback as a normal Rust function.
322325fn wrap_format_embedded (
323- cb_handle : Arc < Mutex < Option < JsFormatEmbeddedCb > > > ,
326+ cb_handle : Arc < RwLock < Option < JsFormatEmbeddedCb > > > ,
324327) -> FormatEmbeddedWithConfigCallback {
325328 Arc :: new ( move |options : & Value , parser_name : & str , code : & str | {
326329 debug_span ! ( "oxfmt::external::format_embedded" , parser = %parser_name) . in_scope ( || {
327- let guard = cb_handle. lock ( ) . unwrap ( ) ;
330+ let guard = cb_handle. read ( ) . unwrap ( ) ;
328331 let Some ( cb) = guard. as_ref ( ) else {
329332 return Err ( "JS callback unavailable (environment shutting down)" . to_string ( ) ) ;
330333 } ;
@@ -360,10 +363,12 @@ fn wrap_format_embedded(
360363}
361364
362365/// Wrap JS `formatFile` callback as a normal Rust function.
363- fn wrap_format_file ( cb_handle : Arc < Mutex < Option < JsFormatFileCb > > > ) -> FormatFileWithConfigCallback {
366+ fn wrap_format_file (
367+ cb_handle : Arc < RwLock < Option < JsFormatFileCb > > > ,
368+ ) -> FormatFileWithConfigCallback {
364369 Arc :: new ( move |options : & Value , parser_name : & str , file_name : & str , code : & str | {
365370 debug_span ! ( "oxfmt::external::format_file" , parser = %parser_name, file = %file_name) . in_scope ( || {
366- let guard = cb_handle. lock ( ) . unwrap ( ) ;
371+ let guard = cb_handle. read ( ) . unwrap ( ) ;
367372 let Some ( cb) = guard. as_ref ( ) else {
368373 return Err ( "JS callback unavailable (environment shutting down)" . to_string ( ) ) ;
369374 } ;
@@ -396,12 +401,12 @@ fn wrap_format_file(cb_handle: Arc<Mutex<Option<JsFormatFileCb>>>) -> FormatFile
396401
397402/// Wrap JS `sortTailwindClasses` callback as a normal Rust function.
398403fn wrap_sort_tailwind_classes (
399- cb_handle : Arc < Mutex < Option < JsSortTailwindClassesCb > > > ,
404+ cb_handle : Arc < RwLock < Option < JsSortTailwindClassesCb > > > ,
400405) -> TailwindWithConfigCallback {
401406 Arc :: new ( move |filepath : & str , options : & Value , classes : Vec < String > | {
402407 debug_span ! ( "oxfmt::external::sort_tailwind" , classes_count = classes. len( ) ) . in_scope (
403408 || {
404- let guard = cb_handle. lock ( ) . unwrap ( ) ;
409+ let guard = cb_handle. read ( ) . unwrap ( ) ;
405410 let Some ( cb) = guard. as_ref ( ) else {
406411 // Return original classes if callback unavailable
407412 return classes;
0 commit comments