Skip to content

Commit

Permalink
refactor(core): add after and minimum parameter support to `@defe…
Browse files Browse the repository at this point in the history
…r` blocks

This commit adds runtime code to support `after` and `minimum` parameters in the `@placeholder` and `@loading` blocks. The code uses the `TimerScheduler` service added earlier for `on timer` triggers.
  • Loading branch information
AndrewKushnir committed Oct 4, 2023
1 parent 84b3cd0 commit 9db76d6
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 21 deletions.
128 changes: 114 additions & 14 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {assertIndexInDeclRange, assertLContainer, assertLView, assertTNodeForLVi
import {bindingUpdated} from '../bindings';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockInternalState, DeferBlockState, DeferBlockTriggers, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, TDeferBlockDetails} from '../interfaces/defer';
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockInternalState, DeferBlockState, DeferBlockTriggers, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, LOADING_AFTER_CLEANUP_FN, NEXT_DEFER_BLOCK_STATE, STATE_IS_FROZEN_UNTIL, TDeferBlockDetails} from '../interfaces/defer';
import {DirectiveDefList, PipeDefList} from '../interfaces/definition';
import {TContainerNode, TNode} from '../interfaces/node';
import {isDestroyed, isLContainer, isLView} from '../interfaces/type_checks';
Expand Down Expand Up @@ -101,9 +101,13 @@ export function ɵɵdefer(
populateDehydratedViewsInLContainer(lContainer, tNode, lView);

// Init instance-specific defer details and store it.
const lDetails = [];
lDetails[DEFER_BLOCK_STATE] = DeferBlockInternalState.Initial;
setLDeferBlockDetails(lView, adjustedIndex, lDetails as LDeferBlockDetails);
const lDetails: LDeferBlockDetails = [
null, // NEXT_DEFER_BLOCK_STATE
DeferBlockInternalState.Initial, // DEFER_BLOCK_STATE
null, // STATE_IS_FROZEN_UNTIL
null // LOADING_AFTER_CLEANUP_FN
];
setLDeferBlockDetails(lView, adjustedIndex, lDetails);
}

/**
Expand Down Expand Up @@ -611,6 +615,22 @@ function getTemplateIndexForState(
}
}

/**
* Returns a minimum amount of time that a given state should be rendered for,
* taking into account `minimum` parameter value. If the `minimum` value is
* not specified - returns `null`.
*/
function getMinimumDurationForState(
tDetails: TDeferBlockDetails, currentState: DeferBlockState): number|null {
const minimum = 0; // slot id
if (currentState === DeferBlockState.Placeholder) {
return tDetails.placeholderBlockConfig?.[minimum] ?? null;
} else if (currentState === DeferBlockState.Loading) {
return tDetails.loadingBlockConfig?.[minimum] ?? null;
}
return null;
}

/**
* Transitions a defer block to the new state. Updates the necessary
* data structures and renders corresponding block.
Expand All @@ -622,6 +642,7 @@ function getTemplateIndexForState(
export function renderDeferBlockState(
newState: DeferBlockState, tNode: TNode, lContainer: LContainer): void {
const hostLView = lContainer[PARENT];
const hostTView = hostLView[TVIEW];

// Check if this view is not destroyed. Since the loading process was async,
// the view might end up being destroyed by the time rendering happens.
Expand All @@ -631,15 +652,97 @@ export function renderDeferBlockState(
ngDevMode && assertTNodeForLView(tNode, hostLView);

const lDetails = getLDeferBlockDetails(hostLView, tNode);
const tDetails = getTDeferBlockDetails(hostTView, tNode);

ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined');

const now = Date.now();
const currentState = lDetails[DEFER_BLOCK_STATE];

if (!isValidStateChange(currentState, newState) ||
!isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState))
return;

if (lDetails[STATE_IS_FROZEN_UNTIL] === null || lDetails[STATE_IS_FROZEN_UNTIL] <= now) {
lDetails[STATE_IS_FROZEN_UNTIL] = null;

const loadingAfter = tDetails.loadingBlockConfig?.[1] ?? null;
const inLoadingAfterPhase = lDetails[LOADING_AFTER_CLEANUP_FN] !== null;
if (newState === DeferBlockState.Loading && loadingAfter !== null && !inLoadingAfterPhase) {
// Trying to render loading, but it has an `after` config,
// so schedule an update action after a timeout.
lDetails[NEXT_DEFER_BLOCK_STATE] = newState;
const cleanupFn =
scheduleDeferBlockUpdate(loadingAfter, lDetails, tNode, lContainer, hostLView);
lDetails[LOADING_AFTER_CLEANUP_FN] = cleanupFn;
} else {
// If we transition to a complete or an error state and there is a pending
// operation to render loading after a timeout - invoke a cleanup operation,
// which stops the timer.
if (newState > DeferBlockState.Loading && inLoadingAfterPhase) {
lDetails[LOADING_AFTER_CLEANUP_FN]!();
lDetails[LOADING_AFTER_CLEANUP_FN] = null;
lDetails[NEXT_DEFER_BLOCK_STATE] = null;
}

applyDeferBlockStateToDom(newState, lDetails, lContainer, hostLView, tNode);

const duration = getMinimumDurationForState(tDetails, newState);
if (duration !== null) {
lDetails[STATE_IS_FROZEN_UNTIL] = now + duration;
scheduleDeferBlockUpdate(duration, lDetails, tNode, lContainer, hostLView);
}
}
} else {
// We are still rendering the previous state.
// Update the `NEXT_DEFER_BLOCK_STATE`, which would be
// picked up once it's time to transition to the next state.
lDetails[NEXT_DEFER_BLOCK_STATE] = newState;
}
}

/**
* Schedules an update operation after a specified timeout.
*/
function scheduleDeferBlockUpdate(
timeout: number, lDetails: LDeferBlockDetails, tNode: TNode, lContainer: LContainer,
hostLView: LView<unknown>): VoidFunction {
const callback = () => {
const nextState = lDetails[NEXT_DEFER_BLOCK_STATE];
lDetails[STATE_IS_FROZEN_UNTIL] = null;
lDetails[NEXT_DEFER_BLOCK_STATE] = null;
if (nextState !== null) {
renderDeferBlockState(nextState, tNode, lContainer);
}
};
// TODO: this needs refactoring to make `TimerScheduler` that is used inside
// of the `scheduleTimerTrigger` function tree-shakable.
return scheduleTimerTrigger(timeout, callback, hostLView, true);
}

/**
* Checks whether we can transition to the next state.
*
* We transition to the next state if the previous state was represented
* with a number that is less than the next state. For example, if the current
* state is "loading" (represented as `1`), we should not show a placeholder
* (represented as `0`), but we can show a completed state (represented as `2`)
* or an error state (represented as `3`).
*/
function isValidStateChange(
currentState: DeferBlockState|DeferBlockInternalState, newState: DeferBlockState): boolean {
return currentState < newState;
}

/**
* Applies changes to the DOM to reflect a given state.
*/
function applyDeferBlockStateToDom(
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer,
hostLView: LView<unknown>, tNode: TNode) {
const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode);
// Note: we transition to the next state if the previous state was represented
// with a number that is less than the next state. For example, if the current
// state is "loading" (represented as `2`), we should not show a placeholder
// (represented as `1`).
if (lDetails[DEFER_BLOCK_STATE] < newState && stateTmplIndex !== null) {

if (stateTmplIndex !== null) {
lDetails[DEFER_BLOCK_STATE] = newState;
const hostTView = hostLView[TVIEW];
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
Expand Down Expand Up @@ -815,13 +918,9 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {
if (!shouldTriggerDeferBlock(injector)) return;

const tDetails = getTDeferBlockDetails(tView, tNode);

// Condition is triggered, try to render loading state and start downloading.
// Note: if a block is in a loading, completed or an error state, this call would be a noop.
renderDeferBlockState(DeferBlockState.Loading, tNode, lContainer);

switch (tDetails.loadingState) {
case DeferDependenciesLoadingState.NOT_STARTED:
renderDeferBlockState(DeferBlockState.Loading, tNode, lContainer);
triggerResourceLoading(tDetails, lView);

// The `loadingState` might have changed to "loading".
Expand All @@ -831,6 +930,7 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {
}
break;
case DeferDependenciesLoadingState.IN_PROGRESS:
renderDeferBlockState(DeferBlockState.Loading, tNode, lContainer);
renderDeferStateAfterResourceLoading(tDetails, tNode, lContainer);
break;
case DeferDependenciesLoadingState.COMPLETE:
Expand Down
36 changes: 30 additions & 6 deletions packages/core/src/render3/interfaces/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export enum DeferDependenciesLoadingState {
export type DeferredLoadingBlockConfig = [minimumTime: number|null, afterTime: number|null];

/** Configuration object for a placeholder block as it is stored in the component constants. */
export type DeferredPlaceholderBlockConfig = [afterTime: number|null];
export type DeferredPlaceholderBlockConfig = [minimumTime: number|null];

/**
* Describes the data shared across all instances of a defer block.
Expand Down Expand Up @@ -133,11 +133,14 @@ export enum DeferBlockInternalState {
Initial = -1,
}

/**
* A slot in the `LDeferBlockDetails` array that contains a number
* that represent a current block state that is being rendered.
*/
export const DEFER_BLOCK_STATE = 0;
export const NEXT_DEFER_BLOCK_STATE = 0;
// Note: it's *important* to keep the state in this slot, because this slot
// is used by runtime logic to differentiate between LViews, LContainers and
// other types (see `isLView` and `isLContainer` functions). In case of defer
// blocks, this slot would always be a number.
export const DEFER_BLOCK_STATE = 1;
export const STATE_IS_FROZEN_UNTIL = 2;
export const LOADING_AFTER_CLEANUP_FN = 3;

/**
* Describes instance-specific defer block data.
Expand All @@ -147,7 +150,28 @@ export const DEFER_BLOCK_STATE = 0;
* (which would require per-instance state).
*/
export interface LDeferBlockDetails extends Array<unknown> {
/**
* Currently rendered block state.
*/
[DEFER_BLOCK_STATE]: DeferBlockState|DeferBlockInternalState;

/**
* Block state that was requested when another state was rendered.
*/
[NEXT_DEFER_BLOCK_STATE]: DeferBlockState|null;

/**
* Timestamp indicating when the current state can be switched to
* the next one, in case teh current state has `minimum` parameter.
*/
[STATE_IS_FROZEN_UNTIL]: number|null;

/**
* Contains a reference to a cleanup function which cancels a timeout
* when Angular waits before rendering loading state. This is used when
* the loading block has the `after` parameter configured.
*/
[LOADING_AFTER_CLEANUP_FN]: VoidFunction|null;
}

/**
Expand Down
Loading

0 comments on commit 9db76d6

Please sign in to comment.