From 4d6334d1fa1109bb7438a0f9c76bd9daac2202ae Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Wed, 11 Mar 2026 22:39:56 +0900 Subject: [PATCH 1/2] fix(core): prevent duplicate setInterval in dispatchEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When dispatchEvent was called rapidly (e.g., push → push → pop), each call created a new setInterval without clearing the previous one. This caused N concurrent intervals running aggregate + isEqual every 16ms, linearly increasing CPU load during transitions. Fix by adding an `intervalRunning` flag so only one interval exists at a time. Since all intervals read from the same `events.value`, a single interval handles all queued events correctly. Resolves FEP-1958 Co-Authored-By: Claude Opus 4.6 --- core/src/makeCoreStore.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/makeCoreStore.ts b/core/src/makeCoreStore.ts index cbdc99aaa..a892b1d2b 100644 --- a/core/src/makeCoreStore.ts +++ b/core/src/makeCoreStore.ts @@ -84,6 +84,8 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { value: aggregate(events.value, new Date().getTime()), }; + let intervalRunning = false; + const actions: StackflowActions = { getStack() { return stack.value; @@ -98,6 +100,12 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { events.value.push(newEvent); setStackValue(nextStackValue); + if (intervalRunning) { + return; + } + + intervalRunning = true; + const interval = setInterval(() => { const nextStackValue = aggregate(events.value, new Date().getTime()); @@ -107,6 +115,7 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { if (nextStackValue.globalTransitionState === "idle") { clearInterval(interval); + intervalRunning = false; } }, INTERVAL_MS); }, From e723f021b2e76ec63650d1438069370b81252994 Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Thu, 12 Mar 2026 16:07:55 +0900 Subject: [PATCH 2/2] fix(core): use currentInterval guard instead of intervalRunning flag The intervalRunning flag caused a regression when onChanged hooks triggered reentrant dispatchEvent calls: the nested dispatch would skip interval creation, but the original interval would clear itself on stale "idle" state, leaving no interval to poll the new transition. Switch to clearInterval(currentInterval) + create new approach, which safely handles reentrancy by always replacing the previous interval. Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-duplicate-interval.md | 5 +++++ core/src/makeCoreStore.ts | 13 +++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-duplicate-interval.md diff --git a/.changeset/fix-duplicate-interval.md b/.changeset/fix-duplicate-interval.md new file mode 100644 index 000000000..771298721 --- /dev/null +++ b/.changeset/fix-duplicate-interval.md @@ -0,0 +1,5 @@ +--- +"@stackflow/core": patch +--- + +fix(core): prevent duplicate setInterval in dispatchEvent diff --git a/core/src/makeCoreStore.ts b/core/src/makeCoreStore.ts index a892b1d2b..0043055c5 100644 --- a/core/src/makeCoreStore.ts +++ b/core/src/makeCoreStore.ts @@ -84,7 +84,7 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { value: aggregate(events.value, new Date().getTime()), }; - let intervalRunning = false; + let currentInterval: ReturnType | null = null; const actions: StackflowActions = { getStack() { @@ -100,12 +100,10 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { events.value.push(newEvent); setStackValue(nextStackValue); - if (intervalRunning) { - return; + if (currentInterval !== null) { + clearInterval(currentInterval); } - intervalRunning = true; - const interval = setInterval(() => { const nextStackValue = aggregate(events.value, new Date().getTime()); @@ -115,9 +113,12 @@ export function makeCoreStore(options: MakeCoreStoreOptions): CoreStore { if (nextStackValue.globalTransitionState === "idle") { clearInterval(interval); - intervalRunning = false; + if (currentInterval === interval) { + currentInterval = null; + } } }, INTERVAL_MS); + currentInterval = interval; }, push: () => {}, replace: () => {},