Browse Source

Fix reactive state management to prevent infinite loops

- Implemented debounced graph updates with 100ms delay
- Created derived graphDependencies to track all update triggers in one place
- Added setTimeout to defer state changes in auto-disable logic
- Broke synchronous update cycles that were causing stuttering
- Used updateTimer to prevent multiple simultaneous updates

The graph now updates smoothly without stuttering or infinite loops.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
master
limina1 9 months ago
parent
commit
9b5767d97f
  1. 114
      src/lib/navigator/EventNetwork/index.svelte

114
src/lib/navigator/EventNetwork/index.svelte

@ -907,40 +907,51 @@
* Watch for changes that should trigger a graph update * Watch for changes that should trigger a graph update
*/ */
let isUpdating = false; let isUpdating = false;
let updateTimer: ReturnType<typeof setTimeout> | null = null;
// Create a derived state that combines all dependencies
const graphDependencies = $derived({
levels: currentLevels,
star: starVisualization,
tags: showTagAnchors,
tagType: selectedTagType,
disabled: disabledTags.size,
persons: showPersonNodes,
disabledPersons: disabledPersons.size,
showSignedBy,
showReferenced,
eventsLength: events?.length || 0
});
$effect(() => { // Debounced update function
// Prevent recursive updates function scheduleGraphUpdate() {
if (isUpdating) return; if (updateTimer) {
clearTimeout(updateTimer);
debug("Effect triggered", { }
hasSvg: !!svg,
eventCount: events?.length,
currentLevels,
});
try {
if (svg && events?.length) {
// Track dependencies
const deps = {
levels: currentLevels,
star: starVisualization,
tags: showTagAnchors,
tagType: selectedTagType,
disabled: disabledTags.size,
persons: showPersonNodes,
disabledPersons: disabledPersons.size,
showSignedBy,
showReferenced
};
updateTimer = setTimeout(() => {
if (!isUpdating && svg && events?.length > 0) {
debug("Scheduled graph update executing", graphDependencies);
isUpdating = true; isUpdating = true;
updateGraph(); try {
isUpdating = false; updateGraph();
} catch (error) {
console.error("Error updating graph:", error);
errorMessage = `Error updating graph: ${error instanceof Error ? error.message : String(error)}`;
} finally {
isUpdating = false;
updateTimer = null;
}
} }
} catch (error) { }, 100); // 100ms debounce
console.error("Error in effect:", error); }
errorMessage = `Error updating graph: ${error instanceof Error ? error.message : String(error)}`;
isUpdating = false; $effect(() => {
// Just track the dependencies and schedule update
const deps = graphDependencies;
if (svg && events?.length > 0) {
scheduleGraphUpdate();
} }
}); });
@ -996,37 +1007,52 @@
/** /**
* Watch for tag anchor count and auto-disable if exceeds threshold * Watch for tag anchor count and auto-disable if exceeds threshold
*/ */
let autoDisableTimer: ReturnType<typeof setTimeout> | null = null;
$effect(() => { $effect(() => {
// Clear any pending timer
if (autoDisableTimer) {
clearTimeout(autoDisableTimer);
autoDisableTimer = null;
}
// Only check when tag anchors are shown and we have tags // Only check when tag anchors are shown and we have tags
if (showTagAnchors && tagAnchorInfo.length > 0) { if (showTagAnchors && tagAnchorInfo.length > 0) {
// If we have more than MAX_TAG_ANCHORS and haven't auto-disabled yet // If we have more than MAX_TAG_ANCHORS and haven't auto-disabled yet
if (tagAnchorInfo.length > MAX_TAG_ANCHORS && !autoDisabledTags) { if (tagAnchorInfo.length > MAX_TAG_ANCHORS && !autoDisabledTags) {
debug(`Auto-disabling tags: ${tagAnchorInfo.length} exceeds maximum of ${MAX_TAG_ANCHORS}`); // Defer the state update to break the sync cycle
autoDisableTimer = setTimeout(() => {
// Disable all tags debug(`Auto-disabling tags: ${tagAnchorInfo.length} exceeds maximum of ${MAX_TAG_ANCHORS}`);
const newDisabledTags = new Set<string>();
tagAnchorInfo.forEach(anchor => { // Disable all tags
const tagId = `${anchor.type}-${anchor.label}`; const newDisabledTags = new Set<string>();
newDisabledTags.add(tagId); tagAnchorInfo.forEach(anchor => {
}); const tagId = `${anchor.type}-${anchor.label}`;
newDisabledTags.add(tagId);
});
disabledTags = newDisabledTags; disabledTags = newDisabledTags;
autoDisabledTags = true; autoDisabledTags = true;
// Optional: Show a notification to the user // Optional: Show a notification to the user
console.info(`[EventNetwork] Auto-disabled ${tagAnchorInfo.length} tag anchors to prevent graph overload. Click individual tags in the legend to enable them.`); console.info(`[EventNetwork] Auto-disabled ${tagAnchorInfo.length} tag anchors to prevent graph overload. Click individual tags in the legend to enable them.`);
}, 0);
} }
// Reset auto-disabled flag if tag count goes back down // Reset auto-disabled flag if tag count goes back down
if (tagAnchorInfo.length <= MAX_TAG_ANCHORS && autoDisabledTags) { if (tagAnchorInfo.length <= MAX_TAG_ANCHORS && autoDisabledTags) {
autoDisabledTags = false; autoDisableTimer = setTimeout(() => {
autoDisabledTags = false;
}, 0);
} }
} }
// Reset when tag anchors are hidden // Reset when tag anchors are hidden
if (!showTagAnchors && autoDisabledTags) { if (!showTagAnchors && autoDisabledTags) {
autoDisabledTags = false; autoDisableTimer = setTimeout(() => {
autoDisabledTags = false;
}, 0);
} }
}); });

Loading…
Cancel
Save