8000 fix(core): prevent stash listener conflicts by arturovt · Pull Request #59635 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

fix(core): prevent stash listener conflicts #59635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.< 8000 /p>

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

arturovt
Copy link
Contributor
@arturovt arturovt commented Jan 20, 2025

The stash event listener is a global function that might be unsafely overridden if multiple microfrontend applications exist on the page.

In this commit, we create a map of APP_ID to stash event listener functions. This map prevents conflicts because multiple applications might be bootstrapped simultaneously on the client (one rendered on the server and one rendering only on the client).

I.e., the code that might be used is:

// Given that `app-root` is rendered on the server
bootstrapApplication(AppComponent, appConfig);

bootstrapApplication(BlogRootComponent, appBlogConfig);

Two bootstrapped applications would conflict and override each other's code.

@angular-robot angular-robot bot added the area: core Issues related to the framework runtime label Jan 20, 2025
@ngbot ngbot bot added this to the Backlog milestone Jan 20, 2025
@arturovt arturovt force-pushed the fix/core-stash-conflicts branch 5 times, most recently from 93414f0 to 20b19ab Compare January 20, 2025 19:36
@arturovt arturovt force-pushed the fix/core-stash-conflicts branch 2 times, most recently from 7c91668 to f29a5a8 Compare January 20, 2025 20:27
@arturovt arturovt marked this pull request as ready for review January 21, 2025 04:13
@pullapprove pullapprove bot requested a review from thePunderWoman January 21, 2025 04:13
Copy link
Contributor
@thePunderWoman thePunderWoman left a comment

Choose a reason for hiding this comment

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

We'll definitely need some tests here for this so we don't regress in the future. Can you add those?

@arturovt arturovt force-pushed the fix/core-stash-conflicts branch 2 times, most recently from b10f3c2 to 021363a Compare April 2, 2025 15:26
The stash event listener is a global function that might be unsafely overridden if multiple microfrontend applications exist on the page.

In this commit, we create a map of `APP_ID` to stash event listener functions. This map prevents conflicts because multiple applications might be bootstrapped simultaneously on the client (one rendered on the server and one rendering only on the client).

I.e., the code that might be used is:

```ts
// Given that `app-root` is rendered on the server
bootstrapApplication(AppComponent, appConfig);

bootstrapApplication(BlogRootComponent, appBlogConfig);
```

Two bootstrapped applications would conflict and override each other's code.
@arturovt arturovt force-pushed the fix/core-stash-conflicts branch from 021363a to ec56161 Compare April 29, 2025 12:09
@arturovt
Copy link
Contributor Author

@thePunderWoman added unit test.

Copy link
Contributor
@thePunderWoman thePunderWoman left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for adding the unit test.

@JeanMeche JeanMeche added action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release labels Apr 29, 2025
@mmalerba
Copy link
Contributor

@arturovt I'm switching this to target the next major release since it has merge conflicts with the patch branch. If you want the change in a patch release you'll need to create a separate PR specifically to the 19.2.x branch.

@mmalerba mmalerba added target: major This PR is targeted for the next major release and removed target: patch This PR is targeted for the next patch release labels Apr 30, 2025
arturovt added a commit to arturovt/angular that referenced this pull request Apr 30, 2025
@mmalerba mmalerba added target: major This PR is targeted for the next major release and removed target: major This PR is targeted for the next major release labels Apr 30, 2025
@mmalerba
Copy link
Contributor

This PR was merged into the repository by commit 624be2e.

The changes were merged into the following branches: main

@mmalerba mmalerba closed this in 624be2e Apr 30, 2025
@arturovt arturovt deleted the fix/core-stash-conflicts branch April 30, 2025 15:54
mmalerba pushed a commit that referenced this pull request Apr 30, 2025
Comment on lines +182 to +184
const appId = lView[INJECTOR].get(APP_ID);
const stashEventListener = stashEventListeners.get(appId);
stashEventListener?.(target as RElement, eventName, wrappedListener);
Copy link
Contributor

Choose a reason for hiding this comment

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

@arturovt thanks for the improvement 👍

I think we should optimize this code path by moving the logic into the stashEventListener function (or a function that manages its assignment). Otherwise, we invoke this code for all listeners all the time, when we only need it in case the event replay feature is enabled (explicitly or via incremental hydration).

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 get your comment. I'm wondering whether we should be checking if event replay feature is enabled in this code path and only then stash the event listener. I have no ideas on having that functionality inside the stashEventListener, I mean I don't get how it would technically look like.

If I understand correctly, stashing is called every time when listener is added, basically like this:

Element.prototype.addEventListener = function (
  this: Element,
  type: string,
  listener: EventListenerOrEventListenerObject,
  options?: boolean | AddEventListenerOptions,
) {
  const stash = stashEventListeners.get(appId);
  stash?.(this, type, listener as VoidFunction);
  return originalAddEventListener.call(this, type, listener, options);
};

Another question is, should we clear the stash event listener once the hydration is done to avoid stashing it after it's functioning in the browser?

Copy link
Contributor Author
@arturovt arturovt Apr 30, 2025

Choose a reason for hiding this comment

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

let stashEventListenerImpl = (
  lView: LView,
  target: RElement,
  eventName: string,
  wrappedListener: EventCallback,
) => {};

export function enableStashEventListenerImpl() {
  stashEventListenerImpl = (
    lView: LView,
    target: RElement,
    eventName: string,
    wrappedListener: EventCallback,
  ) => {
    const appId = lView[INJECTOR].get(APP_ID);
    const stashEventListener = stashEventListeners.get(appId);
    stashEventListener?.(target as RElement, eventName, wrappedListener);
  };
}

And in the code:

stashEventListenerImpl(lView, target, eventName, wrappedListener);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would avoid an injector lookup and a Map lookup every time the listener() is called unless the replay is enabled.

Copy link
Contributor AD4F

Choose a reason for hiding this comment

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

Yes, the solution described in #59635 (comment) looks great and something that I had in mind as well, thank you 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants
0