From 0ca14e2c72c092c2dddfca990d5fae8d8322acdc Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Wed, 30 Oct 2024 11:33:38 -0400 Subject: [PATCH] refactor(core): More cleanup of incremental hydration code (#58430) This cleans up much of incremental.ts and adds documentation. PR Close #58430 --- packages/core/src/hydration/incremental.ts | 100 +++++++++++++-------- packages/core/src/hydration/interfaces.ts | 5 -- packages/core/src/hydration/utils.ts | 4 - 3 files changed, 62 insertions(+), 47 deletions(-) diff --git a/packages/core/src/hydration/incremental.ts b/packages/core/src/hydration/incremental.ts index 97ee20b5b7b..76acc1d9b59 100644 --- a/packages/core/src/hydration/incremental.ts +++ b/packages/core/src/hydration/incremental.ts @@ -24,17 +24,28 @@ import {NGH_DEFER_BLOCKS_KEY} from './utils'; import {onViewport} from '../defer/dom_triggers'; import {fetchAndRenderDeferBlock} from './event_replay'; +/** + * Initializes incremental hydration for non-JSAction triggers. This gathers up + * all the parent / child relationships of defer blocks and identifies all the + * serialized defer blocks that would need to be potentially hydrated later. + */ export function bootstrapIncrementalHydration(doc: Document, injector: Injector) { const deferBlockData = processBlockData(injector); const commentsByBlockId = gatherDeferBlocksCommentNodes(doc, doc.body); processAndInitTriggers(injector, deferBlockData, commentsByBlockId); } +/** + * Summarizes the presence of specific types of triggers anywhere in the DOM + */ interface BlockSummary { data: SerializedDeferBlock; - hydrate: {idle: boolean; immediate: boolean; viewport: boolean; timer: boolean}; + hydrate: {idle: boolean; immediate: boolean; viewport: boolean; timer: number | null}; } +/** + * The details of a specific element's trigger and how it is associated to a block + */ interface ElementTrigger { el: HTMLElement; blockName: string; @@ -45,26 +56,34 @@ function isTimerTrigger(triggerInfo: DeferBlockTrigger | SerializedTriggerDetail return typeof triggerInfo === 'object' && triggerInfo.trigger === DeferBlockTrigger.Timer; } -function hasHydrateTimerTrigger(blockData: SerializedDeferBlock): boolean { - return (blockData[DEFER_HYDRATE_TRIGGERS]?.filter((t) => isTimerTrigger(t)) ?? []).length > 0; +function getHydrateTimerTrigger(blockData: SerializedDeferBlock): number | null { + const trigger = blockData[DEFER_HYDRATE_TRIGGERS]?.find((t) => isTimerTrigger(t)); + return (trigger as SerializedTriggerDetails)?.delay ?? null; } function hasHydrateTrigger(blockData: SerializedDeferBlock, trigger: DeferBlockTrigger): boolean { return blockData[DEFER_HYDRATE_TRIGGERS]?.includes(trigger) ?? false; } +/** + * Creates a summary of the given serialized defer block, which is used later to properly initialize + * specific triggers. + */ function createBlockSummary(blockInfo: SerializedDeferBlock): BlockSummary { return { data: blockInfo, hydrate: { idle: hasHydrateTrigger(blockInfo, DeferBlockTrigger.Idle), immediate: hasHydrateTrigger(blockInfo, DeferBlockTrigger.Immediate), - timer: hasHydrateTimerTrigger(blockInfo), + timer: getHydrateTimerTrigger(blockInfo), viewport: hasHydrateTrigger(blockInfo, DeferBlockTrigger.Viewport), }, }; } +/** + * Processes all of the defer block data in the transfer state and creates a map of the summaries + */ function processBlockData(injector: Injector): Map { const blockData = retrieveDeferBlockData(injector); let blockDetails = new Map(); @@ -74,31 +93,35 @@ function processBlockData(injector: Injector): Map { return blockDetails; } -function gatherDeferBlocksCommentNodes(doc: Document, node?: HTMLElement): Map { - const commentNodesIterator = doc.createNodeIterator(node ?? doc.body, NodeFilter.SHOW_COMMENT, { - acceptNode(node) { - return node.textContent?.match('ngh=') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; - }, - }); +/** + * Retrieves all comments nodes that contain ngh comments referring to a defer block + */ +function gatherDeferBlocksCommentNodes(doc: Document, node: HTMLElement): Map { + const commentNodesIterator = doc.createNodeIterator(node, NodeFilter.SHOW_COMMENT, {acceptNode}); let currentNode: Comment; const nodesByBlockId = new Map(); while ((currentNode = commentNodesIterator.nextNode() as Comment)) { - const result = currentNode?.textContent?.match('d[0-9]+'); - if (result?.length === 1) { - nodesByBlockId.set(result[0], currentNode); + // TODO(incremental-hydration: convert this to use string parsing rather than regex + const regex = new RegExp(/^\s*ngh=(d[0-9]+)/g); + const result = regex.exec(currentNode?.textContent ?? ''); + if (result && result?.length > 0) { + nodesByBlockId.set(result[1], currentNode); } } return nodesByBlockId; } -function getTimerDelay(summary: BlockSummary): number { - const hydrateTrigger = summary.data[DEFER_HYDRATE_TRIGGERS]!.find((t) => - isTimerTrigger(t), - ) as SerializedTriggerDetails; - return hydrateTrigger.delay!; +function acceptNode(node: HTMLElement) { + return node.textContent?.trimStart().startsWith('ngh=') + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_REJECT; } +/** + * Loops through all defer block summaries and ensures all the blocks triggers are + * properly initialized + */ function processAndInitTriggers( injector: Injector, blockData: Map, @@ -118,20 +141,20 @@ function processAndInitTriggers( if (currentNode.nodeType !== Node.ELEMENT_NODE) { continue; } - const et: ElementTrigger = {el: currentNode, blockName: blockId}; + const elementTrigger: ElementTrigger = {el: currentNode, blockName: blockId}; // hydrate if (blockSummary.hydrate.idle) { - idleElements.push(et); + idleElements.push(elementTrigger); } if (blockSummary.hydrate.immediate) { - immediateElements.push(et); + immediateElements.push(elementTrigger); } - if (blockSummary.hydrate.timer) { - et.delay = getTimerDelay(blockSummary); - timerElements.push(et); + if (blockSummary.hydrate.timer !== null) { + elementTrigger.delay = blockSummary.hydrate.timer; + timerElements.push(elementTrigger); } if (blockSummary.hydrate.viewport) { - viewportElements.push(et); + viewportElements.push(elementTrigger); } } } @@ -143,8 +166,8 @@ function processAndInitTriggers( setTimerTriggers(injector, timerElements); } -async function setIdleTriggers(injector: Injector, ets: ElementTrigger[]) { - for (const elementTrigger of ets) { +async function setIdleTriggers(injector: Injector, elementTriggers: ElementTrigger[]) { + for (const elementTrigger of elementTriggers) { const registry = injector.get(DeferBlockRegistry); const onInvoke = () => incrementallyHydrateFromBlockName( @@ -157,20 +180,24 @@ async function setIdleTriggers(injector: Injector, ets: ElementTrigger[]) { } } -async function setViewportTriggers(injector: Injector, ets: ElementTrigger[]) { - for (let et of ets) { +async function setViewportTriggers(injector: Injector, elementTriggers: ElementTrigger[]) { + for (let elementTrigger of elementTriggers) { onViewport( - et.el, + elementTrigger.el, async () => { - await incrementallyHydrateFromBlockName(injector, et.blockName, fetchAndRenderDeferBlock); + await incrementallyHydrateFromBlockName( + injector, + elementTrigger.blockName, + fetchAndRenderDeferBlock, + ); }, injector, ); } } -async function setTimerTriggers(injector: Injector, ets: ElementTrigger[]) { - for (const elementTrigger of ets) { +async function setTimerTriggers(injector: Injector, elementTriggers: ElementTrigger[]) { + for (const elementTrigger of elementTriggers) { const registry = injector.get(DeferBlockRegistry); const onInvoke = async () => await incrementallyHydrateFromBlockName( @@ -184,8 +211,8 @@ async function setTimerTriggers(injector: Injector, ets: ElementTrigger[]) { } } -async function setImmediateTriggers(injector: Injector, ets: ElementTrigger[]) { - for (const elementTrigger of ets) { +async function setImmediateTriggers(injector: Injector, elementTriggers: ElementTrigger[]) { + for (const elementTrigger of elementTriggers) { await incrementallyHydrateFromBlockName( injector, elementTrigger.blockName, @@ -210,9 +237,6 @@ export function retrieveDeferBlockDataImpl(injector: Injector): { if (transferState !== null) { const nghDeferData = transferState.get(NGH_DEFER_BLOCKS_KEY, {}); - // If the `ngh` attribute exists and has a non-empty value, - // the hydration info *must* be present in the TransferState. - // If there is no data for some reasons, this is an error. ngDevMode && assertDefined(nghDeferData, 'Unable to retrieve defer block info from the TransferState.'); return nghDeferData; diff --git a/packages/core/src/hydration/interfaces.ts b/packages/core/src/hydration/interfaces.ts index 3ce145f32b8..1dd6dbf3ad6 100644 --- a/packages/core/src/hydration/interfaces.ts +++ b/packages/core/src/hydration/interfaces.ts @@ -245,11 +245,6 @@ export interface DehydratedView { * removed from the DOM during hydration cleanup. */ dehydratedIcuData?: Map; - - /** - * A mapping of defer block unique ids to the defer block data - */ - dehydratedDeferBlockData?: Record; } /** diff --git a/packages/core/src/hydration/utils.ts b/packages/core/src/hydration/utils.ts index f830f486c9f..d8e6b8bf84c 100644 --- a/packages/core/src/hydration/utils.ts +++ b/packages/core/src/hydration/utils.ts @@ -152,10 +152,6 @@ export function retrieveHydrationInfoImpl( firstChild: rNode.firstChild ?? null, }; - if (nghDeferData) { - dehydratedView.dehydratedDeferBlockData = nghDeferData; - } - if (isRootView) { // If there is hydration info present for the root view, it means that there was // a ViewContainerRef injected in the root component. The root component host element