Skip to content

fix: Prevent multiple effect invocations when reading the same signal multiple times#23978

Open
mshabarov wants to merge 6 commits intomainfrom
multiple-effect-invocations
Open

fix: Prevent multiple effect invocations when reading the same signal multiple times#23978
mshabarov wants to merge 6 commits intomainfrom
multiple-effect-invocations

Conversation

@mshabarov
Copy link
Contributor

@mshabarov mshabarov commented Mar 24, 2026

When an effect reads the same signal multiple times (e.g. signal.get()
called 3 times), each call creates a separate Usage instance and registers
a separate listener. When the signal changes, all listeners fire, causing
the effect to run multiple times instead of once.

Added an AtomicBoolean flag in Effect.revalidate() that ensures
onDependencyChange is called only once per change event, even if multiple
usages refer to the same signal. The flag is reset on each revalidate()
call so subsequent signal changes can trigger the effect again.

Fixes #23974

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

mshabarov and others added 2 commits March 24, 2026 14:33
… multiple times

When an effect reads the same signal multiple times (e.g. signal.get()
called 3 times), each call creates a separate Usage instance and registers
a separate listener. When the signal changes, all listeners fire, causing
the effect to run multiple times instead of once.

Added an AtomicBoolean flag in Effect.revalidate() that ensures
onDependencyChange is called only once per change event, even if multiple
usages refer to the same signal. The flag is reset on each revalidate()
call so subsequent signal changes can trigger the effect again.

Fixes #23974

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

Co-Authored-By: Claude <noreply@anthropic.com>
…gnal multiple times

When an effect reads the same signal multiple times (e.g. signal.get()
called 3 times), each call creates a separate Usage instance and would
register a separate listener. This causes the effect to be invoked
multiple times for a single signal change.

Added getIdentity() method to Usage interface that returns an identity
object for deduplication purposes. Effect now uses a Set of UsageWrapper
objects (which implement equals/hashCode based on usage identity) to
automatically deduplicate usages. Only the first occurrence of each
unique signal identity gets a listener registered.

CombinedUsage calculates its identity as a Set of all combined usage
identities, providing proper identity semantics for combined usages.

This approach prevents duplicate listeners from being registered at the
data structure level, eliminating the need for separate tracking.

Fixes #23974

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

Co-Authored-By: Claude <noreply@anthropic.com>
@mshabarov mshabarov changed the title Multiple effect invocations fix: Prevent duplicate listener registration when reading the same signal multiple times Mar 24, 2026
@github-actions
Copy link

github-actions bot commented Mar 24, 2026

Test Results

 1 386 files  ±0   1 386 suites  ±0   1h 28m 34s ⏱️ - 1m 28s
 9 917 tests +3   9 846 ✅ +3  71 💤 ±0  0 ❌ ±0 
10 390 runs  +3  10 310 ✅ +3  80 💤 ±0  0 ❌ ±0 

Results for commit 8c1f174. ± Comparison against base commit c3eaf5b.

♻️ This comment has been updated with latest results.

… same signal multiple times"

This reverts commit 0b1b5c6.
@mshabarov mshabarov changed the title fix: Prevent duplicate listener registration when reading the same signal multiple times fix: Prevent multiple effect invocations when reading the same signal multiple times Mar 25, 2026
@mshabarov mshabarov marked this pull request as ready for review March 25, 2026 08:48
@github-actions github-actions bot added +0.0.1 and removed +0.1.0 labels Mar 25, 2026
@sonarqubecloud
Copy link

Copy link
Member

@tltv tltv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: activate() has the same duplicate-invocation problem (not fixed)

Effect.java:304-305 — When re-activating without changes, listeners are re-registered directly:

registrations.add(usage.onNextChange(this::onDependencyChange));

This bypasses the changeHandled guard. If usages contains multiple entries for the same signal (from multiple reads before passivation), the same cascading problem occurs: the signal copies its listener list before iterating,
so clearRegistrations() during a synchronous invalidation doesn't prevent remaining copied listeners from firing.

Recommendation: Extract the changeHandled pattern into a shared method, or apply the same guard in activate().

// between the hasChanges check and the onNextChange call,
// in which case firstRun is already set to true above.
for (UsageTracker.Usage usage : usages) {
registrations.add(usage.onNextChange(this::onDependencyChange));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: activate() has the same duplicate-invocation problem (not fixed)

When re-activating without changes, listeners are re-registered directly here.

This bypasses the changeHandled guard. If usages contains multiple entries for the same signal (from multiple reads before passivation), the same cascading problem occurs: the signal copies its listener list before iterating,
so clearRegistrations() during a synchronous invalidation doesn't prevent remaining copied listeners from firing.

Recommendation: Extract the changeHandled pattern into a shared method, or apply the same guard in activate().

boolean[] hasSignalUsage = { false };
// Ensure effect runs only once per change event, even if the same
// signal is read multiple times (each read registers a listener)
AtomicBoolean changeHandled = new AtomicBoolean(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Unnecessary AtomicBoolean for non-concurrent use

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple effect invocations if reading the same signal multiple times

2 participants