Skip to content

fix(core): treat reads/exceptions in equal as part of computation#55818

Closed
leonsenft wants to merge 2 commits intoangular:mainfrom
leonsenft:tc39
Closed

fix(core): treat reads/exceptions in equal as part of computation#55818
leonsenft wants to merge 2 commits intoangular:mainfrom
leonsenft:tc39

Conversation

@leonsenft
Copy link
Contributor

Prevent leaking signal reads and exceptions from a custom equal function of a producer computed() to a consumer.

Upstream tc39/proposal-signals#90.

@pullapprove pullapprove bot requested review from dylhunn and iteriani May 15, 2024 21:19
@pullapprove pullapprove bot added the requires: TGP This PR requires a passing TGP before merging is allowed label May 15, 2024
@leonsenft leonsenft changed the title Treat reads/exceptions in equal as part of computation fix(core): treat reads/exceptions in equal as part of computation May 15, 2024
@leonsenft
Copy link
Contributor Author

@alxhub PTAL–I don't currently have permission to assign you as a reviewer.

@JeanMeche JeanMeche requested review from alxhub and removed request for dylhunn May 15, 2024 21:26
@dylhunn dylhunn added area: core Issues related to the framework runtime cross-cutting: signals labels May 22, 2024
@ngbot ngbot bot modified the milestone: Backlog May 22, 2024
@pkozlowski-opensource pkozlowski-opensource added the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Nov 26, 2024
Copy link
Member

@pkozlowski-opensource pkozlowski-opensource left a comment

Choose a reason for hiding this comment

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

Discussed in person, here is the summary.

To start with, there are probably 2 related concerns addressed here:

  • exceptions thrown in the equals function body
  • reactive context of the equals function body

When it comes to exceptions, it is reasonable to assume that an error thrown in the equals function body marks the reactive node as "errored". The reasoning here is that is a user code provided during a signal construction. In this sense it can be considered an integral part of a reactive node.

As for reactive tracking inside equals: the outcome of the discussion is that we should not track signal reads there. The reasoning being that equals should be a pure function over it arguments. Please note that this is different from the current state of the TC39 proposal (see tc39/proposal-signals#90) so we will need to allign.

The code in this PR need to change to accommodate the above.

@angular-robot angular-robot bot added area: core Issues related to the framework runtime and removed area: core Issues related to the framework runtime labels Nov 27, 2024
@ngbot ngbot bot modified the milestone: Backlog Nov 27, 2024
Prevent leaking signal reads and exceptions from a custom `equal`
function of a producer `computed()` to a consumer.

Upstream tc39/proposal-signals#90 with a notable
change: Angular does **not** track reactive reads in custom `equal`
implementations.
@angular-robot angular-robot bot added area: core Issues related to the framework runtime and removed area: core Issues related to the framework runtime labels Nov 27, 2024
@ngbot ngbot bot modified the milestone: Backlog Nov 27, 2024
@leonsenft
Copy link
Contributor Author

@pkozlowski-opensource I've updated this change to no longer track signal reads in equal as per our discussion.

expect(() => c()).toThrowError('equal');
expect(computedRunCount).toBe(2);
expect(equalRunCount).toBe(1);
});

Choose a reason for hiding this comment

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

I would add another case here that marks a computed as dirty and have a conditional throw in equals - just to make sure that we can get out of the errored state

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This actually exposes an interesting problem with not tracking reactive reads in equal.

      let shouldThrow = false;
      const source = signal(0);
      const derived = computed(source, {
        equal: (a, b) => {
          if (shouldThrow) {
            throw new Error('equal');
          }
          return defaultEquals(a, b);
        },
      });

      expect(derived()).toBe(0);

      // Begin throwing from equal.
      shouldThrow = true;
      source.set(1);
      expect(() => derived()).toThrowError('equal');

      // Stop throwing from equal.
      shouldThrow = false;
      expect(derived()).toBe(1); // ERROR: still throws cached error.

Even if equal stops throwing, because we cached the result, the computed won't stop throwing until the source signal changes again.

source.set(2);
expect(derived()).toBe(2); // Now this stops throwing.

Is this desirable? One could make the argument that if we did track reactivity in equal, then the shouldThrow should be a signal, and this would all work as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed with @alxhub and came to the conclusion that this behavior is by design, and that the computed should cache the error until the source signal changes again (since we're assuming that equals is pure). I've updated the test case accordingly.

@angular-robot angular-robot bot removed the area: core Issues related to the framework runtime label Dec 10, 2024
@ngbot ngbot bot modified the milestone: Backlog Dec 10, 2024
@angular-robot angular-robot bot added area: core Issues related to the framework runtime and removed area: core Issues related to the framework runtime labels Dec 10, 2024
@ngbot ngbot bot modified the milestone: Backlog Dec 10, 2024
@pkozlowski-opensource pkozlowski-opensource removed the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Dec 16, 2024
Copy link
Contributor

@ENAML ENAML left a comment

Choose a reason for hiding this comment

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

Looks good for shared primitives and Wiz!

Copy link
Contributor

@ENAML ENAML left a comment

Choose a reason for hiding this comment

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

Reviewed-for: primitives-shared

@pkozlowski-opensource pkozlowski-opensource added the target: patch This PR is targeted for the next patch release label Jan 14, 2025
@pullapprove pullapprove bot requested a review from iteriani January 14, 2025 14:43
@pkozlowski-opensource
Copy link
Member

Started a TGP on this one.

@pkozlowski-opensource
Copy link
Member

Green TGP after deflaking

@pkozlowski-opensource
Copy link
Member

TESTED=Green TGP after deflaking

@pkozlowski-opensource pkozlowski-opensource added the action: merge The PR is ready for merge by the caretaker label Jan 16, 2025
@pkozlowski-opensource pkozlowski-opensource removed the request for review from iteriani January 16, 2025 13:38
@AndrewKushnir
Copy link
Contributor

This PR was merged into the repository by commit f8b8e80.

The changes were merged into the following branches: main, 19.1.x

AndrewKushnir pushed a commit that referenced this pull request Jan 16, 2025
Prevent leaking signal reads and exceptions from a custom `equal`
function of a producer `computed()` to a consumer.

Upstream tc39/proposal-signals#90 with a notable
change: Angular does **not** track reactive reads in custom `equal`
implementations.

PR Close #55818
PrajaktaB27 pushed a commit to PrajaktaB27/angular that referenced this pull request Feb 7, 2025
…r#55818)

Prevent leaking signal reads and exceptions from a custom `equal`
function of a producer `computed()` to a consumer.

Upstream tc39/proposal-signals#90 with a notable
change: Angular does **not** track reactive reads in custom `equal`
implementations.

PR Close angular#55818
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Feb 16, 2025
@leonsenft leonsenft deleted the tc39 branch October 13, 2025 21:38
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime cross-cutting: signals requires: TGP This PR requires a passing TGP before merging is allowed target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants