Remove event listeners when no longer needed to prevent memory leaks
suspenseyImage.addEventListener('load', resolve);
1/* eslint-disable dot-notation */23// Shared implementation and constants between the inline script and external4// runtime instruction sets.56const ELEMENT_NODE = 1;7const COMMENT_NODE = 8;8const ACTIVITY_START_DATA = '&';9const ACTIVITY_END_DATA = '/&';10const SUSPENSE_START_DATA = '$';11const SUSPENSE_END_DATA = '/$';12const SUSPENSE_PENDING_START_DATA = '$?';13const SUSPENSE_QUEUED_START_DATA = '$~';14const SUSPENSE_FALLBACK_START_DATA = '$!';1516const FALLBACK_THROTTLE_MS = 300;1718const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500;1920// If you have a target goal in mind for a metric to hit, you don't want the21// only reason you miss it by a little bit to be throttling heuristics.22// This tries to avoid throttling if avoiding it would let you hit this metric.23// This is derived from trying to hit an LCP of 2.5 seconds with some head room.24const TARGET_VANITY_METRIC = 2300;2526// TODO: Symbols that are referenced outside this module use dynamic accessor27// notation instead of dot notation to prevent Closure's advanced compilation28// mode from renaming. We could use extern files instead, but I couldn't get it29// working. Closure converts it to a dot access anyway, though, so it's not an30// urgent issue.3132export function revealCompletedBoundaries(batch) {33 window['$RT'] = performance.now();34 for (let i = 0; i < batch.length; i += 2) {35 const suspenseIdNode = batch[i];36 const contentNode = batch[i + 1];37 if (contentNode.parentNode === null) {38 // If the client has failed hydration we may have already deleted the streaming39 // segments. The server may also have emitted a complete instruction but cancelled40 // the segment. Regardless we can ignore this case.41 } else {42 // We can detach the content now.43 // Completions of boundaries within this contentNode will now find the boundary44 // in its designated place.45 contentNode.parentNode.removeChild(contentNode);46 }47 // Clear all the existing children. This is complicated because48 // there can be embedded Suspense boundaries in the fallback.49 // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.50 // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.51 // They never hydrate anyway. However, currently we support incrementally loading the fallback.52 const parentInstance = suspenseIdNode.parentNode;53 if (!parentInstance) {54 // We may have client-rendered this boundary already. Skip it.55 continue;56 }5758 // Find the boundary around the fallback. This is always the previous node.59 const suspenseNode = suspenseIdNode.previousSibling;6061 let node = suspenseIdNode;62 let depth = 0;63 do {64 if (node && node.nodeType === COMMENT_NODE) {65 const data = node.data;66 if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {67 if (depth === 0) {68 break;69 } else {70 depth--;71 }72 } else if (73 data === SUSPENSE_START_DATA ||74 data === SUSPENSE_PENDING_START_DATA ||75 data === SUSPENSE_QUEUED_START_DATA ||76 data === SUSPENSE_FALLBACK_START_DATA ||77 data === ACTIVITY_START_DATA78 ) {79 depth++;80 }81 }8283 const nextNode = node.nextSibling;84 parentInstance.removeChild(node);85 node = nextNode;86 } while (node);8788 const endOfBoundary = node;8990 // Insert all the children from the contentNode between the start and end of suspense boundary.91 while (contentNode.firstChild) {92 parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);93 }9495 suspenseNode.data = SUSPENSE_START_DATA;96 if (suspenseNode['_reactRetry']) {97 requestAnimationFrame(suspenseNode['_reactRetry']);98 }99 }100 batch.length = 0;101}102103export function revealCompletedBoundariesWithViewTransitions(104 revealBoundaries,105 batch,106) {107 let shouldStartViewTransition = false;108 let autoNameIdx = 0;109 const restoreQueue = [];110 function applyViewTransitionName(element, classAttributeName) {111 const className = element.getAttribute(classAttributeName);112 if (!className) {113 return;114 }115 // Add any elements we apply a name to a queue to be reverted when we start.116 const elementStyle = element.style;117 restoreQueue.push(118 element,119 elementStyle['viewTransitionName'],120 elementStyle['viewTransitionClass'],121 );122 if (className !== 'auto') {123 elementStyle['viewTransitionClass'] = className;124 }125 let name = element.getAttribute('vt-name');126 if (!name) {127 // Auto-generate a name for this one.128 // TODO: We don't have a prefix to pick from here but maybe we don't need it129 // since it's only applicable temporarily during this specific animation.130 const idPrefix = '';131 name = '_' + idPrefix + 'T_' + autoNameIdx++ + '_';132 }133 // If the name isn't valid CSS identifier, base64 encode the name instead.134 // This doesn't let you select it in custom CSS selectors but it does work in current135 // browsers.136 const escapedName =137 CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;138 elementStyle['viewTransitionName'] = escapedName;139 shouldStartViewTransition = true;140 }141 try {142 const existingTransition = document['__reactViewTransition'];143 if (existingTransition) {144 // Retry after the previous ViewTransition finishes.145 existingTransition.finished.finally(window['$RV'].bind(null, batch));146 return;147 }148 // First collect all entering names that might form pairs exiting names.149 const appearingViewTransitions = new Map();150 for (let i = 1; i < batch.length; i += 2) {151 const contentNode = batch[i];152 const appearingElements = contentNode.querySelectorAll('[vt-share]');153 for (let j = 0; j < appearingElements.length; j++) {154 const appearingElement = appearingElements[j];155 appearingViewTransitions.set(156 appearingElement.getAttribute('vt-name'),157 appearingElement,158 );159 }160 }161 const suspenseyImages = [];162 // Next we'll find the nodes that we're going to animate and apply names to them..163 for (let i = 0; i < batch.length; i += 2) {164 const suspenseIdNode = batch[i];165 const parentInstance = suspenseIdNode.parentNode;166 if (!parentInstance) {167 // We may have client-rendered this boundary already. Skip it.168 continue;169 }170 const parentRect = parentInstance.getBoundingClientRect();171 if (172 !parentRect.left &&173 !parentRect.top &&174 !parentRect.width &&175 !parentRect.height176 ) {177 // If the parent instance is display: none then we don't animate this boundary.178 // This can happen when this boundary is actually a child of a different boundary that179 // isn't yet revealed or is about to be revealed, but in that case that boundary180 // should do the exit/enter and not this one. Conveniently this also lets us skip181 // this if it's just in a hidden tree in general.182 // TODO: Should we skip it if it's out of viewport? It's possible that it gets183 // brought into the viewport by changing size.184 // TODO: There's a another case where an inner boundary is inside a fallback that185 // is about to be deleted. In that case we should not run exit animations on the inner.186 continue;187 }188189 // Apply exit animations to the immediate elements inside the fallback.190 let node = suspenseIdNode;191 let depth = 0;192 while (node) {193 if (node.nodeType === COMMENT_NODE) {194 const data = node.data;195 if (data === SUSPENSE_END_DATA) {196 if (depth === 0) {197 break;198 } else {199 depth--;200 }201 } else if (202 data === SUSPENSE_START_DATA ||203 data === SUSPENSE_PENDING_START_DATA ||204 data === SUSPENSE_QUEUED_START_DATA ||205 data === SUSPENSE_FALLBACK_START_DATA206 ) {207 depth++;208 }209 } else if (node.nodeType === ELEMENT_NODE) {210 const exitElement = node;211 const exitName = exitElement.getAttribute('vt-name');212 const pairedElement = appearingViewTransitions.get(exitName);213 applyViewTransitionName(214 exitElement,215 pairedElement ? 'vt-share' : 'vt-exit',216 );217 if (pairedElement) {218 // Activate the other side as well.219 applyViewTransitionName(pairedElement, 'vt-share');220 appearingViewTransitions.set(exitName, null); // mark claimed221 }222 // Next we'll look inside this element for pairs to trigger "share".223 const disappearingElements =224 exitElement.querySelectorAll('[vt-share]');225 for (let j = 0; j < disappearingElements.length; j++) {226 const disappearingElement = disappearingElements[j];227 const name = disappearingElement.getAttribute('vt-name');228 const appearingElement = appearingViewTransitions.get(name);229 if (appearingElement) {230 applyViewTransitionName(disappearingElement, 'vt-share');231 applyViewTransitionName(appearingElement, 'vt-share');232 appearingViewTransitions.set(name, null); // mark claimed233 }234 }235 // Relay the exit to nested ViewTransitions that opted in236 const relayExitElements =237 exitElement.querySelectorAll('[vt-parent-exit]');238 for (let j = 0; j < relayExitElements.length; j++) {239 applyViewTransitionName(relayExitElements[j], 'vt-parent-exit');240 }241 }242 node = node.nextSibling;243 }244245 // Apply enter animations to the new nodes about to be inserted.246 const contentNode = batch[i + 1];247 let enterElement = contentNode.firstElementChild;248 while (enterElement) {249 const paired =250 appearingViewTransitions.get(enterElement.getAttribute('vt-name')) ===251 null;252 if (!paired) {253 applyViewTransitionName(enterElement, 'vt-enter');254 }255 // Relay the enter to nested ViewTransitions that opted in256 const relayEnterElements =257 enterElement.querySelectorAll('[vt-parent-enter]');258 for (let j = 0; j < relayEnterElements.length; j++) {259 applyViewTransitionName(relayEnterElements[j], 'vt-parent-enter');260 }261 enterElement = enterElement.nextElementSibling;262 }263264 // Apply update animations to any parents and siblings that might be affected.265 let ancestorElement = parentInstance;266 do {267 let childElement = ancestorElement.firstElementChild;268 while (childElement) {269 // TODO: Bail out if we can270 const updateClassName = childElement.getAttribute('vt-update');271 if (272 updateClassName &&273 updateClassName !== 'none' &&274 !restoreQueue.includes(childElement)275 ) {276 // If we have already handled this element as part of another exit/enter/share, don't override.277 applyViewTransitionName(childElement, 'vt-update');278 }279 childElement = childElement.nextElementSibling;280 }281 } while (282 (ancestorElement = ancestorElement.parentNode) &&283 ancestorElement.nodeType === ELEMENT_NODE &&284 ancestorElement.getAttribute('vt-update') !== 'none'285 );286287 // Find the appearing Suspensey Images inside the new content.288 const appearingImages = contentNode.querySelectorAll(289 'img[src]:not([loading="lazy"])',290 );291 // TODO: Consider marking shouldStartViewTransition if we found any images.292 // But only once we can disable the root animation for that case.293 suspenseyImages.push.apply(suspenseyImages, appearingImages);294 }295 if (shouldStartViewTransition) {296 const transition = (document['__reactViewTransition'] = document[297 'startViewTransition'298 ]({299 update: () => {300 revealBoundaries(batch);301 const blockingPromises = [302 // Force layout to trigger font loading, we stash the actual value to trick minifiers.303 document.documentElement.clientHeight,304 // Block on fonts finishing loading before revealing these boundaries.305 document.fonts.ready,306 ];307 for (let i = 0; i < suspenseyImages.length; i++) {308 const suspenseyImage = suspenseyImages[i];309 if (!suspenseyImage.complete) {310 const rect = suspenseyImage.getBoundingClientRect();311 const inViewport =312 rect.bottom > 0 &&313 rect.right > 0 &&314 rect.top < window.innerHeight &&315 rect.left < window.innerWidth;316 if (inViewport) {317 // TODO: Use decode() instead of the load event here once the fix in318 // https://issues.chromium.org/issues/420748301 has propagated fully.319 const loadingImage = new Promise(resolve => {320 suspenseyImage.addEventListener('load', resolve);321 suspenseyImage.addEventListener('error', resolve);322 });323 blockingPromises.push(loadingImage);324 }325 }326 }327 return Promise.race([328 Promise.all(blockingPromises),329 new Promise(resolve => {330 const currentTime = performance.now();331 const msUntilTimeout =332 // If the throttle would make us miss the target metric, then shorten the throttle.333 // performance.now()'s zero value is assumed to be the start time of the metric.334 currentTime < TARGET_VANITY_METRIC &&335 currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS336 ? TARGET_VANITY_METRIC - currentTime337 : // Otherwise it's throttled starting from last commit time.338 SUSPENSEY_FONT_AND_IMAGE_TIMEOUT;339 setTimeout(resolve, msUntilTimeout);340 }),341 ]);342 },343 types: [], // TODO: Add a hard coded type for Suspense reveals.344 }));345 transition.ready.finally(() => {346 // Restore all the names/classes that we applied to what they were before.347 // We do it in reverse order in case there were duplicates so the first one wins.348 for (let i = restoreQueue.length - 3; i >= 0; i -= 3) {349 const element = restoreQueue[i];350 const elementStyle = element.style;351 const previousName = restoreQueue[i + 1];352 elementStyle['viewTransitionName'] = previousName;353 const previousClassName = restoreQueue[i + 1];354 elementStyle['viewTransitionClass'] = previousClassName;355 if (element.getAttribute('style') === '') {356 element.removeAttribute('style');357 }358 }359 });360 transition.finished.finally(() => {361 if (document['__reactViewTransition'] === transition) {362 document['__reactViewTransition'] = null;363 }364 });365 // Queue any future completions into its own batch since they won't have been366 // snapshotted by this one.367 window['$RB'] = [];368 return;369 }370 // Fall through to reveal.371 } catch (x) {372 // Fall through to reveal.373 }374 // ViewTransitions v2 not supported or no ViewTransitions found. Reveal immediately.375 revealBoundaries(batch);376}377378export function clientRenderBoundary(379 suspenseBoundaryID,380 errorDigest,381 errorMsg,382 errorStack,383 errorComponentStack,384) {385 // Find the fallback's first element.386 const suspenseIdNode = document.getElementById(suspenseBoundaryID);387 if (!suspenseIdNode) {388 // The user must have already navigated away from this tree.389 // E.g. because the parent was hydrated.390 return;391 }392 // Find the boundary around the fallback. This is always the previous node.393 const suspenseNode = suspenseIdNode.previousSibling;394 // Tag it to be client rendered.395 suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;396 // assign error metadata to first sibling397 const dataset = suspenseIdNode.dataset;398 if (errorDigest != null) dataset['dgst'] = errorDigest;399 if (errorMsg) dataset['msg'] = errorMsg;400 if (errorStack) dataset['stck'] = errorStack;401 if (errorComponentStack) dataset['cstck'] = errorComponentStack;402 // Tell React to retry it if the parent already hydrated.403 if (suspenseNode['_reactRetry']) {404 suspenseNode['_reactRetry']();405 }406}407408export function completeBoundary(suspenseBoundaryID, contentID) {409 const contentNodeOuter = document.getElementById(contentID);410 if (!contentNodeOuter) {411 // If the client has failed hydration we may have already deleted the streaming412 // segments. The server may also have emitted a complete instruction but cancelled413 // the segment. Regardless we can ignore this case.414 return;415 }416417 // Find the fallback's first element.418 const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);419 if (!suspenseIdNodeOuter) {420 // We'll never reveal this boundary so we can remove its content immediately.421 // Otherwise we'll leave it in until we reveal it.422 // This is important in case this specific boundary contains other boundaries423 // that may get completed before we reveal this one.424 contentNodeOuter.parentNode.removeChild(contentNodeOuter);425426 // The user must have already navigated away from this tree.427 // E.g. because the parent was hydrated. That's fine there's nothing to do428 // but we have to make sure that we already deleted the container node.429 return;430 }431432 // Mark this Suspense boundary as queued so we know not to client render it433 // at the end of document load.434 const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;435 suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;436 // Queue this boundary for the next batch437 window['$RB'].push(suspenseIdNodeOuter, contentNodeOuter);438439 if (window['$RB'].length === 2) {440 // This is the first time we've pushed to the batch. We need to schedule a callback441 // to flush the batch. This is delayed by the throttle heuristic.442 if (typeof window['$RT'] !== 'number') {443 // If we haven't had our rAF callback yet, schedule everything for the first paint.444 requestAnimationFrame(window['$RV'].bind(null, window['$RB']));445 } else {446 const currentTime = performance.now();447 const msUntilTimeout =448 // If the throttle would make us miss the target metric, then shorten the throttle.449 // performance.now()'s zero value is assumed to be the start time of the metric.450 currentTime < TARGET_VANITY_METRIC &&451 currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS452 ? TARGET_VANITY_METRIC - currentTime453 : // Otherwise it's throttled starting from last commit time.454 window['$RT'] + FALLBACK_THROTTLE_MS - currentTime;455 // We always schedule the flush in a timer even if it's very low or negative to allow456 // for multiple completeBoundary calls that are already queued to have a chance to457 // make the batch.458 setTimeout(window['$RV'].bind(null, window['$RB']), msUntilTimeout);459 }460 }461}462463export function completeBoundaryWithStyles(464 suspenseBoundaryID,465 contentID,466 stylesheetDescriptors,467) {468 const precedences = new Map();469 const thisDocument = document;470 let lastResource, node;471472 // Seed the precedence list with existing resources and collect hoistable style tags473 const nodes = thisDocument.querySelectorAll(474 'link[data-precedence],style[data-precedence]',475 );476 const styleTagsToHoist = [];477 for (let i = 0; (node = nodes[i++]); ) {478 if (node.getAttribute('media') === 'not all') {479 styleTagsToHoist.push(node);480 } else {481 if (node.tagName === 'LINK') {482 window['$RM'].set(node.getAttribute('href'), node);483 }484 precedences.set(node.dataset['precedence'], (lastResource = node));485 }486 }487488 let i = 0;489 const dependencies = [];490 let href, precedence, attr, loadingState, resourceEl, media;491492 function cleanupWith(cb) {493 this['_p'] = null;494 cb();495 }496497 // Sheets Mode498 let sheetMode = true;499 while (true) {500 if (sheetMode) {501 // Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for502 // dependency if they already existed503 const stylesheetDescriptor = stylesheetDescriptors[i++];504 if (!stylesheetDescriptor) {505 // enter <style> Mode506 sheetMode = false;507 i = 0;508 continue;509 }510511 let avoidInsert = false;512 let j = 0;513 href = stylesheetDescriptor[j++];514515 if ((resourceEl = window['$RM'].get(href))) {516 // We have an already inserted stylesheet.517 loadingState = resourceEl['_p'];518 avoidInsert = true;519 } else {520 // We haven't already processed this href so we need to construct a stylesheet and hoist it521 // We construct it here and attach a loadingState. We also check whether it matches522 // media before we include it in the dependency array.523 resourceEl = thisDocument.createElement('link');524 resourceEl.href = href;525 resourceEl.rel = 'stylesheet';526 resourceEl.dataset['precedence'] = precedence =527 stylesheetDescriptor[j++];528 while ((attr = stylesheetDescriptor[j++])) {529 resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);530 }531 loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {532 resourceEl.onload = cleanupWith.bind(resourceEl, resolve);533 resourceEl.onerror = cleanupWith.bind(resourceEl, reject);534 });535 // Save this resource element so we can bailout if it is used again536 window['$RM'].set(href, resourceEl);537 }538 media = resourceEl.getAttribute('media');539 if (loadingState && (!media || window['matchMedia'](media).matches)) {540 dependencies.push(loadingState);541 }542 if (avoidInsert) {543 // We have a link that is already in the document. We don't want to fall through to the insert path544 continue;545 }546 } else {547 // <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.548 resourceEl = styleTagsToHoist[i++];549 if (!resourceEl) {550 // we are done with all style tags551 break;552 }553554 precedence = resourceEl.getAttribute('data-precedence');555 resourceEl.removeAttribute('media');556 }557558 // resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting559 const prior = precedences.get(precedence) || lastResource;560 if (prior === lastResource) {561 lastResource = resourceEl;562 }563 precedences.set(precedence, resourceEl);564565 // Finally, we insert the newly constructed instance at an appropriate location566 // in the Document.567 if (prior) {568 prior.parentNode.insertBefore(resourceEl, prior.nextSibling);569 } else {570 const head = thisDocument.head;571 head.insertBefore(resourceEl, head.firstChild);572 }573 }574575 const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);576 if (suspenseIdNodeOuter) {577 // Mark this Suspense boundary as queued so we know not to client render it578 // at the end of document load.579 const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;580 suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;581 }582583 Promise.all(dependencies).then(584 window['$RC'].bind(null, suspenseBoundaryID, contentID),585 window['$RX'].bind(null, suspenseBoundaryID, 'CSS failed to load'),586 );587}588589export function completeSegment(containerID, placeholderID) {590 const segmentContainer = document.getElementById(containerID);591 const placeholderNode = document.getElementById(placeholderID);592 // We always expect both nodes to exist here because, while we might593 // have navigated away from the main tree, we still expect the detached594 // tree to exist.595 segmentContainer.parentNode.removeChild(segmentContainer);596 while (segmentContainer.firstChild) {597 placeholderNode.parentNode.insertBefore(598 segmentContainer.firstChild,599 placeholderNode,600 );601 }602 placeholderNode.parentNode.removeChild(placeholderNode);603}604605// This is the exact URL string we expect that Fizz renders if we provide a function action.606// We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense607// as a shared module for that reason.608const EXPECTED_FORM_ACTION_URL =609 // eslint-disable-next-line no-script-url610 "javascript:throw new Error('React form unexpectedly submitted.')";611612export function listenToFormSubmissionsForReplaying() {613 // A global replay queue ensures actions are replayed in order.614 // This event listener should be above the React one. That way when615 // we preventDefault in React's handling we also prevent this event616 // from queing it. Since React listens to the root and the top most617 // container you can use is the document, the window is fine.618 // eslint-disable-next-line no-restricted-globals619 addEventListener('submit', event => {620 if (event.defaultPrevented) {621 // We let earlier events to prevent the action from submitting.622 return;623 }624 const form = event.target;625 const submitter = event['submitter'];626 let action = form.action;627 let formDataSubmitter = submitter;628 if (submitter) {629 const submitterAction = submitter.getAttribute('formAction');630 if (submitterAction != null) {631 // The submitter overrides the action.632 action = submitterAction;633 // If the submitter overrides the action, and it passes the test below,634 // that means that it was a function action which conceptually has no name.635 // Therefore, we exclude the submitter from the formdata.636 formDataSubmitter = null;637 }638 }639 if (action !== EXPECTED_FORM_ACTION_URL) {640 // The form is a regular form action, we can bail.641 return;642 }643644 // Prevent native navigation.645 // This will also prevent other React's on the same page from listening.646 event.preventDefault();647648 // Take a snapshot of the FormData at the time of the event.649 const formData = new FormData(form, formDataSubmitter);650651 // Queue for replaying later. This field could potentially be shared with multiple652 // Reacts on the same page since each one will preventDefault for the next one.653 // This means that this protocol is shared with any React version that shares the same654 // javascript: URL placeholder value. So we might not be the first to declare it.655 // We attach it to the form's root node, which is the shared environment context656 // where we preserve sequencing and where we'll pick it up from during hydration.657 // If there's no ownerDocument, then this is the document.658 const root = form.ownerDocument || form;659 (root['$$reactFormReplay'] = root['$$reactFormReplay'] || []).push(660 form,661 submitter,662 formData,663 );664 });665}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.