@@ -10,7 +10,7 @@ import {afterNextRender} from '../render3/after_render/hooks';
1010import { Injector } from '../di' ;
1111import { internalImportProvidersFrom } from '../di/provider_collection' ;
1212import { RuntimeError , RuntimeErrorCode } from '../errors' ;
13- import { cleanupDeferBlock } from '../hydration/cleanup' ;
13+ import { cleanupHydratedDeferBlocks } from '../hydration/cleanup' ;
1414import { BlockSummary , ElementTrigger , NUM_ROOT_NODES } from '../hydration/interfaces' ;
1515import {
1616 assertSsrIdDefined ,
@@ -39,7 +39,6 @@ import {
3939 DeferBlockState ,
4040 DeferBlockTrigger ,
4141 DeferDependenciesLoadingState ,
42- DehydratedDeferBlock ,
4342 LDeferBlockDetails ,
4443 ON_COMPLETE_FNS ,
4544 SSR_BLOCK_STATE ,
@@ -63,6 +62,7 @@ import {
6362 getPrimaryBlockTNode ,
6463 getTDeferBlockDetails ,
6564} from './utils' ;
65+ import { ApplicationRef } from '../application/application_ref' ;
6666
6767/**
6868 * Schedules triggering of a defer block for `on idle` and `on timer` conditions.
@@ -337,61 +337,126 @@ export function triggerDeferBlock(lView: LView, tNode: TNode) {
337337}
338338
339339/**
340- * Triggers hydration from a given defer block's unique SSR ID.
341- * This includes firing any queued events that need to be replayed
342- * and handling any post hydration cleanup.
340+ * The core mechanism for incremental hydration. This triggers
341+ * hydration for all the blocks in the tree that need to be hydrated
342+ * and keeps track of all those blocks that were hydrated along the way.
343+ *
344+ * Note: the `replayQueuedEventsFn` is only provided when hydration is invoked
345+ * as a result of an event replay (via JsAction). When hydration is invoked from
346+ * an instruction set (e.g. `deferOnImmediate`) - there is no need to replay any
347+ * events.
343348 */
344349export async function triggerHydrationFromBlockName (
345350 injector : Injector ,
346351 blockName : string ,
347- replayFn : Function = ( ) => { } ,
348- ) : Promise < void > {
349- const { deferBlock, hydratedBlocks} = await triggerBlockTreeHydrationByName ( injector , blockName ) ;
350- replayFn ( hydratedBlocks ) ;
351- await cleanupDeferBlock ( deferBlock , hydratedBlocks , injector ) ;
352+ replayQueuedEventsFn ?: Function ,
353+ ) {
354+ const dehydratedBlockRegistry = injector . get ( DEHYDRATED_BLOCK_REGISTRY ) ;
355+ const blocksBeingHydrated = dehydratedBlockRegistry . hydrating ;
356+
357+ // Make sure we don't hydrate/trigger the same thing multiple times
358+ if ( blocksBeingHydrated . has ( blockName ) ) {
359+ return ;
360+ }
361+
362+ // The parent promise is the possible case of a list of defer blocks already being queued
363+ // If it is queued, it'll exist; otherwise it'll be null. The hydration queue will contain all
364+ // elements that need to be hydrated, sans any that have promises already
365+ const { parentBlockPromise, hydrationQueue} = getParentBlockHydrationQueue ( blockName , injector ) ;
366+
367+ // The hydrating map in the registry prevents re-triggering hydration for a block that's already in
368+ // the hydration queue. Here we generate promises for each of the blocks about to be hydrated
369+ populateHydratingStateForQueue ( dehydratedBlockRegistry , hydrationQueue ) ;
370+
371+ // Trigger resource loading and hydration for the blocks in the queue in the order of highest block
372+ // to lowest block. Once a block has finished resource loading, after next render fires after hydration
373+ // finishes. The new block will have its defer instruction called and will be in the registry.
374+ // Due to timing related to potential nested control flow, this has to be scheduled after the next render.
375+
376+ // Indicate that we have some pending async work.
377+ const pendingTasks = injector . get ( PendingTasksInternal ) ;
378+ const taskId = pendingTasks . add ( ) ;
379+
380+ // If the parent block was being hydrated, but the process has
381+ // not yet complete, wait until parent block promise settles before
382+ // going over dehydrated blocks from the queue.
383+ if ( parentBlockPromise !== null ) {
384+ await parentBlockPromise ;
385+ }
386+
387+ // Actually do the triggering and hydration of the queue of blocks
388+ for ( const dehydratedBlockId of hydrationQueue ) {
389+ await triggerDeferBlockResourceLoading ( dehydratedBlockId , dehydratedBlockRegistry ) ;
390+ await nextRender ( injector ) ;
391+ // TODO(incremental-hydration): assert (in dev mode) that a defer block is present in the dehydrated registry
392+ // at this point. If not - it means that the block has not been hydrated, for example due to different
393+ // `@if` conditions on the client and the server. If we detect this case, we should also do the cleanup
394+ // of all child block (promises, registry state, etc).
395+ // TODO(incremental-hydration): call `rejectFn` when lDetails[DEFER_BLOCK_STATE] is `DeferBlockState.Error`.
396+ blocksBeingHydrated . get ( dehydratedBlockId ) ! . resolve ( ) ;
397+
398+ // TODO(incremental-hydration): consider adding a wait for stability here
399+ }
400+
401+ // Await hydration completion for the requested block.
402+ await blocksBeingHydrated . get ( blockName ) ?. promise ;
403+
404+ // All async work is done, remove the taskId from the registry.
405+ pendingTasks . remove ( taskId ) ;
406+
407+ // Replay any queued events, if any exist and the replay operation was requested.
408+ if ( replayQueuedEventsFn ) {
409+ replayQueuedEventsFn ( hydrationQueue ) ;
410+ }
411+
412+ // Cleanup after hydration of all affected defer blocks.
413+ cleanupHydratedDeferBlocks (
414+ dehydratedBlockRegistry . get ( blockName ) ,
415+ hydrationQueue ,
416+ dehydratedBlockRegistry ,
417+ injector . get ( ApplicationRef ) ,
418+ ) ;
352419}
353420
354421/**
355- * Triggers the resource loading for a defer block and passes back a promise
356- * to handle cleanup on completion
422+ * Generates a new promise for every defer block in the hydrating queue
357423 */
358- export function triggerAndWaitForCompletion (
359- dehydratedBlockId : string ,
360- dehydratedBlockRegistry : DehydratedBlockRegistry ,
361- injector : Injector ,
362- ) : Promise < void > {
363- // TODO(incremental-hydration): This is a temporary fix to resolve control flow
364- // cases where nested defer blocks are inside control flow. We wait for each nested
365- // defer block to load and render before triggering the next one in a sequence. This is
366- // needed to ensure that corresponding LViews & LContainers are available for a block
367- // before we trigger it. We need to investigate how to get rid of the `afterNextRender`
368- // calls (in the nearest future) and do loading of all dependencies of nested defer blocks
369- // in parallel (later).
424+ function populateHydratingStateForQueue ( registry : DehydratedBlockRegistry , queue : string [ ] ) {
425+ for ( let blockId of queue ) {
426+ registry . hydrating . set ( blockId , Promise . withResolvers ( ) ) ;
427+ }
428+ }
370429
430+ // Waits for the next render cycle to complete
431+ function nextRender ( injector : Injector ) : Promise < void > {
371432 let resolve : VoidFunction ;
372433 const promise = new Promise < void > ( ( resolveFn ) => {
373434 resolve = resolveFn ;
374435 } ) ;
436+ afterNextRender ( ( ) => resolve ( ) , { injector} ) ;
437+ return promise ;
438+ }
375439
376- afterNextRender (
377- ( ) => {
378- const deferBlock = dehydratedBlockRegistry . get ( dehydratedBlockId ) ;
379- // Since we trigger hydration for nested defer blocks in a sequence (parent -> child),
380- // there is a chance that a defer block may not be present at hydration time. For example,
381- // when a nested block was in an `@if` condition, which has changed.
382- // TODO(incremental-hydration): add tests to verify the behavior mentioned above.
383- if ( deferBlock !== null ) {
384- const { tNode, lView} = deferBlock ;
385- const lDetails = getLDeferBlockDetails ( lView , tNode ) ;
386- onDeferBlockCompletion ( lDetails , resolve ) ;
387- triggerDeferBlock ( lView , tNode ) ;
388- // TODO(incremental-hydration): handle the cleanup for cases when
389- // defer block is no longer present during hydration (e.g. `@if` condition
390- // has changed during hydration/rendering).
391- }
392- } ,
393- { injector} ,
394- ) ;
440+ function triggerDeferBlockResourceLoading (
441+ dehydratedBlockId : string ,
442+ dehydratedBlockRegistry : DehydratedBlockRegistry ,
443+ ) {
444+ let resolve : Function ;
445+ const promise = new Promise ( ( resolveFn ) => ( resolve = resolveFn ) ) ;
446+ const deferBlock = dehydratedBlockRegistry . get ( dehydratedBlockId ) ;
447+ // Since we trigger hydration for nested defer blocks in a sequence (parent -> child),
448+ // there is a chance that a defer block may not be present at hydration time. For example,
449+ // when a nested block was in an `@if` condition, which has changed.
450+ if ( deferBlock !== null ) {
451+ const { tNode, lView} = deferBlock ;
452+ const lDetails = getLDeferBlockDetails ( lView , tNode ) ;
453+ onDeferBlockCompletion ( lDetails , ( ) => resolve ( ) ) ;
454+ triggerDeferBlock ( lView , tNode ) ;
455+
456+ // TODO(incremental-hydration): handle the cleanup for cases when
457+ // defer block is no longer present during hydration (e.g. `@if` condition
458+ // has changed during hydration/rendering).
459+ }
395460 return promise ;
396461}
397462
@@ -406,47 +471,6 @@ function onDeferBlockCompletion(lDetails: LDeferBlockDetails, callback: VoidFunc
406471 lDetails [ ON_COMPLETE_FNS ] . push ( callback ) ;
407472}
408473
409- /**
410- * The core mechanism for incremental hydration. This triggers
411- * hydration for all the blocks in the tree that need to be hydrated and keeps
412- * track of all those blocks that were hydrated along the way.
413- */
414- async function triggerBlockTreeHydrationByName (
415- injector : Injector ,
416- blockName : string ,
417- ) : Promise < {
418- deferBlock : DehydratedDeferBlock | null ;
419- hydratedBlocks : Set < string > ;
420- } > {
421- const dehydratedBlockRegistry = injector . get ( DEHYDRATED_BLOCK_REGISTRY ) ;
422-
423- // Make sure we don't hydrate/trigger the same thing multiple times
424- if ( dehydratedBlockRegistry . hydrating . has ( blockName ) )
425- return { deferBlock : null , hydratedBlocks : new Set < string > ( ) } ;
426-
427- // Step 1: Get the queue of items that needs to be hydrated
428- const hydrationQueue = getParentBlockHydrationQueue ( blockName , injector ) ;
429-
430- // Step 2: Add all the items in the queue to the registry at once so we don't trigger hydration on them while
431- // the sequence of triggers fires.
432- hydrationQueue . forEach ( ( id ) => dehydratedBlockRegistry . hydrating . add ( id ) ) ;
433-
434- // Step 3: hydrate each block in the queue. It will be in descending order from the top down.
435- for ( const dehydratedBlockId of hydrationQueue ) {
436- // Step 4: Run the actual trigger function to fetch dependencies.
437- // Triggering a block adds any of its child defer blocks to the registry.
438- await triggerAndWaitForCompletion ( dehydratedBlockId , dehydratedBlockRegistry , injector ) ;
439- }
440-
441- const hydratedBlocks = new Set < string > ( hydrationQueue ) ;
442-
443- // The last item in the queue was the original target block;
444- const hydratedBlockId = hydrationQueue . slice ( - 1 ) [ 0 ] ;
445- const hydratedBlock = dehydratedBlockRegistry . get ( hydratedBlockId ) ! ;
446-
447- return { deferBlock : hydratedBlock , hydratedBlocks} ;
448- }
449-
450474/**
451475 * Determines whether "hydrate" triggers should be activated. Triggers are activated in the following cases:
452476 * - on the server, when incremental hydration is enabled, to trigger the block and render the main content
@@ -576,7 +600,7 @@ export function processAndInitTriggers(
576600 setTimerTriggers ( injector , timerElements ) ;
577601}
578602
579- async function setIdleTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
603+ function setIdleTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
580604 for ( const elementTrigger of elementTriggers ) {
581605 const registry = injector . get ( DEHYDRATED_BLOCK_REGISTRY ) ;
582606 const onInvoke = ( ) => triggerHydrationFromBlockName ( injector , elementTrigger . blockName ) ;
@@ -585,35 +609,34 @@ async function setIdleTriggers(injector: Injector, elementTriggers: ElementTrigg
585609 }
586610}
587611
588- async function setViewportTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
612+ function setViewportTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
589613 if ( elementTriggers . length > 0 ) {
590614 const registry = injector . get ( DEHYDRATED_BLOCK_REGISTRY ) ;
591615 for ( let elementTrigger of elementTriggers ) {
592616 const cleanupFn = onViewport (
593617 elementTrigger . el ,
594- async ( ) => {
595- await triggerHydrationFromBlockName ( injector , elementTrigger . blockName ) ;
596- } ,
618+ ( ) => triggerHydrationFromBlockName ( injector , elementTrigger . blockName ) ,
597619 injector ,
598620 ) ;
599621 registry . addCleanupFn ( elementTrigger . blockName , cleanupFn ) ;
600622 }
601623 }
602624}
603625
604- async function setTimerTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
626+ function setTimerTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
605627 for ( const elementTrigger of elementTriggers ) {
606628 const registry = injector . get ( DEHYDRATED_BLOCK_REGISTRY ) ;
607- const onInvoke = async ( ) =>
608- await triggerHydrationFromBlockName ( injector , elementTrigger . blockName ) ;
629+ const onInvoke = ( ) => triggerHydrationFromBlockName ( injector , elementTrigger . blockName ) ;
609630 const timerFn = onTimer ( elementTrigger . delay ! ) ;
610631 const cleanupFn = timerFn ( onInvoke , injector ) ;
611632 registry . addCleanupFn ( elementTrigger . blockName , cleanupFn ) ;
612633 }
613634}
614635
615- async function setImmediateTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
636+ function setImmediateTriggers ( injector : Injector , elementTriggers : ElementTrigger [ ] ) {
616637 for ( const elementTrigger of elementTriggers ) {
617- await triggerHydrationFromBlockName ( injector , elementTrigger . blockName ) ;
638+ // Note: we intentionally avoid awaiting each call and instead kick off
639+ // th hydration process simultaneously for all defer blocks with this trigger;
640+ triggerHydrationFromBlockName ( injector , elementTrigger . blockName ) ;
618641 }
619642}
0 commit comments