Describe the bug
The EffectBatchManager.endBatch() method, responsible for executing pending effects, had a subtle bug that could lead to an infinite loop. If an effect being run within the forEach loop caused another effect (or itself) to be re-queued into pendingEffects synchronously, it could result in the forEach iterating indefinitely or triggering subsequent endBatch() calls in a recursive manner.
To Reproduce
Steps to reproduce the behavior:
- Have an
Neo.core.Effect (Effect A) that, when run, triggers a reactive change.
- Have another
Neo.core.Effect (Effect B) that depends on the reactive change caused by Effect A.
- If Effect A is processed by
EffectBatchManager.endBatch() and its execution synchronously causes Effect B to be re-queued into pendingEffects (or if Effect A itself is re-queued), it could lead to an infinite loop within the endBatch()'s forEach iteration.
Expected behavior
The EffectBatchManager.endBatch() should process all pending effects in a given batch without re-triggering effects within the same batch, even if their execution causes new effects to be queued.
Solution
To prevent this, EffectBatchManager.endBatch() now creates a snapshot of pendingEffects (using Array.from()) and clears the original pendingEffects Set before iterating and running the effects. This ensures that any new effects queued during the execution of the current batch are added to a new batch, preventing interference with the current processing and breaking the synchronous infinite loop.
Impact
This bug could lead to application freezes and "Maximum call stack size exceeded" errors in scenarios where effects trigger other effects synchronously within a batch.
Affected Files
src/core/EffectBatchManager.mjs
Describe the bug The
EffectBatchManager.endBatch()method, responsible for executing pending effects, had a subtle bug that could lead to an infinite loop. If an effect being run within theforEachloop caused another effect (or itself) to be re-queued intopendingEffectssynchronously, it could result in theforEachiterating indefinitely or triggering subsequentendBatch()calls in a recursive manner.To Reproduce Steps to reproduce the behavior:
Neo.core.Effect(Effect A) that, when run, triggers a reactive change.Neo.core.Effect(Effect B) that depends on the reactive change caused by Effect A.EffectBatchManager.endBatch()and its execution synchronously causes Effect B to be re-queued intopendingEffects(or if Effect A itself is re-queued), it could lead to an infinite loop within theendBatch()'sforEachiteration.Expected behavior The
EffectBatchManager.endBatch()should process all pending effects in a given batch without re-triggering effects within the same batch, even if their execution causes new effects to be queued.Solution To prevent this,
EffectBatchManager.endBatch()now creates a snapshot ofpendingEffects(usingArray.from()) and clears the originalpendingEffectsSet before iterating and running the effects. This ensures that any new effects queued during the execution of the current batch are added to a new batch, preventing interference with the current processing and breaking the synchronous infinite loop.Impact This bug could lead to application freezes and "Maximum call stack size exceeded" errors in scenarios where effects trigger other effects synchronously within a batch.
Affected Files
src/core/EffectBatchManager.mjs