Skip to content

Commit db467e1

Browse files
thePunderWomanalxhub
authored andcommitted
refactor(core): Additional cleanup for incremental hydration (angular#58394)
This adds comments, removes some duplicate logic, and eliminates some unnecessary complexity of the incremental hydration core logic. PR Close angular#58394
1 parent 4434f3c commit db467e1

File tree

5 files changed

+72
-101
lines changed

5 files changed

+72
-101
lines changed

packages/core/src/defer/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,18 @@ export function isTDeferBlockDetails(value: unknown): value is TDeferBlockDetail
166166
typeof (value as TDeferBlockDetails).primaryTmplIndex === 'number'
167167
);
168168
}
169+
170+
/**
171+
* Whether a given TNode represents a defer block.
172+
*/
173+
export function isDeferBlock(tView: TView, tNode: TNode): boolean {
174+
let tDetails: TDeferBlockDetails | null = null;
175+
const slotIndex = getDeferBlockDataIndex(tNode.index);
176+
// Check if a slot index is in the reasonable range.
177+
// Note: we do `-1` on the right border, since defer block details are stored
178+
// in the `n+1` slot, see `getDeferBlockDataIndex` for more info.
179+
if (HEADER_OFFSET < slotIndex && slotIndex < tView.bindingStartIndex) {
180+
tDetails = getTDeferBlockDetails(tView, tNode);
181+
}
182+
return !!tDetails && isTDeferBlockDetails(tDetails);
183+
}

packages/core/src/hydration/annotate.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
HydrateTriggerDetails,
1515
TDeferBlockDetails,
1616
} from '../defer/interfaces';
17-
import {getLDeferBlockDetails, getTDeferBlockDetails} from '../defer/utils';
17+
import {getLDeferBlockDetails, getTDeferBlockDetails, isDeferBlock} from '../defer/utils';
1818
import {isDetachedByI18n} from '../i18n/utils';
1919
import {ViewEncapsulation} from '../metadata';
2020
import {Renderer2} from '../render';
@@ -91,7 +91,6 @@ import {
9191
TextNodeMarker,
9292
} from './utils';
9393
import {Injector} from '../di';
94-
import {isDeferBlock} from './blocks';
9594

9695
/**
9796
* A collection that tracks all serialized views (`ngh` DOM annotations)

packages/core/src/hydration/api.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import {
4646
import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY, SSR_CONTENT_INTEGRITY_MARKER} from './utils';
4747
import {enableFindMatchingDehydratedViewImpl} from './views';
4848
import {bootstrapIncrementalHydration, enableRetrieveDeferBlockDataImpl} from './incremental';
49-
import {enableHydrateFromBlockNameImpl} from './blocks';
5049

5150
/**
5251
* Indicates whether the hydration-related code was added,
@@ -124,7 +123,6 @@ function enableIncrementalHydrationRuntimeSupport() {
124123
if (!isIncrementalHydrationRuntimeSupportEnabled) {
125124
isIncrementalHydrationRuntimeSupportEnabled = true;
126125
enableRetrieveDeferBlockDataImpl();
127-
enableHydrateFromBlockNameImpl();
128126
}
129127
}
130128

packages/core/src/hydration/blocks.ts

Lines changed: 35 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,26 @@ import {Injector} from '../di';
1313
import {TransferState} from '../transfer_state';
1414
import {removeListenersFromBlocks} from '../event_delegation_utils';
1515
import {cleanupLContainer} from './cleanup';
16-
import {TNode} from '../render3/interfaces/node';
17-
import {HEADER_OFFSET, TView} from '../render3/interfaces/view';
18-
import {DeferBlock, TDeferBlockDetails} from '../defer/interfaces';
19-
import {getDeferBlockDataIndex, getTDeferBlockDetails, isTDeferBlockDetails} from '../defer/utils';
16+
import {DeferBlock} from '../defer/interfaces';
2017
import {whenStable, ApplicationRef} from '../application/application_ref';
2118

2219
/**
2320
* Finds first hydrated parent `@defer` block for a given block id.
2421
* If there are any dehydrated `@defer` blocks found along the way,
2522
* they are also stored and returned from the function (as a list of ids).
23+
* Note: This is utilizing serialized information to navigate up the tree
2624
*/
2725
export function findFirstHydratedParentDeferBlock(deferBlockId: string, injector: Injector) {
2826
const deferBlockRegistry = injector.get(DeferBlockRegistry);
2927
const transferState = injector.get(TransferState);
3028
const deferBlockParents = transferState.get(NGH_DEFER_BLOCKS_KEY, {});
3129
const dehydratedBlocks: string[] = [];
3230

33-
let deferBlock = deferBlockRegistry.get(deferBlockId) ?? null;
31+
let deferBlock = deferBlockRegistry.get(deferBlockId);
3432
let currentBlockId: string | null = deferBlockId;
33+
// at each level we check if the registry has the given defer block id
34+
// - if it does, we know it was already hydrated and can stop here
35+
// - if it does not, we continue on
3536
while (!deferBlock) {
3637
dehydratedBlocks.unshift(currentBlockId);
3738
currentBlockId = deferBlockParents[currentBlockId][DEFER_PARENT_BLOCK_ID];
@@ -42,24 +43,21 @@ export function findFirstHydratedParentDeferBlock(deferBlockId: string, injector
4243
}
4344

4445
/**
45-
* Hydrates a defer block by block name through jsaction code paths
46-
*/
47-
let _hydrateFromBlockNameImpl: typeof hydrateFromBlockNameImpl = () => {
48-
return Promise.resolve({deferBlock: null, hydratedBlocks: new Set<string>()});
49-
};
50-
51-
/**
52-
* Hydrates a defer block by block name using non jsaction code paths
46+
* The core mechanism for incremental hydration. This recursively triggers
47+
* hydration for all the blocks in the tree that need to be hydrated and keeps
48+
* track of all those blocks that were hydrated along the way.
49+
*
50+
* @param injector
51+
* @param blockName
52+
* @param onTriggerFn The function that triggers the block and fetches deps
53+
* @param hydratedBlocks The set of blocks currently being hydrated in the tree
54+
* @returns
5355
*/
54-
let _incrementallyHydrateFromBlockNameImpl: typeof incrementallyHydrateFromBlockNameImpl = () => {
55-
return Promise.resolve();
56-
};
57-
58-
async function hydrateFromBlockNameImpl(
56+
export async function hydrateFromBlockName(
5957
injector: Injector,
6058
blockName: string,
61-
onTriggerFn: (deferBlock: any) => void,
62-
hydratedBlocks: Set<string>,
59+
onTriggerFn: (deferBlock: DeferBlock) => void,
60+
hydratedBlocks: Set<string> = new Set(),
6361
): Promise<{
6462
deferBlock: DeferBlock | null;
6563
hydratedBlocks: Set<string>;
@@ -74,80 +72,48 @@ async function hydrateFromBlockNameImpl(
7472
injector,
7573
);
7674
if (deferBlock && blockId) {
75+
// Step 2: Add the current block to the tracking sets to prevent
76+
// attempting to trigger hydration on a block more than once
77+
// simulataneously.
7778
hydratedBlocks.add(blockId);
7879
deferBlockRegistry.hydrating.add(blockId);
7980

81+
// Step 3: Run the actual trigger function to fetch dependencies
8082
await onTriggerFn(deferBlock);
83+
84+
// Step 4: Recursively trigger, fetch, and hydrate from the top of the hierarchy down
8185
let hydratedBlock: DeferBlock | null = deferBlock;
8286
for (const dehydratedBlock of dehydratedBlocks) {
83-
const hydratedInfo = await hydrateFromBlockNameImpl(
87+
const hydratedInfo = await hydrateFromBlockName(
8488
injector,
8589
dehydratedBlock,
8690
onTriggerFn,
8791
hydratedBlocks,
8892
);
8993
hydratedBlock = hydratedInfo.deferBlock;
9094
}
91-
// this is going to be the wrong defer block. We need to get the final one.
95+
// TODO(incremental-hydration): this is likely where we want to do Step 5: some cleanup work in the
96+
// DeferBlockRegistry.
9297
return {deferBlock: hydratedBlock, hydratedBlocks};
9398
} else {
9499
// TODO(incremental-hydration): this is likely an error, consider producing a `console.error`.
95100
return {deferBlock: null, hydratedBlocks};
96101
}
97102
}
98103

99-
/**
100-
* Sets the implementation for the `retrieveDeferBlockData` function.
101-
*/
102-
export function enableHydrateFromBlockNameImpl() {
103-
_hydrateFromBlockNameImpl = hydrateFromBlockNameImpl;
104-
_incrementallyHydrateFromBlockNameImpl = incrementallyHydrateFromBlockNameImpl;
105-
}
106-
107-
export async function hydrateFromBlockName(
108-
injector: Injector,
109-
blockName: string,
110-
onTriggerFn: (deferBlock: any) => void,
111-
): Promise<{
112-
deferBlock: DeferBlock | null;
113-
hydratedBlocks: Set<string>;
114-
}> {
115-
return await _hydrateFromBlockNameImpl(injector, blockName, onTriggerFn, new Set<string>());
116-
}
117-
118-
async function incrementallyHydrateFromBlockNameImpl(
104+
export async function incrementallyHydrateFromBlockName(
119105
injector: Injector,
120106
blockName: string,
121-
triggerFn: (deferBlock: any) => void,
107+
triggerFn: (deferBlock: DeferBlock) => void,
122108
): Promise<void> {
123109
const {deferBlock, hydratedBlocks} = await hydrateFromBlockName(injector, blockName, triggerFn);
124-
removeListenersFromBlocks([...hydratedBlocks], injector);
125110
if (deferBlock !== null) {
111+
// hydratedBlocks is a set, and needs to be converted to an array
112+
// for removing listeners
113+
removeListenersFromBlocks([...hydratedBlocks], injector);
126114
cleanupLContainer(deferBlock.lContainer);
127-
const appRef = injector.get(ApplicationRef);
128-
await whenStable(appRef);
129-
}
130-
}
131-
132-
export function incrementallyHydrateFromBlockName(
133-
injector: Injector,
134-
blockName: string,
135-
triggerFn: (deferBlock: any) => void,
136-
): Promise<void> {
137-
return _incrementallyHydrateFromBlockNameImpl(injector, blockName, triggerFn);
138-
}
139-
140-
/**
141-
* Whether a given TNode represents a defer block.
142-
*/
143-
export function isDeferBlock(tView: TView, tNode: TNode): boolean {
144-
let tDetails: TDeferBlockDetails | null = null;
145-
const slotIndex = getDeferBlockDataIndex(tNode.index);
146-
// Check if a slot index is in the reasonable range.
147-
// Note: we do `-1` on the right border, since defer block details are stored
148-
// in the `n+1` slot, see `getDeferBlockDataIndex` for more info.
149-
if (HEADER_OFFSET < slotIndex && slotIndex < tView.bindingStartIndex) {
150-
tDetails = getTDeferBlockDetails(tView, tNode);
115+
// we need to wait for app stability here so we don't continue before
116+
// the hydration process has finished, which could result in problems
117+
await whenStable(injector.get(ApplicationRef));
151118
}
152-
return !!tDetails && isTDeferBlockDetails(tDetails);
153119
}

packages/core/src/hydration/event_replay.ts

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,13 @@ import {DeferBlock, DeferBlockTrigger, HydrateTriggerDetails} from '../defer/int
4444
import {triggerAndWaitForCompletion} from '../defer/instructions';
4545
import {cleanupDehydratedViews, cleanupLContainer} from './cleanup';
4646
import {hoverEventNames, interactionEventNames} from '../defer/dom_triggers';
47+
import {DeferBlockRegistry} from '../defer/registry';
4748

4849
/** Apps in which we've enabled event replay.
4950
* This is to prevent initializing event replay more than once per app.
5051
*/
5152
const appsWithEventReplay = new WeakSet<ApplicationRef>();
5253

53-
/**
54-
* A set of in progress hydrating blocks
55-
*/
56-
let hydratingBlocks = new Set<string>();
57-
5854
/**
5955
* A list of block events that need to be replayed
6056
*/
@@ -94,6 +90,9 @@ export function withEventReplay(): Provider[] {
9490
provide: ENVIRONMENT_INITIALIZER,
9591
useValue: () => {
9692
const injector = inject(Injector);
93+
// We have to check for the appRef here due to the possibility of multiple apps
94+
// being present on the same page. We only want to enable event replay for the
95+
// apps that actually want it.
9796
const appRef = injector.get(ApplicationRef);
9897
if (!appsWithEventReplay.has(appRef)) {
9998
const jsActionMap = inject(BLOCK_ELEMENT_MAP);
@@ -118,6 +117,9 @@ export function withEventReplay(): Provider[] {
118117
return;
119118
}
120119

120+
// We have to check for the appRef here due to the possibility of multiple apps
121+
// being present on the same page. We only want to enable event replay for the
122+
// apps that actually want it.
121123
if (!appsWithEventReplay.has(appRef)) {
122124
appsWithEventReplay.add(appRef);
123125
appRef.onDestroy(() => appsWithEventReplay.delete(appRef));
@@ -242,30 +244,16 @@ async function hydrateAndInvokeBlockListeners(
242244
currentTarget: Element,
243245
) {
244246
blockEventQueue.push({event, currentTarget});
245-
if (!hydratingBlocks.has(blockName)) {
246-
hydratingBlocks.add(blockName);
247-
await triggerBlockHydration(injector, blockName, fetchAndRenderDeferBlock);
248-
hydratingBlocks.delete(blockName);
249-
}
250-
}
251-
252-
export async function fetchAndRenderDeferBlock(deferBlock: DeferBlock): Promise<DeferBlock> {
253-
await triggerAndWaitForCompletion(deferBlock);
254-
return deferBlock;
255-
}
256-
257-
async function triggerBlockHydration(
258-
injector: Injector,
259-
blockName: string,
260-
onTriggerFn: (deferBlock: any) => void,
261-
) {
262-
// grab the list of dehydrated blocks and queue them up
263-
const {dehydratedBlocks} = findFirstHydratedParentDeferBlock(blockName, injector);
264-
for (let block of dehydratedBlocks) {
265-
hydratingBlocks.add(block);
266-
}
267-
const {deferBlock, hydratedBlocks} = await hydrateFromBlockName(injector, blockName, onTriggerFn);
247+
const {deferBlock, hydratedBlocks} = await hydrateFromBlockName(
248+
injector,
249+
blockName,
250+
fetchAndRenderDeferBlock,
251+
);
268252
if (deferBlock !== null) {
253+
// TODO(incremental-hydration): extract this work into a post
254+
// hydration cleanup function
255+
const deferBlockRegistry = injector.get(DeferBlockRegistry);
256+
deferBlockRegistry.hydrating.delete(blockName);
269257
hydratedBlocks.add(blockName);
270258
const appRef = injector.get(ApplicationRef);
271259
await appRef.whenStable();
@@ -274,6 +262,11 @@ async function triggerBlockHydration(
274262
}
275263
}
276264

265+
export async function fetchAndRenderDeferBlock(deferBlock: DeferBlock): Promise<DeferBlock> {
266+
await triggerAndWaitForCompletion(deferBlock);
267+
return deferBlock;
268+
}
269+
277270
function replayQueuedBlockEvents(hydratedBlocks: Set<string>, injector: Injector) {
278271
// clone the queue
279272
const queue = [...blockEventQueue];

0 commit comments

Comments
 (0)