diff --git a/src/sidebar/flowchart-panel.js b/src/sidebar/flowchart-panel.js
index c5ebd9b..2d2404d 100644
--- a/src/sidebar/flowchart-panel.js
+++ b/src/sidebar/flowchart-panel.js
@@ -28,21 +28,6 @@ const PREVIEW_DEBOUNCE_MS = 250;
const PERSIST_DEBOUNCE_MS = 500;
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) {
const { getUserDataPath, readFile, writeFile, insertAtCursor, renderMermaid = () => {} } = deps;
if (typeof getUserDataPath !== 'function') {
@@ -55,6 +40,10 @@ function renderFlowChartPanel(container, deps) {
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 = `
@@ -83,14 +72,14 @@ function renderFlowChartPanel(container, deps) {
let selectedEdgeId = null;
const store = createStore({
- persistencePath: persistencePath(getUserDataPath),
+ persistencePath: persistenceFile,
readFile,
writeFile,
now: () => Date.now(),
});
// Hydrate from disk (defensively).
- readFile(persistencePath(getUserDataPath))
+ readFile(persistenceFile)
.then((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
.
+ let previewTimer = null;
+ function runPreview() {
const source = toMermaid(store.getGraph());
previewSourceEl.textContent = source;
try {
@@ -132,26 +124,41 @@ function renderFlowChartPanel(container, deps) {
} catch (err) {
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(() => {
- writeFile(persistencePath(getUserDataPath), store.serialize()).catch((err) => {
- if (statusEl) statusEl.textContent = `Save failed: ${err.message || err}`;
- });
- }, PERSIST_DEBOUNCE_MS);
+ // Debounced persistence. Track the timer handle so destroy() can cancel a
+ // queued writeFile that would otherwise fire after unmount.
+ let persistTimer = null;
+ function debouncedPersist() {
+ 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();
debouncedPersist();
});
- insertBtn.addEventListener('click', () => {
+ function onInsertClick() {
const source = toMermaid(store.getGraph());
insertAtCursor('```mermaid\n' + source + '\n```');
- });
+ }
+ insertBtn.addEventListener('click', onInsertClick);
// Keyboard shortcuts — panel-scoped.
- container.addEventListener('keydown', (ev) => {
+ function onContainerKeyDown(ev) {
if (ev.ctrlKey && !ev.metaKey && ev.key.toLowerCase() === 'z') {
ev.preventDefault();
if (ev.shiftKey) store.redo();
@@ -169,7 +176,8 @@ function renderFlowChartPanel(container, deps) {
selectedEdgeId = null;
}
}
- });
+ }
+ container.addEventListener('keydown', onContainerKeyDown);
return {
getStore: () => store,
@@ -181,6 +189,20 @@ function renderFlowChartPanel(container, deps) {
selectedEdgeId = id;
},
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();
},
};