Skip to content

Use an AddonsApplication to manage load/reload of all addons #504

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.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Feb 11, 2025

Conversation

humitos
Copy link
Member

@humitos humitos commented Jan 22, 2025

This PR follows the approach exposed in #491 (comment). Basically, it manage all the ContextRoot logic in one place and update all the addons as needed.

This approach is a lot simpler and easier to follow than the one originally done in #419. It also requires less code changes.

Closes #157

humitos and others added 3 commits January 22, 2025 12:48
Additional elements shouldn't be needed in our structure. This is how we
can use ContextRoot with `document.html` as the context root for
providers/consumers of the config context.
Everything is loaded by the `application.js` file now.
humitos added a commit to readthedocs/readthedocs.org that referenced this pull request Jan 22, 2025
This is required to be able to dynamically update the URLs from the versions in
the flyout when the page is changed using `history` object (without reloading /
SPA) as Docusaurus does.

In that scenario we cannot use the HTML META tag because it was injected with
the old value by CF Worker. Note that the CF Worker is not run after the initial
request on SPA.

The addons frontend will use this attribute to update those URLs accordingly
when the URL changes.

Required by:
* readthedocs/addons#504
We cannot use the META HTML tag because it's added only on the first request.
See readthedocs/readthedocs.org#11940
@humitos
Copy link
Member Author

humitos commented Jan 22, 2025

I was able to make everything work here 🎉 and I'm very happy with the solution I arrived at. It's a lot simpler than the original proposal. This PR is ready for review.

@humitos humitos marked this pull request as ready for review January 22, 2025 12:41
@humitos humitos requested a review from a team as a code owner January 22, 2025 12:41
@humitos humitos requested a review from agjohnson January 22, 2025 12:41
@humitos humitos force-pushed the humitos/lit-context-app branch from 63c3a6b to fb64f99 Compare January 22, 2025 12:49
@agjohnson
Copy link
Contributor

Hrm, I'm a bit confused on this pattern, there seems to be a few patterns mixing here.

Why use the context API at all here? The context API is specifically for reactive element properties, so if this context isn't read from each addon element (this.config.getValue), it's the same thing as just passing in a native object to the addon and skipping the context API entirely.

What you have here doesn't seem like it will give a reactive property to each addon -- if we update config, these updates won't flow to the addon elements. How important is this?

With this approach, we also don't need a readthedocs-application element. We're not using the context API as reactive element properties, so an Element isn't needed to execute logic. All of this logic can happen at index.js and without any elements being registered -- instead of injecting and element and forcing render() to execute this logic.

It's a little difficult to follow but it feels like what you have here could be simplified down to executing this logic in index.js, outside any element, and pushing the config to each addon. But this is essentially where we are currently. Is there something this PR does that we're not doing currently?

@humitos
Copy link
Member Author

humitos commented Jan 23, 2025

Is there something this PR does that we're not doing currently?

Yes.

  1. This PR listen to EVENT_READTHEDOCS_URL_CHANGED --which is triggered by ourselves when we detect the URL has changed using the history object (eg. SPA, like Docusaurus or Material forMkDocs with a specific config)
  2. The handler of that event, makes a new requests to the Addons API and fetch the updated data based on the new URL.
  3. Once the data is retrieved, the event EVENT_READTHEDOCS_ADDONS_DATA_READY is fired.
  4. The handler of this event updates the ContextProvider with the data fetched by the API.
  5. The AddonsApplication is subscribed as a ContextConsumer and will re-initialize all the addons with the new data (config object).
  6. At this point, all the addons were updated with the new data (eg. flyout URLs now point to the new URL)

Currently, without the code of this PR, the Addons API is fetch only once when the page is loaded. If the user clicks on another page that doesn't perform a full reload (eg. as Docusaurus does) there is no new request to Addons API and addons are not re-initialized. That means that all the URLs from the flyout are out of date and point to the wrong page.

You can try the current behavior following these steps:

  1. Go to https://test-builds.readthedocs.io/en/docusaurus/docs/intro
  2. Take a look at the URLs in the flyout (they should contain /docs/intro)
  3. Click on "Next" link at the bottom
  4. Take a look at the URLs in the flyout again (they will contain /docs/intro -- which is wrong, since the page has changed)

@humitos
Copy link
Member Author

humitos commented Jan 23, 2025

The question is: can we use ContextProvider / ContextConsumer outside a LitElement? I've created a AddonsApplication to be able to subscribe as a ContextConsumer, but maybe that's not needed. I understand that's what confuses you about this pattern.

We don't require a reactive property on each addon -- we only need to subscribe as a ContextConsumer and re-initialize the addons with the fresh data. If we can make the AddonsApplication to not be a LitElement and still use the ContextConsumer for this, that would be great. I will give it a try!

@humitos
Copy link
Member Author

humitos commented Jan 23, 2025

If we can make the AddonsApplication to not be a LitElement and still use the ContextConsumer for this, that would be great. I will give it a try!

It seems that's not possible. I gave it a quick try and I got the error:

Uncaught TypeError: this.host.addController is not a function

@humitos humitos force-pushed the humitos/lit-context-app branch 2 times, most recently from ecef590 to 4ee0573 Compare January 23, 2025 08:51
@humitos
Copy link
Member Author

humitos commented Jan 23, 2025

Boom! 💯

Lit Context are not required at all for what I want to do, actually. Thanks for your comment, it helped me to understand the workflow a lot better. I'm using just events to communicate internally and pass the config data we need 👍🏼

I re-wrote the code to remove everything related to Lit Context. The structure was correct (using a manager class, like AddonsApplication) and was pretty easy to migrate it to regular events.

Let me know your thoughts! I think it's very clear now.

@humitos humitos force-pushed the humitos/lit-context-app branch from 4ee0573 to 237aa0d Compare January 23, 2025 09:10
The `AddonsApplication` keeps a list of all the addons instances created when
the page was loaded (`AddonsApplication.addonsInstances`).

Then, when the `READTHEDOCS_URL_CHANGED` event is fired, we check if we already
have instances created and, in that case, we call `addon.loadConfig(config)` to
re-configure those instances with the new `config` object --without creating new
ones.

This commit includes some refactor as well:

- Share all the `constructor()` method all accross addons
- Override the `constructor()` for those addons that are not LitElements
- Create `loadConfig()` methods for those addons that are not LitElements
- Remove code commented out
Before dispatching the URL changed event, we check for the third argument (url).
If it's passed it means that we are changing the URL and we trigger the event in
that scenario. This avoids hitting the API twice on each page change.
@humitos
Copy link
Member Author

humitos commented Jan 30, 2025

this should at very least avoid recreating/finding elements unnecessarily. Once we initialize *Adddon classes, these classes should have reference of the elements they are connected to

I went ahead and implemented this. Now, we call new *Addon() only once when the page loads for the first time and we save all the instances created. After that, every time the "URL changed" event is fired, we iterate over the saved instances and call .loadConfig(config) on them 👍🏼

We are immediatley calling `.loadConfig(config)` that changes the `config`
reactive propertly, which will trigger a render automatically.
@agjohnson
Copy link
Contributor

I wrote about the problems/cons I've found with ContextRoot in #491 (comment)

Yes, already saw that previously. I described above using the context API from the *Addon classes instead of from our elements which I think avoids most of the concerns you listed.

However, one of the big downsides of making config a re-active property and use ContextRoot for this, is that we have to move all the validation logic inside render()

I wasn't describing moving validation logic into render(), I understand the concerns of validation/etc on render. I described above using the context API ContextRequestEvent from *Addon instead above.

This would still use a reactive property config in our elements. We are already using a reactive config property in our elements, this doesn't change. The change would be using the context API from *Addon and updating the config property on connected elements.

I wasn't able to trigger the side effect you mention here

Side effects will happen without us noticing or knowing what to watch. Logic beyond validation and updating the element config -- subscribing to events, emitting events, whatever -- will happen unless we're careful to avoid these.

That is why if we only need validation and to update config, I feel it's best to limit our logic to just that.

every time the "URL changed" event is fired, we iterate over the saved instances and call .loadConfig(config) on them

This is safer for now. If we're able to use the context API, the code will look similar to this -- we won't reinitialize each addon again.

Comment on lines +98 to +99
// We cannot use `render(this.elements[0], document.body)` because there is a race conditions between all the addons.
// So, we append the web-component first and then request an update of it.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you describe this in more detail here? What race condition?

I feel this should be a solvable problem, but likely comes from using promises to initialize addons asynchronously? If we truly want promises handling the addon initialization, we can structure the promises to respect initialization order.

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't write that note. I just copied and pasted from another addon while doing the refactor. I think you wrote it when you started with the flyout work 😄

In any case, I tried using render() and it doesn't work 🤷🏼

@humitos
Copy link
Member Author

humitos commented Feb 4, 2025

This is safer for now. If we're able to use the context API, the code will look similar to this -- we won't reinitialize each addon again.

Cool! I will keep this in mind and keep experimenting with Context API in future iteration. I think it's still useful to give it a try if we can remove all the complexity I found myself limiting moving forward with that. I suppose we can change the "base event handling at ApplicationApp for Context API" and everything should keep working in a similar way --but I don't want to open that melon now 😄

@humitos humitos requested a review from agjohnson February 4, 2025 08:49
Check if from/to URL are different after removing `?readthedocs-diff=`
attribute because we don't want to trigger the even when the user
enabled
docdiff in the current page.

Closes #506
@humitos
Copy link
Member Author

humitos commented Feb 11, 2025

The filetreediff test started to failed, but I'm not sure why.

AssertionError: Snapshot Filetreediff tests snapshot filetreediff completely does not match the saved snapshot on disk

I'll merge this PR for now, and research a little more why this is happening here.

@humitos humitos merged commit 5425a3e into main Feb 11, 2025
3 of 4 checks passed
@humitos humitos deleted the humitos/lit-context-app branch February 11, 2025 09:52
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.

Support Single Page Applications (SPA) for addons that depend on the URL
2 participants