From a9bfbf1c15a206110597bf7bf724dcaa91812bd9 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 6 May 2025 17:11:52 +0300 Subject: [PATCH] feat(typeahead_input): Refactor and enhance TypeaheadInput component for better lifecycle management and WASM compatibility. --- src/components/typeahead_input.rs | 1034 ++++++++++++++++++++--------- 1 file changed, 710 insertions(+), 324 deletions(-) diff --git a/src/components/typeahead_input.rs b/src/components/typeahead_input.rs index d9f2b45..2839713 100644 --- a/src/components/typeahead_input.rs +++ b/src/components/typeahead_input.rs @@ -1,16 +1,96 @@ use leptos::*; use wasm_bindgen::prelude::*; use crate::models::item::WikidataSuggestion; -use js_sys::{Object, Array, Function, Reflect}; -use leptos::html::Input; -use gloo_utils::format::JsValueSerdeExt; -use wasm_bindgen::JsCast; -use web_sys::HtmlInputElement; +use leptos::html::Input; use leptos::logging::log; use std::time::Duration; use std::rc::Rc; use std::cell::RefCell; +// Only include these imports when targeting wasm +#[cfg(target_arch = "wasm32")] +use { + js_sys::{Object, Array, Function, Reflect}, + gloo_utils::format::JsValueSerdeExt, + wasm_bindgen::JsCast, + web_sys::HtmlInputElement, + std::sync::atomic::{AtomicBool, Ordering}, + std::sync::Arc, +}; + +// Add Bloodhound wrapper struct - only for wasm +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = ["window"])] + type Bloodhound; + + #[wasm_bindgen(constructor, js_namespace = ["window"])] + fn new(options: &JsValue) -> Bloodhound; + + #[wasm_bindgen(method, js_namespace = ["window"])] + fn initialize(this: &Bloodhound, reinitialize: bool); +} + +// Enhanced closure management with explicit lifecycle tracking - only for wasm +#[cfg(target_arch = "wasm32")] +struct TypeaheadClosures { + selection_closure: Option>, + remote_fn_closure: Option>, + // Track if component is still alive to prevent accessing invalid memory + is_alive: Arc, +} + +#[cfg(target_arch = "wasm32")] +impl TypeaheadClosures { + fn new() -> Self { + Self { + selection_closure: None, + remote_fn_closure: None, + is_alive: Arc::new(AtomicBool::new(true)), + } + } + + fn cleanup(&mut self) { + // Mark as no longer alive before dropping closures + self.is_alive.store(false, Ordering::SeqCst); + + // Take ownership of closures to drop them + let _ = self.selection_closure.take(); + let _ = self.remote_fn_closure.take(); + + log!("[CLEANUP] TypeaheadClosures cleaned up"); + } + + // Get a clone of the is_alive flag for use in closures + fn get_alive_flag(&self) -> Arc { + self.is_alive.clone() + } +} + +// Drop implementation to ensure cleanup happens - only for wasm +#[cfg(target_arch = "wasm32")] +impl Drop for TypeaheadClosures { + fn drop(&mut self) { + self.cleanup(); + } +} + +// Create a dummy TypeaheadClosures for non-wasm targets +#[cfg(not(target_arch = "wasm32"))] +struct TypeaheadClosures {} + +#[cfg(not(target_arch = "wasm32"))] +impl TypeaheadClosures { + fn new() -> Self { + Self {} + } + + fn cleanup(&mut self) { + // No-op for non-wasm + } +} + #[component] pub fn TypeaheadInput( value: String, @@ -25,132 +105,277 @@ pub fn TypeaheadInput( // Create a unique ID for this component instance let component_id = format!("typeahead-{}", uuid::Uuid::new_v4()); - // Flag to track if component is mounted - let is_mounted = Rc::new(RefCell::new(true)); - let is_mounted_clone = is_mounted.clone(); - - // Clone necessary values for the async task - let fetch_suggestions_clone = fetch_suggestions.clone(); - let on_select_clone = on_select.clone(); - let node_ref_clone = node_ref.clone(); - let component_id_clone = component_id.clone(); - - // Create a cancellation token for the async task - let (cancel_token, set_cancel_token) = create_signal(false); - - // Spawn the initialization task using spawn_local instead of spawn_local_with_handle - spawn_local(async move { - log!("[INIT] Component mounted: {}", component_id_clone); + // WASM-specific initialization + #[cfg(target_arch = "wasm32")] + { + // Create a storage for closures with explicit lifecycle tracking + let closures = Rc::new(RefCell::new(TypeaheadClosures::new())); + let closures_for_cleanup = closures.clone(); - let mut retries = 0; - while retries < 10 && !cancel_token.get() { - // Check if component is still mounted before proceeding - if !*is_mounted.borrow() { - log!("[INIT] Component unmounted, aborting initialization: {}", component_id_clone); - return; - } - - if let Some(input) = node_ref_clone.get() { - log!("[INIT] Input element found: {}", component_id_clone); - - // Only proceed if component is still mounted - if !*is_mounted.borrow() || cancel_token.get() { - log!("[INIT] Component unmounted after input found, aborting: {}", component_id_clone); - return; - } - - let bloodhound = initialize_bloodhound(fetch_suggestions_clone.clone(), &component_id_clone); - - // Store bloodhound in a component-specific global variable - let bloodhound_var = format!("bloodhoundInstance_{}", component_id_clone.replace("-", "_")); - if let Err(_) = js_sys::Reflect::set( - &js_sys::global(), - &bloodhound_var.into(), - &bloodhound - ) { - log!("[ERROR] Failed to store bloodhound instance: {}", component_id_clone); - } - - // Only proceed if component is still mounted - if !*is_mounted.borrow() || cancel_token.get() { - log!("[INIT] Component unmounted before typeahead init, aborting: {}", component_id_clone); - return; - } - - initialize_typeahead(&input, bloodhound, on_select_clone.clone(), node_ref_clone.clone(), &component_id_clone); - - // Only set initialized if component is still mounted - if *is_mounted.borrow() && !cancel_token.get() { - // Use a try_update to safely update the signal - let _ = try_with_owner(Owner::current().unwrap(), move || { - set_initialized.set(true); - }); - } - break; - } - - // Check if component is still mounted before sleeping - if !*is_mounted.borrow() || cancel_token.get() { - log!("[INIT] Component unmounted during retry loop, aborting: {}", component_id_clone); - return; - } - - gloo_timers::future::sleep(Duration::from_millis(100)).await; - retries += 1; - } - }); - - // Clone component_id for on_cleanup - let component_id_for_cleanup = component_id.clone(); - - // Comprehensive cleanup function - on_cleanup(move || { - log!("[CLEANUP] TypeaheadInput component unmounting: {}", component_id_for_cleanup); + // Clone necessary values for the async task + let fetch_suggestions_clone = fetch_suggestions.clone(); + let on_select_clone = on_select.clone(); + let node_ref_clone = node_ref.clone(); + let component_id_clone = component_id.clone(); + let closures_clone = closures.clone(); - // Signal the async task to cancel - set_cancel_token.set(true); + // Create a cancellation token for the async task + let (cancel_token, set_cancel_token) = create_signal(false); - // Mark component as unmounted - *is_mounted_clone.borrow_mut() = false; - - // Clean up component-specific global references - let bloodhound_var = format!("bloodhoundInstance_{}", component_id_for_cleanup.replace("-", "_")); - let prepare_fn_var = format!("bloodhoundPrepare_{}", component_id_for_cleanup.replace("-", "_")); - - let cleanup_script = format!(r#" - try {{ - // Clean up the bloodhound instance - if (window['{bloodhound_var}']) {{ - delete window['{bloodhound_var}']; + // Register global cleanup function in JavaScript + let register_cleanup_script = format!( + r#" + // Create a global registry for typeahead components if it doesn't exist + if (!window.typeaheadRegistry) {{ + window.typeaheadRegistry = {{}}; }} - // Clean up the prepare function - if (window['{prepare_fn_var}']) {{ - delete window['{prepare_fn_var}']; - }} + // Register this component + window.typeaheadRegistry['{component_id}'] = {{ + initialized: false, + bloodhound: null, + handlers: {{}}, + // Method to safely call handlers + callHandler: function(handlerName, ...args) {{ + try {{ + const handler = this.handlers[handlerName]; + if (handler && typeof handler === 'function') {{ + return handler(...args); + }} + }} catch (e) {{ + console.error('[JS] Error calling handler:', e); + }} + return null; + }}, + // Method to register a handler + registerHandler: function(name, fn) {{ + this.handlers[name] = fn; + return true; + }}, + // Method to unregister a handler + unregisterHandler: function(name) {{ + if (this.handlers[name]) {{ + delete this.handlers[name]; + return true; + }} + return false; + }}, + // Method to clean up all resources + cleanup: function() {{ + try {{ + // Clean up typeahead + const inputId = 'typeahead-input-{component_id}'; + const $input = $('#' + inputId); + if ($input.length > 0) {{ + $input.typeahead('destroy'); + }} + + // Clear all handlers + this.handlers = {{}}; + + // Mark as cleaned up + this.initialized = false; + this.bloodhound = null; + + console.log('[JS] Component {component_id} cleaned up successfully'); + return true; + }} catch (e) {{ + console.error('[JS] Error during cleanup:', e); + return false; + }} + }} + }}; - // Clean up any typeahead instances - if (window.typeaheadCleanupFunctions && window.typeaheadCleanupFunctions['{component_id}']) {{ - window.typeaheadCleanupFunctions['{component_id}'](); - delete window.typeaheadCleanupFunctions['{component_id}']; - }} - - console.log('[JS] Global cleanup completed for {component_id}'); - }} catch (e) {{ - console.error('[JS] Cleanup error for {component_id}:', e); - }} - "#, - bloodhound_var = bloodhound_var, - prepare_fn_var = prepare_fn_var, - component_id = component_id_for_cleanup + console.log('[JS] Registered component {component_id}'); + true + "#, + component_id = component_id ); - if let Err(e) = js_sys::eval(&cleanup_script) { - log!("[RUST] Cleanup script eval error: {:?}", e); + // Execute the registration script + match js_sys::eval(®ister_cleanup_script) { + Ok(_) => log!("[INIT] Registered cleanup handlers for {}", component_id), + Err(e) => log!("[ERROR] Failed to register cleanup handlers: {:?}", e), } - }); + + // Spawn the initialization task + spawn_local(async move { + log!("[INIT] Component mounted: {}", component_id_clone); + + let mut retries = 0; + while retries < 10 && !cancel_token.get() { + // Check if component is still alive + if !closures_clone.borrow().is_alive.load(Ordering::SeqCst) { + log!("[INIT] Component no longer alive, aborting initialization: {}", component_id_clone); + return; + } + + if let Some(input) = node_ref_clone.get() { + log!("[INIT] Input element found: {}", component_id_clone); + + // Check again if component is still alive + if !closures_clone.borrow().is_alive.load(Ordering::SeqCst) || cancel_token.get() { + log!("[INIT] Component no longer alive after input found, aborting: {}", component_id_clone); + return; + } + + // Initialize Bloodhound with safe closure handling + let bloodhound = match initialize_bloodhound(fetch_suggestions_clone.clone(), &component_id_clone, closures_clone.clone()) { + Ok(bh) => bh, + Err(e) => { + log!("[ERROR] Failed to initialize Bloodhound: {:?}", e); + return; + } + }; + + // Store bloodhound in the component registry + let store_bloodhound_script = format!( + r#" + if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{ + window.typeaheadRegistry['{component_id}'].bloodhound = bloodhoundInstance; + console.log('[JS] Stored bloodhound instance for {component_id}'); + true + }} else {{ + console.error('[JS] Component registry not found for {component_id}'); + false + }} + "#, + component_id = component_id_clone + ); + + // Convert bloodhound to a JSON string + let bloodhound_json = match js_sys::JSON::stringify(&bloodhound) { + Ok(json_str) => json_str.as_string().unwrap_or_default(), + Err(_) => "{}".to_string() + }; + + // Create the full script + let full_script = format!( + "var bloodhoundInstance = {}; {}", + bloodhound_json, + store_bloodhound_script + ); + + // Evaluate the script + let bloodhound_stored = match js_sys::eval(&full_script) { + Ok(result) => result.as_bool().unwrap_or(false), + Err(e) => { + log!("[ERROR] Failed to store bloodhound instance: {:?}", e); + false + } + }; + + if !bloodhound_stored { + log!("[ERROR] Failed to store bloodhound instance in registry"); + return; + } - // CSS + // Check again if component is still alive + if !closures_clone.borrow().is_alive.load(Ordering::SeqCst) || cancel_token.get() { + log!("[INIT] Component no longer alive before typeahead init, aborting: {}", component_id_clone); + return; + } + + // Initialize typeahead with safe closure handling + if let Err(e) = initialize_typeahead(&input, bloodhound, on_select_clone.clone(), node_ref_clone.clone(), &component_id_clone, closures_clone.clone()) { + log!("[ERROR] Failed to initialize typeahead: {:?}", e); + return; + } + + // Mark as initialized in the registry + let mark_initialized_script = format!( + r#" + if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{ + window.typeaheadRegistry['{component_id}'].initialized = true; + console.log('[JS] Marked component {component_id} as initialized'); + true + }} else {{ + console.error('[JS] Component registry not found for {component_id}'); + false + }} + "#, + component_id = component_id_clone + ); + + match js_sys::eval(&mark_initialized_script) { + Ok(_) => log!("[INIT] Marked component as initialized in registry"), + Err(e) => log!("[ERROR] Failed to mark as initialized: {:?}", e), + } + + // Only set initialized if component is still alive + if closures_clone.borrow().is_alive.load(Ordering::SeqCst) && !cancel_token.get() { + // Use a try_update to safely update the signal + let _ = try_with_owner(Owner::current().unwrap(), move || { + set_initialized.set(true); + }); + } + break; + } + + // Check if component is still alive before sleeping + if !closures_clone.borrow().is_alive.load(Ordering::SeqCst) || cancel_token.get() { + log!("[INIT] Component no longer alive during retry loop, aborting: {}", component_id_clone); + return; + } + + gloo_timers::future::sleep(Duration::from_millis(100)).await; + retries += 1; + } + }); + + // Clone component_id for on_cleanup + let component_id_for_cleanup = component_id.clone(); + + // Comprehensive cleanup function + on_cleanup(move || { + log!("[CLEANUP] TypeaheadInput component unmounting: {}", component_id_for_cleanup); + + // Signal the async task to cancel + set_cancel_token.set(true); + + // Perform JavaScript cleanup to prevent any further calls to Rust closures + let js_cleanup_script = format!( + r#" + // Perform cleanup in JavaScript first + if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{ + // Clean up the component + const result = window.typeaheadRegistry['{component_id}'].cleanup(); + + // Remove from registry + delete window.typeaheadRegistry['{component_id}']; + + console.log('[JS] Component {component_id} removed from registry'); + result + }} else {{ + console.warn('[JS] Component {component_id} not found in registry'); + false + }} + "#, + component_id = component_id_for_cleanup + ); + + // Execute JavaScript cleanup + match js_sys::eval(&js_cleanup_script) { + Ok(result) => { + if let Some(success) = result.as_bool() { + log!("[CLEANUP] JavaScript cleanup {}", if success { "successful" } else { "failed" }); + } + }, + Err(e) => log!("[CLEANUP] JavaScript cleanup error: {:?}", e), + } + + // Now clean up Rust resources + if let Ok(mut closures) = closures_for_cleanup.try_borrow_mut() { + closures.cleanup(); + } else { + log!("[CLEANUP] Warning: Could not borrow closures for cleanup"); + } + + log!("[CLEANUP] TypeaheadInput component cleanup completed"); + }); + } + + // CSS styles for the typeahead input let css = r#" .typeahead.tt-input { background: transparent !important; @@ -237,118 +462,231 @@ pub fn TypeaheadInput( } } -fn initialize_bloodhound(fetch: Callback>, component_id: &str) -> JsValue { +// Only include these functions when targeting wasm +#[cfg(target_arch = "wasm32")] +fn initialize_bloodhound( + fetch: Callback>, + component_id: &str, + closures: Rc> +) -> Result { + log!("[BLOODHOUND] Initializing for component: {}", component_id); + let bloodhound_options = Object::new(); - // Use component-specific names for global functions - let prepare_fn_var = format!("bloodhoundPrepare_{}", component_id.replace("-", "_")); + // Get a clone of the is_alive flag for use in the closure + let is_alive = closures.borrow().get_alive_flag(); + + // Register the fetch handler in the component registry + let register_fetch_handler_script = format!( + r#" + if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{ + window.typeaheadRegistry['{component_id}'].registerHandler('fetch', function(query, syncFn, asyncFn) {{ + // This function will be called by the transport function + if (window.rustFetchHandler_{component_id_safe}) {{ + try {{ + window.rustFetchHandler_{component_id_safe}(query, syncFn, asyncFn); + }} catch (e) {{ + console.error('[JS] Error calling Rust fetch handler:', e); + syncFn([]); + }} + }} else {{ + console.error('[JS] Rust fetch handler not found'); + syncFn([]); + }} + }}); + console.log('[JS] Registered fetch handler for {component_id}'); + true + }} else {{ + console.error('[JS] Component registry not found for {component_id}'); + false + }} + "#, + component_id = component_id, + component_id_safe = component_id.replace("-", "_") + ); + + let handler_registered = match js_sys::eval(®ister_fetch_handler_script) { + Ok(result) => result.as_bool().unwrap_or(false), + Err(e) => { + log!("[ERROR] Failed to register fetch handler: {:?}", e); + return Err(JsValue::from_str("Failed to register fetch handler")); + } + }; + + if !handler_registered { + return Err(JsValue::from_str("Failed to register fetch handler in registry")); + } // Create a closure that will be called by Bloodhound to fetch suggestions - let remote_fn = Closure::wrap(Box::new(move |query: JsValue, sync: Function, _async_fn: Function| { - let query_str = query.as_string().unwrap_or_default(); - log!("[BLOODHOUND] Fetching suggestions for: {}", query_str); + let remote_fn = Closure::wrap(Box::new({ + // Clone these values for use in the closure + let is_alive = is_alive.clone(); + let fetch = fetch.clone(); - // Safely call the fetch callback - let suggestions = match try_with_owner(Owner::current().unwrap(), move || { - fetch.call(query_str.clone()) - }) { - Ok(suggs) => suggs, - Err(e) => { - log!("[ERROR] Failed to fetch suggestions: {:?}", e); - Vec::new() - } - }; - - // Create a JavaScript array to hold the suggestions - let js_suggestions = Array::new(); - - // Convert each suggestion to a JavaScript object - for suggestion in suggestions { - let obj = Object::new(); - - // Store the original ID, label, and description - Reflect::set(&obj, &"id".into(), &JsValue::from_str(&suggestion.id)).unwrap_or_default(); - Reflect::set(&obj, &"label".into(), &JsValue::from_str(&suggestion.label)).unwrap_or_default(); - Reflect::set(&obj, &"description".into(), &JsValue::from_str(&suggestion.description)).unwrap_or_default(); - - // Store the display values directly on the object for easier access - Reflect::set(&obj, &"displayLabel".into(), - &JsValue::from_str(&suggestion.display.label.value)).unwrap_or_default(); - Reflect::set(&obj, &"displayDescription".into(), - &JsValue::from_str(&suggestion.display.description.value)).unwrap_or_default(); - - // Store the full suggestion for later retrieval - if let Ok(full_suggestion) = JsValue::from_serde(&suggestion) { - Reflect::set(&obj, &"fullSuggestion".into(), &full_suggestion).unwrap_or_default(); + move |query: JsValue, sync: Function, _async_fn: Function| { + // First check if the component is still alive + if !is_alive.load(Ordering::SeqCst) { + log!("[BLOODHOUND] Component no longer alive, aborting fetch"); + // Call sync with empty results to avoid JS errors + let empty_results = Array::new(); + let _ = sync.call1(&JsValue::NULL, &empty_results); + return; } - // Add the object to the array - js_suggestions.push(&obj); + let query_str = query.as_string().unwrap_or_default(); + log!("[BLOODHOUND] Fetching suggestions for: {}", query_str); + + // Safely call the fetch callback + let suggestions = match try_with_owner(Owner::current().unwrap(), { + // Clone these values for the inner closure + let is_alive = is_alive.clone(); + let fetch = fetch.clone(); + let query_str = query_str.clone(); + + move || { + // Check again if component is still alive + if !is_alive.load(Ordering::SeqCst) { + log!("[BLOODHOUND] Component no longer alive before fetch, aborting"); + return Vec::new(); + } + + fetch.call(query_str.clone()) + } + }) { + Ok(suggs) => suggs, + Err(e) => { + log!("[ERROR] Failed to fetch suggestions: {:?}", e); + Vec::new() + } + }; + + // Check again if component is still alive + if !is_alive.load(Ordering::SeqCst) { + log!("[BLOODHOUND] Component no longer alive after fetch, aborting processing"); + let empty_results = Array::new(); + let _ = sync.call1(&JsValue::NULL, &empty_results); + return; + } + + // Create a JavaScript array to hold the suggestions + let js_suggestions = Array::new(); + + // Convert each suggestion to a JavaScript object + for suggestion in suggestions { + let obj = Object::new(); + + // Store the original ID, label, and description + Reflect::set(&obj, &"id".into(), &JsValue::from_str(&suggestion.id)).unwrap_or_default(); + Reflect::set(&obj, &"label".into(), &JsValue::from_str(&suggestion.label)).unwrap_or_default(); + Reflect::set(&obj, &"description".into(), &JsValue::from_str(&suggestion.description)).unwrap_or_default(); + + // Store the display values directly on the object for easier access + Reflect::set(&obj, &"displayLabel".into(), + &JsValue::from_str(&suggestion.display.label.value)).unwrap_or_default(); + Reflect::set(&obj, &"displayDescription".into(), + &JsValue::from_str(&suggestion.display.description.value)).unwrap_or_default(); + + // Store the full suggestion for later retrieval + if let Ok(full_suggestion) = JsValue::from_serde(&suggestion) { + Reflect::set(&obj, &"fullSuggestion".into(), &full_suggestion).unwrap_or_default(); + } + + // Add the object to the array + js_suggestions.push(&obj); + } + + // Final check if component is still alive + if !is_alive.load(Ordering::SeqCst) { + log!("[BLOODHOUND] Component no longer alive before returning results, aborting"); + let empty_results = Array::new(); + let _ = sync.call1(&JsValue::NULL, &empty_results); + return; + } + + log!("[BLOODHOUND] Processed suggestions: {} items", js_suggestions.length()); + + // Call the sync function with the suggestions + let _ = sync.call1(&JsValue::NULL, &js_suggestions); } - - log!("[BLOODHOUND] Processed suggestions: {:?}", js_suggestions); - - // Call the sync function with the suggestions - let _ = sync.call1(&JsValue::NULL, &js_suggestions); - }) as Box); + }) as Box); + // Store the Rust fetch handler globally + let rust_handler_name = format!("rustFetchHandler_{}", component_id.replace("-", "_")); + js_sys::Reflect::set( + &js_sys::global(), + &rust_handler_name.into(), + remote_fn.as_ref().unchecked_ref() + ).map_err(|e| { + log!("[ERROR] Failed to store Rust fetch handler: {:?}", e); + e + })?; + // Configure the remote options let remote_config = Object::new(); - // Set transport function to avoid AJAX requests + // Set transport function to use our registry-based handler let transport_fn = js_sys::Function::new_with_args( "query, syncResults, asyncResults", &format!(r#" - // Call our custom prepare function directly - if (window['{prepare_fn_var}']) {{ - window['{prepare_fn_var}'](query, syncResults, asyncResults); + // Call our handler through the registry + if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{ + try {{ + window.typeaheadRegistry['{component_id}'].callHandler('fetch', query, syncResults, asyncResults); + }} catch (e) {{ + console.error('[JS] Error calling fetch handler through registry:', e); + syncResults([]); + }} }} else {{ - console.error('[JS] Prepare function not found: {prepare_fn_var}'); + console.error('[JS] Component registry not found for {component_id}'); syncResults([]); }} - "#, prepare_fn_var = prepare_fn_var) + "#, component_id = component_id) ); Reflect::set( &remote_config, &"transport".into(), &transport_fn - ).unwrap_or_default(); + ).map_err(|e| { + log!("[ERROR] Failed to set transport function: {:?}", e); + e + })?; // Set a dummy URL (not actually used with custom transport) Reflect::set( &remote_config, &"url".into(), &JsValue::from_str("/dummy?query=%QUERY") - ).unwrap_or_default(); - - // Store our prepare function globally with component-specific name - // Clone prepare_fn_var before using .into() which consumes it - let prepare_fn_var_for_log = prepare_fn_var.clone(); - if let Err(_) = js_sys::Reflect::set( - &js_sys::global(), - &prepare_fn_var.into(), - remote_fn.as_ref().unchecked_ref() - ) { - log!("[ERROR] Failed to store prepare function: {}", prepare_fn_var_for_log); - } + ).map_err(|e| { + log!("[ERROR] Failed to set URL: {:?}", e); + e + })?; // Set rate limiting to prevent too many requests Reflect::set( &remote_config, &"rateLimitWait".into(), &JsValue::from(300) - ).unwrap_or_default(); + ).map_err(|e| { + log!("[ERROR] Failed to set rate limit: {:?}", e); + e + })?; // Set the wildcard for query replacement Reflect::set( &remote_config, &"wildcard".into(), &JsValue::from_str("%QUERY") - ).unwrap_or_default(); + ).map_err(|e| { + log!("[ERROR] Failed to set wildcard: {:?}", e); + e + })?; // Add the remote config to the options - Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap_or_default(); + Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).map_err(|e| { + log!("[ERROR] Failed to set remote config: {:?}", e); + e + })?; // Set the tokenizers let tokenizer = js_sys::Function::new_no_args( @@ -363,150 +701,200 @@ fn initialize_bloodhound(fetch: Callback>, compo &bloodhound_options, &"datumTokenizer".into(), &tokenizer - ).unwrap_or_default(); + ).map_err(|e| { + log!("[ERROR] Failed to set datumTokenizer: {:?}", e); + e + })?; Reflect::set( &bloodhound_options, &"queryTokenizer".into(), &tokenizer - ).unwrap_or_default(); + ).map_err(|e| { + log!("[ERROR] Failed to set queryTokenizer: {:?}", e); + e + })?; // Create and initialize the Bloodhound instance let bloodhound = Bloodhound::new(&bloodhound_options.into()); bloodhound.initialize(true); - // Prevent the closure from being garbage collected - remote_fn.forget(); + // Store the closure in our struct instead of forgetting it + if let Ok(mut closures_mut) = closures.try_borrow_mut() { + closures_mut.remote_fn_closure = Some(remote_fn); + } else { + log!("[ERROR] Failed to store remote_fn_closure: could not borrow closures"); + return Err(JsValue::from_str("Failed to store remote_fn_closure")); + } // Return the Bloodhound instance - bloodhound.into() + Ok(bloodhound.into()) } +// Only include this function when targeting wasm +#[cfg(target_arch = "wasm32")] fn initialize_typeahead( input: &HtmlInputElement, bloodhound: JsValue, on_select: Callback, node_ref: NodeRef, component_id: &str, -) { + closures: Rc>, +) -> Result<(), JsValue> { log!("[TYPEAHEAD] Initializing for input: {} ({})", input.id(), component_id); let input_id = format!("typeahead-input-{}", component_id); input.set_id(&input_id); - // Create selection handler closure - let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| { - log!("[TYPEAHEAD] Selection made"); - - // Safely call the on_select callback - let _ = try_with_owner(Owner::current().unwrap(), move || { - // Try to get the full suggestion from the suggestion object - if let Some(full_suggestion) = js_sys::Reflect::get(&suggestion, &"fullSuggestion".into()).ok() { - if let Ok(data) = full_suggestion.into_serde::() { - log!("[TYPEAHEAD] Selected suggestion: {:?}", data); - on_select.call(data.clone()); - if let Some(input) = node_ref.get() { - input.set_value(&data.label); - } - return; - } - } - - // Fallback: try to deserialize the suggestion directly - if let Ok(data) = suggestion.into_serde::() { - log!("[TYPEAHEAD] Selected suggestion (fallback): {:?}", data); - on_select.call(data.clone()); - if let Some(input) = node_ref.get() { - input.set_value(&data.label); - } - } else { - log!("[ERROR] Failed to deserialize suggestion"); - } - }); - }) as Box); + // Get a clone of the is_alive flag for use in the closure + let is_alive = closures.borrow().get_alive_flag(); - // Register global handler with component-specific name - let handler_name = format!("handler_{}", input_id.replace("-", "_")); - - log!("[TYPEAHEAD] Registering handler with name: {}", handler_name); - - // Clone handler_name before using it in error log - let handler_name_for_log = handler_name.clone(); - if let Err(_) = js_sys::Reflect::set( - &js_sys::global(), - &handler_name.clone().into(), - closure.as_ref().unchecked_ref(), - ) { - log!("[ERROR] Failed to register handler: {}", handler_name_for_log); - } - - // We'll clean this up in the component's cleanup function - closure.forget(); - - // Register cleanup function for this specific typeahead instance - let cleanup_script = format!( + // Register the selection handler in the component registry + let register_selection_handler_script = format!( r#" - // Store a reference to the cleanup function for this input - if (!window.typeaheadCleanupFunctions) {{ - window.typeaheadCleanupFunctions = {{}}; - }} - - window.typeaheadCleanupFunctions['{component_id}'] = function() {{ - try {{ - // Destroy the typeahead instance - $('#{input_id}').typeahead('destroy'); - - // Remove the handler - if (window['{handler_name}']) {{ - delete window['{handler_name}']; + if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{ + window.typeaheadRegistry['{component_id}'].registerHandler('select', function(event, suggestion) {{ + // This function will be called by the typeahead:select event + if (window.rustSelectHandler_{component_id_safe}) {{ + try {{ + window.rustSelectHandler_{component_id_safe}(event, suggestion); + }} catch (e) {{ + console.error('[JS] Error calling Rust select handler:', e); + }} + }} else {{ + console.error('[JS] Rust select handler not found'); }} - - console.log('[JS] Typeahead cleanup for #{input_id} completed'); - }} catch (e) {{ - console.error('[JS] Typeahead cleanup error for #{input_id}:', e); - }} - }}; + }}); + console.log('[JS] Registered selection handler for {component_id}'); + true + }} else {{ + console.error('[JS] Component registry not found for {component_id}'); + false + }} "#, component_id = component_id, - input_id = input_id, - handler_name = handler_name_for_log + component_id_safe = component_id.replace("-", "_") ); - if let Err(e) = js_sys::eval(&cleanup_script) { - log!("[RUST] Cleanup script eval error: {:?}", e); + let handler_registered = match js_sys::eval(®ister_selection_handler_script) { + Ok(result) => result.as_bool().unwrap_or(false), + Err(e) => { + log!("[ERROR] Failed to register selection handler: {:?}", e); + return Err(JsValue::from_str("Failed to register selection handler")); + } + }; + + if !handler_registered { + return Err(JsValue::from_str("Failed to register selection handler in registry")); + } + + // Create selection handler closure + let closure = Closure::wrap(Box::new({ + // Clone these values for use in the closure + let is_alive = is_alive.clone(); + let on_select = on_select.clone(); + let node_ref = node_ref.clone(); + + move |_event: web_sys::Event, suggestion: JsValue| { + // First check if the component is still alive + if !is_alive.load(Ordering::SeqCst) { + log!("[TYPEAHEAD] Component no longer alive, aborting selection handler"); + return; + } + + log!("[TYPEAHEAD] Selection made"); + + // Safely call the on_select callback + let _ = try_with_owner(Owner::current().unwrap(), { + // Clone these values again for the inner closure + let is_alive = is_alive.clone(); + let on_select = on_select.clone(); + let node_ref = node_ref.clone(); + let suggestion = suggestion.clone(); + + move || { + // Check again if component is still alive + if !is_alive.load(Ordering::SeqCst) { + log!("[TYPEAHEAD] Component no longer alive during selection callback, aborting"); + return; + } + + // Try to get the full suggestion from the suggestion object + if let Some(full_suggestion) = js_sys::Reflect::get(&suggestion, &"fullSuggestion".into()).ok() { + if let Ok(data) = full_suggestion.into_serde::() { + log!("[TYPEAHEAD] Selected suggestion: {:?}", data); + + // Final check before calling callback + if !is_alive.load(Ordering::SeqCst) { + log!("[TYPEAHEAD] Component no longer alive before callback, aborting"); + return; + } + + on_select.call(data.clone()); + + if let Some(input) = node_ref.get() { + input.set_value(&data.label); + } + return; + } + } + + // Fallback: try to deserialize the suggestion directly + if let Ok(data) = suggestion.into_serde::() { + log!("[TYPEAHEAD] Selected suggestion (fallback): {:?}", data); + + // Final check before calling callback + if !is_alive.load(Ordering::SeqCst) { + log!("[TYPEAHEAD] Component no longer alive before fallback callback, aborting"); + return; + } + + on_select.call(data.clone()); + + if let Some(input) = node_ref.get() { + input.set_value(&data.label); + } + } else { + log!("[ERROR] Failed to deserialize suggestion"); + } + } + }); + } + }) as Box); + + // Store the Rust selection handler globally with a component-specific name + let rust_handler_name = format!("rustSelectHandler_{}", component_id.replace("-", "_")); + js_sys::Reflect::set( + &js_sys::global(), + &rust_handler_name.into(), + closure.as_ref().unchecked_ref(), + ).map_err(|e| { + log!("[ERROR] Failed to store Rust selection handler: {:?}", e); + e + })?; + + // Store the closure in our struct instead of forgetting it + if let Ok(mut closures_mut) = closures.try_borrow_mut() { + closures_mut.selection_closure = Some(closure); + } else { + log!("[ERROR] Failed to store selection_closure: could not borrow closures"); + return Err(JsValue::from_str("Failed to store selection_closure")); } - // Get the bloodhound instance from the component-specific global variable - let bloodhound_var = format!("bloodhoundInstance_{}", component_id.replace("-", "_")); - // Initialization script with enhanced logging and error handling let init_script = format!( r#" console.log('[JS] Starting Typeahead init for #{input_id}'); try {{ - // Get the bloodhound instance from our component-specific variable - var bloodhound = window['{bloodhound_var}']; - if (!bloodhound) {{ - throw new Error('Bloodhound instance not found: {bloodhound_var}'); + // Get the component from registry + if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{ + throw new Error('Component not found in registry: {component_id}'); }} - // Define a custom source function that directly uses our Rust callback - var customSource = function(query, syncResults, asyncResults) {{ - console.log('[JS] Custom source called with query:', query); - - // Get the prepare function from our component-specific variable - var prepareFn = window['{prepare_fn_var}']; - if (!prepareFn) {{ - console.error('[JS] Prepare function not found: {prepare_fn_var}'); - syncResults([]); - return; - }} - - // Call our prepare function - prepareFn(query, function(suggestions) {{ - console.log('[JS] Suggestions from custom source:', suggestions); - syncResults(suggestions); - }}, asyncResults); - }}; + // Get the bloodhound instance from the registry + var bloodhound = window.typeaheadRegistry['{component_id}'].bloodhound; + if (!bloodhound) {{ + throw new Error('Bloodhound instance not found in registry'); + }} // Initialize typeahead with error handling var $input = $('#{input_id}'); @@ -526,7 +914,10 @@ fn initialize_typeahead( if (!data) return ''; return data.displayLabel || data.label || ''; }}, - source: customSource, + source: function(query, syncResults, asyncResults) {{ + // Call the fetch handler through the registry + window.typeaheadRegistry['{component_id}'].callHandler('fetch', query, syncResults, asyncResults); + }}, templates: {{ suggestion: function(data) {{ if (!data) return $('
').addClass('empty-suggestion').text('Invalid data'); @@ -550,45 +941,40 @@ fn initialize_typeahead( console.log('[JS] Selection event received with suggestion:', suggestion); - // Get the handler from our component-specific variable - var handler = window['{handler_name}']; - if (!handler) {{ - console.error('[JS] Handler function not found: {handler_name}'); - return; - }} - - // Call the handler - handler(ev, suggestion); + // Call the selection handler through the registry + window.typeaheadRegistry['{component_id}'].callHandler('select', ev, suggestion); }}); console.log('[JS] Typeahead initialized successfully for #{input_id}'); + true }} catch (e) {{ console.error('[JS] Typeahead init error for #{input_id}:', e); + false }} "#, - input_id = input_id, - bloodhound_var = bloodhound_var, - prepare_fn_var = format!("bloodhoundPrepare_{}", component_id.replace("-", "_")), - handler_name = handler_name_for_log + component_id = component_id, + input_id = input_id ); log!("[RUST] Running initialization script for: {}", input_id); - if let Err(e) = js_sys::eval(&init_script) { - log!("[RUST] Eval error: {:?}", e); + match js_sys::eval(&init_script) { + Ok(result) => { + if let Some(success) = result.as_bool() { + if success { + log!("[RUST] Initialization script executed successfully"); + Ok(()) + } else { + log!("[RUST] Initialization script failed"); + Err(JsValue::from_str("Initialization script failed")) + } + } else { + log!("[RUST] Initialization script returned non-boolean result"); + Ok(()) + } + }, + Err(e) => { + log!("[RUST] Eval error: {:?}", e); + Err(e) + } } } - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_name = "Bloodhound")] - type Bloodhound; - - #[wasm_bindgen(constructor, js_namespace = window)] - fn new(options: &JsValue) -> Bloodhound; - - #[wasm_bindgen(method)] - fn initialize(this: &Bloodhound, prefetch: bool); - - #[wasm_bindgen(method, js_name = "ttAdapter")] - fn tt_adapter(this: &Bloodhound) -> JsValue; -} \ No newline at end of file