Compare commits

..

No commits in common. "a2a1a48ea62a4f5de460273236ab5fa375b6209f" and "e528f9e6840a789e4d3243d86e673e9db1c4169c" have entirely different histories.

5 changed files with 15 additions and 382 deletions

View file

@ -135,16 +135,8 @@ pub fn TypeaheadInput(
initialized: false,
bloodhound: null,
handlers: {{}},
// EXPLICIT ALIVE FLAG
alive: true,
// Method to safely call handlers
callHandler: function(handlerName, ...args) {{
// DEFENSIVE: Check alive flag before calling any handler
if (!this.alive) {{
console.warn('[JS] Component {component_id} is no longer alive, ignoring handler call: ' + handlerName);
return null;
}}
try {{
const handler = this.handlers[handlerName];
if (handler && typeof handler === 'function') {{
@ -157,12 +149,6 @@ pub fn TypeaheadInput(
}},
// Method to register a handler
registerHandler: function(name, fn) {{
// DEFENSIVE: Don't register handlers if component is not alive
if (!this.alive) {{
console.warn('[JS] Component {component_id} is no longer alive, ignoring handler registration: ' + name);
return false;
}}
this.handlers[name] = fn;
return true;
}},
@ -177,43 +163,11 @@ pub fn TypeaheadInput(
// Method to clean up all resources
cleanup: function() {{
try {{
// IMPORTANT: Set alive to false FIRST to prevent any new calls
this.alive = false;
console.log('[JS] Component {component_id} marked as not alive');
// Clean up typeahead
const inputId = 'typeahead-input-{component_id}';
const $input = $('#' + inputId);
if ($input.length > 0) {{
// Remove all event handlers first
$input.off('typeahead:select');
$input.off('typeahead:active');
$input.off('typeahead:idle');
$input.off('typeahead:open');
$input.off('typeahead:close');
$input.off('typeahead:change');
$input.off('typeahead:render');
$input.off('typeahead:autocomplete');
$input.off('typeahead:cursorchange');
$input.off('typeahead:asyncrequest');
$input.off('typeahead:asynccancel');
$input.off('typeahead:asyncreceive');
console.log('[JS] Removed all typeahead event handlers for {component_id}');
// Now destroy the typeahead
$input.typeahead('destroy');
console.log('[JS] Destroyed typeahead for {component_id}');
}}
// Explicitly null out the global handler references
if (window.rustSelectHandler_{component_id_safe}) {{
window.rustSelectHandler_{component_id_safe} = null;
console.log('[JS] Nulled rustSelectHandler_{component_id_safe}');
}}
if (window.rustFetchHandler_{component_id_safe}) {{
window.rustFetchHandler_{component_id_safe} = null;
console.log('[JS] Nulled rustFetchHandler_{component_id_safe}');
}}
// Clear all handlers
@ -227,8 +181,6 @@ pub fn TypeaheadInput(
return true;
}} catch (e) {{
console.error('[JS] Error during cleanup:', e);
// Still mark as not alive even if cleanup fails
this.alive = false;
return false;
}}
}}
@ -237,8 +189,7 @@ pub fn TypeaheadInput(
console.log('[JS] Registered component {component_id}');
true
"#,
component_id = component_id,
component_id_safe = component_id.replace("-", "_")
component_id = component_id
);
// Execute the registration script
@ -391,34 +342,20 @@ pub fn TypeaheadInput(
r#"
// Perform cleanup in JavaScript first
if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{
console.log('[JS] Starting cleanup for component {component_id}');
// Clean up the component
const result = window.typeaheadRegistry['{component_id}'].cleanup();
// DEFENSIVE: Explicitly null out global handlers even if cleanup fails
if (window.rustSelectHandler_{component_id_safe}) {{
window.rustSelectHandler_{component_id_safe} = null;
console.log('[JS] Nulled rustSelectHandler_{component_id_safe} during cleanup');
}}
if (window.rustFetchHandler_{component_id_safe}) {{
window.rustFetchHandler_{component_id_safe} = null;
console.log('[JS] Nulled rustFetchHandler_{component_id_safe} during 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 during cleanup');
console.warn('[JS] Component {component_id} not found in registry');
false
}}
"#,
component_id = component_id_for_cleanup,
component_id_safe = component_id_for_cleanup.replace("-", "_")
component_id = component_id_for_cleanup
);
// Execute JavaScript cleanup
@ -706,27 +643,18 @@ fn initialize_bloodhound(
let transport_fn = js_sys::Function::new_with_args(
"query, syncResults, asyncResults",
&format!(r#"
// DEFENSIVE: Check if registry exists and component is alive
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
console.warn('[JS] Component registry not found for {component_id}, returning empty results');
syncResults([]);
return;
}}
// DEFENSIVE: Check alive flag explicitly
if (!window.typeaheadRegistry['{component_id}'].alive) {{
console.warn('[JS] Component {component_id} is no longer alive, returning empty results');
syncResults([]);
return;
}}
// 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] Component registry not found for {component_id}');
syncResults([]);
}}
"#, component_id = component_id)
);
@ -980,16 +908,11 @@ fn initialize_typeahead(
r#"
console.log('[JS] Starting Typeahead init for #{input_id}');
try {{
// DEFENSIVE: Check if registry exists and component is alive
// Get the component from registry
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
throw new Error('Component not found in registry: {component_id}');
}}
// DEFENSIVE: Check alive flag explicitly
if (!window.typeaheadRegistry['{component_id}'].alive) {{
throw new Error('Component {component_id} is no longer alive');
}}
// Get the bloodhound instance from the registry
var bloodhound = window.typeaheadRegistry['{component_id}'].bloodhound;
if (!bloodhound) {{
@ -1002,9 +925,6 @@ fn initialize_typeahead(
throw new Error('Input element not found: #{input_id}');
}}
// DEFENSIVE: Remove any existing typeahead to prevent duplicate handlers
$input.typeahead('destroy');
$input.typeahead(
{{
hint: true,
@ -1018,20 +938,6 @@ fn initialize_typeahead(
return data.displayLabel || data.label || '';
}},
source: function(query, syncResults, asyncResults) {{
// DEFENSIVE: Check if registry exists and component is alive
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
console.warn('[JS] Component registry not found for {component_id}, returning empty results');
syncResults([]);
return;
}}
// DEFENSIVE: Check alive flag explicitly
if (!window.typeaheadRegistry['{component_id}'].alive) {{
console.warn('[JS] Component {component_id} is no longer alive, returning empty results');
syncResults([]);
return;
}}
// Call the fetch handler through the registry
window.typeaheadRegistry['{component_id}'].callHandler('fetch', query, syncResults, asyncResults);
}},
@ -1051,18 +957,6 @@ fn initialize_typeahead(
}}
)
.on('typeahead:select', function(ev, suggestion) {{
// DEFENSIVE: Check if registry exists and component is alive
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
console.warn('[JS] Component registry not found for {component_id}, ignoring selection event');
return;
}}
// DEFENSIVE: Check alive flag explicitly
if (!window.typeaheadRegistry['{component_id}'].alive) {{
console.warn('[JS] Component {component_id} is no longer alive, ignoring selection event');
return;
}}
if (!suggestion) {{
console.error('[JS] Selection event received with null suggestion');
return;

View file

@ -5,7 +5,6 @@ use tokio::sync::Mutex;
use compareware::db::Database;
use compareware::api::{ItemRequest, create_item, get_items, get_selected_properties, add_selected_property};
use compareware::models::item::Item;
use compareware::utils::panic_hook;
#[cfg(feature = "ssr")]
#[actix_web::main]
@ -20,8 +19,6 @@ async fn main() -> std::io::Result<()> {
use std::sync::Arc;
use tokio::sync::Mutex;
panic_hook::init();
// Setup logging
std::env::set_var("RUST_LOG", "info");
@ -170,15 +167,12 @@ pub fn main() {
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
pub fn main() {
// Initialize custom panic hook for better diagnostics
panic_hook::init();
// a client-side main function is required for using `trunk serve`
// prefer using `cargo leptos serve` instead
// to run: `trunk serve --open --features csr`
use compareware::app::*;
// console_error_panic_hook::set_once();
console_error_panic_hook::set_once();
leptos::mount_to_body(App);
}

View file

@ -1,2 +1 @@
pub mod leptos_owner;
pub mod panic_hook;

View file

@ -1,54 +0,0 @@
use std::panic;
use leptos::logging::log;
use wasm_bindgen::prelude::*;
/// Sets up a custom panic hook that provides more context for Leptos owner disposal panics
pub fn set_custom_panic_hook() {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
// Call the original hook first
original_hook(panic_info);
// Extract panic message
let message = if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else {
"Unknown panic".to_string()
};
// Check if this is an owner disposal panic
if message.contains("OwnerDisposed") {
log!("[PANIC] Leptos owner disposal detected. This usually happens when:");
log!("[PANIC] 1. A component has been unmounted but JavaScript is still calling into Rust");
log!("[PANIC] 2. An effect or signal update is running after the component is gone");
log!("[PANIC] 3. A closure or callback is being called after cleanup");
// Log current component registry state
let js_code = r#"
if (window.typeaheadRegistry) {
console.log('[PANIC] Current typeahead registry:',
Object.keys(window.typeaheadRegistry).map(id => ({
id,
alive: window.typeaheadRegistry[id].alive,
initialized: window.typeaheadRegistry[id].initialized
}))
);
} else {
console.log('[PANIC] No typeahead registry found');
}
"#;
let _ = js_sys::eval(js_code);
}
}));
}
/// Call in main.rs or app initialization
pub fn init() {
log!("[PANIC_HOOK] Setting up custom panic hook");
set_custom_panic_hook();
log!("[PANIC_HOOK] Custom panic hook set up successfully");
}

View file

@ -1,200 +0,0 @@
use wasm_bindgen_test::*;
use leptos::*;
use wasm_bindgen::JsValue;
use std::time::Duration;
use gloo_timers::future::sleep;
use compareware::components::typeahead_input::TypeaheadInput;
use compareware::models::item::WikidataSuggestion;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn test_typeahead_initialization() {
// Setup
let document = web_sys::window().unwrap().document().unwrap();
let container = document.create_element("div").unwrap();
document.body().unwrap().append_child(&container).unwrap();
container.set_id("test-container");
// Track initialization
let init_called = create_rw_signal(false);
// Create a test component
let test_component = move || {
let node_ref = create_node_ref::<html::Input>();
// Mock callbacks
let on_select = Callback::new(move |suggestion: WikidataSuggestion| {
log!("Selected: {}", suggestion.label);
});
let fetch_suggestions = Callback::new(move |query: String| {
log!("Fetching: {}", query);
init_called.set(true);
vec![]
});
view! {
<div>
<TypeaheadInput
value="".to_string()
on_select=on_select
fetch_suggestions=fetch_suggestions
node_ref=node_ref
/>
</div>
}
};
// Mount the component
mount_to(&container, test_component);
// Wait for initialization
for _ in 0..10 {
if init_called.get() {
break;
}
sleep(Duration::from_millis(100)).await;
}
// Verify initialization
assert!(init_called.get(), "Initialization callback was not called");
// Cleanup
document.body().unwrap().remove_child(&container).unwrap();
}
#[wasm_bindgen_test]
async fn test_typeahead_cleanup() {
// Setup
let document = web_sys::window().unwrap().document().unwrap();
let container = document.create_element("div").unwrap();
document.body().unwrap().append_child(&container).unwrap();
container.set_id("cleanup-test-container");
// Create a unique component ID for tracking
let component_id = format!("test-typeahead-{}", uuid::Uuid::new_v4());
// Create a test component
let test_component = move || {
let node_ref = create_node_ref::<html::Input>();
// Mock callbacks
let on_select = Callback::new(move |_: WikidataSuggestion| {});
let fetch_suggestions = Callback::new(move |_: String| vec![]);
view! {
<div>
<TypeaheadInput
value="".to_string()
on_select=on_select
fetch_suggestions=fetch_suggestions
node_ref=node_ref
/>
</div>
}
};
// Mount the component
let dispose = mount_to(&container, test_component);
// Wait for initialization
sleep(Duration::from_millis(500)).await;
// Check registry before unmount
let registry_before = js_sys::eval(&format!(
"window.typeaheadRegistry ? Object.keys(window.typeaheadRegistry).length : 0"
)).unwrap();
// Unmount the component
dispose();
// Wait for cleanup
sleep(Duration::from_millis(500)).await;
// Check if component was properly removed from registry
let registry_after = js_sys::eval(&format!(
"window.typeaheadRegistry ? Object.keys(window.typeaheadRegistry).length : 0"
)).unwrap();
// Registry should have one fewer entry
assert!(
registry_before.as_f64().unwrap() > registry_after.as_f64().unwrap(),
"Component was not properly removed from registry"
);
// Cleanup
document.body().unwrap().remove_child(&container).unwrap();
}
#[wasm_bindgen_test]
async fn test_rapid_mount_unmount() {
// Setup
let document = web_sys::window().unwrap().document().unwrap();
let container = document.create_element("div").unwrap();
document.body().unwrap().append_child(&container).unwrap();
container.set_id("rapid-test-container");
// Perform rapid mount/unmount cycles to test for race conditions
for i in 0..5 {
log!("Mount/unmount cycle {}", i);
// Create a test component
let test_component = move || {
let node_ref = create_node_ref::<html::Input>();
let on_select = Callback::new(move |_: WikidataSuggestion| {});
let fetch_suggestions = Callback::new(move |_: String| vec![]);
view! {
<div>
<TypeaheadInput
value="".to_string()
on_select=on_select
fetch_suggestions=fetch_suggestions
node_ref=node_ref
/>
</div>
}
};
// Mount
let dispose = mount_to(&container, test_component);
// Wait briefly
sleep(Duration::from_millis(50)).await;
// Unmount
dispose();
// Wait briefly
sleep(Duration::from_millis(50)).await;
}
// Wait for any pending cleanup
sleep(Duration::from_millis(500)).await;
// Check if registry is clean
let registry_size = js_sys::eval(
"window.typeaheadRegistry ? Object.keys(window.typeaheadRegistry).length : 0"
).unwrap();
// Registry should be empty or at least not growing
assert!(
registry_size.as_f64().unwrap() < 5.0,
"Registry has too many entries after rapid mount/unmount cycles"
);
// Cleanup
document.body().unwrap().remove_child(&container).unwrap();
}
// Helper function to mount a component to a container
fn mount_to(container: &web_sys::Element, component: impl FnOnce() -> View + 'static) -> impl FnOnce() {
let runtime = create_runtime();
let view = component();
leptos::mount_to_with_runtime(container, || view, runtime.clone());
move || {
runtime.dispose();
}
}