RDEV-10097 - Load each script once per document, and release load listeners - #216
Open
helderjgoncalves wants to merge 3 commits into
Open
RDEV-10097 - Load each script once per document, and release load listeners#216helderjgoncalves wants to merge 3 commits into
helderjgoncalves wants to merge 3 commits into
Conversation
waitForLoad left its load listener attached to the script or link element after the resource had loaded, and had no error handler at all, so a resource that failed to load kept both the listener and the pending timeout alive for the lifetime of the document. Both listeners are now removed and the timeout cleared as soon as the outcome is known. The timeout still only warns and tears nothing down, so a resource arriving after the warning resolves as before, and a failed resource is still not reported back, since no caller handles one today. Co-authored-by: Cursor <cursoragent@cursor.com>
Inner views are shadow roots rather than frames, and shadow dom encapsulates styles but not scripts, so a script appended for one view has executed for the whole document. loadScript nevertheless tracked loads in a per-view map, with a fallback to the main frame's map. That fallback never worked. `a || !b ? c : null` parses as `(a || !b) ? c : null`, so the view's own entry was only ever used as a truthiness test and the value taken was always the main frame's task. For an inner view `!isMain` is true, making the condition constant, so the lookup only ever consulted the main frame and the entry written for the view itself was unreachable. Inner views therefore re-loaded, and re-executed, scripts another inner view had already loaded. Replaced with a single document-wide map, which is what the lifetime of a script actually matches, and dropped the now unused per-view scriptsLoadTasks. A script that fails is removed so a later view can attempt it again; one that times out is kept, since it may still arrive. The head check now runs before the task is registered. It previously ran after, leaving behind an entry nothing could resolve, and did so inside an async promise executor, where the throw became an unhandled rejection and the caller hung instead. Every call site is async, so it now surfaces as a rejection. Co-authored-by: Cursor <cursoragent@cursor.com>
Both changes on this branch are bug fixes, which the versioning rules in the README take as a patch increment. Co-authored-by: Cursor <cursoragent@cursor.com>
helderjgoncalves
requested review from
fmgracias
and removed request for
tbragaf
August 6, 2026 16:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two fixes to
ResourcesLoader, rescued from #208.1. Scripts were loaded, and re-executed, once per view
Inner views are shadow roots rather than frames —
attachShadowinViewPortalandViewFrame, with nocontentDocumentanywhere — and shadow DOM encapsulates styles but not scripts. A<script src>appended for one view has therefore executed for the whole document.loadScriptnevertheless tracked loads in a per-view map, falling back to the main frame's map.That fallback never worked:
||binds tighter than?:, so this parses as(frameScripts.get(scriptSrc) || !view.isMain) ? mainFrameTask : null. The view's own entry is only ever a truthiness test and the value taken is always the main frame's task. For an inner view!isMainistrue, so the condition is constant and the lookup only ever consults the main frame — the entry written for the view itself is unreachable. Inner views re-loaded, and re-executed, scripts that another inner view had already loaded. The cost is not the network request, which the HTTP cache absorbs, but re-running a UMD bundle into the same document.Replaced with a single document-wide map, which is what the lifetime of a script actually matches, and dropped the now unused
scriptsLoadTasksfromViewMetadata. A script that fails is removed so a later view can attempt it again; one that times out is kept, since it may still arrive.2. Load listeners and timeouts outlived the resource
waitForLoadleft itsloadlistener attached to the element after the resource had loaded, and had noerrorhandler at all, so a resource that failed to load kept both the listener and the pending timeout alive for the lifetime of the document. Both are now released as soon as the outcome is known.Behaviour changes to be aware of
loadScriptnow throws synchronously whenview.headis unset, and does so before registering the task. It previously threw after registering, leaving an entry nothing could resolve, and from inside an async promise executor — so the throw became an unhandled rejection and the caller hung. Every call site is async, so it now surfaces as a rejection.onErrordoes not reject. None of theawaitsites inBootstrap.tsorLoader.tshandles an error, so a caller awaiting a failed resource still hangs, as it does today. Turning that into a real error is worth doing but needs those call sites audited first.Not taken from #208
The rest of that PR is either superseded by #211, which covers the
ViewFrame/ViewPortal/Loader.Viewrework behind thegetEnsureDisposeInnerViewsFlag()flag, or was debug scaffolding —debuggerstatements,console.logs and a commented-out render cache. Its version of this fix put the registry onwindowunder two keys, but assignedwindow[LoadedScriptsKey]in both initialiser branches, so the task map was never actually stored; it also recorded a script as loaded before the load completed, so a failed script was permanently treated as present.Test plan
tsc --noEmit -p ReactViewResources/Loader/tsconfig.jsonpasses (verified locally)<script>to the documentImportant
RDEV-0000needs replacing with a real ticket before merge — #208 had no ticket to inherit.