Skip to content

fix: don't notify signals tracking AsyncSignal.future when transitioning to loading state#434

Open
Yegair wants to merge 1 commit intorodydavis:mainfrom
Yegair:fix/433-future-signal-executed-too-often
Open

fix: don't notify signals tracking AsyncSignal.future when transitioning to loading state#434
Yegair wants to merge 1 commit intorodydavis:mainfrom
Yegair:fix/433-future-signal-executed-too-often

Conversation

@Yegair
Copy link
Contributor

@Yegair Yegair commented Dec 9, 2025

Fixes #433

  • Fixes AsyncSignal.future to no longer notify when the async signal transitions into a loading state. The notification now happens when the async signal completes with data or error.
  • Adds AsyncSignal.completion for tracking dependencies across async gaps without getting notified about state changes to loading states.


/// The future of the signal completer
Future<T> get completionFuture {
untracked(() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems redundant as it was only accessed so it could be tracked.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it is necessary, to avoid creating a dependency from the caller to the signal.value, which is what causes the original problem

Copy link
Contributor

@zupat zupat Dec 9, 2025

Choose a reason for hiding this comment

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

I am saying that it should be removed completely because value was not used or returned, it's original purpose was to register the caller as a dependent. Now that you're returning the internal signal, you can remove the untracked statement as it's no longer needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh I see. The reason for this is that I somehow have to "trigger" the async computation. Without this call, the future won't ever be resolved. This is related to how StreamSignal is implemented, which is the base for FutureSignal.

@override
AsyncState<T> get value {
_cleanup ??= _stream.subscribe((src) {
reset();
execute(src);
});
return super.value;
}

However, I just did it, to get it working quickly. There might be a more elegant way to do this. Will take a closer look at this 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I didn't realize that .value had a side effect that needs to run to kick things off. This should probably be moved to the constructor but I admit that it is more efficient to not subscribe until the value is accessed.

Copy link
Owner

Choose a reason for hiding this comment

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

This is related to how StreamSignal is implemented, which is the base for FutureSignal

Would it be better if FutureSignal did not extend StreamSignal? I have been thinking about making that change for v7

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I generally like that there is a single implementation taking care of handling the async computation.

Given my current knowledge about the codebase I can't say whether separating FutureSignal from StreamSignal would make it easier or harder to implement changes/fixes such as this one.

If I had to make that decision, I'd look out for implementation details within StreamSignal that exist just to make FutureSignal work. If such code exists, then I'd say there is a good reason to no longer base FutureSignal on StreamSignal. However, if such code does not exist, then I think it makes more sense to keep it as it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved the call into StreamSignal, assuming that it is not needed for all other async signals. This should make it more obvious why it exists for future readers.

@Yegair
Copy link
Contributor Author

Yegair commented Dec 10, 2025

Did a first implementation that passes all the existing tests.

Here is what I'm planning to do before submitting the PR for review:

  • Add more tests (have to figure out all the possible edge cases first)
  • Update commit messages
  • Update PR title & description

@Yegair Yegair changed the title wip: adds experimental .completionFuture and .completion to AsyncSignal fix: don't notify signals tracking AsyncSignal.future when transitioning to loading state Dec 10, 2025
@codecov
Copy link

codecov bot commented Dec 10, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.81%. Comparing base (14c0fa9) to head (f62c54b).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #434      +/-   ##
==========================================
+ Coverage   64.42%   64.81%   +0.38%     
==========================================
  Files          87       88       +1     
  Lines        2145     2194      +49     
==========================================
+ Hits         1382     1422      +40     
- Misses        763      772       +9     
Files with missing lines Coverage Δ
packages/signals_core/lib/src/async/computed.dart 100.00% <ø> (ø)
packages/signals_core/lib/src/async/future.dart 100.00% <ø> (ø)
packages/signals_core/lib/src/async/signal.dart 100.00% <100.00%> (ø)
packages/signals_core/lib/src/async/stream.dart 100.00% <100.00%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Yegair Yegair force-pushed the fix/433-future-signal-executed-too-often branch from f62c54b to a067d1b Compare December 10, 2025 21:36
@Yegair Yegair marked this pull request as ready for review December 10, 2025 21:37
Comment on lines +485 to +486
bool _initialized = false;
bool get isCompleted => _initialized && _completer.isCompleted;
Copy link
Contributor Author

@Yegair Yegair Dec 10, 2025

Choose a reason for hiding this comment

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

The _initialized flag is needed to make an existing test for simple AsyncSignals work, in case they are created in a completed state.

test('isCompleted', () async {
final s = asyncSignal(AsyncState<int>.data(0));
expect(s.isCompleted, false);
s.setValue(1);
expect(s.isCompleted, true);
});

Didn't manage to figure out why an async signal is considered not completed after creating with data, so added this edge case in order to not break things.

Copy link
Owner

Choose a reason for hiding this comment

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

Yep that sounds good!

Comment on lines +510 to +517
static Future<T> _ignoreUncaughtErrors<T>(Future<T> future) {
// Makes sure the future never reports an uncaught error to
// the current zone. Seems to be necessary to avoid uncaught
// errors to be reported when async signals are being used
// synchronously, i.e. when their .future is not used.
future.ignore();
return future;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here I'm really not sure if this is a good idea, but IMHO we have no other choice. Otherwise users might see a lot of uncaught errors even when they are handled synchronously through AsyncError

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FutureSignal awaiting an AsyncSignal.future is executed too often

3 participants