fix(memory issues): made the followng to help debug memory issues.

- Reduce Closure Captures: Take snapshots of signal values before using them in closures.
-Simplify Event Handlers: Break down complex event handlers into smaller, more manageable pieces.
-Improve Error Handling: Add proper error handling to prevent crashes when unexpected conditions occur.
-Optimize Data Flow: Ensure data flows in a more predictable way through the application.
-Isolate Side Effects: Move side effects like API calls into separate tasks to avoid blocking the main thread.

(in progress)
This commit is contained in:
ryan 2025-04-24 18:03:45 +03:00
parent 26724d9c45
commit 1fd4131298

View file

@ -597,59 +597,65 @@ pub fn ItemsList(
let set_property_labels = set_property_labels.clone(); let set_property_labels = set_property_labels.clone();
let property_cache = property_cache.clone(); let property_cache = property_cache.clone();
let set_property_cache = set_property_cache.clone(); let set_property_cache = set_property_cache.clone();
let set_custom_properties = set_custom_properties.clone();
let set_selected_properties = set_selected_properties.clone();
Arc::new(move |property: String| { Arc::new(move |property: String| {
// Normalize the property ID // Normalize the property ID
let normalized_property = property.replace("http://www.wikidata.org/prop/", ""); let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
let normalized_property_clone = normalized_property.clone();
// Early return if property is empty
// Check if property is empty if normalized_property.is_empty() {
if normalized_property.is_empty() { log!("Attempted to add empty property, ignoring");
log!("Attempted to add empty property, ignoring"); return;
return; }
}
// Create local copies of all signals to avoid capturing them in closures
// Check if label already exists let property_labels_snapshot = property_labels.get();
if !property_labels.get().contains_key(&normalized_property) { let selected_properties_snapshot = selected_properties.get();
spawn_local({ let custom_properties_snapshot = custom_properties.get();
let normalized_property = normalized_property.clone();
let set_property_labels = set_property_labels.clone(); // Check if label already exists
async move { if !property_labels_snapshot.contains_key(&normalized_property) {
// Add a placeholder label while fetching let normalized_property_clone = normalized_property.clone();
set_property_labels.update(|map| { let set_property_labels_clone = set_property_labels.clone();
map.insert(normalized_property.clone(), normalized_property.clone());
}); // Add a placeholder label immediately
set_property_labels.update(|map| {
match fetch_property_labels(vec![normalized_property.clone()]).await { map.insert(normalized_property_clone.clone(), normalized_property_clone.clone());
});
// Fetch the actual label in a separate task
spawn_local(async move {
match fetch_property_labels(vec![normalized_property_clone.clone()]).await {
Ok(labels) => { Ok(labels) => {
set_property_labels.update(|map| { set_property_labels_clone.update(|map| {
map.extend(labels); map.extend(labels);
}); });
}, },
Err(e) => { Err(e) => {
log!("Error fetching property labels: {:?}", e); log!("Error fetching property labels: {:?}", e);
// Keep the placeholder label
} }
} }
} });
}); }
}
// Check if property is already selected
// Check if property is already selected if !selected_properties_snapshot.contains_key(&normalized_property) {
if !selected_properties.get().contains_key(&normalized_property) && !normalized_property.is_empty() { // Add property to selected properties
// Add property to selected properties set_selected_properties.update(|selected| {
set_selected_properties.update(|selected| { selected.insert(normalized_property.clone(), true);
selected.insert(normalized_property.clone(), true); });
});
// Save the selected property to the database
// Save the selected property to the database let current_url_clone = Rc::clone(&current_url);
spawn_local({ let normalized_property_clone = normalized_property.clone();
let current_url = Rc::clone(&current_url);
let normalized_property = normalized_property_clone.clone(); spawn_local(async move {
async move {
let response = gloo_net::http::Request::post( let response = gloo_net::http::Request::post(
&format!("/api/urls/{}/properties", encode(&current_url)) &format!("/api/urls/{}/properties", encode(&current_url_clone))
) )
.json(&normalized_property) .json(&normalized_property_clone)
.unwrap() .unwrap()
.send() .send()
.await; .await;
@ -666,85 +672,84 @@ pub fn ItemsList(
log!("Error saving property: {:?}", err); log!("Error saving property: {:?}", err);
} }
} }
}
});
}
set_custom_properties.update(|props| {
if !props.contains(&normalized_property) && !normalized_property.is_empty() {
props.push(normalized_property.clone());
//update the selected_properties state when a new property is added
set_selected_properties.update(|selected| {
selected.insert(normalized_property.clone(), true);
}); });
}
// Ensure the grid updates reactively
// Update custom properties if not already present
if !custom_properties_snapshot.contains(&normalized_property) {
set_custom_properties.update(|props| {
props.push(normalized_property.clone());
});
// Update items with the new property
set_items.update(|items| { set_items.update(|items| {
for item in items { for item in items {
// Safely insert the property if it doesn't exist // Only add if it doesn't exist
if !item.custom_properties.contains_key(&normalized_property) { if !item.custom_properties.contains_key(&normalized_property) {
item.custom_properties.insert(normalized_property.clone(), "".to_string()); item.custom_properties.insert(normalized_property.clone(), "".to_string());
} }
// Save the updated item to the database
let item_clone = item.clone();
spawn_local({
let current_url = Rc::clone(&current_url);
async move {
save_item_to_db(item_clone, selected_properties, current_url.to_string()).await;
}
});
} }
}); });
// Use the property label from the property_labels signal // Save each item to the database
let property_label = property_labels.get().get(&normalized_property).cloned().unwrap_or_else(|| normalized_property.clone()); let items_snapshot = items.get();
for item in items_snapshot {
let item_clone = item.clone();
let current_url_clone = Rc::clone(&current_url);
let selected_properties_clone = selected_properties;
spawn_local(async move {
save_item_to_db(item_clone, selected_properties_clone, current_url_clone.to_string()).await;
});
}
// Log the addition
let property_label = property_labels_snapshot
.get(&normalized_property)
.cloned()
.unwrap_or_else(|| normalized_property.clone());
log!("Added property with label: {}", property_label); log!("Added property with label: {}", property_label);
} }
});
// Fetch the relevant value for each item and populate the corresponding cells // Fetch Wikidata properties for items with IDs
set_items.update(|items| { let items_snapshot = items.get();
for item in items { for item in items_snapshot {
// Initialize property with empty string if it doesn't exist
item.custom_properties.entry(normalized_property.clone())
.or_insert_with(|| "".to_string());
// Only fetch properties if Wikidata ID exists
if let Some(wikidata_id) = &item.wikidata_id { if let Some(wikidata_id) = &item.wikidata_id {
let wikidata_id = wikidata_id.clone(); let wikidata_id_clone = wikidata_id.clone();
let set_items = set_items.clone(); let normalized_property_clone = normalized_property.clone();
let set_fetched_properties = set_fetched_properties.clone(); let set_items_clone = set_items.clone();
let property_clone = normalized_property.clone(); let set_property_labels_clone = set_property_labels.clone();
let property_cache_clone = property_cache.clone();
let set_property_cache_clone = set_property_cache.clone();
let property_labels_clone = property_labels.clone();
spawn_local(async move { spawn_local(async move {
let properties = fetch_item_properties( let properties = fetch_item_properties(
&wikidata_id, &wikidata_id_clone,
set_property_labels.clone(), set_property_labels_clone,
property_cache.clone(), property_cache_clone,
set_property_cache.clone(), set_property_cache_clone,
property_labels.clone() property_labels_clone
).await; ).await;
// Update the specific property for this item if let Some(value) = properties.get(&normalized_property_clone) {
if let Some(value) = properties.get(&property_clone) { set_items_clone.update(|items| {
set_items.update(|items| { for item in items {
if let Some(item) = items.iter_mut() if item.wikidata_id.as_ref() == Some(&wikidata_id_clone) {
.find(|i| i.wikidata_id.as_ref() == Some(&wikidata_id)) item.custom_properties.insert(
{ normalized_property_clone.clone(),
item.custom_properties.insert( value.clone()
property_clone.clone(), );
value.clone() }
);
} }
}); });
} }
}); });
} }
} }
}); })
})}; };
// Update item fields // Update item fields
let update_item = { let update_item = {
@ -835,19 +840,22 @@ pub fn ItemsList(
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{properties.into_iter().map(|property| { {properties.into_iter().map(|property| {
let update_item_cloned = Arc::clone(&update_item); let update_item_cloned = Arc::clone(&update_item);
let current_url_for_closure = Rc::clone(&current_url); let current_url_for_closure = Rc::clone(&current_url);
log!("Rendering property: {}", property); log!("Rendering property: {}", property);
view! { view! {
<tr> <tr>
<td>{ property }</td> <td>{ property }</td>
{{ {{
// Clone current_url before the nested closure
let current_url_for_inner = Rc::clone(&current_url_for_closure);
move || { move || {
let items = items.get(); let items = items.get();
items.iter().enumerate().map(|(index, item)| { items.iter().enumerate().map(|(index, item)| {
let update_item_clone = Arc::clone(&update_item_cloned); let update_item_clone = Arc::clone(&update_item_cloned);
let current_url_clone = Rc::clone(&current_url_for_closure); let current_url_clone = Rc::clone(&current_url_for_inner);
view! { view! {
<td> <td>
@ -856,54 +864,69 @@ pub fn ItemsList(
<div class="typeahead-container"> <div class="typeahead-container">
<TypeaheadInput <TypeaheadInput
value=item.name.clone() value=item.name.clone()
fetch_suggestions=Callback::new(move |query: String| -> Vec<WikidataSuggestion> { fetch_suggestions=Callback::new({
let key = format!("name-{}", index); let key = format!("name-{}", index);
fetch_wikidata_suggestions(key.clone(), query.clone()); let wikidata_suggestions_clone = wikidata_suggestions.clone();
// Add a small delay to ensure state is updated move |query: String| -> Vec<WikidataSuggestion> {
let suggestions = wikidata_suggestions.get(); // Fetch suggestions in a separate function to avoid capturing too much
suggestions.get(&key).cloned().unwrap_or_default() fetch_wikidata_suggestions(key.clone(), query.clone());
// Return current suggestions from the signal
let suggestions = wikidata_suggestions_clone.get();
suggestions.get(&key).cloned().unwrap_or_default()
}
}) })
on_select=Callback::new(move |suggestion: WikidataSuggestion| { on_select=Callback::new({
set_items.update(|items| { let set_items_clone = set_items.clone();
if let Some(item) = items.get_mut(index) { let set_property_labels_clone = set_property_labels.clone();
item.name = suggestion.display.label.value.clone(); let property_cache_clone = property_cache.clone();
item.description = suggestion.display.description.value.clone(); let set_property_cache_clone = set_property_cache.clone();
item.wikidata_id = Some(suggestion.id.clone()); let property_labels_clone = property_labels.clone();
// Automatically fetch properties when Wikidata ID is set move |suggestion: WikidataSuggestion| {
if let Some(wikidata_id) = &item.wikidata_id { let wikidata_id = suggestion.id.clone();
spawn_local({
let set_property_labels = set_property_labels.clone(); set_items_clone.update(|items| {
let property_cache = property_cache.clone(); if let Some(item) = items.get_mut(index) {
let set_property_cache = set_property_cache.clone(); item.name = suggestion.display.label.value.clone();
let property_labels = property_labels.clone(); item.description = suggestion.display.description.value.clone();
let wikidata_id = wikidata_id.clone(); item.wikidata_id = Some(wikidata_id.clone());
async move {
fetch_item_properties(
&wikidata_id,
set_property_labels,
property_cache,
set_property_cache,
property_labels
).await;
}
});
} }
} });
});
// Fetch properties in a separate task
let set_property_labels_for_task = set_property_labels_clone.clone();
let property_cache_for_task = property_cache_clone.clone();
let set_property_cache_for_task = set_property_cache_clone.clone();
let property_labels_for_task = property_labels_clone.clone();
let wikidata_id_for_task = wikidata_id.clone();
spawn_local(async move {
fetch_item_properties(
&wikidata_id_for_task,
set_property_labels_for_task,
property_cache_for_task,
set_property_cache_for_task,
property_labels_for_task
).await;
});
}
}) })
is_last_row={index == items.len() - 1} is_last_row={index == items.len() - 1}
on_input=Callback::new({ on_input=Callback::new({
// Clone items.len() before moving into the closure // Clone items.len() before moving into the closure
let items_len = items.len(); let items_len = items.len();
let current_url_for_spawn = Rc::clone(&current_url_clone); let set_items_clone = set_items.clone();
let current_url_clone = Rc::clone(&current_url_clone);
let selected_properties_clone = selected_properties.clone();
move |value: String| { move |value: String| {
if index == items_len - 1 && !value.is_empty() { if index == items_len - 1 && !value.is_empty() {
// Use the cloned current_url let current_url_for_new_item = Rc::clone(&current_url_clone);
let current_url_for_new_item = Rc::clone(&current_url_for_spawn); let selected_properties_for_new_item = selected_properties_clone.clone();
set_items.update(|items| {
set_items_clone.update(|items| {
let new_item = Item { let new_item = Item {
id: Uuid::new_v4().to_string(), id: Uuid::new_v4().to_string(),
name: String::new(), name: String::new(),
@ -913,13 +936,17 @@ pub fn ItemsList(
}; };
items.push(new_item.clone()); items.push(new_item.clone());
// Save the new item to the database // Save the new item to the database in a separate task
spawn_local({ let new_item_clone = new_item.clone();
let current_url = Rc::clone(&current_url_for_new_item); let current_url_for_task = Rc::clone(&current_url_for_new_item);
let selected_properties = selected_properties; let selected_properties_for_task = selected_properties_for_new_item;
async move {
save_item_to_db(new_item, selected_properties, current_url.to_string()).await; spawn_local(async move {
} save_item_to_db(
new_item_clone,
selected_properties_for_task,
current_url_for_task.to_string()
).await;
}); });
}); });
} }
@ -952,12 +979,12 @@ pub fn ItemsList(
}} }}
</td> </td>
} }
}).collect::<Vec<_>>() }).collect::<Vec<_>>()
} }
}} }}
</tr> </tr>
} }
}).collect::<Vec<_>>()} }).collect::<Vec<_>>()}
// Dynamically adding custom properties as columns // Dynamically adding custom properties as columns
{{ {{
let update_item_outer = Arc::clone(&update_item); let update_item_outer = Arc::clone(&update_item);
@ -1025,36 +1052,53 @@ pub fn ItemsList(
</tbody> </tbody>
</table> </table>
<div style="margin-bottom: 20px;"> <div style="margin-bottom: 20px;">
<input type="text" id="new-property" placeholder="Add New Property" list="properties" on:keydown=move |event| { <input
if event.key() == "Enter" { type="text"
let input_element = event.target().unwrap().dyn_into::<web_sys::HtmlInputElement>().unwrap(); id="new-property"
let input_value = input_element.value(); placeholder="Add New Property"
list="properties"
// Extract property ID from "Label (P123)" format on:keydown=move |event| {
let property_id = input_value if event.key() == "Enter" {
.split(" (") // Safely get the input element
.last() if let Some(target) = event.target() {
.and_then(|s| s.strip_suffix(')')) if let Ok(input_element) = target.dyn_into::<web_sys::HtmlInputElement>() {
.unwrap_or(&input_value) let input_value = input_element.value();
.to_string();
// Extract property ID from "Label (P123)" format
if !property_id.is_empty() { let property_id = if input_value.contains(" (") && input_value.ends_with(')') {
// Add the property using the extracted ID let parts: Vec<&str> = input_value.rsplitn(2, " (").collect();
add_property(property_id); if parts.len() == 2 {
input_element.set_value(""); parts[0].trim_end_matches(')').to_string()
} else {
input_value.clone()
}
} else {
input_value.clone()
};
if !property_id.is_empty() {
// Add the property using the extracted ID
add_property(property_id);
input_element.set_value("");
}
}
}
} }
} }
} /> />
<datalist id="properties"> <datalist id="properties">
{move || { {move || {
let property_labels = property_labels.get().clone(); let property_labels_snapshot = property_labels.get();
property_labels.into_iter().map(|(property_id, label)| { property_labels_snapshot.iter()
view! { .map(|(property_id, label)| {
<option value={format!("{} ({})", label, property_id)}> let option_value = format!("{} ({})", label, property_id);
{ format!("{} ({})", label, property_id) } view! {
</option> <option value={option_value.clone()}>
} {option_value}
}).collect::<Vec<_>>() </option>
}
})
.collect::<Vec<_>>()
}} }}
</datalist> </datalist>
</div> </div>