fix(typeahead): implement component isolation to prevent memory leaks and race conditions

This commit is contained in:
ryan 2025-04-25 17:52:30 +03:00
parent 1fd4131298
commit 0f28394fce

View file

@ -22,70 +22,134 @@ pub fn TypeaheadInput(
) -> impl IntoView { ) -> impl IntoView {
let (is_initialized, set_initialized) = create_signal(false); let (is_initialized, set_initialized) = create_signal(false);
// Create a unique ID for this component instance
let component_id = format!("typeahead-{}", uuid::Uuid::new_v4());
// Flag to track if component is mounted // Flag to track if component is mounted
let is_mounted = Rc::new(RefCell::new(true)); let is_mounted = Rc::new(RefCell::new(true));
let is_mounted_clone = is_mounted.clone(); let is_mounted_clone = is_mounted.clone();
// Cleanup function to run when component is unmounted
on_cleanup(move || {
log!("[CLEANUP] TypeaheadInput component unmounting");
*is_mounted_clone.borrow_mut() = false;
});
// Clone necessary values for the async task // Clone necessary values for the async task
let fetch_suggestions_clone = fetch_suggestions.clone(); let fetch_suggestions_clone = fetch_suggestions.clone();
let on_select_clone = on_select.clone(); let on_select_clone = on_select.clone();
let node_ref_clone = node_ref.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 { spawn_local(async move {
log!("[INIT] Component mounted"); log!("[INIT] Component mounted: {}", component_id_clone);
let mut retries = 0; let mut retries = 0;
while retries < 10 { while retries < 10 && !cancel_token.get() {
// Check if component is still mounted before proceeding // Check if component is still mounted before proceeding
if !*is_mounted.borrow() { if !*is_mounted.borrow() {
log!("[INIT] Component unmounted, aborting initialization"); log!("[INIT] Component unmounted, aborting initialization: {}", component_id_clone);
return; return;
} }
if let Some(input) = node_ref_clone.get() { if let Some(input) = node_ref_clone.get() {
log!("[INIT] Input element found"); log!("[INIT] Input element found: {}", component_id_clone);
// Only proceed if component is still mounted // Only proceed if component is still mounted
if !*is_mounted.borrow() { if !*is_mounted.borrow() || cancel_token.get() {
log!("[INIT] Component unmounted after input found, aborting"); log!("[INIT] Component unmounted after input found, aborting: {}", component_id_clone);
return; return;
} }
let bloodhound = initialize_bloodhound(fetch_suggestions_clone.clone()); let bloodhound = initialize_bloodhound(fetch_suggestions_clone.clone(), &component_id_clone);
// Store bloodhound globally // Store bloodhound in a component-specific global variable
js_sys::Reflect::set( let bloodhound_var = format!("bloodhoundInstance_{}", component_id_clone.replace("-", "_"));
if let Err(_) = js_sys::Reflect::set(
&js_sys::global(), &js_sys::global(),
&"bloodhoundInstance".into(), &bloodhound_var.into(),
&bloodhound &bloodhound
).unwrap(); ) {
log!("[ERROR] Failed to store bloodhound instance: {}", component_id_clone);
}
// Only proceed if component is still mounted // Only proceed if component is still mounted
if !*is_mounted.borrow() { if !*is_mounted.borrow() || cancel_token.get() {
log!("[INIT] Component unmounted before typeahead init, aborting"); log!("[INIT] Component unmounted before typeahead init, aborting: {}", component_id_clone);
return; return;
} }
initialize_typeahead(&input, bloodhound, on_select_clone.clone(), node_ref_clone.clone()); initialize_typeahead(&input, bloodhound, on_select_clone.clone(), node_ref_clone.clone(), &component_id_clone);
// Only set initialized if component is still mounted // Only set initialized if component is still mounted
if *is_mounted.borrow() { 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); set_initialized.set(true);
});
} }
break; 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; gloo_timers::future::sleep(Duration::from_millis(100)).await;
retries += 1; 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);
// 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}'];
}}
// Clean up the prepare function
if (window['{prepare_fn_var}']) {{
delete window['{prepare_fn_var}'];
}}
// 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
);
if let Err(e) = js_sys::eval(&cleanup_script) {
log!("[RUST] Cleanup script eval error: {:?}", e);
}
});
// CSS // CSS
let css = r#" let css = r#"
.typeahead.tt-input { .typeahead.tt-input {
@ -141,6 +205,11 @@ pub fn TypeaheadInput(
} }
"#; "#;
// Clone component_id for event handlers
let component_id_for_focus = component_id.clone();
let component_id_for_blur = component_id.clone();
let component_id_for_input = component_id.clone();
view! { view! {
<style> <style>
{css} {css}
@ -151,11 +220,11 @@ pub fn TypeaheadInput(
class="typeahead-input" class="typeahead-input"
prop:value=value prop:value=value
node_ref=node_ref node_ref=node_ref
on:focus=move |_| log!("[FOCUS] Name input focused") on:focus=move |_| log!("[FOCUS] Name input focused: {}", component_id_for_focus)
on:blur=move |_| log!("[FOCUS] Name input blurred") on:blur=move |_| log!("[FOCUS] Name input blurred: {}", component_id_for_blur)
on:input=move |ev| { on:input=move |ev| {
let value = event_target_value(&ev); let value = event_target_value(&ev);
log!("[INPUT] Value changed: {}", value); log!("[INPUT] Value changed: {} ({})", value, component_id_for_input);
// If this is the last row and we have an on_input callback, call it // If this is the last row and we have an on_input callback, call it
if is_last_row && !value.is_empty() { if is_last_row && !value.is_empty() {
@ -163,40 +232,32 @@ pub fn TypeaheadInput(
callback.call(value.clone()); callback.call(value.clone());
} }
} }
let _ = js_sys::eval("console.log('jQuery version:', $.fn.jquery)");
let _ = js_sys::eval("console.log('Typeahead version:', $.fn.typeahead ? 'loaded' : 'missing')");
} }
/> />
} }
} }
#[wasm_bindgen] fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>, component_id: &str) -> JsValue {
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;
}
fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> JsValue {
let bloodhound_options = Object::new(); let bloodhound_options = Object::new();
// Use component-specific names for global functions
let prepare_fn_var = format!("bloodhoundPrepare_{}", component_id.replace("-", "_"));
// Create a closure that will be called by Bloodhound to fetch suggestions // 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 remote_fn = Closure::wrap(Box::new(move |query: JsValue, sync: Function, _async_fn: Function| {
let query_str = query.as_string().unwrap_or_default(); let query_str = query.as_string().unwrap_or_default();
log!("[BLOODHOUND] Fetching suggestions for: {}", query_str); log!("[BLOODHOUND] Fetching suggestions for: {}", query_str);
// Get suggestions from the callback // Safely call the fetch callback
let suggestions = fetch.call(query_str.clone()); 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 // Create a JavaScript array to hold the suggestions
let js_suggestions = Array::new(); let js_suggestions = Array::new();
@ -206,19 +267,20 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
let obj = Object::new(); let obj = Object::new();
// Store the original ID, label, and description // Store the original ID, label, and description
Reflect::set(&obj, &"id".into(), &JsValue::from_str(&suggestion.id)).unwrap(); Reflect::set(&obj, &"id".into(), &JsValue::from_str(&suggestion.id)).unwrap_or_default();
Reflect::set(&obj, &"label".into(), &JsValue::from_str(&suggestion.label)).unwrap(); Reflect::set(&obj, &"label".into(), &JsValue::from_str(&suggestion.label)).unwrap_or_default();
Reflect::set(&obj, &"description".into(), &JsValue::from_str(&suggestion.description)).unwrap(); Reflect::set(&obj, &"description".into(), &JsValue::from_str(&suggestion.description)).unwrap_or_default();
// Store the display values directly on the object for easier access // Store the display values directly on the object for easier access
Reflect::set(&obj, &"displayLabel".into(), Reflect::set(&obj, &"displayLabel".into(),
&JsValue::from_str(&suggestion.display.label.value)).unwrap(); &JsValue::from_str(&suggestion.display.label.value)).unwrap_or_default();
Reflect::set(&obj, &"displayDescription".into(), Reflect::set(&obj, &"displayDescription".into(),
&JsValue::from_str(&suggestion.display.description.value)).unwrap(); &JsValue::from_str(&suggestion.display.description.value)).unwrap_or_default();
// Store the full suggestion for later retrieval // Store the full suggestion for later retrieval
let full_suggestion = JsValue::from_serde(&suggestion).unwrap(); if let Ok(full_suggestion) = JsValue::from_serde(&suggestion) {
Reflect::set(&obj, &"fullSuggestion".into(), &full_suggestion).unwrap(); Reflect::set(&obj, &"fullSuggestion".into(), &full_suggestion).unwrap_or_default();
}
// Add the object to the array // Add the object to the array
js_suggestions.push(&obj); js_suggestions.push(&obj);
@ -236,55 +298,64 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
// Set transport function to avoid AJAX requests // Set transport function to avoid AJAX requests
let transport_fn = js_sys::Function::new_with_args( let transport_fn = js_sys::Function::new_with_args(
"query, syncResults, asyncResults", "query, syncResults, asyncResults",
r#" &format!(r#"
// Call our custom prepare function directly // Call our custom prepare function directly
window.bloodhoundPrepare(query, syncResults, asyncResults); if (window['{prepare_fn_var}']) {{
"# window['{prepare_fn_var}'](query, syncResults, asyncResults);
}} else {{
console.error('[JS] Prepare function not found: {prepare_fn_var}');
syncResults([]);
}}
"#, prepare_fn_var = prepare_fn_var)
); );
Reflect::set( Reflect::set(
&remote_config, &remote_config,
&"transport".into(), &"transport".into(),
&transport_fn &transport_fn
).unwrap(); ).unwrap_or_default();
// Set a dummy URL (not actually used with custom transport) // Set a dummy URL (not actually used with custom transport)
Reflect::set( Reflect::set(
&remote_config, &remote_config,
&"url".into(), &"url".into(),
&JsValue::from_str("/dummy?query=%QUERY") &JsValue::from_str("/dummy?query=%QUERY")
).unwrap(); ).unwrap_or_default();
// Store our prepare function globally // Store our prepare function globally with component-specific name
js_sys::Reflect::set( // 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(), &js_sys::global(),
&"bloodhoundPrepare".into(), &prepare_fn_var.into(),
remote_fn.as_ref().unchecked_ref() remote_fn.as_ref().unchecked_ref()
).unwrap(); ) {
log!("[ERROR] Failed to store prepare function: {}", prepare_fn_var_for_log);
}
// Set rate limiting to prevent too many requests // Set rate limiting to prevent too many requests
Reflect::set( Reflect::set(
&remote_config, &remote_config,
&"rateLimitWait".into(), &"rateLimitWait".into(),
&JsValue::from(300) &JsValue::from(300)
).unwrap(); ).unwrap_or_default();
// Set the wildcard for query replacement // Set the wildcard for query replacement
Reflect::set( Reflect::set(
&remote_config, &remote_config,
&"wildcard".into(), &"wildcard".into(),
&JsValue::from_str("%QUERY") &JsValue::from_str("%QUERY")
).unwrap(); ).unwrap_or_default();
// Add the remote config to the options // Add the remote config to the options
Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap(); Reflect::set(&bloodhound_options, &"remote".into(), &remote_config).unwrap_or_default();
// Set the tokenizers // Set the tokenizers
let tokenizer = js_sys::Function::new_no_args( let tokenizer = js_sys::Function::new_no_args(
r#" r#"
return function(query) { return function(datum) {
return query.trim().split(/\s+/); return datum.toString().trim().split(/\s+/);
} };
"# "#
); );
@ -292,13 +363,13 @@ fn initialize_bloodhound(fetch: Callback<String, Vec<WikidataSuggestion>>) -> Js
&bloodhound_options, &bloodhound_options,
&"datumTokenizer".into(), &"datumTokenizer".into(),
&tokenizer &tokenizer
).unwrap(); ).unwrap_or_default();
Reflect::set( Reflect::set(
&bloodhound_options, &bloodhound_options,
&"queryTokenizer".into(), &"queryTokenizer".into(),
&tokenizer &tokenizer
).unwrap(); ).unwrap_or_default();
// Create and initialize the Bloodhound instance // Create and initialize the Bloodhound instance
let bloodhound = Bloodhound::new(&bloodhound_options.into()); let bloodhound = Bloodhound::new(&bloodhound_options.into());
@ -316,15 +387,18 @@ fn initialize_typeahead(
bloodhound: JsValue, bloodhound: JsValue,
on_select: Callback<WikidataSuggestion>, on_select: Callback<WikidataSuggestion>,
node_ref: NodeRef<Input>, node_ref: NodeRef<Input>,
component_id: &str,
) { ) {
log!("[TYPEAHEAD] Initializing for input: {}", input.id()); log!("[TYPEAHEAD] Initializing for input: {} ({})", input.id(), component_id);
let input_id = format!("typeahead-{}", uuid::Uuid::new_v4()); let input_id = format!("typeahead-input-{}", component_id);
input.set_id(&input_id); input.set_id(&input_id);
// Create selection handler closure // Create selection handler closure
let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| { let closure = Closure::wrap(Box::new(move |_event: web_sys::Event, suggestion: JsValue| {
log!("[TYPEAHEAD] Selection made"); 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 // 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 Some(full_suggestion) = js_sys::Reflect::get(&suggestion, &"fullSuggestion".into()).ok() {
if let Ok(data) = full_suggestion.into_serde::<WikidataSuggestion>() { if let Ok(data) = full_suggestion.into_serde::<WikidataSuggestion>() {
@ -347,21 +421,28 @@ fn initialize_typeahead(
} else { } else {
log!("[ERROR] Failed to deserialize suggestion"); log!("[ERROR] Failed to deserialize suggestion");
} }
});
}) as Box<dyn FnMut(web_sys::Event, JsValue)>); }) as Box<dyn FnMut(web_sys::Event, JsValue)>);
// Register global handler // Register global handler with component-specific name
let handler_name = format!("handler_{}", input_id.replace("-", "_")); let handler_name = format!("handler_{}", input_id.replace("-", "_"));
log!("[TYPEAHEAD] Registering handler with name: {}", handler_name); log!("[TYPEAHEAD] Registering handler with name: {}", handler_name);
js_sys::Reflect::set( // 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(), &js_sys::global(),
&handler_name.clone().into(), &handler_name.clone().into(),
closure.as_ref().unchecked_ref(), closure.as_ref().unchecked_ref(),
).unwrap(); ) {
log!("[ERROR] Failed to register handler: {}", handler_name_for_log);
}
// We'll clean this up in the component's cleanup function
closure.forget(); closure.forget();
// Cleanup code to remove the typeahead when component is unmounted // Register cleanup function for this specific typeahead instance
let cleanup_script = format!( let cleanup_script = format!(
r#" r#"
// Store a reference to the cleanup function for this input // Store a reference to the cleanup function for this input
@ -369,43 +450,71 @@ fn initialize_typeahead(
window.typeaheadCleanupFunctions = {{}}; window.typeaheadCleanupFunctions = {{}};
}} }}
window.typeaheadCleanupFunctions['{id}'] = function() {{ window.typeaheadCleanupFunctions['{component_id}'] = function() {{
try {{ try {{
$('#{id}').typeahead('destroy'); // Destroy the typeahead instance
delete window['{handler}']; $('#{input_id}').typeahead('destroy');
console.log('[JS] Typeahead cleanup for #{id} completed');
// Remove the handler
if (window['{handler_name}']) {{
delete window['{handler_name}'];
}}
console.log('[JS] Typeahead cleanup for #{input_id} completed');
}} catch (e) {{ }} catch (e) {{
console.error('[JS] Typeahead cleanup error:', e); console.error('[JS] Typeahead cleanup error for #{input_id}:', e);
}} }}
}}; }};
"#, "#,
id = input_id, component_id = component_id,
handler = handler_name input_id = input_id,
handler_name = handler_name_for_log
); );
if let Err(e) = js_sys::eval(&cleanup_script) { if let Err(e) = js_sys::eval(&cleanup_script) {
log!("[RUST] Cleanup script eval error: {:?}", e); log!("[RUST] Cleanup script eval error: {:?}", e);
} }
// Initialization script with enhanced logging // 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!( let init_script = format!(
r#" r#"
console.log('[JS] Starting Typeahead init for #{id}'); console.log('[JS] Starting Typeahead init for #{input_id}');
try {{ try {{
var bloodhound = window.bloodhoundInstance; // 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}');
}}
// Define a custom source function that directly uses our Rust callback // Define a custom source function that directly uses our Rust callback
var customSource = function(query, syncResults, asyncResults) {{ var customSource = function(query, syncResults, asyncResults) {{
console.log('[JS] Custom source called with query:', query); console.log('[JS] Custom source called with query:', query);
// Call our global prepare function directly // Get the prepare function from our component-specific variable
window.bloodhoundPrepare(query, function(suggestions) {{ 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); console.log('[JS] Suggestions from custom source:', suggestions);
syncResults(suggestions); syncResults(suggestions);
}}, asyncResults); }}, asyncResults);
}}; }};
$('#{id}').typeahead( // Initialize typeahead with error handling
var $input = $('#{input_id}');
if ($input.length === 0) {{
throw new Error('Input element not found: #{input_id}');
}}
$input.typeahead(
{{ {{
hint: true, hint: true,
highlight: true, highlight: true,
@ -414,58 +523,72 @@ fn initialize_typeahead(
{{ {{
name: 'suggestions', name: 'suggestions',
display: function(data) {{ display: function(data) {{
console.log('[JS] Display function called with data:', data); if (!data) return '';
return data.displayLabel || data.label || ''; return data.displayLabel || data.label || '';
}}, }},
source: customSource, source: customSource,
templates: {{ templates: {{
suggestion: function(data) {{ suggestion: function(data) {{
console.log('[JS] Rendering suggestion:', data); if (!data) return $('<div>').addClass('empty-suggestion').text('Invalid data');
return $('<div>') return $('<div>')
.addClass('suggestion-item') .addClass('suggestion-item')
.append($('<div>').addClass('label').text(data.displayLabel || data.label)) .append($('<div>').addClass('label').text(data.displayLabel || data.label || ''))
.append($('<div>').addClass('description').text(data.displayDescription || data.description)); .append($('<div>').addClass('description').text(data.displayDescription || data.description || ''));
}}, }},
empty: function() {{ empty: function() {{
console.log('[JS] No suggestions found');
return $('<div>').addClass('empty-suggestion').text('No matches found'); return $('<div>').addClass('empty-suggestion').text('No matches found');
}} }}
}} }}
}} }}
) )
.on('typeahead:select', function(ev, suggestion) {{ .on('typeahead:select', function(ev, suggestion) {{
if (!suggestion) {{
console.error('[JS] Selection event received with null suggestion');
return;
}}
console.log('[JS] Selection event received with suggestion:', suggestion); console.log('[JS] Selection event received with suggestion:', suggestion);
window['{handler}'](ev, 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);
}}); }});
console.log('[JS] Typeahead initialized successfully');
console.log('[JS] Typeahead initialized successfully for #{input_id}');
}} catch (e) {{ }} catch (e) {{
console.error('[JS] Typeahead init error:', e); console.error('[JS] Typeahead init error for #{input_id}:', e);
}} }}
"#, "#,
id = input_id, input_id = input_id,
handler = handler_name bloodhound_var = bloodhound_var,
prepare_fn_var = format!("bloodhoundPrepare_{}", component_id.replace("-", "_")),
handler_name = handler_name_for_log
); );
log!("[RUST] Initialization script: {}", init_script); log!("[RUST] Running initialization script for: {}", input_id);
if let Err(e) = js_sys::eval(&init_script) { if let Err(e) = js_sys::eval(&init_script) {
log!("[RUST] Eval error: {:?}", e); log!("[RUST] Eval error: {:?}", e);
} }
// Register cleanup function to run when component is unmounted
on_cleanup(move || {
log!("[CLEANUP] Running typeahead cleanup for input: {}", input_id);
let cleanup_call = format!(
r#"
if (window.typeaheadCleanupFunctions && window.typeaheadCleanupFunctions['{id}']) {{
window.typeaheadCleanupFunctions['{id}']();
delete window.typeaheadCleanupFunctions['{id}'];
}}
"#,
id = input_id
);
if let Err(e) = js_sys::eval(&cleanup_call) {
log!("[RUST] Cleanup call eval error: {:?}", 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;
} }