55
66use std:: path:: { Path , PathBuf } ;
77use std:: process:: { Command , ExitCode , Stdio } ;
8+ use std:: sync:: { Mutex , OnceLock } ;
89
910use crate :: {
1011 BridgeCommandResult , CliError ,
@@ -25,6 +26,41 @@ fn strip_win_prefix(path: PathBuf) -> PathBuf {
2526}
2627
2728const PACKAGE_NAME : & str = "@truenine/memory-sync-cli" ;
29+ static PLUGIN_RUNTIME_CACHE : OnceLock < Mutex < Option < PathBuf > > > = OnceLock :: new ( ) ;
30+ static NODE_CACHE : OnceLock < Mutex < Option < String > > > = OnceLock :: new ( ) ;
31+
32+ fn read_cached_success < T : Clone > ( cache : & Mutex < Option < T > > ) -> Option < T > {
33+ match cache. lock ( ) {
34+ Ok ( guard) => guard. clone ( ) ,
35+ Err ( poisoned) => poisoned. into_inner ( ) . clone ( ) ,
36+ }
37+ }
38+
39+ fn store_cached_success < T : Clone > ( cache : & Mutex < Option < T > > , value : & T ) {
40+ match cache. lock ( ) {
41+ Ok ( mut guard) => {
42+ * guard = Some ( value. clone ( ) ) ;
43+ }
44+ Err ( poisoned) => {
45+ * poisoned. into_inner ( ) = Some ( value. clone ( ) ) ;
46+ }
47+ }
48+ }
49+
50+ fn detect_with_cached_success < T : Clone , F > ( cache : & Mutex < Option < T > > , detect : F ) -> Option < T >
51+ where
52+ F : FnOnce ( ) -> Option < T > ,
53+ {
54+ if let Some ( cached) = read_cached_success ( cache) {
55+ return Some ( cached) ;
56+ }
57+
58+ let detected = detect ( ) ;
59+ if let Some ( value) = detected. as_ref ( ) {
60+ store_cached_success ( cache, value) ;
61+ }
62+ detected
63+ }
2864
2965/// Locate the plugin runtime JS entry point.
3066///
@@ -37,6 +73,11 @@ const PACKAGE_NAME: &str = "@truenine/memory-sync-cli";
3773/// 6. npm/pnpm global install: `<global_root>/@truenine/memory-sync-cli/dist/plugin-runtime.mjs`
3874/// 7. Embedded JS extracted to `~/.aindex/.cache/plugin-runtime-<version>.mjs`
3975pub ( crate ) fn find_plugin_runtime ( ) -> Option < PathBuf > {
76+ let cache = PLUGIN_RUNTIME_CACHE . get_or_init ( || Mutex :: new ( None ) ) ;
77+ detect_with_cached_success ( cache, detect_plugin_runtime)
78+ }
79+
80+ fn detect_plugin_runtime ( ) -> Option < PathBuf > {
4081 let mut candidates: Vec < PathBuf > = Vec :: new ( ) ;
4182
4283 // Relative to binary location
@@ -166,6 +207,11 @@ fn extract_embedded_runtime() -> Option<PathBuf> {
166207
167208/// Find the `node` executable.
168209pub ( crate ) fn find_node ( ) -> Option < String > {
210+ let cache = NODE_CACHE . get_or_init ( || Mutex :: new ( None ) ) ;
211+ detect_with_cached_success ( cache, detect_node)
212+ }
213+
214+ fn detect_node ( ) -> Option < String > {
169215 // Try `node` in PATH
170216 if Command :: new ( "node" )
171217 . arg ( "--version" )
@@ -452,6 +498,8 @@ fn find_index_mjs() -> Option<PathBuf> {
452498#[ cfg( test) ]
453499mod tests {
454500 use super :: * ;
501+ use std:: cell:: Cell ;
502+ use std:: sync:: Mutex ;
455503
456504 #[ test]
457505 fn test_strip_win_prefix_with_prefix ( ) {
@@ -473,4 +521,29 @@ mod tests {
473521 let result = strip_win_prefix ( path. clone ( ) ) ;
474522 assert_eq ! ( result, path) ;
475523 }
524+
525+ #[ test]
526+ fn test_detect_with_cached_success_retries_until_success ( ) {
527+ let cache = Mutex :: new ( None ) ;
528+ let attempts = Cell :: new ( 0 ) ;
529+
530+ let first = detect_with_cached_success ( & cache, || {
531+ attempts. set ( attempts. get ( ) + 1 ) ;
532+ Option :: < String > :: None
533+ } ) ;
534+ assert_eq ! ( first, None ) ;
535+
536+ let second = detect_with_cached_success ( & cache, || {
537+ attempts. set ( attempts. get ( ) + 1 ) ;
538+ Some ( String :: from ( "node" ) )
539+ } ) ;
540+ assert_eq ! ( second, Some ( String :: from( "node" ) ) ) ;
541+
542+ let third = detect_with_cached_success ( & cache, || {
543+ attempts. set ( attempts. get ( ) + 1 ) ;
544+ Some ( String :: from ( "other" ) )
545+ } ) ;
546+ assert_eq ! ( third, Some ( String :: from( "node" ) ) ) ;
547+ assert_eq ! ( attempts. get( ) , 2 ) ;
548+ }
476549}
0 commit comments