mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
fix(sidebar): clean up listeners + timers + store subscription on panel destroy
destroy() previously only called canvas.destroy(), leaking: - the container keydown listener - the insertBtn click listener - the debouncedPreview setTimeout handle (could write to a detached <pre>) - the debouncedPersist setTimeout handle (could writeFile after unmount) - the store.subscribe listener (store kept a stale closure) Also compute the persistence path once on mount instead of invoking getUserDataPath() three times per panel lifecycle. Inline debouncedPreview / debouncedPersist so their timer handles are reachable from destroy(). Capture store.subscribe's unsubscribe. destroy() order: clear timers, unsubscribe store, remove listeners, then canvas.destroy(). Amit Haridas
This commit is contained in:
@@ -28,21 +28,6 @@ const PREVIEW_DEBOUNCE_MS = 250;
|
|||||||
const PERSIST_DEBOUNCE_MS = 500;
|
const PERSIST_DEBOUNCE_MS = 500;
|
||||||
const PERSISTENCE_FILENAME = 'flowchart-session.json';
|
const PERSISTENCE_FILENAME = 'flowchart-session.json';
|
||||||
|
|
||||||
function persistencePath(getUserDataPath) {
|
|
||||||
return `${getUserDataPath()}/${PERSISTENCE_FILENAME}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function debounce(fn, ms) {
|
|
||||||
let handle = null;
|
|
||||||
return (...args) => {
|
|
||||||
if (handle) clearTimeout(handle);
|
|
||||||
handle = setTimeout(() => {
|
|
||||||
handle = null;
|
|
||||||
fn(...args);
|
|
||||||
}, ms);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFlowChartPanel(container, deps) {
|
function renderFlowChartPanel(container, deps) {
|
||||||
const { getUserDataPath, readFile, writeFile, insertAtCursor, renderMermaid = () => {} } = deps;
|
const { getUserDataPath, readFile, writeFile, insertAtCursor, renderMermaid = () => {} } = deps;
|
||||||
if (typeof getUserDataPath !== 'function') {
|
if (typeof getUserDataPath !== 'function') {
|
||||||
@@ -55,6 +40,10 @@ function renderFlowChartPanel(container, deps) {
|
|||||||
throw new Error('flowchart-panel: insertAtCursor is required');
|
throw new Error('flowchart-panel: insertAtCursor is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute the persistence path ONCE on mount. Re-resolving per call would
|
||||||
|
// hit the filesystem / IPC bridge unnecessarily on every debounced write.
|
||||||
|
const persistenceFile = `${getUserDataPath()}/${PERSISTENCE_FILENAME}`;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="flowchart-panel" tabindex="0">
|
<div class="flowchart-panel" tabindex="0">
|
||||||
<div class="flowchart-toolbar">
|
<div class="flowchart-toolbar">
|
||||||
@@ -83,14 +72,14 @@ function renderFlowChartPanel(container, deps) {
|
|||||||
let selectedEdgeId = null;
|
let selectedEdgeId = null;
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
persistencePath: persistencePath(getUserDataPath),
|
persistencePath: persistenceFile,
|
||||||
readFile,
|
readFile,
|
||||||
writeFile,
|
writeFile,
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hydrate from disk (defensively).
|
// Hydrate from disk (defensively).
|
||||||
readFile(persistencePath(getUserDataPath))
|
readFile(persistenceFile)
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (json) store.deserialize(json);
|
if (json) store.deserialize(json);
|
||||||
})
|
})
|
||||||
@@ -124,7 +113,10 @@ function renderFlowChartPanel(container, deps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const debouncedPreview = debounce(() => {
|
// Debounced live preview. Track the timer handle so destroy() can cancel
|
||||||
|
// any in-flight update that would otherwise write to a detached <pre>.
|
||||||
|
let previewTimer = null;
|
||||||
|
function runPreview() {
|
||||||
const source = toMermaid(store.getGraph());
|
const source = toMermaid(store.getGraph());
|
||||||
previewSourceEl.textContent = source;
|
previewSourceEl.textContent = source;
|
||||||
try {
|
try {
|
||||||
@@ -132,26 +124,41 @@ function renderFlowChartPanel(container, deps) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
previewRenderEl.textContent = `Preview error: ${err && err.message ? err.message : 'unknown'}`;
|
previewRenderEl.textContent = `Preview error: ${err && err.message ? err.message : 'unknown'}`;
|
||||||
}
|
}
|
||||||
}, PREVIEW_DEBOUNCE_MS);
|
}
|
||||||
|
function debouncedPreview() {
|
||||||
|
if (previewTimer) clearTimeout(previewTimer);
|
||||||
|
previewTimer = setTimeout(() => {
|
||||||
|
previewTimer = null;
|
||||||
|
runPreview();
|
||||||
|
}, PREVIEW_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
const debouncedPersist = debounce(() => {
|
// Debounced persistence. Track the timer handle so destroy() can cancel a
|
||||||
writeFile(persistencePath(getUserDataPath), store.serialize()).catch((err) => {
|
// queued writeFile that would otherwise fire after unmount.
|
||||||
if (statusEl) statusEl.textContent = `Save failed: ${err.message || err}`;
|
let persistTimer = null;
|
||||||
});
|
function debouncedPersist() {
|
||||||
}, PERSIST_DEBOUNCE_MS);
|
if (persistTimer) clearTimeout(persistTimer);
|
||||||
|
persistTimer = setTimeout(() => {
|
||||||
|
persistTimer = null;
|
||||||
|
writeFile(persistenceFile, store.serialize()).catch((err) => {
|
||||||
|
if (statusEl) statusEl.textContent = `Save failed: ${err.message || err}`;
|
||||||
|
});
|
||||||
|
}, PERSIST_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
store.subscribe(() => {
|
const unsubscribeStore = store.subscribe(() => {
|
||||||
debouncedPreview();
|
debouncedPreview();
|
||||||
debouncedPersist();
|
debouncedPersist();
|
||||||
});
|
});
|
||||||
|
|
||||||
insertBtn.addEventListener('click', () => {
|
function onInsertClick() {
|
||||||
const source = toMermaid(store.getGraph());
|
const source = toMermaid(store.getGraph());
|
||||||
insertAtCursor('```mermaid\n' + source + '\n```');
|
insertAtCursor('```mermaid\n' + source + '\n```');
|
||||||
});
|
}
|
||||||
|
insertBtn.addEventListener('click', onInsertClick);
|
||||||
|
|
||||||
// Keyboard shortcuts — panel-scoped.
|
// Keyboard shortcuts — panel-scoped.
|
||||||
container.addEventListener('keydown', (ev) => {
|
function onContainerKeyDown(ev) {
|
||||||
if (ev.ctrlKey && !ev.metaKey && ev.key.toLowerCase() === 'z') {
|
if (ev.ctrlKey && !ev.metaKey && ev.key.toLowerCase() === 'z') {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (ev.shiftKey) store.redo();
|
if (ev.shiftKey) store.redo();
|
||||||
@@ -169,7 +176,8 @@ function renderFlowChartPanel(container, deps) {
|
|||||||
selectedEdgeId = null;
|
selectedEdgeId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
container.addEventListener('keydown', onContainerKeyDown);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStore: () => store,
|
getStore: () => store,
|
||||||
@@ -181,6 +189,20 @@ function renderFlowChartPanel(container, deps) {
|
|||||||
selectedEdgeId = id;
|
selectedEdgeId = id;
|
||||||
},
|
},
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
|
// Clear timers BEFORE canvas.destroy(): canvas teardown may trigger a
|
||||||
|
// last pointer-move that schedules another preview/persist; we want
|
||||||
|
// those timers cancelled before canvas.destroy() runs.
|
||||||
|
if (previewTimer) {
|
||||||
|
clearTimeout(previewTimer);
|
||||||
|
previewTimer = null;
|
||||||
|
}
|
||||||
|
if (persistTimer) {
|
||||||
|
clearTimeout(persistTimer);
|
||||||
|
persistTimer = null;
|
||||||
|
}
|
||||||
|
unsubscribeStore();
|
||||||
|
container.removeEventListener('keydown', onContainerKeyDown);
|
||||||
|
insertBtn.removeEventListener('click', onInsertClick);
|
||||||
canvas.destroy();
|
canvas.destroy();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user